<?php

declare(strict_types=1);

namespace App\Framework\Http\Session;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\Security\CsrfToken;
use App\Framework\Security\CsrfTokenData;
use App\Framework\Security\CsrfTokenGenerator;

final readonly class CsrfProtection
{
    private const int TOKEN_LIFETIME = 7200; // 2h - Better UX for long forms
    private const int MAX_TOKENS_PER_FORM = 3;
    private const int RE_SUBMIT_WINDOW = 30; // 30 seconds for re-submits
    private const int USED_TOKEN_CLEANUP_TIME = 300; // 5 minutes

    private Session $session;

    public function __construct(
        Session $session,
        private CsrfTokenGenerator $tokenGenerator,
        private Clock $clock
    ) {
        $this->session = $session;

        // Initialization happens via SessionData now, no need to set here
    }

    public function generateToken(string $formId): CsrfToken
    {
        $token = $this->tokenGenerator->generate();

        // Get current CSRF data from session
        $sessionData = $this->session->getSessionData();
        $csrfData = $sessionData->csrfData;

        // Get tokens for this form
        $formTokens = $csrfData->getFormData($formId);

        // Add new token
        $formTokens[] = CsrfTokenData::create($token, $this->clock)->toArray();

        // Cleanup old tokens
        $formTokens = $this->cleanupOldTokens($formTokens);

        // Update CSRF data
        $newCsrfData = $csrfData->withFormData($formId, $formTokens);

        // Update session with new CSRF data
        $this->session->updateSessionData(function ($data) use ($newCsrfData) {
            return $data->withCsrfData($newCsrfData);
        });

        return $token;
    }

    /**
     * Generates a fresh token and rotates the existing one for a form.
     * This should be called after successful form submission.
     */
    public function rotateToken(string $formId): CsrfToken
    {
        // Invalidate all existing tokens for this form
        $sessionData = $this->session->getSessionData();
        $csrfData = $sessionData->csrfData;

        // Clear tokens for this form
        $newCsrfData = $csrfData->withFormData($formId, []);

        // Update session
        $this->session->updateSessionData(function ($data) use ($newCsrfData) {
            return $data->withCsrfData($newCsrfData);
        });

        // Generate a fresh token
        return $this->generateToken($formId);
    }

    /**
     * Checks if a token exists and is valid (not expired)
     */
    public function isTokenValid(string $formId, CsrfToken $token): bool
    {
        $sessionData = $this->session->getSessionData();
        $csrfData = $sessionData->csrfData;
        $formTokens = $csrfData->getFormData($formId);

        if (empty($formTokens)) {
            return false;
        }

        $currentTime = $this->clock->time();

        foreach ($formTokens as $tokenData) {
            if ($token->equalsString($tokenData['token'])) {
                $age = $currentTime - $tokenData['created_at'];

                return $age < self::TOKEN_LIFETIME;
            }
        }

        return false;
    }

    /**
     * Get count of active tokens for a form
     */
    public function getActiveTokenCount(string $formId): int
    {
        $sessionData = $this->session->getSessionData();
        $csrfData = $sessionData->csrfData;

        return count($csrfData->getFormData($formId));
    }

    public function validateToken(string $formId, CsrfToken $token): bool
    {
        $sessionData = $this->session->getSessionData();
        $csrfData = $sessionData->csrfData;
        $formTokens = $csrfData->getFormData($formId);

        if (empty($formTokens)) {
            return false;
        }

        $reSubmitWindow = Duration::fromSeconds(self::RE_SUBMIT_WINDOW);

        foreach ($formTokens as $index => $tokenArray) {
            $tokenData = CsrfTokenData::fromArray($tokenArray, $this->clock);

            if ($tokenData->matches($token->toString())) {
                // Check if token was already used and if re-submit window has passed
                if ($tokenData->usedAt && ! $tokenData->wasUsedRecently($this->clock, $reSubmitWindow)) {
                    return false; // Token was used too long ago
                }

                // Mark token as used and save back to session
                $updatedTokenData = $tokenData->markAsUsed($this->clock);
                $formTokens[$index] = $updatedTokenData->toArray();

                // Update CSRF data with marked token
                $newCsrfData = $csrfData->withFormData($formId, $formTokens);

                // Update session
                $this->session->updateSessionData(function ($data) use ($newCsrfData) {
                    return $data->withCsrfData($newCsrfData);
                });

                return true;
            }
        }

        return false;
    }

    private function cleanupOldTokens(array $tokens): array
    {
        $lifetime = Duration::fromSeconds(self::TOKEN_LIFETIME);
        $usedWindow = Duration::fromSeconds(self::USED_TOKEN_CLEANUP_TIME);

        $cleaned = [];

        foreach ($tokens as $tokenArray) {
            $tokenData = CsrfTokenData::fromArray($tokenArray, $this->clock);

            // Keep tokens that are either not expired or were used recently
            if ($tokenData->shouldKeep($this->clock, $lifetime, $usedWindow)) {
                $cleaned[] = $tokenArray; // Keep original array format for session storage
            }
        }

        // Keep only the newest N tokens
        return array_slice($cleaned, -self::MAX_TOKENS_PER_FORM);
    }
}
