<?php

namespace Diamond\Application\System;

use Core\Compress\Minify;
use Core\Concrete\RequestAwareTrait;
use Core\Contracts\RequestAwareInterface;
use Core\Exceptions\TooManyRequestsException;
use Core\Template\Template;
use Core\TwigAwareInterface;
use Core\TwigAwareTrait;
use Diamond\Application\Content\Exceptions\PageNotFoundException;
use Diamond\Application\User\Exceptions\UserNotAuthException;
use Diamond\Application\User\UserService;
use Diamond\Config\Counters;
use Diamond\Domain\Page\Contracts\PageEntityInterface;
use Diamond\Domain\Stat\Contracts\StatEntityInterface;
use Diamond\Infrastructure\Models\Stat\Stat;
use Diamond\Infrastructure\Models\StatBan\StatBan;
use Diamond\Infrastructure\Models\StatBan\StatBanQuery;
use Diamond\Infrastructure\Models\StatRule\StatRuleQuery;
use Diamond\Repositories\Stat\StatRepository;
use Jaybizzle\CrawlerDetect\CrawlerDetect;
use Diamond\Infrastructure\Models\Stat\Stat as StatModel;
use Nette\Utils\Json;
use Psr\Log\LoggerAwareInterface;
use Symfony\Component\HttpFoundation\Request;


class SystemService implements LoggerAwareInterface,
    TwigAwareInterface, RequestAwareInterface
{

    use \Psr\Log\LoggerAwareTrait;
    use TwigAwareTrait;
    use RequestAwareTrait;

    private static StatEntityInterface|null $statEntity = null;

    public function __construct(protected \Mobile_Detect       $mobileDetect,
                                protected CrawlerDetect        $crawlerDetect,
                                protected StatRepository       $statRepo,
                                protected \Diamond\Config\Main $settings,
                                protected Template             $template,
                                protected Counters             $counters,
                                protected Minify               $minify,
                                protected UserService          $userService
    )
    {

    }


    public function parseRules(Request $request): void
    {

        $s = $request->headers->get('User-Agent');
        $type = '%';
        if ($this->crawlerDetect->isCrawler($s)) {
            $type = 'robot';
        } else {
            if ($this->mobileDetect->isMobile()) {
                $type = 'mobile';
            } elseif ($this->mobileDetect->isTablet()) {
                $type = 'tablet';
            } else {
                $type = 'browser';
            }
        }

        $ruleAccess = StatRuleQuery::create('b')
            ->where("? LIKE b.ClientType", $type)
            ->where("? LIKE b.IpMask", $request->getClientIp())
            ->where("? LIKE b.UriMask", $request->getRequestUri())
            ->where("? LIKE b.AgentMask", $request->headers->get('User-Agent'))
            ->where("? LIKE b.HostMask", $request->getHost())
            ->where("rule = 'ACCESS'")
            ->findOne();

        if ($ruleAccess) {
            return;
        }

        $rule = StatRuleQuery::create('b')
            ->where("? LIKE b.ClientType", $type)
            ->where("? LIKE b.IpMask", $request->getClientIp())
            ->where("? LIKE b.UriMask", $request->getRequestUri())
            ->where("? LIKE b.AgentMask", $request->headers->get('User-Agent'))
            ->where("? LIKE b.HostMask", $request->getHost())
            ->where("rule <> 'ACCESS'")
            ->findOne();

        if (!$rule) {
            return;
        }

        $con = \Propel\Runtime\Propel::getServiceContainer()->getConnection('default');


        $period = $rule->getPeriod();

        $query = "SELECT COUNT(*) as c FROM `_stat`
                                                      WHERE
        `_stat`.`remote_addr` LIKE  '" . $request->getClientIp() . "'
        AND `_stat`.`requested_page` LIKE '" . $rule->getUriMask() . "'
        AND `_stat`.`user_agent` LIKE '" . $rule->getHostMask() . "'
        AND `_stat`.`client_type` LIKE '" . $rule->getClientType() . "'
        AND `_stat`.`datetime` BETWEEN DATE_SUB(NOW(), INTERVAL '" . $rule->getPeriod() . "' SECOND) AND NOW()";

        $stmt = $con->prepare($query);
        $res = $stmt->execute();
        $stat = $stmt->fetchColumn(0);

        if ($stat > $rule->getBanRate()) {
            $statBan = (new StatBan())
                ->setIpMask($request->getClientIp())
                ->setPeriod($rule->getPeriod())
                ->setRule($rule->getRule())
                ->setUriMask($request->getRequestUri())
                ->setHostName($request->getHost())
                ->setAgentMask($request->headers->get('User-Agent'));
            $statBan->save();
            $this->applySecurity($rule->getRule());
        }

    }


    /**
     * проверка страницы в бане
     * @param Request $request
     * @return void
     */
    public function checkSecurity(Request $request): void
    {
        $ban = StatBanQuery::create('b')
            ->where("? LIKE b.IpMask", $request->getClientIp())
            ->where("NOW() BETWEEN b.CreatedAt AND DATE_ADD(b.CreatedAt, INTERVAL b.Period SECOND)")
            ->where("? LIKE b.UriMask", $request->getRequestUri())
            ->where("? LIKE b.AgentMask", $request->headers->get('User-Agent'))
            ->findOne();

        if ($ban) {
            $this->applySecurity($ban->getRule());
        }
    }


    /**
     * Создаение и сохранение статистики
     *
     * @param Request $request
     * @return StatModel
     */
    public function createStat(Request $request): \Diamond\Infrastructure\Models\Stat\Stat
    {
        $stat
            = (new Stat)
            ->setRemoteAddr($request->getClientIp())
            ->setRequestUri($request->getRequestUri())
            ->setRequestedPage($request->getPathInfo())
            ->setUserAgent($request->headers->get('User-Agent'))
            ->setRequestMethod($request->getMethod())
            ->setReferer($request->server->get('REDIRECT_URL'))
            ->setHost($request->getHost());

        try {
            $user = $this->userService->getAuthUser();
            $stat->setAdminUser($user->getId());
        } catch (UserNotAuthException $ex) {

        }


        //определение паука
        //$s = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
        $s = $request->headers->get('User-Agent');

        if ($this->crawlerDetect->isCrawler($s)) {
            $stat->setClientType('robot');
            $stat->setClient($this->crawlerDetect->getMatches());
        } else {

            if ($this->mobileDetect->isMobile()) {
                $stat->setClientType('browser');
                $stat->setClient('mobile');
            } elseif ($this->mobileDetect->isTablet()) {
                $stat->setClientType('browser');
                $stat->setClient('tablet');
            } else {
                $stat->setClientType('browser');
            }
        }

        if ($request->getMethod() === 'POST') {
            $stat->setPostData(json_encode($request->request->all()));
        }

//	if(!$stat->save()){
//	    throw new Exception("aplication stat save error");
//	}

        static::$statEntity = $stat;


        return $stat;
    }

    /**
     * @deprecated
     */
    /**
     * @param StatModel $stat
     */
    public function saveStat(StatModel $stat): void
    {
        $this->statRepo->save($stat);
    }

    protected function applySecurity(string $rule): void
    {
        switch ($rule) {
            case 'DENY':
                throw new TooManyRequestsException('find page in ban, rule ' . $rule);
        }
    }


    /**
     * @param PageEntityInterface $page
     * @return string
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     */
    public function renderPage(PageEntityInterface $page): string
    {

        $package = '';
        $name = $templateName = $page->getTemplate();

        if (isset($name[0]) && '@' === $name[0]) {
            if (false === $pos = strpos($name, '/')) {
                $templateName = $page->getTemplate();
            } else {
                $package = substr($name, 1, $pos - 1);
                $templateName = substr($name, $pos + 1);
                //$name = ucfirst($package) . '\ServiceProvider';
                //$this->twig->getLoader()->prependPath(realpath($name::getViewsPath()), $package);
            }
        }

        $template = $this->template->setName($templateName);

        $template->addTitle($this->settings->getSite_title());
        $template->setTitle_direction($this->settings->getTitle_direction());
        $template->setTitle_separator($this->settings->getTitle_separator());
        $template->setKeyWords($this->settings->getSite_keywords());

        if ($page->getMtTitle()) {
            $template->addTitle($page->getMtTitle());
        } else {
            $template->addTitle($page->getTitle());
        }


        $template->setPageHeader($page->getTitle());
        $template->setPageUrl($page->getUrl());
        $description = !empty($page->getMtDescription()) ? $page->getMtDescription() : (($this->settings->getSite_description() ?? ''));
        $template->setDescription($description);


        if ($template->getName() > '') {
            $templatePath = sprintf('%s/templates/%s/', '@' . $package, $template->getName());
            $this->loadConfigFromJson($templatePath);
        } else {
            $templatePath = null;
        }


        if ($page->getContent() > '') {
            $template->setContent(
                $this->twig->createTemplate(
                    $page->getContent()
                )->render([])
            );

        } elseif ($page->getModule() > '' && $content = App::getModule($page->getModule())) {
            $template->setContent($content);
        }

        if ($page->getScript() > '') {
            $path = '@' . $package . '/pages/' . $page->getScript();
            //$data['content'] = $this->twig->render('@' . $package . '/pages/' . $page->getScript() );
        } else {
            $path = '@' . $package . ('/templates/' . $templateName . '/base.twig');
        }
        $data['content'] = $template->getContent();

        $data = array_merge($data, [
            'copyright' => $this->settings->getCopyright(),
            'title' => $template->getTitle(),
            'page_head' => $template->getPageHeader(),
            'meta' => $template->getMeta(),
            'template' => $templatePath,
            'js_path' => $this->getJsPath(),
            'description' => $template->getDescription(),
            'keywords' => !empty($page->getMtKeywords()) ? $page->getMtKeywords() : $template->getKeyWords(),
            'css_path' => $this->getcssPath(),
            'debug' => getEnv('MODE'),
            'top_counters' => $this->counters->getTop_counters(),
            'bottom_counters' => $this->counters->getBootom_counters(),
            'css_template_path' => $this->getCssTemplatePath($template->getName()),
            'js_template_path' => $this->getJsTemplatePath($template->getName())
        ]);

        return !$templatePath ? $data['content'] : $this->twig->render($path, $data);
    }

    /**
     * @param string $templatePath
     * @throws \Nette\Utils\JsonException
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     */
    protected function loadConfigFromJson(string $templatePath): void
    {

        $jsonPath = dirname($this->twig->load($templatePath . '/base.twig')->getSourceContext()->getPath()) . '/config.json';

        if (file_exists($jsonPath)) {

            /**
             * @todo перехватить в Exception
             */
            $data = Json::decode(file_get_contents($jsonPath), true);
            $this->template->addHeaderStyleLinks($data['header_styles'] ?? []);
            $this->template->addScriptLinks($data['footer_scripts'] ?? []);
            $this->template->addStyleLinks($data['footer_styles'] ?? []);
            $this->template->setAdminTemplate($data['admin'] ?? false);
        }

    }


    public function renderAdminPage(PageEntityInterface $page): string
    {
        $package = $subpackage = '';
        $rule1 = '|^\/' . trim(admin_path(), '/') . '\/(.*?)\/(.*?)\/(.*?)$|is';
        $rule2 = '|^\/' . trim(admin_path(), '/') . '\/(.*?)\/(.*?)$|is';

        if (preg_match($rule1, $page->getUrl(), $m)) {
            list(, $package, $subpackage, $adminPage) = $m;
            $adminPage .= '.twig';
            $page->setScript($subpackage . '/' . $adminPage);
        } elseif (preg_match($rule2, $page->getUrl(), $m)) {
            list(, $package, $adminPage) = $m;
            $adminPage .= '.twig';
            $page->setScript($adminPage);
        }

        if (!$package) {
            throw new PageNotFoundException($page->getUrl());
        }

        $templateName = $page->getTemplate();

        $template = $this->template->setName($templateName);

        $templatePath = sprintf('@diamond/templates/%s/', $template->getName());

        $this->loadConfigFromJson($templatePath);

        //     try {

        $data = [
            'title' => $page->getTitle(),
            'site_name' => $this->settings->getSite_name(),
            'copyright' => $this->settings->getCopyright(),
            'template' => $templatePath,
            'package' => $package,
            'subpackage' => $subpackage,
            'left_menu_collapsed' => $this->request->cookies->get('leftmenu') === 'collapsed',
            'js_path' => $this->getJsPath(),
            'css_path' => $this->getcssPath(),
            'debug' => getEnv('MODE'),
            'theme' => $this->settings->getColorTheme(),
            'css_template_path' => $this->getCssTemplatePath($template->getName()),
            'js_template_path' => $this->getJsTemplatePath($template->getName())
        ];

        $path = '@' . $package . '/admin/' . $page->getScript();

        if (!$this->twig->getLoader()->exists($path)) {
            throw new PageNotFoundException($page->getUrl());
        }

        return $this->twig->render($path, $data);
//        } catch (NotFoundException $e) {
//            throw new PageNotFoundException($page->getUrl());
//        }
    }

    /**
     * @param $template
     * @return string
     */
    protected function getCssTemplatePath($template): string
    {
        return sprintf('/assets/templates/%s/css/', $template);
    }

    /**
     * @param $template
     * @return string
     */
    protected function getJsTemplatePath($template): string
    {
        return sprintf('/assets/templates/%s/js/', $template);
    }

    /**
     * @return string
     */
    protected function getJsPath(): string
    {
        return '/assets/js/';
    }

    /**
     * @return string
     */
    protected function getCssPath(): string
    {
        return '/assets/css/';
    }

    /**
     * @param array $js
     * @param string|null $jsPath
     * @param string|null $jsTemplatePath
     * @return array
     */
    public function prepareTemplateScripts(array $js, string $jsPath = null, string $jsTemplatePath = null): array
    {
        $scripts = array_unique(array_merge($js, $this->template->getScripts()));
        $scriptsNew = [];
        $otherScripts = [];
        $files = '';
        foreach ($scripts as $file) {

            if (strpos($file, 'http') !== false) {
                $otherScripts [] = $file;
                continue;
            }

            $file = str_replace('{js_path}', $jsPath, $file);
            $file = str_replace('{js_template_path}', $jsTemplatePath, $file);
            $scriptsNew[] = $file;
            $file = str_replace('.js', '', $file);
            $files .= ',' . $file;

        }

        $siteRoot = BASEPATH . '/public_html/';

        if ($this->settings->getCompress_output() === 'Y' && $this->isAdmin() === false) {

            $key = md5($files);

            $minPath = 'cache/' . $key . '.min.js';

            $cacheJsPath = $siteRoot . $minPath;

            if (!file_exists($cacheJsPath)) {

                if (!is_dir($siteRoot . 'cache') && !mkdir($siteRoot . 'cache', 0755, true)) {
                    throw new \RuntimeException('cache dir not created /public_html/cache');
                }

                $filesAr = array_filter($scriptsNew, function ($el) {
                    return !empty($el);
                });

                $minifier = new \MatthiasMullie\Minify\JS();
                foreach ($filesAr as $file) {
                    if (str_contains($file, '/_diamond')) {
                        $file = str_replace('_diamond', 'diamond', $file);
                        $root = BASEPATH . '/vendor/';
                    } else {
                        $root = $siteRoot;
                    }
                    $file = ltrim($file, '/');

                    $file = preg_replace('~\.js.*?$~is', '.js', $file);
                    $minifier->add($root . $file);
                }

                $jsMin = $minifier->minify();
                file_put_contents($cacheJsPath, $jsMin);
            }

            $scriptsNew = ['/' . $minPath];
        }


        $scripts = array_merge( $scriptsNew, $otherScripts);
        $this->template->addScriptLinks($scripts);

        return $scripts;
    }

    /**
     * @param array $js
     * @param $jsPath
     * @param $jsTemplatePath
     * @return array
     */
    public function getTemplateScripts(array $js, $jsPath, $jsTemplatePath): array
    {
        return $this->prepareTemplateScripts($js, $jsPath, $jsTemplatePath);
    }

    /**
     *
     * @return bool
     */
    private function isAdmin(): bool
    {
        return str_contains($_SERVER['REQUEST_URI'], 'admin') || $this->template->isAdminTemplate() === true;
    }


    /**
     * @param array $css
     * @param string $cssPath
     * @param string $cssTemplatePath
     * @param string $jsPath
     * @return array
     */
    public function getTemplateStyles(array $css, string $cssPath = null, string $cssTemplatePath = null, string $jsPath = null): array
    {
        $scripts = array_unique(array_merge($css, $this->template->getStyles()));

        $scriptsNew = [];
        $files = '';

        foreach ($scripts as $file) {

            $file = str_replace('{css_path}', $cssPath, $file);
            $file = str_replace('{css_template_path}', $cssTemplatePath, $file);
            $file = str_replace('{js_path}', $jsPath, $file);
            $scriptsNew[] = $file;

            $file = str_replace('.css', '', $file);
            $files .= ',' . $file;
        }
        $siteRoot = BASEPATH . '/public_html/';

        if ($this->settings->getCompress_output() === 'Y' && $this->isAdmin() === false) {

            $key = md5(implode(',', $scripts));

            $minPath = 'cache/' . $key . '.min.css';

            $cachePath = $siteRoot . $minPath;

            if (!file_exists($cachePath)) {

                if (!is_dir($siteRoot . 'cache') && !mkdir($siteRoot . 'cache', 0755, true)) {
                    throw new \RuntimeException('cache dir not created /public_html/cache');
                }

                $filesAr = array_filter($scriptsNew, function ($el) {
                    return !empty($el);
                });

                $minifier = new \MatthiasMullie\Minify\CSS();
                foreach ($filesAr as $file) {
                    if (str_contains($file, '/_diamond')) {
                        $file = str_replace('_diamond', 'diamond', $file);
                        $root = BASEPATH . '/vendor/';
                    } else {
                        $root = $siteRoot;
                    }
                    $file = ltrim($file, '/');

                    $file = preg_replace('~\.css.*?$~is', '.css', $file);
                    $minifier->add($root . $file);
                }

                $jsMin = $minifier->minify();
                file_put_contents($cachePath, $jsMin);
            }

            $scriptsNew = ['/' . $minPath];

        }

        return $scriptsNew;
    }

    /**
     * @param $files
     * @param $type
     */
    public function compress($files, $type): void
    {
        $this->minify->compress($files, $type);
    }


    function __destruct()
    {
        if (static::$statEntity) {
            static::$statEntity->save();
        }

    }

}