<?php

declare(strict_types=1);

namespace ApiModel\Service;

use ApiModel\Domain\Repository\PasswordResetRepositoryInterface;
use ApiModel\Domain\Repository\PlatformAuditRepositoryInterface;
use ApiModel\Domain\Repository\RefreshTokenRepositoryInterface;
use ApiModel\Domain\Repository\UserRepositoryInterface;
use ApiModel\Infrastructure\Persistence\Database;
use ApiModel\Infrastructure\Persistence\Repositories\PdoUserRepository;
use ApiModel\Infrastructure\Queue\AsyncJobQueue;
use ApiModel\Infrastructure\Queue\JobDispatcher;
use ApiModel\Infrastructure\Security\JwtSecretResolver;
use ApiModel\Shared\Dto\Auth\LoginInput;
use ApiModel\Shared\Exceptions\BadRequestException;
use ApiModel\Shared\Exceptions\UnauthorizedException;
use ApiModel\Infrastructure\Logging\StructuredLog;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;

final class AuthService
{
    public function __construct(
        private readonly UserRepositoryInterface $users,
        private readonly RefreshTokenRepositoryInterface $refreshTokens,
        private readonly PasswordResetRepositoryInterface $passwordReset,
        private readonly PlatformAuditRepositoryInterface $platformAudit,
        private readonly ?AsyncJobQueue $queue = null,
        private readonly ?JobDispatcher $dispatcher = null,
    ) {}

    /** @return array{access_token: string, refresh_token: string, user: array} */
    public function login(LoginInput $input): array
    {
        $email = strtolower(trim($input->email));
        $password = $input->password;
        $row = $this->users->findFullByEmail($email);
        $mock = ($_ENV['EXTERNAL_APP_MOCK'] ?? '') === 'true';
        $externalAppUrl = trim((string) ($_ENV['EXTERNAL_APP_API_URL'] ?? ''));

        if ($row && $row['role'] !== 'CLIENTE') {
            if (! Database::fromBool($row['is_active'])) {
                $this->logLoginFailed($email, $row['id'] ?? null, 'inactive_user');
                throw new UnauthorizedException('Credenciales inválidas');
            }
            if (! password_verify($password, (string) $row['password_hash'])) {
                $this->logLoginFailed($email, $row['id'] ?? null, 'invalid_password');
                throw new UnauthorizedException('Credenciales inválidas');
            }

            return $this->finalizeLogin($row);
        }

        if ($row && $row['role'] === 'CLIENTE') {
            if (! $externalAppUrl && ! $mock) {
                $this->logLoginFailed($email, $row['id'] ?? null, 'policy_not_configured');
                throw new UnauthorizedException('Credenciales inválidas');
            }
            if ($mock) {
                if (! Database::fromBool($row['is_active'])) {
                    $this->logLoginFailed($email, $row['id'] ?? null, 'inactive_client');
                    throw new UnauthorizedException('Credenciales inválidas');
                }

                return $this->finalizeLogin($row);
            }
            $ext = $this->validateExternalApp($externalAppUrl, $email, $password);
            if (! $ext) {
                $this->logLoginFailed($email, $row['id'] ?? null, 'policy_validation_failed');
                throw new UnauthorizedException('Credenciales inválidas');
            }
            $this->users->updateUserAfterExternalSync($row['id'], $ext['name'], $ext['client_id']);
            $row = $this->users->findFullByEmail($email);
            if (! $row) {
                $this->logLoginFailed($email, null, 'user_missing_after_sync');
                throw new UnauthorizedException('Credenciales inválidas');
            }

            return $this->finalizeLogin($row);
        }

        if ($externalAppUrl && ! $mock) {
            $ext = $this->validateExternalApp($externalAppUrl, $email, $password);
            if (! $ext) {
                $this->logLoginFailed($email, null, 'policy_validation_failed');
                throw new UnauthorizedException('Credenciales inválidas');
            }
            $id = Database::uuid();
            $this->users->insertClientUser($id, $ext['name'], $email, $ext['client_id'], 'es');

            return $this->finalizeLogin([
                'id' => $id,
                'name' => $ext['name'],
                'email' => $email,
                'role' => 'CLIENTE',
                'language' => 'es',
            ]);
        }

        $this->logLoginFailed($email, null, 'user_not_found');
        throw new UnauthorizedException('Credenciales inválidas');
    }

    private function logLoginFailed(string $email, mixed $userId, string $reason): void
    {
        StructuredLog::emit('warning', 'auth.login_failed', [
            'email' => $email,
            'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
            'user_id' => is_string($userId) ? $userId : null,
            'reason' => $reason,
        ]);

        $this->logAuthEvent(
            'USER_LOGIN_FAILED',
            is_string($userId) ? $userId : null,
            is_string($userId) ? $userId : null,
            [
                'email' => $email,
                'reason' => $reason,
                'request' => '/auth/login',
                'message' => 'Intento de inicio de sesión fallido',
            ],
        );
    }

    /** @param array<string, mixed> $user */
    private function finalizeLogin(array $user): array
    {
        $session = $this->issueSession($user);

        $this->logAuthEvent('USER_LOGIN', (string) $user['id'], (string) $user['id'], [
            'email' => $user['email'] ?? null,
            'role' => $user['role'] ?? null,
            'request' => '/auth/login',
            'message' => 'Inicio de sesión exitoso',
        ]);

        return $session;
    }

    /** @return null|array{client_id: string, name: string} */
    private function validateExternalApp(string $baseUrl, string $email, string $password): ?array
    {
        $url = rtrim($baseUrl, '/') . '/auth/validate';
        $payload = json_encode(['email' => $email, 'password' => $password]);
        $ctx = stream_context_create([
            'http' => [
                'method' => 'POST',
                'header' => "Content-Type: application/json\r\nContent-Length: " . strlen((string) $payload),
                'content' => $payload,
                'timeout' => 8,
            ],
        ]);
        $res = @file_get_contents($url, false, $ctx);
        if ($res === false) {
            return null;
        }
        $j = json_decode($res, true);
        if (! is_array($j) || empty($j['valid']) || empty($j['client_id']) || empty($j['name'])) {
            return null;
        }

        return ['client_id' => (string) $j['client_id'], 'name' => (string) $j['name']];
    }

    /** @param array<string, mixed> $user */
    private function issueSession(array $user): array
    {
        $access = $this->signAccess($user);
        $plain = rtrim(strtr(base64_encode(random_bytes(48)), '+/', '-_'), '=');
        $hash = hash('sha256', $plain);
        $days = (int) ($_ENV['JWT_REFRESH_EXPIRES_DAYS'] ?? 7);
        $exp = (new \DateTimeImmutable())->modify("+{$days} days")->format('Y-m-d H:i:s');
        $this->refreshTokens->insert($user['id'], $hash, $exp);

        return [
            'access_token' => $access,
            'refresh_token' => $plain,
            'user' => [
                'id' => $user['id'],
                'name' => $user['name'],
                'email' => $user['email'],
                'role' => $user['role'],
                'language' => $user['language'] ?? 'es',
                'photo' => PdoUserRepository::formatPhotoForClient($user['photo'] ?? null),
            ],
        ];
    }

    /** @param array<string, mixed> $user */
    private function signAccess(array $user): string
    {
        $secret = JwtSecretResolver::signingSecret();
        $ttl = $_ENV['JWT_EXPIRES_IN'] ?? '15m';
        $expSec = 900;
        if (preg_match('/^(\d+)m$/', $ttl, $m)) {
            $expSec = (int) $m[1] * 60;
        } elseif (preg_match('/^(\d+)h$/', $ttl, $m)) {
            $expSec = (int) $m[1] * 3600;
        }

        $now = time();
        $kid = trim((string) ($_ENV['JWT_KID'] ?? ''));

        return JWT::encode([
            'sub' => $user['id'],
            'role' => $user['role'],
            'lang' => $user['language'] ?? 'es',
            'iat' => $now,
            'exp' => $now + $expSec,
        ], $secret, 'HS256', $kid !== '' ? $kid : null);
    }

    /** @return array{access_token: string, refresh_token: string, user: array} */
    public function refresh(string $refreshPlain): array
    {
        $hash = hash('sha256', $refreshPlain);
        $row = $this->refreshTokens->findValidSessionByTokenHash($hash);
        if (! $row || ! Database::fromBool($row['is_active'])) {
            throw new UnauthorizedException('Sesión expirada');
        }
        if (strtotime((string) $row['expires_at']) <= time()) {
            throw new UnauthorizedException('Sesión expirada');
        }

        $this->refreshTokens->revokeByTokenId((string) $row['id']);

        return $this->issueSession([
            'id' => $row['uid'],
            'name' => $row['name'],
            'email' => $row['email'],
            'role' => $row['role'],
            'language' => $row['language'],
        ]);
    }

    public function logout(string $userId, string $refreshPlain): void
    {
        $hash = hash('sha256', $refreshPlain);
        $this->refreshTokens->revokeByUserIdAndTokenHash($userId, $hash);

        $user = $this->users->findFullById($userId);
        $this->logAuthEvent('USER_LOGOUT', $userId, $userId, [
            'email' => is_array($user) ? ($user['email'] ?? null) : null,
            'role' => is_array($user) ? ($user['role'] ?? null) : null,
            'request' => '/auth/logout',
            'message' => 'Cierre de sesión',
        ]);
    }

    /** @return array{ok: true} */
    public function forgotPassword(string $emailRaw, string $lang = 'es'): array
    {
        $email = strtolower(trim($emailRaw));
        $this->passwordReset->deleteAllForEmail($email);
        $user = $this->users->findFullByEmail($email);
        if ($user && Database::fromBool($user['is_active'])) {
            $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
            $secret = $_ENV['PASSWORD_RESET_CODE_SECRET'] ?? $_ENV['JWT_SECRET'] ?? '';
            $codeHash = hash('sha256', $email . ':' . $code . ':' . $secret);
            $ttl = (int) ($_ENV['PASSWORD_RESET_CODE_TTL_MIN'] ?? 15);
            $exp = (new \DateTimeImmutable())->modify("+{$ttl} minutes")->format('Y-m-d H:i:s');
            $this->passwordReset->insert($email, $codeHash, $exp);
            error_log("[api] Código recuperación para {$email}: {$code} (solo desarrollo)");

            if ($this->dispatcher) {
                $this->dispatcher->dispatch('auth.password_reset_email', [
                    'email' => $email,
                    'code' => $code,
                    'lang' => $lang,
                ]);
            } else {
                $this->queue?->enqueue('auth.password_reset_email', [
                    'email' => $email,
                    'code' => $code,
                    'lang' => $lang,
                ], 5);
            }

            $this->logPlatformEvent('PASSWORD_RESET_REQUESTED', null, $user['id'], json_encode(['email' => $email]));
        }

        return ['ok' => true];
    }

    public function resetPassword(string $emailRaw, string $code, string $newPassword): void
    {
        $email = strtolower(trim($emailRaw));
        $secret = $_ENV['PASSWORD_RESET_CODE_SECRET'] ?? $_ENV['JWT_SECRET'] ?? '';
        $hash = hash('sha256', $email . ':' . trim($code) . ':' . $secret);
        $row = $this->passwordReset->findLatestValid($email, $hash);
        $now = time();
        if (! $row || strtotime((string) $row['expires_at']) <= $now) {
            throw new BadRequestException('Código inválido o expirado');
        }
        $user = $this->users->findFullByEmail($email);
        if (! $user || ! Database::fromBool($user['is_active'])) {
            throw new BadRequestException('Código inválido o expirado');
        }
        $ph = password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => 10]);
        $this->users->updatePasswordHash($user['id'], $ph);
        $this->passwordReset->markConsumedById((string) $row['id']);
        $this->refreshTokens->revokeAllForUser($user['id']);
        $this->logPlatformEvent('PASSWORD_RESET_COMPLETED', null, $user['id'], json_encode(['email' => $email]));
    }

    /** @param array<string, mixed> $meta */
    private function logAuthEvent(string $action, ?string $actorId, ?string $targetId, array $meta): void
    {
        $payload = array_merge($meta, [
            'method' => $_SERVER['REQUEST_METHOD'] ?? null,
            'class' => 'auth',
            'agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
            'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
        ]);

        $this->logPlatformEvent(
            $action,
            $actorId,
            $targetId,
            json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
        );
    }

    private function logPlatformEvent(string $type, ?string $actor, ?string $target, string $meta): void
    {
        try {
            $this->platformAudit->log($type, $actor, $target, $meta);
        } catch (\Throwable) {
            // ignorar
        }
    }

    /** @return array{userId: string, role: string, language: string} */
    public static function verifyBearer(string $token): array
    {
        foreach (JwtSecretResolver::verificationSecrets() as $secret) {
            try {
                $decoded = JWT::decode($token, new Key($secret, 'HS256'));
            } catch (ExpiredException) {
                throw new UnauthorizedException('Sesión expirada');
            } catch (SignatureInvalidException) {
                continue;
            }
            $a = (array) $decoded;
            if (empty($a['sub']) || empty($a['role'])) {
                throw new UnauthorizedException('Token inválido');
            }

            return [
                'userId' => (string) $a['sub'],
                'role' => (string) $a['role'],
                'language' => (string) ($a['lang'] ?? 'es'),
            ];
        }

        throw new UnauthorizedException('Token inválido');
    }
}

