<?php

namespace Teknisa\Libs\Util;

use Zeedhi\Framework\DTO\Request;
use Zeedhi\Framework\Events\PreDispatch\Listener;
use Zeedhi\Framework\HTTP\Kernel;
use Zeedhi\Framework\Session\Session;
use Teknisa\Libs\Service\Api;
use Teknisa\Libs\Exception\Login as LoginException;
use Teknisa\Libs\Exception\Token as TokenException;


class RequestListenerOAuth extends Listener {

    const OAUTH_HEADER = "OAuth-Token";
    const OAUTH_HASH = "OAuth-Hash";
    const OAUTH_PROJECT = "OAuth-Project";
    const OAUTH_SECRET_ID = "OAuth-SecretId";
    const OAUTH_KEEP_CONNECTED = "OAuth-KeepConnected";
    const KEEP_CONNECTED_POSITIVE = "Yes";

    private $httpKernel;
    private $oauth;
    private $sessionManager;
    private $apiService;
    private $concurrentAccessIgnoredRoutes = array('/lib_startSession', '/lib_validateExpiredSession', '/findUserData',
        '/login', '/requestNewPassword', '/updatePassword', '/productLanguages', '/testLicenseConnection', '/setLogAccess/save',
        '/findPrivacyPolicyByAuthentication', '/lib_logout', '/acl_zhu_acl_rules', '/lib_findFavorites', '/validateRedirectionUrl',
        '/getParamLoginGates');

    public function __construct(Kernel $kernel, OAuthCustom $oauth, Session $sessionManager, Api $apiService) {
        $this->httpKernel = $kernel;
        $this->oauth = $oauth;
        $this->sessionManager = $sessionManager;
        $this->apiService = $apiService;
    }

    private function readPublicRoutes() {
        $urls = file_get_contents(__DIR__ . "/../../../../config/publicRoutes.json");
        $publicRoutes = json_decode($urls, true);
        $productUrls = @file_get_contents(Utilities::getProductBasePath(true) . "/backend/config/publicRoutes.json");
        if(!empty($productUrls)) {
            $publicRoutes = array_merge($publicRoutes, json_decode($productUrls, true));
        }
        return $publicRoutes;
    }

    public function preDispatch(Request $request)
    {
        try {
            $this->apiService->setLastRequestTime($request->getRoutePath());
            $token   = $this->httpKernel->getHttpRequest()->getHeaders()->get(self::OAUTH_HEADER);
            $hash    = $this->httpKernel->getHttpRequest()->getHeaders()->get(self::OAUTH_HASH);
            $project = $this->httpKernel->getHttpRequest()->getHeaders()->get(self::OAUTH_PROJECT);

            if (!empty($token)) {
                $this->checkAccessAndSetSession($token, $request->getRoutePath());
            } else if(!empty($hash) && empty($token) && !in_array($request->getRoutePath(), $this->concurrentAccessIgnoredRoutes)){
                TokenException::invalidToken($token);
            }
            $user = $this->sessionManager->get("USER_ID");
            $isSupportOperator = $this->sessionManager->get('IS_SUPPORT_OPERATOR');

            if (!empty($hash) && !empty($user) && !in_array($request->getRoutePath(), $this->concurrentAccessIgnoredRoutes) && !$isSupportOperator) {
                $this->checkConcurrentAccess($hash, $user, $project);
            }

            // validation support operator
            if ($isSupportOperator && !in_array($request->getRoutePath(), $this->concurrentAccessIgnoredRoutes)) {
                $supportOperatorExpirationDate = $this->sessionManager->get('SUPPORT_OPERATOR_EXPIRATION_DATE');
                $datetime = new \DateTime();
                $supportOperatorExpirationDate = $datetime::createFromFormat('d/m/Y H:i:s', $supportOperatorExpirationDate);
                if($datetime > $supportOperatorExpirationDate){
                    TokenException::expiredTokenSupportOperator();
                }
            }

            if (!$isSupportOperator && !in_array($request->getRoutePath(), $this->concurrentAccessIgnoredRoutes)) {
                $this->checkAccessHours();
            }

            $this->checkPublicRoute($request);
        } catch (\Exception $e){
            throw new \Exception($e->getMessage(), $e->getCode());
        }
    }

    private function checkAccessHours() {
        if($this->sessionManager->has("ACCESS_HOURS_INI")) {
            $weekDays = $this->sessionManager->get("ACCESS_HOURS_DAYS");
            if(!in_array(date('w'), $weekDays)) {
                LoginException::accessBlockedByTimeDay();
            }
            
            $timeIni = strtotime($this->sessionManager->get("ACCESS_HOURS_INI"));
            $timeEnd = strtotime($this->sessionManager->get("ACCESS_HOURS_END"));
            $actualTime = time();
            if($actualTime < $timeIni || $actualTime > $timeEnd) {
                LoginException::accessBlockedByTimeDay();
            }
        }
    }

    /**
     * @param $hash
     * @param $project
     * @param $user
     */

    private function checkConcurrentAccess($hash, $user, $project) {
        $organizationId = $this->sessionManager->get("ORGANIZATION_ID");
        if(!empty($organizationId) && $user != "000000000997") {
            if(Utilities::getConcurrentAccessSaveInMongoParameter()) {
                $criteria = array("nrProdutoId" => intval($project), "cdOperador" => $user, "nrOrg" => $organizationId);
                $userAccessHash = InstanceProvider::getMongoCache()->findAccess($criteria);
                $userAccessHash = isset($userAccessHash['dsChaveUltAcesso']) ? $userAccessHash['dsChaveUltAcesso'] : null;
            } else {
                $userAccessHash = InstanceProvider::getLibUserDataRepository()->getOperatorHash($user, $project, $organizationId);
                $userAccessHash = isset($userAccessHash['DSCHAVEULTACESSO']) ? $userAccessHash['DSCHAVEULTACESSO'] : null;
            }
            if(!empty($userAccessHash) && $userAccessHash != $hash) {
                LoginException::sessionWillBeEnded();
            }
        }
    }

    /**
     * @param $token
     */
    private function checkAccessAndSetSession($token, $route) {
        $secretId = $this->httpKernel->getHttpRequest()->getHeaders()->get(self::OAUTH_SECRET_ID);
        $keepConn = $this->httpKernel->getHttpRequest()->getHeaders()->get(self::OAUTH_KEEP_CONNECTED);
        $keepConn = $keepConn == self::KEEP_CONNECTED_POSITIVE || $route == '/lib_getModulesVersion' ? true : false;
        $access   = $this->oauth->checkAccess($token, $secretId, array(), $keepConn);

        if (!empty($access)) {
            if($this->sessionManager->getId() != $access['sessionId']) {
                $this->sessionManager->start();
                $this->sessionManager->destroy();
                $this->sessionManager->setId($access['sessionId']);
            }
            $this->sessionManager->start();

            if(!$this->sessionManager->has("NRORG")) {
                $this->sessionManager->set('NRORG', $access['service']->getNrOrg());
                $this->sessionManager->set('CDOPERADOR', $access['service']->getCdOperator());
            }

            if(!$this->sessionManager->has("ORGANIZATION_ID")) {
                $this->sessionManager->set('ORGANIZATION_ID', $access['service']->getNrOrg());
                $this->sessionManager->set('USER_ID', $access['service']->getCdOperator());
            }
        }
    }

    /**
     * Returns true if the route is matched, false otherwise.
     * 
     * @param string $route         The route to match.
     * @param array  $publicRoutes  The public routes array.
     */
    private function routeMatch(string $route, array $publicRoutes) {
        foreach ($publicRoutes as $pattern) {
            $subPatterns = [];
            $matched = preg_match_all('/#[^#]+#/', $pattern, $subPatterns);
            if($matched) {
                $pattern = preg_replace('/#[^#]+#/', '@', $pattern);
                $pattern = str_replace('/', '\/', $pattern);
                foreach ($subPatterns[0] as $subPattern) {
                    $pattern = preg_replace('/\@/', substr($subPattern, 1, -1), $pattern, 1);
                }
                if (preg_match('/'.$pattern.'/', $route)) return true;
            } else {
                if ($route === $pattern) return true; 
            }
        }
        return false;
    }

    /**
     * @param Request $request
     * @throws \Exception
     */
    private function checkPublicRoute(Request $request) {
        $doesMatch = $this->routeMatch($request->getRoutePath(), $this->readPublicRoutes());
        if (!$doesMatch && !$this->sessionManager->has("NRORG")) {
            throw new \Exception("Access denied.", 500);
        } else if($doesMatch && Utilities::useRateLimit()) {
            $this->validateRateLimit($request->getRoutePath());
        }
    }

    private function validateRateLimit($route) {
        $rateLimitConfig = [
            'limit' => Utilities::getQuantLimit(), // Número máximo de requisições
            'window' => Utilities::getSecRateLimit(), // Janela de tempo em segundos
        ];
        $collection = InstanceProvider::getMongoDB()->getCollection('rate_limit');
        $userKey =  $_SERVER['HTTP_CLIENT_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ;
        $productId = Utilities::getProjectIdParameter();
        if ($this->isRateLimited($userKey, $productId, $rateLimitConfig, $collection,$route)) {
            throw new \Exception("Too Many Requests. IP: $userKey | Route: $route", 429);
        }
    }

    private function isRateLimited($userKey, $productId, $config, $collection,$route)
    {
        $currentTime = time(); // Timestamp atual
        $windowStart = $currentTime - $config['window']; // Início da janela

        // Realizar uma operação atômica para verificar e limitar requisições
        $result = $collection->findOneAndUpdate(
            [
                'key' => $userKey,
                'product_id' => $productId,
                'route' => $route,
            ],
            [
                '$push' => [
                    'requests' => [
                        '$each' => [$currentTime], // Adiciona o timestamp atual
                        '$slice' => -$config['limit'], // Mantém apenas os últimos 'limit' timestamps
                    ],
                ],
            ],
            [
                'upsert' => true, // Cria o documento se não existir
                'returnDocument' => \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_BEFORE,
            ]
        );
        $requests = isset($result['requests']) ? (array)$result['requests'] : [];
        // Verificar o número de requisições na janela
        if ($result) {
            $requests = isset($result['requests']) ? array_filter($requests, function ($timestamp) use ($windowStart,$currentTime) {
                return $timestamp >= $windowStart && $timestamp <= $currentTime; // Apenas dentro da janela
            }) : [];
        } else {
            $requests = [];
        }

        if (count($requests) >= $config['limit']) {
            return true; // Bloqueado
        }

        return false; // Não bloqueado
    }

}