<?php

declare(strict_types=1);

namespace App\Framework\Exception\Authentication;

use App\Framework\Exception\Core\AuthErrorCode;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;

/**
 * Ausnahme für abgelaufene Authentication-Token
 *
 * Verwendet OWASP-konforme Nachrichten für Token-Expiration
 */
final class TokenExpiredException extends FrameworkException
{
    /**
     * @param string $tokenType Art des Tokens (session, jwt, api_key, etc.)
     * @param string $identifier Token-Identifikator oder Benutzer-ID
     * @param \DateTimeImmutable $expiredAt Zeitpunkt der Ablaufzeit
     * @param \Throwable|null $previous Vorherige Ausnahme
     */
    public function __construct(
        public readonly string $tokenType,
        public readonly string $identifier,
        public readonly \DateTimeImmutable $expiredAt,
        ?\Throwable $previous = null
    ) {
        // OWASP-konforme Nachricht mit Platzhaltern
        $message = "User {$this->identifier} {$this->tokenType} token expired";

        $context = ExceptionContext::forOperation('authentication.token_validation', 'Auth')
            ->withData([
                'token_type' => $this->tokenType,
                'user_identifier' => $this->identifier,
                'expired_at' => $this->expiredAt->format('Y-m-d H:i:s'),
                'expired_since_seconds' => $this->getExpiredSinceSeconds(),
                'event_identifier' => "authn_token_expired:{$this->identifier},{$this->tokenType}",
                'category' => 'authentication',
                'requires_alert' => false, // Token-Ablauf ist normal
            ])
            ->withMetadata([
                'security_event' => true,
                'owasp_compliant' => true,
                'log_level' => 'INFO',
                'token_expired' => true,
            ]);

        parent::__construct(
            message: $message,
            context: $context,
            code: 401, // Unauthorized
            previous: $previous,
            errorCode: AuthErrorCode::TOKEN_EXPIRED
        );
    }

    // === Factory Methods für verschiedene Token-Typen ===

    public static function sessionToken(string $sessionId, \DateTimeImmutable $expiredAt): self
    {
        return new self('session', $sessionId, $expiredAt);
    }

    public static function jwtToken(string $userId, \DateTimeImmutable $expiredAt): self
    {
        return new self('jwt', $userId, $expiredAt);
    }

    public static function apiKey(string $keyId, \DateTimeImmutable $expiredAt): self
    {
        return new self('api_key', $keyId, $expiredAt);
    }

    public static function refreshToken(string $userId, \DateTimeImmutable $expiredAt): self
    {
        return new self('refresh_token', $userId, $expiredAt);
    }

    public static function passwordResetToken(string $userId, \DateTimeImmutable $expiredAt): self
    {
        return new self('password_reset', $userId, $expiredAt);
    }

    public static function twoFactorToken(string $userId, \DateTimeImmutable $expiredAt): self
    {
        return new self('two_factor', $userId, $expiredAt);
    }

    /**
     * Gibt OWASP-konforme Event-Daten zurück
     */
    public function getSecurityEventData(): array
    {
        return [
            'event_identifier' => "authn_token_expired:{$this->identifier},{$this->tokenType}",
            'description' => "User {$this->identifier} {$this->tokenType} token expired",
            'category' => 'authentication',
            'log_level' => 'INFO',
            'requires_alert' => false,
            'token_type' => $this->tokenType,
            'user_identifier' => $this->identifier,
            'expired_at' => $this->expiredAt->format('Y-m-d H:i:s'),
            'expired_since_seconds' => $this->getExpiredSinceSeconds(),
        ];
    }

    /**
     * Berechnet wie lange der Token schon abgelaufen ist (in Sekunden)
     */
    public function getExpiredSinceSeconds(): int
    {
        $now = new \DateTimeImmutable();

        return max(0, $now->getTimestamp() - $this->expiredAt->getTimestamp());
    }

    /**
     * Prüft ob der Token kürzlich abgelaufen ist (< 5 Minuten)
     */
    public function isRecentlyExpired(): bool
    {
        return $this->getExpiredSinceSeconds() < 300; // 5 Minuten
    }

    /**
     * Prüft ob der Token schon länger abgelaufen ist (> 1 Stunde)
     */
    public function isLongExpired(): bool
    {
        return $this->getExpiredSinceSeconds() > 3600; // 1 Stunde
    }

    /**
     * Gibt eine benutzerfreundliche Nachricht über die Ablaufzeit zurück
     */
    public function getExpirationMessage(): string
    {
        $expiredSince = $this->getExpiredSinceSeconds();

        if ($expiredSince < 60) {
            return "Token expired {$expiredSince} seconds ago";
        } elseif ($expiredSince < 3600) {
            $minutes = floor($expiredSince / 60);

            return "Token expired {$minutes} minutes ago";
        } else {
            $hours = floor($expiredSince / 3600);

            return "Token expired {$hours} hours ago";
        }
    }

    /**
     * Token-Ablauf erfordert normalerweise keine Alerts
     */
    public function requiresAlert(): bool
    {
        return false;
    }
}
