<?php
/**
 * Mirasvit
 *
 * This source file is subject to the Mirasvit Software License, which is available at https://mirasvit.com/license/.
 * Do not edit or add to this file if you wish to upgrade the to newer versions in the future.
 * If you wish to customize this module for your needs.
 * Please refer to http://www.magentocommerce.com for more information.
 *
 * @category  Mirasvit
 * @package   mirasvit/module-cache-warmer
 * @version   1.11.13
 * @copyright Copyright (C) 2026 Mirasvit (https://mirasvit.com/)
 */



namespace Mirasvit\CacheWarmer\Service;

use Magento\Framework\App\Http\Context as HttpContext;
use Magento\Framework\App\PageCache\Cache;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\Module\ModuleListInterface;
use Magento\Framework\Registry;
use Magento\PageCache\Model\Config as PageCacheConfig;
use Mirasvit\CacheWarmer\Api\Data\PageInterface;
use Mirasvit\CacheWarmer\Api\Data\PageTypeInterface;
use Mirasvit\CacheWarmer\Api\Data\SourceInterface;
use Mirasvit\CacheWarmer\Api\Data\UserAgentInterface;
use Mirasvit\CacheWarmer\Api\Repository\PageRepositoryInterface;
use Mirasvit\CacheWarmer\Api\Repository\PageTypeRepositoryInterface;
use Mirasvit\CacheWarmer\Api\Repository\WarmRuleRepositoryInterface;
use Mirasvit\CacheWarmer\Api\Service\FilterServiceInterface;
use Mirasvit\CacheWarmer\Api\Service\PageServiceInterface;
use Mirasvit\CacheWarmer\Api\Service\WarmerServiceInterface;
use Mirasvit\CacheWarmer\Logger\PageStatusVarnishLogger;
use Mirasvit\CacheWarmer\Model\Config;
use Mirasvit\CacheWarmer\Repository\SourceRepository;
use Mirasvit\CacheWarmer\Service\Config\ExtendedConfig;
use Mirasvit\CacheWarmer\Service\CookieService;
use Mirasvit\CacheWarmer\Service\PageCacheIdService as CacheIdentifier;
use \Magento\Store\Model\StoreManagerInterface;
use Mirasvit\CacheWarmer\Api\Service\SessionServiceInterface;
use Mirasvit\CacheWarmer\Model\ResourceModel\Page\Collection as PageCollection;
use Mirasvit\Core\Service\SerializeService;

/**
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class PageService implements PageServiceInterface
{
    protected $curlService;

    private $pageRepository;

    private $cacheIdentifier;

    private $registry;

    private $httpContext;

    private $cache;

    private $config;

    private $extendedConfig;

    private $cookieService;

    private $pageTypeRepository;

    private $filterService;

    private $moduleList;

    protected $dateFactory;

    private $storeManager;

    private $sessionService;

    private $logger;

    private $warmRuleRepository;

    private $warmRuleService;

    private $sourceRepository;

    public function __construct(
        WarmRuleRepositoryInterface $warmRuleRepository,
        WarmRuleService $warmRuleService,
        PageRepositoryInterface $pageRepository,
        CacheIdentifier $cacheIdentifier,
        CurlService $curlService,
        Registry $registry,
        HttpContext $httpContext,
        Cache $cache,
        Config $config,
        ExtendedConfig $extendedConfig,
        CookieService $cookieService,
        PageTypeRepositoryInterface $pageTypeRepository,
        FilterServiceInterface $filterService,
        SessionServiceInterface $sessionService,
        ModuleListInterface $moduleList,
        StoreManagerInterface $storeManager,
        \Magento\Framework\Stdlib\DateTime\DateTimeFactory $dateFactory,
        PageStatusVarnishLogger $logger,
        SourceRepository $sourceRepository
    ) {
        $this->warmRuleRepository = $warmRuleRepository;
        $this->warmRuleService    = $warmRuleService;
        $this->pageRepository     = $pageRepository;
        $this->cacheIdentifier    = $cacheIdentifier;
        $this->curlService        = $curlService;
        $this->registry           = $registry;
        $this->httpContext        = $httpContext;
        $this->cache              = $cache;
        $this->config             = $config;
        $this->extendedConfig     = $extendedConfig;
        $this->cookieService      = $cookieService;
        $this->pageTypeRepository = $pageTypeRepository;
        $this->filterService      = $filterService;
        $this->sessionService     = $sessionService;
        $this->moduleList         = $moduleList;
        $this->storeManager       = $storeManager;
        $this->dateFactory        = $dateFactory;
        $this->logger             = $logger;
        $this->sourceRepository   = $sourceRepository;
    }

    /**
     * @param PageInterface $page
     */
    public function setUncacheableStatus(PageInterface $page)
    {
        $page->setStatus(PageInterface::STATUS_UNCACHEABLE);
        $this->pageRepository->save($page);
    }

    /**
     * @param PageInterface $page
     */
    public function setCachedStatus(PageInterface $page)
    {
        if ($page->getStatus() != PageInterface::STATUS_CACHED) {
            $page->setStatus(PageInterface::STATUS_CACHED);
            //using this, because default magento function is not working correctly in some configurations
            $page->setCachedAt(gmdate("Y-m-d H:i:s"));
            $page->setFlushedAt(null);
        }
        $this->pageRepository->save($page);
    }

    /**
     * @param PageInterface $page
     */
    public function setPendingStatus(PageInterface $page)
    {
        if ($page->getStatus() != PageInterface::STATUS_PENDING) {
            $page->setStatus(PageInterface::STATUS_PENDING);
            $page->setCachedAt(null);
            //using this, because default magento function is not working correctly in some configurations
            $page->setFlushedAt(gmdate("Y-m-d H:i:s"));
        }
        $this->pageRepository->save($page);
    }

    /**
     * {@inheritdoc}
     */
    public function isCached(PageInterface $page)
    {
        if ($page->getStatus() == PageInterface::STATUS_UNCACHEABLE) {
            return false;
        }
        if ($this->config->isFishPigFpcEnabled() || $this->config->isNitroPackCacheEnabled()) {
            if ($this->cache->load($page->getCacheId())) {
                $this->setCachedStatus($page);

                return true;
            }

            $channel = $this->curlService->initChannel();

            $channel->setUrl($page->getUri());
            $channel->addCookies($this->sessionService->getCookies($page));

            $response = $this->curlService->request($channel);
            $headers  = $response->getHeaders();

            if (
                $this->config->isNitroPackCacheEnabled() &&
                (isset($headers['x-nitro-cache']) && $headers['x-nitro-cache'] == 'HIT'
                 || isset($headers['X-Nitro-Cache']) && $headers['X-Nitro-Cache'] == 'HIT')
            ) {
                $this->setCachedStatus($page);

                return true;
            }

            if (
                (isset($headers['X-Cached-By']) && $headers['X-Cached-By'] == 'Bolt')
                || (isset($headers['X-Bolt-Id']) && $headers['X-Bolt-Id'])
                || (isset($headers['x-bolt-id']) && $headers['x-bolt-id'])
            ) {
                $this->setCachedStatus($page);

                return true;
            }
            $this->setPendingStatus($page);

            return false;
        } elseif ($this->config->getCacheType() == PageCacheConfig::BUILT_IN) {
            if ($this->cache->load($page->getCacheId())) {
                $this->setCachedStatus($page);

                return true;
            }
            $this->setPendingStatus($page);

            return false;
        } else {
            $channel = $this->curlService->initChannel();

            $rule = null;

            if ($page->getMainRuleId()) {
                $rule = $this->warmRuleRepository->get($page->getMainRuleId());
            }

            if ($rule && $rule->getId()) {
                $page = $this->warmRuleService->modifyPage($page, $rule);
            }

            $channel->setUrl($page->getUri());
            $channel->setUserAgent($page->getUserAgent());
            $channel->addCookie(WarmerServiceInterface::STATUS_COOKIE, 1);
            $channel->addCookies($this->sessionService->getCookies($page));
            $channel->setHeaders($page->getHeaders());

            $response = $this->curlService->request($channel);

            if ($response->getBody() === '*') {
                $this->setPendingStatus($page);

                return false;
            }

            $code = $response->getCode();
            if ($code !== 200 ) {
                $message = sprintf(
                    'Page %s (id: %s) respond with status code %s. ',
                    $page->getUri(),
                    $page->getId(),
                    $code
                );

                if (in_array($code, [301, 302, 404])) {
                    $message .= 'Removing page...';
                    $this->pageRepository->delete($page);
                }

                if($this->config->isRequestLogEnabled()) {
                    $isBacktraceLogFileEnabled = $this->config->isBacktraceLogFileEnabled();

                    $body = strlen($response->getBody()) > 500
                        ? $body = substr($response->getBody(), 0, 500)."..."
                        : $response->getBody();

                    $this->logger->debug($message, [
                        'cli'       => php_sapi_name() == "cli" ? "Yes" : "No",
                        'code'      => $code,
                        'body'      => $body,
                        'backtrace' => $isBacktraceLogFileEnabled
                                       ? \Magento\Framework\Debug::backtrace(true, false, false)
                                       : null,
                    ]);
                }

                return false;
            }

            $this->setCachedStatus($page);

            return true;
        }
    }

    /**
     * Remove google gclid from URLs. Same as in default magento config for Varnish.
     * Handles custom base_link_url configurations with or without store code in URL.
     */
    public function prepareUri(RequestInterface $request): string
    {
        $uri = $request->getUriString();

        $uri = preg_replace('/(.*)\\?gclid=[^&]+$/', '$1', $uri, -1);
        $uri = preg_replace('/(.*)\\?gclid=[^&]+&/', '$1?', $uri, -1);
        $uri = preg_replace('/(.*)&gclid=[^&]+/', '$1', $uri, -1);

        $baseUrl = $this->storeManager->getStore()->getBaseUrl();

        if (strpos($uri, $baseUrl) === 0) {
            return $uri;
        }

        $requestPath = $request->getRequestUri();

        $basePath = parse_url($baseUrl, PHP_URL_PATH) ?: '/';
        $storeCode = $this->storeManager->getStore()->getCode();
        $storeCodeSuffix = '/' . $storeCode;
        $storeCodePrefix = $storeCode . '/';

        if (substr(rtrim($basePath, '/'), -strlen($storeCodeSuffix)) === $storeCodeSuffix
            && strpos(ltrim($requestPath, '/'), $storeCodePrefix) === 0
        ) {
            $requestPath = '/' . substr(ltrim($requestPath, '/'), strlen($storeCodePrefix));
        }

        $storeUrl = $baseUrl . ltrim($requestPath, '/');

        if ($uri !== $storeUrl) {
            return $storeUrl;
        }

        return $uri;
    }

    /**
     * @param RequestInterface $request
     *
     * @return string
     */
    public function getVaryDataHash(RequestInterface $request)
    {
        $hash = $request->get(\Magento\Framework\App\Response\Http::COOKIE_VARY_STRING) ? : $this->httpContext->getVaryString();

        return (string)$hash;
    }

    /**
     * @param RequestInterface $request
     *
     * @return string
     */
    public function getVaryDataString(RequestInterface $request)
    {
        //some 3rd party plugins modify vary data by using this method.
        //we need to make sure that vary data is modifed.
        $this->httpContext->getVaryString();

        $varyData = $this->prepareVaryData($this->httpContext->getData());
        if ($this->config->getCacheType() == PageCacheConfig::BUILT_IN) {
            return $varyData;
        }
        //on non-default stores, vary data is not empty
        //if we use varnish, first request goes without cookie and vary data
        //but we need to crawl it. so we clear vary data
        /** @var \Magento\Framework\App\Request\Http $request */
        if (!$request->get(\Magento\Framework\App\Response\Http::COOKIE_VARY_STRING)) {
            $varyData = $this->prepareVaryData([]);
        }

        return $varyData;
    }

    /**
     * @param string $cookie
     *
     * @return string
     */
    private function prepareCookie($cookie)
    {
        return $this->cookieService->prepareCookie($cookie);
    }

    /**
     * {@inheritdoc}
     */
    public function collect(RequestInterface $request, ResponseInterface $response)
    {
        if (!$this->config->isEnabled()) {
            return false;
        }

        if ($response->getStatusCode() && $response->getStatusCode() != 200) {
            return false;
        }

        if (!$this->isValidUrl($request->getUriString())) {
            return false;
        }

        /** @var \Magento\Framework\App\Response\Http $response */
        /** @var \Magento\Framework\App\Request\Http $request */
        $cacheId      = $this->cacheIdentifier->getValue();
        $varyDataHash = $this->getVaryDataHash($request);
        $varyData     = $this->getVaryDataString($request);
        $userAgent    = $request->getHeader('User-Agent');

        if (!$this->isCanCollect($userAgent)) {
            return false;
        }

        //remove cache warmer agent
        if (strpos($userAgent, WarmerServiceInterface::USER_AGENT) !== false) {
            $userAgent = substr($userAgent, 0, strpos($userAgent, WarmerServiceInterface::USER_AGENT));
            $userAgent .= ";W";//mark for debug purposes
        }

        $page     = $this->pageRepository->getByCacheId($cacheId, $varyDataHash);
        $pageType = $this->getPageType($request);
        $uri      = $this->prepareUri($request);

        $storeId = $this->storeManager->getStore()->getId();
        $cookie  = $this->prepareCookie($request->getHeader('Cookie'));

        if (!$page && $request->getFullActionName() !== '__'
            && strpos($uri, '_=') === false) {

            /** @var PageInterface $page */
            $page = $this->ensurePage(
                $request,
                $this->pageRepository->getByURI($uri, $varyDataHash)
            );

            $pageTypeCollection = $this->pageTypeRepository->getCollection()
                ->addFieldToFilter(PageTypeInterface::PAGE_TYPE, $pageType);

            if ($pageTypeCollection->getSize() == 0) {
                $this->pageTypeRepository->save(
                    $this->pageTypeRepository->create()->setPageType($pageType)
                );
            }

            if (!$this->config->isIgnoredPage($page)) {
                $this->pageRepository->save($page);
            }
        } elseif (is_object($page) && $page->getId() && $pageType != $page->getPageType()) {
            $page->setUri($uri)
                ->setCacheId($cacheId)
                ->setPageType($pageType)
                ->setStoreId($storeId)
                ->setVaryData($varyData)
                ->setVaryDataHash($varyDataHash)
                ->setUserAgent($userAgent)
                ->setCookie($cookie)
                ->setStatus(PageInterface::STATUS_PENDING);

            $this->pageRepository->save($page);
        }

        return true;
    }

    /**
     * @param string $userAgent
     * @return bool
     */
    private function isCanCollect($userAgent)
    {
        $sources = $this->sourceRepository->getCollection();

        if (!$sources->count()) {
            return false; // no active sources present
        }

        $defaultSource = $this->sourceRepository->getDefaultSource();

        if ($defaultSource && $defaultSource->getIsActive()) {
            return true;
        }

        $crawlerSources = $this->sourceRepository->getCollection()
            ->addFieldToFilter(SourceInterface::SOURCE_TYPE, SourceInterface::TYPE_CRAWLER);

        if ($crawlerSources->count() && $userAgent == UserAgentInterface::DESKTOP_USER_AGENT) {
            return true;
        }

        $sitemapSources = $this->sourceRepository->getCollection()
            ->addFieldToFilter(SourceInterface::SOURCE_TYPE, SourceInterface::TYPE_SITEMAP);

        if ($sitemapSources->count() && $userAgent == UserAgentInterface::SITEMAP_USER_AGENT) {
            return true;
        }

        $fileSources = $this->sourceRepository->getCollection()
            ->addFieldToFilter(SourceInterface::SOURCE_TYPE, SourceInterface::TYPE_FILE);

        if ($fileSources->count() && $userAgent == UserAgentInterface::FILE_USER_AGENT) {
            return true;
        }

        return false;
    }

    /**
     * @param RequestInterface $request
     * @return string
     */
    public function getPageType(RequestInterface $request)
    {
        $pageType = $request->getFullActionName();
        $uri      = $this->prepareUri($request);

        if (strpos($uri, '?') !== false) {
            $pageType .= '_*';
        } elseif ($this->filterService->isSeoFilterPage($pageType, $request->getParams())) {
            $pageType .= '_SeoFilter';
        }

        return $pageType;
    }

    /**
     * {@inheritdoc}
     */
    public function isValidUrl($url)
    {
        if (strpos($url, 'https://') === false
            && strpos($url, 'http://') === false) {
            return false;
        }
        $parsedUrl = parse_url($url);
        //Assume that URL like https://123.123.123.123 is not valid. no cetificate
        if (isset($parsedUrl['host'])
            && $parsedUrl['host']
            && strpos($url, 'http://') === false
            && filter_var($parsedUrl['host'], FILTER_VALIDATE_IP)
        ) {
            return false;
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function prepareVaryData($varyData)
    {
        if (is_array($varyData)) {
            ksort($varyData);
        }

        return SerializeService::encode($varyData);
    }

    /**
     * @param RequestInterface $request
     * @param bool|PageInterface $page
     * @return bool|PageInterface
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function ensurePage($request, $page = false)
    {
        if (!$page) {
            $page = $this->pageRepository->create();
        }

        $uri          = $this->prepareUri($request);
        $cookie       = $this->prepareCookie($request->getHeader('Cookie'));
        $product      = $this->registry->registry('current_product');
        $category     = $this->registry->registry('current_category');
        $productId    = $product ? $product->getId() : 0;
        $categoryId   = $category ? $category->getId() : 0;

        if (!$productId && $this->registry->registry(PageServiceInterface::PRODUCT_REG)) {
            $productId = $this->registry->registry(PageServiceInterface::PRODUCT_REG);
        }

        if (!$categoryId && $this->registry->registry(PageServiceInterface::CATEGORY_REG)) {
            $categoryId = $this->registry->registry(PageServiceInterface::CATEGORY_REG);
        }

        $page->setUri($uri)
            ->setCacheId($this->cacheIdentifier->getValue())
            ->setPageType($this->getPageType($request))
            ->setStoreId($this->storeManager->getStore()->getId())
            ->setVaryData($this->getVaryDataString($request))
            ->setVaryDataHash($this->getVaryDataHash($request))
            ->setUserAgent($request->getHeader('User-Agent'))
            ->setProductId($productId)
            ->setCategoryId($categoryId)
            ->setCookie($cookie)
            ->setStatus(PageInterface::STATUS_PENDING);

        return $page;
    }
}
