<?php

namespace Actigraph\ActipageBaseBundle\Services\Account;

use App\Entity\LocalAccount;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class SsoAccount
{
    private const SESSION_STATE_KEY = 'sso_oidc_state';
    private const SESSION_VERIFIER_KEY = 'sso_oidc_code_verifier';
    private const SESSION_NONCE_KEY = 'sso_oidc_nonce';
    private const SESSION_REQUESTS_KEY = 'sso_oidc_requests';
    private const MAX_PENDING_REQUESTS = 10;
    private const DEFAULT_SCOPES = 'openid email';

    private $type;
    private $em;
    private $error;
    private $session;
    private $params;
    private $httpClient;
    private $router;

    public function __construct(
        EntityManagerInterface $em,
        SessionInterface $session,
        HttpClientInterface $httpClient,
        \Symfony\Component\Routing\RouterInterface $router,
        $params
    ) {
        $this->type = 'api';
        $this->em = $em;
        $this->error = null;
        $this->session = $session;
        $this->httpClient = $httpClient;
        $this->router = $router;
        $this->params = $params;
    }

    public function getType()
    {
        return $this->type;
    }

    public function checkLogin(array $data)
    {
        $this->error = 'Connexion locale indisponible. Utilisez la connexion SSO.';

        return false;
    }

    public function getError()
    {
        return $this->error;
    }

    public function login($user)
    {
        if (!$user instanceof LocalAccount) {
            return false;
        }

        $this->session->set('accountId', $user->getId());
        $user->setLastLoginAt(new \DateTime());
        $this->em->flush();

        return true;
    }

    public function getUser()
    {
        if ($this->session->get('accountId') == null) {
            return false;
        }

        return $this->em->getRepository(LocalAccount::class)->find($this->session->get('accountId'));
    }

    public function logout()
    {
        $this->session->set('accountId', null);
        $this->session->remove('loginReferer');
        $this->session->remove(self::SESSION_STATE_KEY);
        $this->session->remove(self::SESSION_VERIFIER_KEY);
        $this->session->remove(self::SESSION_NONCE_KEY);
        $this->session->remove(self::SESSION_REQUESTS_KEY);

        return true;
    }

    public function initiateLogin(Request $request): RedirectResponse
    {
        try {
            $config = $this->getSsoConfig();
            $metadata = $this->fetchMetadata($config['discovery_url']);

            $authorizationEndpoint = (string) ($metadata['authorization_endpoint'] ?? '');
            if ($authorizationEndpoint === '') {
                throw new UnprocessableEntityHttpException('OIDC discovery metadata is missing authorization_endpoint.');
            }

            $state = $this->randomBase64Url(32);
            $nonce = $this->randomBase64Url(32);
            $codeVerifier = $this->randomBase64Url(64);
            $codeChallenge = $this->base64UrlEncode(hash('sha256', $codeVerifier, true));

            // Legacy keys kept for backward compatibility with older sessions.
            $this->session->set(self::SESSION_STATE_KEY, $state);
            $this->session->set(self::SESSION_NONCE_KEY, $nonce);
            $this->session->set(self::SESSION_VERIFIER_KEY, $codeVerifier);

            $requests = $this->getPendingOidcRequests();
            $requests[$state] = [
                'verifier' => $codeVerifier,
                'nonce' => $nonce,
                'created_at' => time(),
            ];
            if (\count($requests) > self::MAX_PENDING_REQUESTS) {
                uasort($requests, static function (array $a, array $b): int {
                    return ((int) ($a['created_at'] ?? 0)) <=> ((int) ($b['created_at'] ?? 0));
                });
                $requests = \array_slice($requests, -self::MAX_PENDING_REQUESTS, null, true);
            }
            $this->session->set(self::SESSION_REQUESTS_KEY, $requests);

            $query = [
                'response_type' => 'code',
                'client_id' => $config['client_id'],
                'redirect_uri' => $config['redirect_uri'],
                'scope' => $config['scopes'],
                'state' => $state,
                'nonce' => $nonce,
                'code_challenge' => $codeChallenge,
                'code_challenge_method' => 'S256',
            ];

            return new RedirectResponse($authorizationEndpoint.'?'.http_build_query($query, '', '&', PHP_QUERY_RFC3986));
        } catch (\Throwable $e) {
            $this->resetSessionAfterFailure();
            throw $e;
        }
    }

    public function handleCallback(Request $request): LocalAccount
    {
        try {
            $state = (string) $request->query->get('state', '');
            $code = (string) $request->query->get('code', '');
            $error = (string) $request->query->get('error', '');

            if ($error !== '') {
                throw new UnauthorizedHttpException('OIDC', sprintf('OIDC login failed (%s).', $error));
            }

            if ($state === '' || $code === '') {
                throw new BadRequestHttpException('Missing OIDC callback parameters.');
            }

            $requests = $this->getPendingOidcRequests();
            $requestData = $requests[$state] ?? null;
            if (!is_array($requestData)) {
                // Legacy fallback for sessions created before multi-state support.
                $expectedState = (string) $this->session->get(self::SESSION_STATE_KEY, '');
                $legacyVerifier = (string) $this->session->get(self::SESSION_VERIFIER_KEY, '');
                if ($expectedState === '' || !hash_equals($expectedState, $state)) {
                    throw new BadRequestHttpException('Invalid OIDC state.');
                }
                if ($legacyVerifier === '') {
                    throw new BadRequestHttpException('Missing PKCE verifier in session.');
                }
                $codeVerifier = $legacyVerifier;
            } else {
                $codeVerifier = (string) ($requestData['verifier'] ?? '');
                if ($codeVerifier === '') {
                    throw new BadRequestHttpException('Missing PKCE verifier in session.');
                }
                unset($requests[$state]);
                $this->session->set(self::SESSION_REQUESTS_KEY, $requests);
            }

            $config = $this->getSsoConfig();
            $metadata = $this->fetchMetadata($config['discovery_url']);
            $tokenEndpoint = (string) ($metadata['token_endpoint'] ?? '');
            $userinfoEndpoint = (string) ($metadata['userinfo_endpoint'] ?? '');

            if ($tokenEndpoint === '') {
                throw new UnprocessableEntityHttpException('OIDC discovery metadata is missing token_endpoint.');
            }

            $tokenResponse = $this->httpClient->request('POST', $tokenEndpoint, [
                'body' => [
                    'grant_type' => 'authorization_code',
                    'code' => $code,
                    'redirect_uri' => $config['redirect_uri'],
                    'client_id' => $config['client_id'],
                    'code_verifier' => $codeVerifier,
                    'client_secret' => $config['client_secret'],
                ],
            ]);
            $tokenPayload = $tokenResponse->toArray(false);

            $accessToken = (string) ($tokenPayload['access_token'] ?? '');
            $idToken = (string) ($tokenPayload['id_token'] ?? '');

            if ($accessToken === '') {
                throw new UnauthorizedHttpException('OIDC', 'OIDC token response does not contain an access_token.');
            }

            $claims = [];
            if ($userinfoEndpoint !== '') {
                $userInfoResponse = $this->httpClient->request('GET', $userinfoEndpoint, [
                    'headers' => [
                        'Authorization' => 'Bearer '.$accessToken,
                    ],
                ]);
                $claims = $userInfoResponse->toArray(false);
            } elseif ($idToken !== '') {
                $claims = $this->decodeJwtPayload($idToken);
            }

            $email = strtolower(trim((string) ($claims['email'] ?? '')));
            if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
                throw new UnprocessableEntityHttpException('OIDC profile does not contain a valid email claim.');
            }

            $user = $this->em->getRepository(LocalAccount::class)->findOneBy(['email' => $email]);
            if (!$user instanceof LocalAccount) {
                $user = new LocalAccount();
                $user
                    ->setEmail($email)
                    ->setPassword(bin2hex(random_bytes(32)))
                    ->setIsEnable(true)
                    ->setActiveToken('')
                    ->setFirstname((string) ($claims['given_name'] ?? $claims['preferred_username'] ?? ''))
                    ->setLastname((string) ($claims['family_name'] ?? ''))
                ;
                $this->em->persist($user);
            }

            $this->session->remove(self::SESSION_STATE_KEY);
            $this->session->remove(self::SESSION_VERIFIER_KEY);
            $this->session->remove(self::SESSION_NONCE_KEY);
            $this->session->remove(self::SESSION_REQUESTS_KEY);

            $this->login($user);

            return $user;
        } catch (\Throwable $e) {
            $this->resetSessionAfterFailure();
            throw $e;
        }
    }

    private function getSsoConfig(): array
    {
        $account = (array) ($this->params['account'] ?? []);
        $sso = (array) ($account['sso'] ?? []);

        $discoveryUrl = trim((string) ($sso['discovery_url'] ?? ''));
        $clientId = trim((string) ($sso['client_id'] ?? ''));
        $clientSecret = (string) ($sso['client_secret'] ?? '');
        $scopes = trim((string) ($sso['scopes'] ?? self::DEFAULT_SCOPES));
        $redirectUri = trim((string) ($sso['redirect_uri'] ?? ''));

        if ($redirectUri === '') {
            $redirectUri = $this->router->generate('auth_sso_callback', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
        }

        if ($discoveryUrl === '' || $clientId === '') {
            throw new \InvalidArgumentException('Missing OIDC SSO configuration (discovery_url/client_id).');
        }

        return [
            'discovery_url' => $discoveryUrl,
            'client_id' => $clientId,
            'client_secret' => $clientSecret,
            'scopes' => $scopes,
            'redirect_uri' => $redirectUri,
        ];
    }

    private function fetchMetadata(string $discoveryUrl): array
    {
        $response = $this->httpClient->request('GET', $discoveryUrl);
        $metadata = $response->toArray(false);
        if (!is_array($metadata)) {
            throw new UnprocessableEntityHttpException('OIDC discovery metadata must be a JSON object.');
        }

        return $metadata;
    }

    private function getPendingOidcRequests(): array
    {
        $requests = $this->session->get(self::SESSION_REQUESTS_KEY, []);
        if (!is_array($requests)) {
            return [];
        }

        $filtered = [];
        foreach ($requests as $state => $data) {
            if (!is_string($state) || $state === '' || !is_array($data)) {
                continue;
            }
            $verifier = (string) ($data['verifier'] ?? '');
            if ($verifier === '') {
                continue;
            }
            $filtered[$state] = $data;
        }

        return $filtered;
    }

    private function resetSessionAfterFailure(): void
    {
        try {
            $this->session->invalidate();
            return;
        } catch (\Throwable $e) {
            // Fallback for non-standard session handlers.
        }

        $this->session->clear();
    }

    private function randomBase64Url(int $bytesLength): string
    {
        return $this->base64UrlEncode(random_bytes($bytesLength));
    }

    private function base64UrlEncode(string $raw): string
    {
        return rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
    }

    private function decodeJwtPayload(string $jwt): array
    {
        $parts = explode('.', $jwt);
        if (count($parts) < 2) {
            return [];
        }

        $payload = $parts[1];
        $payload .= str_repeat('=', (4 - (strlen($payload) % 4)) % 4);
        $decoded = base64_decode(strtr($payload, '-_', '+/'), true);
        if ($decoded === false) {
            return [];
        }

        $data = json_decode($decoded, true);
        return is_array($data) ? $data : [];
    }
}
