<?php

namespace PrestaShop\Module\LCVPrestaConnector\Controller;
 
use PrestaShop\Module\LCVPrestaConnector\OidcAuthenticator;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

/**
 * Contrôleur admin du flow SSO OIDC (backdoor équipe technique).
 *
 * Anonyme (cf. _anonymous_controller: true dans routes.yml et
 * isAuthenticationRequired() ci-dessous) car il doit fonctionner avant
 * qu'une session admin soit établie — c'est précisément le but du SSO.
 *
 * Le paramètre 'step' aiguille vers l'une des deux phases du flow :
 *   - step=login    (défaut) : démarre le flow, redirige vers Authentik
 *   - step=callback          : reçoit le code Authentik, valide et ouvre la session
 *
 * On regroupe les deux étapes dans un seul contrôleur car :
 *   - elles partagent toute leur infrastructure (instance OidcAuthenticator,
 *     vérification du toggle, gestion d'erreur, logging) ;
 *   - cela limite la surface exposée à une seule URL publique ;
 *   - cela simplifie la configuration côté Authentik (un seul redirect_uri).
 *
 * La protection est assurée par OidcAuthenticator (état CSRF en session,
 * validation crypto de l'id_token au callback, whitelist stricte du domaine
 * email).
 */
class OidcAdminControllerV8 extends AdminController
{
    public function __construct(        
        private readonly CsrfTokenManagerInterface $csrfTokenManager,
        private readonly TokenStorageInterface    $tokenStorage,
    ) 
    {
        parent::__construct();
    }

    /**
     * Pas d'authentification admin requise — le but du SSO est précisément
     * de pouvoir s'authentifier sans avoir de session.
     */
    protected function isAuthenticationRequired(): bool
    {
        return false;
    }

    /**
     * Point d'entrée du login SSO OIDC (redirection vers Authentik puis callback).
     * 
     * @return Response
     */
    public function oidcAction(Request $request) : Response
    {
        try
        {
            $auth = new OidcAuthenticator($this->module);

            if (!$auth->isEnabled())
                return new Response('SSO désactivé.', 403);

            $step = $request->query->get('step', 'login');

            switch ($step)
            {
                case 'login':
                    return $this->doLogin($auth);

                case 'callback':
                    return $this->doCallback($auth);

                default:
                    return new Response('Étape SSO inconnue.', 400);
            }
        }
        catch (\Throwable $e)
        {
            \PrestaShopLogger::addLog(
                'LCVPrestaConnector SSO : erreur fatale dans le contrôleur : ' . $e->getMessage(), 3
            );

            return new Response(
                '<!doctype html><html><head><meta charset="utf-8"><title>Erreur SSO</title></head><body>'
                . '<h1>Erreur lors du SSO</h1>'
                . '<p>' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8') . '</p>'
                . '</body></html>',
                500,
                ['Content-Type' => 'text/html; charset=utf-8']
            );
        }
    }

    /**
     * Étape 1 : démarre le flow OIDC, redirige vers Authentik.
     * Ne retourne pas une vraie Response — la lib jumbojett appelle exit()
     * après avoir envoyé un header Location:.
     */
    private function doLogin(OidcAuthenticator $auth) : Response
    {
        $auth->startLoginFlow();

        // Inatteignable en théorie
        return new Response('Redirection vers Authentik...', 302);
    }

    /**
     * Étape 2 : reçoit le retour Authentik (code + state), valide tout,
     * ouvre/promeut le compte admin local et établit la session admin.
     */
    private function doCallback(OidcAuthenticator $auth) : Response
    {
        try
        {
            // Validation complète : state, code, id_token, JWKS, email_verified, domaine
            $userInfo = $auth->handleCallback();

            // Get-or-create + promote du compte admin local
            $employee = $auth->getOrCreateAdminEmployee();

            // On vide le cookie legacy avant d'établir la session Symfony.
            // Sans ça, un vieux cookie de session précédente est encore présent
            // et LegacyRouterChecker::isLoggedBack() le lit, puis appelle
            // getDefaultShopID() sur un employee dont associated_shops est vide
            // → Warning: Undefined array key 0 → crash.
            \Context::getContext()->cookie->logout();

            // Pose du cookie admin (équivalent processLogin / authenticateEmployee
            // d'AdminLoginController), avec expiration absolue à +1h.
            $url = $this->openAdminSession($employee, $userInfo['username']);            
            
            // Redirection vers le tableau de bord du module      
            return new Response(
                '<!doctype html><html><head><meta charset="utf-8">'
                . '<meta http-equiv="refresh" content="0;url=' . htmlspecialchars($url, ENT_QUOTES) . '">'
                . '<title>Connexion en cours...</title></head><body>'
                . '<p>Connexion réussie, redirection vers le backoffice...</p>'
                . '<p><a href="' . htmlspecialchars($url, ENT_QUOTES) . '">Cliquer ici si la redirection ne se fait pas.</a></p>'
                . '</body></html>',
                200,
                ['Content-Type' => 'text/html; charset=utf-8']
            );
        }
        catch (\Throwable $e)
        {
            \PrestaShopLogger::addLog(
                'LCVPrestaConnector SSO : échec callback : ' . $e->getMessage(), 3
            );

            return new Response(
                '<!doctype html><html><head><meta charset="utf-8"><title>Connexion SSO refusée</title></head><body>'
                . '<h1>Connexion SSO refusée</h1>'
                . '<p>' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8') . '</p>'
                . '<p><a href="javascript:history.back()">Retour</a></p>'
                . '</body></html>',
                403,
                ['Content-Type' => 'text/html; charset=utf-8']
            );
        }
    }

    /**
     * Établit la session admin Prestashop pour cet employee via Symfony Security.
     *
     * PS 9 : dispatch AuthenticationTokenCreatedEvent → EmployeeSessionSubscriber
     *        crée l'EmployeeSession en base et l'attache au token, exactement
     *        comme le fait FormLoginAuthenticator lors d'un login normal.
     *
     * PS 8 : Adapter\Security\Admin::onKernelRequest() lit le cookie legacy
     *        (isLoggedBack) et crée le token Symfony lui-même — il suffit de
     *        poser correctement le cookie.
     *
     * @throws \RuntimeException si l'employee n'est pas rattaché à une boutique
     */
    private function openAdminSession(\Employee $employee, string $authenticatedUserName): string
    {
        $associatedShops = $employee->getAssociatedShops();
        if (empty($associatedShops) && !$employee->isSuperAdmin())
            throw new \RuntimeException('Cet employé n\'est plus rattaché à aucune boutique.');
 
        \PrestaShopLogger::addLog(
            sprintf('%sLCVPrestaConnector SSO : connexion depuis %s (via SSO %s, user=%s)',
                $this->module->getBridge()->getId(),
                \Tools::getRemoteAddr(), 
                $this->module->getBridge()->getId(),
                $authenticatedUserName),
            1, null, '', 0, true, (int) $employee->id
        );      
            
        // PS 8 : on se contente de poser le cookie legacy, Admin::onKernelRequest()
        // s'occupe de créer le token Symfony à partir du cookie.
        $employee->remote_addr = (int) ip2long(\Tools::getRemoteAddr());

        $context = \Context::getContext();
        $context->employee = $employee;        
        
        $cookie = $context->cookie;        
        $cookie->id_employee   = (int) $employee->id;
        $cookie->email         = $employee->email;
        $cookie->profile       = (int) $employee->id_profile;
        $cookie->passwd        = $employee->passwd;
        $cookie->remote_addr   = $context->employee->remote_addr;
        $cookie->last_activity = time();

        // isSessionAlive() vérifie session_id + session_token en PS8 —
        // exactement comme AdminLoginController::processLogin() le fait.
        $session = new \EmployeeSession();
        $cookie->registerSession($session);        
        
        // SessionRenewer::renew() régénère le PHPSESSID et vide les
        // tokens CSRF — protection contre la session fixation
        // (CVE-2022-24895), exactement comme processLogin() le fait.
        $this->module->get(\PrestaShopBundle\Security\Admin\SessionRenewer::class)->renew();

        $cookie->write();            
        
        // On redirige sur la route actuelle (prendre l'URL actuelle et la nettoyer après le premier répertoire qui commence par"admin", 
        // exemple : /ps/admin1234343/index.php/some/route => /ps/admin1234343)
        $url = preg_replace('/^(.*\/admin[^\/]+).*$/', '$1', $_SERVER['REQUEST_URI']);

        return $url;
    }
}
