<?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 ungültige Anmeldedaten
 *
 * Verwendet OWASP-konforme Nachrichten für Authentifizierungsfehler
 */
final class InvalidCredentialsException extends FrameworkException
{
    /**
     * @param string $identifier Benutzer-Identifikator (E-Mail, Username, etc.)
     * @param string $reason Grund für den Fehler (invalid_credentials, password_expired, etc.)
     * @param int $attemptCount Anzahl der Fehlversuche
     * @param \Throwable|null $previous Vorherige Ausnahme
     */
    public function __construct(
        public readonly string $identifier,
        public readonly string $reason = 'invalid_credentials',
        public readonly int $attemptCount = 1,
        ?\Throwable $previous = null
    ) {
        // OWASP-konforme Nachricht mit Platzhaltern
        $message = "User {$this->identifier} authentication failure";

        $context = ExceptionContext::forOperation('authentication.login', 'Auth')
            ->withData([
                'user_identifier' => $this->identifier,
                'failure_reason' => $this->reason,
                'attempt_count' => $this->attemptCount,
                'event_identifier' => "authn_login_fail:{$this->identifier}",
                'category' => 'authentication',
                'requires_alert' => $this->attemptCount >= 3, // Alert ab 3 Versuchen
            ])
            ->withMetadata([
                'security_event' => true,
                'owasp_compliant' => true,
                'log_level' => 'WARN',
            ]);

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

    // === Factory Methods für verschiedene Authentifizierungsfehler ===

    public static function invalidPassword(string $identifier, int $attemptCount = 1): self
    {
        return new self($identifier, 'invalid_password', $attemptCount);
    }

    public static function invalidUsername(string $identifier, int $attemptCount = 1): self
    {
        return new self($identifier, 'invalid_username', $attemptCount);
    }

    public static function passwordExpired(string $identifier): self
    {
        return new self($identifier, 'password_expired', 1);
    }

    public static function twoFactorRequired(string $identifier): self
    {
        return new self($identifier, 'two_factor_required', 1);
    }

    public static function accountNotVerified(string $identifier): self
    {
        return new self($identifier, 'account_not_verified', 1);
    }

    /**
     * Gibt OWASP-konforme Event-Daten zurück
     */
    public function getSecurityEventData(): array
    {
        return [
            'event_identifier' => "authn_login_fail:{$this->identifier}",
            'description' => "User {$this->identifier} login failure",
            'category' => 'authentication',
            'log_level' => 'WARN',
            'requires_alert' => $this->attemptCount >= 3,
            'user_identifier' => $this->identifier,
            'failure_reason' => $this->reason,
            'attempt_count' => $this->attemptCount,
        ];
    }

    /**
     * Prüft ob Alert erforderlich ist (basierend auf Anzahl Versuche)
     */
    public function requiresAlert(): bool
    {
        return $this->attemptCount >= 3;
    }

    /**
     * Gibt sicheren Identifier für Logs zurück (ohne sensible Daten)
     */
    public function getSafeIdentifier(): string
    {
        // Wenn E-Mail, zeige nur ersten Teil
        if (str_contains($this->identifier, '@')) {
            $parts = explode('@', $this->identifier);

            return substr($parts[0], 0, 2) . '***@' . $parts[1];
        }

        // Bei anderen Identifiern zeige nur ersten Teil
        return substr($this->identifier, 0, 3) . '***';
    }
}
