<?php

declare(strict_types=1);

namespace App\Framework\Auth;

use App\Framework\Core\ValueObjects\Hash;
use App\Framework\Http\IpAddress;
use App\Framework\Http\Session\SessionId;
use App\Framework\Http\Session\SessionIdGenerator;
use InvalidArgumentException;
use SensitiveParameter;

/**
 * Authentication Service
 *
 * Provides user authentication, session management, and security features
 * including rate limiting, account lockout, and brute force protection.
 */
final readonly class AuthenticationService
{
    // Rate limiting constants
    private const int MAX_LOGIN_ATTEMPTS = 5;
    private const int LOCKOUT_DURATION = 900; // 15 minutes
    private const int RATE_LIMIT_WINDOW = 300; // 5 minutes

    // Session security
    private const int SESSION_TIMEOUT = 3600; // 1 hour
    private const int REMEMBER_TOKEN_LENGTH = 32;

    public function __construct(
        private PasswordHasher $passwordHasher,
        private SessionIdGenerator $sessionIdGenerator,
        private AuthenticationRepository $repository,
        private ?RateLimitService $rateLimiter = null
    ) {
    }

    /**
     * Authenticate user with credentials
     */
    public function authenticate(
        string $identifier,
        #[SensitiveParameter]
        string $password,
        ?IpAddress $ipAddress = null,
        bool $remember = false
    ): AuthenticationResult {
        // Rate limiting check
        if ($this->rateLimiter && $ipAddress) {
            if ($this->rateLimiter->isRateLimited($ipAddress, 'login')) {
                return AuthenticationResult::rateLimited(
                    $this->rateLimiter->getRetryAfter($ipAddress, 'login')
                );
            }
        }

        // Find user by identifier (email, username, etc.)
        $user = $this->repository->findUserByIdentifier($identifier);
        if (! $user) {
            $this->recordFailedAttempt($identifier, $ipAddress, 'user_not_found');

            return AuthenticationResult::failed('Invalid credentials');
        }

        // Check account lockout
        if ($this->isAccountLocked($user->getId())) {
            return AuthenticationResult::accountLocked(
                $this->getAccountLockoutExpiresAt($user->getId())
            );
        }

        // Verify password
        if (! $this->passwordHasher->verify($password, $user->getHashedPassword())) {
            $this->recordFailedAttempt($identifier, $ipAddress, 'invalid_password');
            $this->incrementFailedLoginAttempts($user->getId());

            return AuthenticationResult::failed('Invalid credentials');
        }

        // Check if password needs rehashing
        if ($this->passwordHasher->needsRehash($user->getHashedPassword())) {
            $newHash = $this->passwordHasher->hash($password);
            $this->repository->updateUserPassword($user->getId(), $newHash);
        }

        // Clear failed login attempts on successful authentication
        $this->clearFailedLoginAttempts($user->getId());

        // Create session
        $sessionId = $this->sessionIdGenerator->generate();
        $session = $this->createSession($sessionId, $user->getId(), $ipAddress);

        // Create remember token if requested
        $rememberToken = null;
        if ($remember) {
            $rememberToken = $this->createRememberToken($user->getId());
        }

        return AuthenticationResult::success(
            $user,
            $session,
            $rememberToken
        );
    }

    /**
     * Authenticate with session
     */
    public function authenticateWithSession(
        SessionId $sessionId,
        ?IpAddress $ipAddress = null
    ): AuthenticationResult {
        $session = $this->repository->findSessionById($sessionId);
        if (! $session) {
            return AuthenticationResult::failed('Session not found');
        }

        // Check session expiration
        if ($this->isSessionExpired($session)) {
            $this->repository->deleteSession($sessionId);

            return AuthenticationResult::sessionExpired();
        }

        // Check IP address consistency (optional security feature)
        if ($ipAddress && $session->getIpAddress() && ! $session->getIpAddress()->equals($ipAddress)) {
            // Log suspicious activity but don't automatically fail
            // This could be legitimate (mobile network changes, VPN, etc.)
            $this->recordSecurityEvent('ip_address_changed', [
                'session_id' => $sessionId->toString(),
                'original_ip' => (string) $session->getIpAddress(),
                'new_ip' => (string) $ipAddress,
            ]);
        }

        // Load user
        $user = $this->repository->findUserById($session->getUserId());
        if (! $user) {
            $this->repository->deleteSession($sessionId);

            return AuthenticationResult::failed('User not found');
        }

        // Update session activity
        $this->repository->updateSessionActivity($sessionId, $ipAddress);

        return AuthenticationResult::success($user, $session);
    }

    /**
     * Authenticate with remember token
     */
    public function authenticateWithRememberToken(
        string $tokenValue,
        ?IpAddress $ipAddress = null
    ): AuthenticationResult {
        $tokenHash = $this->hashToken($tokenValue);
        $rememberToken = $this->repository->findRememberToken($tokenHash);

        if (! $rememberToken) {
            return AuthenticationResult::failed('Invalid remember token');
        }

        // Check token expiration
        if ($this->isRememberTokenExpired($rememberToken)) {
            $this->repository->deleteRememberToken($tokenHash);

            return AuthenticationResult::tokenExpired();
        }

        // Load user
        $user = $this->repository->findUserById($rememberToken->getUserId());
        if (! $user) {
            $this->repository->deleteRememberToken($tokenHash);

            return AuthenticationResult::failed('User not found');
        }

        // Create new session
        $sessionId = $this->sessionIdGenerator->generate();
        $session = $this->createSession($sessionId, $user->getId(), $ipAddress);

        // Rotate remember token for security
        $this->repository->deleteRememberToken($tokenHash);
        $newRememberToken = $this->createRememberToken($user->getId());

        return AuthenticationResult::success($user, $session, $newRememberToken);
    }

    /**
     * Logout user and clean up session
     */
    public function logout(SessionId $sessionId): bool
    {
        return $this->repository->deleteSession($sessionId);
    }

    /**
     * Logout from all sessions (global logout)
     */
    public function logoutAll(string $userId): bool
    {
        $this->repository->deleteAllUserSessions($userId);
        $this->repository->deleteAllUserRememberTokens($userId);

        return true;
    }

    /**
     * Change user password
     */
    public function changePassword(
        string $userId,
        #[SensitiveParameter]
        string $currentPassword,
        #[SensitiveParameter]
        string $newPassword
    ): PasswordChangeResult {
        $user = $this->repository->findUserById($userId);
        if (! $user) {
            return PasswordChangeResult::failed('User not found');
        }

        // Verify current password
        if (! $this->passwordHasher->verify($currentPassword, $user->getHashedPassword())) {
            return PasswordChangeResult::failed('Current password is incorrect');
        }

        // Validate new password strength
        $validation = $this->passwordHasher->validatePasswordStrength($newPassword);
        if (! $validation->isValid) {
            return PasswordChangeResult::validationFailed($validation);
        }

        // Hash new password
        $newHashedPassword = $this->passwordHasher->hash($newPassword);

        // Update password in repository
        $success = $this->repository->updateUserPassword($userId, $newHashedPassword);
        if (! $success) {
            return PasswordChangeResult::failed('Failed to update password');
        }

        // Invalidate all sessions except current (optional security measure)
        // $this->repository->deleteAllUserSessionsExcept($userId, $currentSessionId);

        return PasswordChangeResult::success();
    }

    /**
     * Hash token using secure hash algorithm
     */
    private function hashToken(string $token): Hash
    {
        return Hash::sha256($token);
    }

    /**
     * Create new authentication session
     */
    private function createSession(SessionId $sessionId, string $userId, ?IpAddress $ipAddress): AuthenticationSession
    {
        $expiresAt = new \DateTimeImmutable('+' . self::SESSION_TIMEOUT . ' seconds');

        $session = new AuthenticationSession(
            id: $sessionId,
            userId: $userId,
            ipAddress: $ipAddress,
            createdAt: new \DateTimeImmutable(),
            expiresAt: $expiresAt,
            lastActivity: new \DateTimeImmutable()
        );

        $this->repository->storeSession($session);

        return $session;
    }

    /**
     * Create remember token
     */
    private function createRememberToken(string $userId): RememberToken
    {
        $tokenValue = bin2hex(random_bytes(self::REMEMBER_TOKEN_LENGTH));
        $tokenHash = $this->hashToken($tokenValue);
        $expiresAt = new \DateTimeImmutable('+30 days');

        $rememberToken = new RememberToken(
            hash: $tokenHash,
            userId: $userId,
            createdAt: new \DateTimeImmutable(),
            expiresAt: $expiresAt
        );

        $this->repository->storeRememberToken($rememberToken);

        return $rememberToken->withPlainTextValue($tokenValue);
    }

    /**
     * Check if session is expired
     */
    private function isSessionExpired(AuthenticationSession $session): bool
    {
        return $session->getExpiresAt() < new \DateTimeImmutable();
    }

    /**
     * Check if remember token is expired
     */
    private function isRememberTokenExpired(RememberToken $token): bool
    {
        return $token->getExpiresAt() < new \DateTimeImmutable();
    }

    /**
     * Check if account is locked due to failed attempts
     */
    private function isAccountLocked(string $userId): bool
    {
        $attempts = $this->repository->getFailedLoginAttempts($userId);

        if ($attempts < self::MAX_LOGIN_ATTEMPTS) {
            return false;
        }

        $lastAttempt = $this->repository->getLastFailedAttemptTime($userId);
        if (! $lastAttempt) {
            return false;
        }

        $lockoutExpires = $lastAttempt->modify('+' . self::LOCKOUT_DURATION . ' seconds');

        return new \DateTimeImmutable() < $lockoutExpires;
    }

    /**
     * Get account lockout expiration time
     */
    private function getAccountLockoutExpiresAt(string $userId): ?\DateTimeImmutable
    {
        $lastAttempt = $this->repository->getLastFailedAttemptTime($userId);
        if (! $lastAttempt) {
            return null;
        }

        return $lastAttempt->modify('+' . self::LOCKOUT_DURATION . ' seconds');
    }

    /**
     * Increment failed login attempts
     */
    private function incrementFailedLoginAttempts(string $userId): void
    {
        $this->repository->incrementFailedLoginAttempts($userId);
    }

    /**
     * Clear failed login attempts after successful authentication
     */
    private function clearFailedLoginAttempts(string $userId): void
    {
        $this->repository->clearFailedLoginAttempts($userId);
    }

    /**
     * Record failed authentication attempt for monitoring
     */
    private function recordFailedAttempt(
        string $identifier,
        ?IpAddress $ipAddress,
        string $reason
    ): void {
        $this->recordSecurityEvent('authentication_failed', [
            'identifier' => $identifier,
            'ip_address' => $ipAddress ? (string) $ipAddress : null,
            'reason' => $reason,
            'timestamp' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
        ]);
    }

    /**
     * Record security event for monitoring and auditing
     * @param array<string, mixed> $data
     */
    private function recordSecurityEvent(string $event, array $data): void
    {
        // Implementation would depend on your logging/monitoring system
        // This could log to database, send to SIEM, trigger alerts, etc.
        error_log(sprintf(
            'SECURITY_EVENT: %s - %s',
            $event,
            json_encode($data, JSON_THROW_ON_ERROR)
        ));
    }

    /**
     * Validate authentication request
     */
    private function validateAuthenticationRequest(string $identifier, string $password): void
    {
        if (empty($identifier)) {
            throw new InvalidArgumentException('Identifier cannot be empty');
        }

        if (empty($password)) {
            throw new InvalidArgumentException('Password cannot be empty');
        }

        if (mb_strlen($identifier) > 255) {
            throw new InvalidArgumentException('Identifier too long');
        }

        if (mb_strlen($password) > PasswordHasher::MAX_PASSWORD_LENGTH) {
            throw new InvalidArgumentException('Password too long');
        }
    }
}
