<?php
namespace Teknisa\Libs\Events\PreDispatch\Listener;

use Zeedhi\Framework\DTO\Request;
use Zeedhi\Framework\Events\PreDispatch\Listener;
use Zeedhi\Framework\Session\Session;
use Teknisa\Libs\Service\Api;
use Teknisa\Libs\Util\OAuthCustom;
use Teknisa\Libs\Util\Utilities;

class OAuth2 extends Listener {
    const OAUTH_HEADER = "Authorization";
    const JWT_HEADER = "Zeedhi-JWT";

    /** @var OAuthCustom */
    private $oauth;
    /** @var Session */
    private $sessionManager;
    /** @var Api */
    private $apiService;

    /**
     * Constructor.
     *
     * @param OAuthCustom   $oauth          The
     * @param Session       $sessionManager The Session Manager.
     * @param Api           $apiService     The API Service.
     */
    public function __construct(OAuthCustom $oauth, Session $sessionManager, Api $apiService) {
        $this->oauth = $oauth;
        $this->sessionManager = $sessionManager;
        $this->apiService = $apiService;
        header('Content-Type: application/json');
    }

    /**
     * {@inheritDoc}
     */
    public function preDispatch(Request $request) {
        try {
            $this->apiService->setLastRequestTime($request->getRoutePath());

            $headers = getallheaders();
            $token = isset($headers[self::OAUTH_HEADER]) ? $headers[self::OAUTH_HEADER] : false;
            $jwt = isset($headers[self::JWT_HEADER]) ? $headers[self::JWT_HEADER] : false;

            if ($token && $jwt) {
                $this->checkAccessAndSetSession(str_replace('Bearer ', '', $token));
            } else {
                $this->checkPublicRoute($request);
            }
        } catch (\Exception $e) {
            $this->throwForbiddenException($e->getMessage());
        }
    }

    /**
     * Retrieves public routes.
     */
    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;
    }

    /**
     * @param string $token
     */
    private function checkAccessAndSetSession($token) {
        try {
            $access = $this->oauth->checkAccess($token, '', [], true);
            if($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());
                }
            } else {
                throw new \Exception('Not authenticated!');
            }
        } catch (\Exception $e) {
            $this->throwUnauthenticatedException($e->getMessage());
        }
    }

    /**
     * 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 compareRoutes(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) {
        $isPublic = $this->compareRoutes(
            $request->getRoutePath(),
            $this->readPublicRoutes()
        );

        if (!$isPublic) {
            $this->throwUnauthenticatedException("Access denied.");
        }
    }

    /**
     * @param string $message
     */
    private function throwForbiddenException($message) {
        header('HTTP/1.1 403 Forbidden');
        die($message);
    }

    /**
     * @param string $message
     */
    private function throwUnauthenticatedException($message) {
        header('WWW-Authenticate: Basic realm="Authentica via oauth2/token.", charset="UTF-8"');
        header('HTTP/1.1 401 Unauthorized');
        die($message);
    }
}