<?php
namespace WorkbloomERP\Module\v0\Auth\Middlewares;

use WorkbloomERP\Utils\JwtUtil;
use KrothiumAPI\Utils\HttpUtil;
use WorkbloomERP\Utils\CacheUtil;
use WorkbloomERP\Utils\CypherUtil;
use WorkbloomERP\Exceptions\AppException;
use WorkbloomERP\Module\v0\Auth\Factories\ServiceFactory;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioSessionModel;

class AuthMiddleware {
    /**
     * Valida se o token JWT da requisição atual corresponde a um estado de "Login Inicial".
     *
     * Este método atua como um guardião de segurança (Guard) para rotas que exigem 
     * ações obrigatórias do usuário logo após o primeiro acesso ao sistema, como a 
     * troca obrigatória de senha ou a configuração de MFA (Autenticação de Dois Fatores).
     *
     * ---
     * ## Fluxo de Execução
     * 1. **Extração:** Obtém o token JWT do cabeçalho `Authorization: Bearer`.
     * 2. **Validação:** Verifica a integridade e assinatura do token através do `JwtUtil`.
     * 3. **Verificação de Claim:** Decodifica o token e confirma se a claim `initial_login` está presente e definida como `true`.
     * 4. **Retorno:**
     *    - Retorna `true` se o token for válido e corresponder ao estado de login inicial.
     *    - Retorna um `array` estruturado contendo detalhes do erro caso a validação falhe.
     *
     * ---
     * ## Observações Técnicas
     * - O método utiliza variáveis de ambiente (`$_ENV`) para configurar o `JwtUtil`, garantindo que as configurações de `secretKey`, `algorithm` e `ttl` estejam sincronizadas com a política de segurança da aplicação.
     * - Este método captura internamente instâncias de `AppException`, tratando o fluxo de erro de forma silenciosa para o chamador, retornando um status de erro estruturado em vez de propagar a exceção.
     *
     * @return true|array{status: string, response_code: int, message: string} 
     *         Retorna `true` em caso de sucesso. Retorna um array com o erro caso o token seja inválido, ausente ou não possua a claim necessária.
     */
    public static function isInitialLogin(): true|array {
        try {
            // Valida o token JWT presente no header Authorization (Bearer Token)
            
            $bearerToken = HttpUtil::getBearerToken();
            if (!$bearerToken) {
                throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
            }
                
            $jwtUtil = new JwtUtil(
                secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
                ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
            );
            $decodedToken = $jwtUtil->validate(token: $bearerToken);
            if (!$decodedToken) {
                throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
            }

            $decodedToken = json_decode(json: json_encode($decodedToken, flags: JSON_THROW_ON_ERROR), associative: true, flags: JSON_THROW_ON_ERROR);
            $userData = $decodedToken['user_data'] ?? null;
            if (!$userData) {
                throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
            }

            if (!isset($userData['initial_login']) || $userData['initial_login'] !== true) {
                throw new AppException(message: 'Token de acesso não corresponde a um login inicial.', code: 403);
            }

            return true;
        } catch(AppException $e) {
            return [
                'status' => 'error',
                'response_code' => $e->getCode() ?? 500,
                'message' => $e->getMessage()
            ];
        }
    }

    /**
     * Middleware de autenticação para validar sessões e tokens JWT em requisições protegidas.
     *
     * Este método atua como um "Gatekeeper" (Guard). Ele implementa uma estratégia híbrida
     * de validação (Cache-First, seguida de DB e JWT) para equilibrar performance com 
     * segurança. Ele verifica a existência do token, a validade da assinatura JWT, 
     * a revogação da sessão no banco de dados e a integridade do dispositivo (IP/User-Agent).
     *
     * ---
     * ## Fluxo de Execução
     * 1. **Extração:** Obtém o token do header `Authorization`. Retorna erro 401 se ausente.
     * 2. **Cache-First Check:** Verifica no Redis (`login:main:{token}`). Se encontrado, autoriza imediatamente (retorna `null`), evitando carga no banco de dados.
     * 3. **Validação de Persistência:** Consulta o `UsuarioSessionRepo` (via hash do token). Valida revogação (`revoked_at`) e integridade.
     * 4. **Validação JWT:** Decodifica e verifica a assinatura/expiração (`exp`) do JWT.
     * 5. **Validação de Integridade (Guard):** Invoca `validateSessionData` para garantir que o IP e User-Agent atuais coincidem com a sessão original.
     * 6. **Caching:** Se tudo estiver correto, armazena o payload no cache para acelerar futuras requisições e retorna `null` (acesso concedido).
     *
     * ---
     * ## Arquitetura de Validação
     * 
     *
     * ---
     * ## Observações Técnicas
     * - **Interface de Retorno:** 
     *   - `null`: A autenticação foi bem-sucedida; o fluxo de execução pode continuar.
     *   - `array`: A autenticação falhou; o array contém a resposta de erro estruturada para o cliente.
     * - **Segurança:** Utiliza `CypherUtil` para garantir que tokens sensíveis não sejam comparados em texto puro contra o banco de dados.
     *
     * @return array|null Retorna `null` se o usuário estiver autenticado e autorizado. Retorna um `array` com `response_code`, `message` e `output` em caso de erro.
     * @see JwtUtil::validate() Para detalhes sobre a validação da assinatura criptográfica.
     * @see self::validateSessionData() Para a verificação de segurança de IP e User-Agent.
     */
    public static function handle(): ?array {
        try {
            // Valida o token JWT presente no header Authorization (Bearer Token)
            $bearerToken = HttpUtil::getBearerToken();
            if (!$bearerToken) {
                throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
            }

            $jwtUtil = new JwtUtil(
                secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
                ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
            );

            // Valida o token JWT e decodifica seu payload para extrair os dados do usuário e as informações de sessão.
            $decodedToken = $jwtUtil->validate(token: $bearerToken);
            if (!$decodedToken) {
                throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
            }

            // Verifica se o token corresponde a um login inicial, bloqueando o acesso a rotas protegidas caso seja verdadeiro.
            $decodedToken = json_decode(json: json_encode($decodedToken, flags: JSON_THROW_ON_ERROR), associative: true, flags: JSON_THROW_ON_ERROR);
            if ($decodedToken['user_data']['initial_login'] == true) {
                throw new AppException(message: 'Acesso negado. O token corresponde a um login inicial e não pode ser usado para acessar esta rota.', code: 403);
            }

            // Verifica se a sessão principal já está em cache para evitar consultas desnecessárias ao banco de dados e validações repetitivas. 
            // Se a sessão estiver em cache, considera o token como válido e retorna null para permitir o acesso à rota protegida.
            if (CacheUtil::get(key: "login:main:{$bearerToken}")) {
                return null;
            }

            $usuarioSessionRepo = ServiceFactory::makeUsuarioSessionRepo();

            // Busca a sessão do usuário no banco de dados utilizando o hash do token para garantir que o token é válido e não foi revogado.
            $usuarioSessionModel = $usuarioSessionRepo->findByIdentifier(identifier: 'token_hash', value: CypherUtil::hash(data: $bearerToken));
            if (!$usuarioSessionModel || !CypherUtil::verify(data: $bearerToken, hash: $usuarioSessionModel->getTokenHash()) || $usuarioSessionModel->getRevokedAt()) {
                throw new AppException(message: 'Sessão inválida ou expirada. Faça login novamente.', code: 401);
            }

            // Valida o token JWT e decodifica seu payload para extrair os dados do usuário e as informações de sessão.
            // Se o token for inválido, expirado ou tiver sua assinatura comprometida, uma exceção é lançada.
            $decodedToken = $jwtUtil->validate(token: $bearerToken);
            if (!$decodedToken) {
                throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
            }

            $decodedToken = json_decode(json: json_encode($decodedToken, JSON_THROW_ON_ERROR), associative: true, flags: JSON_THROW_ON_ERROR);
            $remainingTime = $decodedToken['exp'] - time();
            if ($remainingTime <= 0) {
                throw new AppException(message: 'Sessão expirada. Faça login novamente.', code: 401);
            }

            self::validateSessionData(sessionModel: $usuarioSessionModel, tokenData: $decodedToken);
            CacheUtil::set(key: "login:main:{$bearerToken}", ttl: $remainingTime, value: $decodedToken);
            return null;
        } catch (AppException $e) {
            return [
                'status' => 'error',
                'response_code' => $e->getCode() ?: 500,
                'message' => $e->getMessage(),
                'output' => $e->getDetails() ? ['errors' => $e->getDetails()] : null,
            ];
        }
    }

    /**
     * Valida a integridade e autenticidade da sessão do usuário comparando metadados.
     *
     * Este método atua como uma barreira de segurança (Guard) que garante que a sessão 
     * atual está sendo utilizada pelo mesmo dispositivo e rede que a iniciou. Ao comparar 
     * o IP e o User-Agent da requisição atual com os dados persistidos no banco de dados 
     * (`UsuarioSessionModel`) e os metadados contidos no token JWT (`$tokenData`), 
     * protegemos o sistema contra ataques de sequestro de sessão (session hijacking).
     *
     * ---
     * ## Fluxo de Execução
     * 1. **Captura:** Extrai o IP (`REMOTE_ADDR`) e o User-Agent (`HTTP_USER_AGENT`) da variável superglobal `$_SERVER`.
     * 2. **Comparação (Match):** Utiliza uma expressão `match` para validar quatro pontos de convergência:
     *    - IP da Sessão (DB) vs. IP Atual.
     *    - User-Agent da Sessão (DB) vs. User-Agent Atual.
     *    - IP registrado no Payload do Token vs. IP Atual.
     *    - User-Agent registrado no Payload do Token vs. User-Agent Atual.
     * 3. **Bloqueio:** Caso qualquer divergência seja detectada, a sessão é considerada inválida e uma `AppException` (401 Unauthorized) é disparada.
     *
     * ---
     * ## Observações Técnicas
     * - O uso de `match` (PHP 8+) torna o código altamente legível e performático para a verificação booleana múltipla.
     * - Este método é fundamental em cenários onde a segurança de acesso exige que o usuário não troque de rede ou navegador enquanto a sessão estiver ativa.
     * - Caso a requisição venha através de um proxy, certifique-se de que a configuração de captura de IP (`REMOTE_ADDR`) esteja ajustada corretamente no ambiente.
     *
     * @param UsuarioSessionModel $sessionModel O modelo contendo os dados da sessão persistidos no banco.
     * @param array $tokenData O payload decodificado do token JWT contendo os metadados da sessão.
     * @return void
     * @throws AppException Caso haja qualquer incompatibilidade nos dados (IP ou User-Agent), invalidando a sessão.
     */
    private static function validateSessionData(UsuarioSessionModel $sessionModel, array $tokenData): void {
        $ipAddress = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';

        $isInvalidSession = match(true) {
            $sessionModel->getIpAddress() !== $ipAddress,
            $sessionModel->getUserAgent() !== $userAgent,
            $tokenData['session_data']['ip_address'] !== $ipAddress,
            $tokenData['session_data']['user_agent'] !== $userAgent => true,
            default => false
        };
        if ($isInvalidSession) {
            throw new AppException(message: 'Sessão inválida ou expirada. Faça login novamente.', code: 401);
        }
    }
}