<?php

declare(strict_types=1);

namespace App\Application\Security\Guards;

use App\Application\Security\Events\Auth\{
    AccountLockedEvent,
    AuthenticationFailedEvent,
    AuthenticationSuccessEvent
};
use App\Domain\User\{User};
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Database\Example\UserRepository;

final class AuthenticationGuard
{
    private const MAX_FAILED_ATTEMPTS = 5;
    private const LOCKOUT_DURATION = 900; // 15 Minuten

    public function __construct(
        private EventDispatcher $eventDispatcher,
        private UserRepository $userRepository
    ) {
    }

    public function authenticate(string $email, string $password): ?User
    {
        try {
            $user = $this->userRepository->findByEmail($email);

            if (! $user) {
                $this->dispatchFailedAttempt($email, 'user_not_found');

                return null;
            }

            if ($this->isAccountLocked($user)) {
                $this->dispatchAccountLocked($email, 'too_many_failed_attempts', $user->failed_attempts);

                return null;
            }

            if (! $this->verifyPassword($password, $user->password_hash)) {
                $this->handleFailedAttempt($user);

                return null;
            }

            $this->handleSuccessfulLogin($user);

            return $user;

        } catch (\Exception $e) {
            $this->dispatchFailedAttempt($email, 'authentication_error');

            throw $e;
        }
    }

    public function logout(User $user): void
    {
        $sessionId = session_id();

        // Session invalidieren
        session_destroy();

        $this->eventDispatcher->dispatch(new \App\Application\Security\Events\Auth\SessionTerminatedEvent(
            email: $user->email ?? '',
            sessionId: $sessionId,
            reason: 'manual_logout'
        ));
    }

    private function handleSuccessfulLogin(User $user): void
    {
        // Failed attempts zurücksetzen - mit immutable object pattern
        $updatedUser = $user->withFailedAttemptsReset();
        $this->userRepository->save($updatedUser);

        // Session regenerieren
        session_regenerate_id(true);
        $_SESSION['user_id'] = $updatedUser->id;

        $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent(
            email: $updatedUser->email ?? '',
            sessionId: session_id(),
            method: 'password'
        ));
    }

    private function handleFailedAttempt(User $user): void
    {
        $updatedUser = $user->withIncrementedFailedAttempts();

        if ($updatedUser->failed_attempts >= self::MAX_FAILED_ATTEMPTS) {
            $lockedUser = $updatedUser->withLockout(
                new \DateTimeImmutable('+' . self::LOCKOUT_DURATION . ' seconds')
            );
            $this->userRepository->save($lockedUser);

            $this->dispatchAccountLocked($lockedUser->email ?? '', 'max_attempts_exceeded', $lockedUser->failed_attempts);
        } else {
            $this->userRepository->save($updatedUser);
            $this->dispatchFailedAttempt($updatedUser->email ?? '', 'invalid_password', $updatedUser->failed_attempts);
        }
    }

    private function dispatchFailedAttempt(string $email, string $reason, int $attempts = 1): void
    {
        $this->eventDispatcher->dispatch(new AuthenticationFailedEvent(
            email: $email,
            reason: $reason,
            failedAttempts: $attempts
        ));
    }

    private function dispatchAccountLocked(string $email, string $reason, int $attempts): void
    {
        $this->eventDispatcher->dispatch(new AccountLockedEvent(
            email: $email,
            reason: $reason,
            failedAttempts: $attempts
        ));
    }

    private function isAccountLocked(User $user): bool
    {
        return $user->locked_until && $user->locked_until > new \DateTimeImmutable();
    }

    private function verifyPassword(string $password, string $hash): bool
    {
        return password_verify($password, $hash);
    }
}
