<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
declare(strict_types=1);

namespace Magento\Customer\Model;

use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\Request;
use Magento\Framework\App\RequestSafetyInterface;
use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\HTTP\Header;
use Magento\Framework\Indexer\IndexerRegistry;
use Magento\Framework\Model\AbstractModel;
use Magento\Framework\Model\Context as ModelContext;
use Magento\Framework\Model\ResourceModel\AbstractResource;
use Magento\Framework\Registry;
use Magento\Framework\Session\Config as SessionConfig;
use Magento\Framework\Session\SessionManagerInterface;
use Magento\Framework\Stdlib\DateTime;
use Magento\Store\Model\ScopeInterface;

/**
 * Class Visitor responsible for initializing visitor's.
 *
 *  Used to track sessions of the logged in customers
 *
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
 */
class Visitor extends AbstractModel
{
    const VISITOR_TYPE_CUSTOMER = 'c';
    const VISITOR_TYPE_VISITOR = 'v';
    const DEFAULT_ONLINE_MINUTES_INTERVAL = 15;
    const XML_PATH_ONLINE_INTERVAL = 'customer/online_customers/online_minutes_interval';
    private const SECONDS_24_HOURS = 86400;

    /**
     * @var string[]
     */
    protected $ignoredUserAgents;

    /**
     * @var SessionManagerInterface
     */
    protected $session;

    /**
     * @var Header
     */
    protected $httpHeader;

    /**
     * @var bool
     */
    protected $skipRequestLogging = false;

    /**
     * Ignored Modules
     *
     * @var array
     */
    protected $ignores;

    /**
     * Core store config
     *
     * @var ScopeConfigInterface
     */
    protected $scopeConfig;

    /**
     * @var DateTime
     */
    protected $dateTime;

    /**
     * @var IndexerRegistry
     */
    protected $indexerRegistry;

    /**
     * @var RequestSafetyInterface
     */
    private $requestSafety;

    /**
     * @param ModelContext $context
     * @param Registry $registry
     * @param SessionManagerInterface $session
     * @param Header $httpHeader
     * @param ScopeConfigInterface $scopeConfig
     * @param DateTime $dateTime
     * @param IndexerRegistry $indexerRegistry
     * @param AbstractResource|null $resource
     * @param AbstractDb|null $resourceCollection
     * @param array $ignoredUserAgents
     * @param array $ignores
     * @param array $data
     * @param RequestSafetyInterface|null $requestSafety
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
     */
    public function __construct(
        ModelContext $context,
        Registry $registry,
        SessionManagerInterface $session,
        Header $httpHeader,
        ScopeConfigInterface $scopeConfig,
        DateTime $dateTime,
        IndexerRegistry $indexerRegistry,
        AbstractResource $resource = null,
        AbstractDb $resourceCollection = null,
        array $ignoredUserAgents = [],
        array $ignores = [],
        array $data = [],
        RequestSafetyInterface $requestSafety = null
    ) {
        $this->session = $session;
        $this->httpHeader = $httpHeader;
        $this->ignoredUserAgents = $ignoredUserAgents;
        parent::__construct($context, $registry, $resource, $resourceCollection, $data);
        $this->ignores = $ignores;
        $this->scopeConfig = $scopeConfig;
        $this->dateTime = $dateTime;
        $this->indexerRegistry = $indexerRegistry;
        $this->requestSafety = $requestSafety ?? ObjectManager::getInstance()->get(RequestSafetyInterface::class);
    }

    /**
     * Object initialization
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_init(ResourceModel\Visitor::class);
        $userAgent = $this->httpHeader->getHttpUserAgent();
        if ($this->ignoredUserAgents) {
            if (in_array($userAgent, $this->ignoredUserAgents)) {
                $this->skipRequestLogging = true;
            }
        }
    }

    /**
     * Skip request logging
     *
     * @param bool $skipRequestLogging
     * @return Visitor
     */
    public function setSkipRequestLogging($skipRequestLogging)
    {
        $this->skipRequestLogging = (bool)$skipRequestLogging;
        return $this;
    }

    /**
     * Initialization visitor by request. Used in event "controller_action_predispatch"
     *
     * @param EventObserver $observer
     * @return Visitor
     */
    public function initByRequest($observer)
    {
        if ($this->skipRequestLogging || $this->isModuleIgnored($observer)) {
            return $this;
        }

        if ($this->session->getVisitorData()) {
            $this->setData($this->session->getVisitorData());
            if ($this->getSessionId() != $this->session->getSessionId()) {
                $this->setSessionId($this->session->getSessionId());
            }
        }

        $this->setLastVisitAt((new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT));

        // prevent saving Visitor for safe methods, e.g. GET request
        if ($this->requestSafety->isSafeMethod()) {
            return $this;
        }

        if (!$this->getId()) {
            $this->setSessionId($this->session->getSessionId());
            $this->save();
            $this->_eventManager->dispatch('visitor_init', ['visitor' => $this]);
            $this->session->setVisitorData($this->getData());
        }
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function beforeSave()
    {
        $this->unsetData("session_id");
        return parent::beforeSave();
    }

    /**
     * @inheritdoc
     */
    public function afterSave()
    {
        $this->setSessionId($this->session->getSessionId());
        return parent::afterSave();
    }


    /**
     * Save visitor by request
     *
     * Used in event "controller_action_postdispatch"
     *
     * @param EventObserver $observer
     * @return Visitor
     */
    public function saveByRequest($observer)
    {
        // prevent saving Visitor for safe methods, e.g. GET request
        if (($this->skipRequestLogging || $this->requestSafety->isSafeMethod() || $this->isModuleIgnored($observer))
            && !$this->sessionIdHasChanged()
        ) {
            return $this;
        }

        try {
            if ($this->session->getSessionId() && $this->getSessionId() != $this->session->getSessionId()) {
                $this->setSessionId($this->session->getSessionId());
            }
            $this->save();
            $this->_eventManager->dispatch('visitor_activity_save', ['visitor' => $this]);
            $this->session->setVisitorData($this->getData());
        } catch (\Exception $e) {
            $this->_logger->critical($e);
        }
        return $this;
    }

    /**
     * Check if visitor session id was changed.
     *
     * @return bool
     */
    private function sessionIdHasChanged(): bool
    {
        $visitorData = $this->session->getVisitorData();
        $hasChanged = false;

        if (isset($visitorData['session_id'])) {
            $hasChanged = $this->session->getSessionId() !== $visitorData['session_id'];
        }

        return $hasChanged;
    }

    /**
     * Returns true if the module is required
     *
     * @param EventObserver $observer
     * @return bool
     */
    public function isModuleIgnored($observer)
    {
        if (is_array($this->ignores) && $observer) {
            $curModule = $this->requestSafety->getRouteName();
            if (isset($this->ignores[$curModule])) {
                return true;
            }
        }
        return false;
    }

    /**
     * Bind customer data when customer login
     *
     * Used in event "customer_login"
     *
     * @param EventObserver $observer
     * @return Visitor
     */
    public function bindCustomerLogin($observer)
    {
        /** @var CustomerInterface $customer */
        $customer = $observer->getEvent()->getCustomer();
        if (!$this->getCustomerId()) {
            $this->setDoCustomerLogin(true);
            $this->setCustomerId($customer->getId());
        }
        return $this;
    }

    /**
     * Bind customer data when customer logout
     *
     * Used in event "customer_logout"
     *
     * @param EventObserver $observer
     * @return Visitor
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function bindCustomerLogout($observer)
    {
        if ($this->getCustomerId()) {
            $this->setDoCustomerLogout(true);
        }
        return $this;
    }

    /**
     * Create binding of checkout quote
     *
     * @param EventObserver $observer
     * @return Visitor
     */
    public function bindQuoteCreate($observer)
    {
        $quote = $observer->getEvent()->getQuote();
        if ($quote) {
            if ($quote->getIsCheckoutCart()) {
                $this->setQuoteId($quote->getId());
                $this->setDoQuoteCreate(true);
            }
        }
        return $this;
    }

    /**
     * Destroy binding of checkout quote
     *
     * @param EventObserver $observer
     * @return Visitor
     */
    public function bindQuoteDestroy($observer)
    {
        $quote = $observer->getEvent()->getQuote();
        if ($quote) {
            $this->setDoQuoteDestroy(true);
        }
        return $this;
    }

    /**
     * Return clean time in seconds for visitor's outdated records
     *
     * @return string
     */
    public function getCleanTime()
    {
        return self::SECONDS_24_HOURS + $this->scopeConfig->getValue(
            SessionConfig::XML_PATH_COOKIE_LIFETIME,
            ScopeInterface::SCOPE_STORE
        );
    }

    /**
     * Clean visitor's outdated records
     *
     * @return $this
     */
    public function clean()
    {
        $this->getResource()->clean($this);
        return $this;
    }

    /**
     * Retrieve Online Interval (in minutes)
     *
     * @return int Minutes Interval
     */
    public function getOnlineInterval()
    {
        $configValue = (int)$this->scopeConfig->getValue(
            static::XML_PATH_ONLINE_INTERVAL,
            ScopeInterface::SCOPE_STORE
        );
        return $configValue ?: static::DEFAULT_ONLINE_MINUTES_INTERVAL;
    }
}
