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

use Ramsey\Uuid\Uuid;
use DateTimeImmutable;
use KrothiumAPI\Utils\HttpUtil;
use WorkbloomERP\Utils\JwtUtil;
use WorkbloomERP\Utils\CacheUtil;
use WorkbloomERP\Utils\CypherUtil;
use WorkbloomERP\Services\DBService;
use WorkbloomERP\Exceptions\AppException;
use WorkbloomERP\Module\v0\Auth\Utils\AuthUtil;
use WorkbloomERP\Module\v0\Empresa\Repos\EmpresaRepo;
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioRepo;
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioSessionRepo;
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioEmpresaRepo;
use WorkbloomERP\Module\v0\Empresa\Models\EmpresaModel;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioModel;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioSessionModel;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioEmpresaModel;

class AuthService {
    public function __construct(
        private DBService $db,
        private UsuarioRepo $usuarioRepo,
        private EmpresaRepo $empresaRepo,
        private UsuarioSessionRepo $usuarioSessionRepo,
        private UsuarioEmpresaRepo $usuarioEmpresaRepo,
    ) {}

    /**
     * Realiza a autenticação de usuário e inicia uma sessão no sistema.
     *
     * Este método é o coração do processo de autenticação. Ele valida as credenciais
     * fornecidas, gera um token JWT (JSON Web Token) para comunicação segura, persiste
     * a sessão no banco de dados para fins de auditoria/rastreabilidade e armazena
     * o token no cache (Redis) para validação rápida de requests subsequentes.
     *
     * ---
     * ## Fluxo de Execução
     * 1. **Validação de Entrada:** Verifica se `login` e `senha` foram fornecidos.
     * 2. **Autenticação:** Busca o usuário pelo login e utiliza `password_verify` para validar a hash da senha.
     * 3. **Geração de Token:** Instancia o `JwtUtil` com configurações de ambiente e gera um novo token JWT.
     * 4. **Persistência de Sessão (Atomicidade):** 
     *    - Insere o registro de sessão na tabela `usuario_session` (contendo dados como IP e User-Agent).
     *    - O hash do token é armazenado para permitir revogação futura.
     * 5. **Caching:** Armazena o token no Redis com a mesma TTL da expiração do token para otimizar a validação de acesso.
     *
     * ---
     * ## Observações Técnicas
     * - **Transacionalidade:** Toda a criação de sessão e persistência ocorre dentro de uma transação de banco de dados, garantindo que não existam sessões órfãs caso o cache ou a criação do token falhem.
     * - **Segurança:** O token JWT gerado é hasheado (`CypherUtil::hash`) antes de ser salvo no banco para evitar a exposição do token real em caso de vazamento de dados.
     * - **Infraestrutura:** Requer acesso funcional a: `JWT_SECRET_KEY`, `JWT_ALGORITHM`, `JWT_EXPIRATION_TIME`, `JWT_ISSUER`.
     *
     * @param string $login O nome de usuário ou e-mail.
     * @param string $senha A senha em texto puro fornecida pelo usuário.
     * @return array{response_code: int, message: string, output: array{token: string}} Resposta estruturada contendo o código de sucesso e o token JWT.
     * @throws AppException Caso as credenciais sejam inválidas (401), o payload esteja vazio (400) ou falhas críticas ocorram na geração de token/sessão (500).
     */
    public function login(string $login, string $senha): array {
        try {
            // Validação básica de entrada
            if (empty($login) || empty($senha)) {
                throw new AppException(message: 'Login e senha são obrigatórios.', code: 400);
            }

            // Inicia uma transação para garantir a atomicidade das operações de autenticação
            return $this->db->transaction(
                callback: function() use ($login, $senha) {
                    $usuarioModel = $this->usuarioRepo->findByLogin(login: $login);
                    if (!$usuarioModel || !password_verify(password: $senha, hash: $usuarioModel->getSenhaHash())) {
                        throw new AppException(message: 'Login ou senha inválidos.', code: 401);
                    }

                    $payload = [
                        'user_data' => [
                            'initial_login' => true,
                            'uuid' => $usuarioModel->getUuid(),
                            'nome_usuario' => $usuarioModel->getNomeCompleto(),
                            'email' => $usuarioModel->getEmail(),
                        ],
                        'exp' => (new DateTimeImmutable())->modify("+5 minutes")->getTimestamp(),
                    ];

                    $jwtUtil = new JwtUtil(
                        secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
                        ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
                    );
                    $jwtToken = $jwtUtil->generate(payload: $payload);
                    if (!$jwtToken) {
                        throw new AppException(message: 'Erro ao gerar token de acesso.', code: 500);
                    }

                    // Armazena o token JWT no cache Redis com uma TTL de 10 minutos (600 segundos)
                    CacheUtil::set(key: "login:init:{$jwtToken}", ttl: 600, value: $payload);

                    return [
                        'response_code' => 200,
                        'message' => 'Login realizado com sucesso.',
                        'output' => [
                            'tmp_token' => $jwtToken,
                            'empresas' => $this->getEmpresasByUsuarioId($usuarioModel->getId())
                        ]
                    ];
                }
            );
        } catch(AppException $e) {
            throw $e;
        }
    }

    /**
     * Recupera e organiza a lista de empresas vinculadas a um determinado usuário.
     *
     * Este método realiza o mapeamento entre o ID do usuário e suas respectivas 
     * empresas. Ele filtra as associações através do repositório `usuarioEmpresaRepo`, 
     * hidrata os objetos `EmpresaModel` e organiza os dados em uma estrutura 
     * associativa agrupada pelo nome empresarial.
     *
     * ---
     * ## Fluxo de Execução
     * 1. **Busca:** Recupera todos os registros de associação `usuario_id` -> `empresa_id`.
     * 2. **Hidratação:** Itera sobre as associações, buscando o `EmpresaModel` completo para cada ID encontrado.
     * 3. **Estruturação:** Monta um array multidimensional onde as chaves são os nomes das empresas e os valores são arrays contendo metadados (UUID, nome, CNPJ, tipo formatado).
     * 4. **Tratamento de Dados:** Normaliza o campo `tipo` (MATRIZ/FILIAL) para o formato "Capitalizado" (ex: Matriz).
     *
     * ---
     * ## Observações Técnicas
     * - Caso o usuário não possua empresas vinculadas ou o repositório retorne vazio, o método retorna um array vazio.
     * - Ignora silenciosamente qualquer `empresa_id` que não resulte em um `EmpresaModel` válido (falha de integridade referencial).
     * - O agrupamento pelo nome empresarial facilita o consumo da estrutura pelo frontend.
     *
     * @param int $usuario_id O identificador numérico interno do usuário.
     * @return array<string, array<int, array{uuid: string, nome_empresarial: string, nome_fantasia: string, cnpj: string, tipo: string}>> Array estruturado de empresas.
     * @throws AppException Caso ocorra falha de acesso aos repositórios.
     */
    private function getEmpresasByUsuarioId(int $usuario_id): array {
        try {
            $empresas = $this->usuarioEmpresaRepo->findAllByUsuarioId(usuario_id: $usuario_id);
            if (empty($empresas)) {
                return ['Nenhuma empresa vinculada ao usuário.'];
            }

            $preparedEmpresas = [];
            foreach($empresas as $i => $empresa) {
                $empresaModel = $this->empresaRepo->findByIdentifier(identifier: 'id', value: $empresa['empresa_id']);
                if (!$empresaModel) {
                    continue;
                }

                $preparedEmpresas[$empresaModel->getNomeEmpresarial()][] = [
                    'uuid' => $empresaModel->getUuid(),
                    'nome_empresarial' => $empresaModel->getNomeEmpresarial(),
                    'nome_fantasia' => $empresaModel->getNomeFantasia(),
                    'cnpj' => $empresaModel->getDocumentCnpj(),
                    'tipo' => ucfirst(string: strtolower(string: $empresaModel->getTipo()))
                ];
            }

            return $preparedEmpresas;
        } catch(AppException $e) {
            throw $e;
        }
    }

    /**
     * Seleciona e define a empresa ativa para o contexto da sessão do usuário.
     *
     * Este método é utilizado em sistemas multi-empresa onde o usuário precisa alternar
     * entre diferentes entidades (empresas) sem realizar um novo login completo.
     * O método valida se o usuário possui vínculo com a empresa solicitada, gera
     * um novo token JWT com o contexto da empresa atualizado e registra essa nova
     * sessão no banco de dados e no cache (Redis).
     *
     * ---
     * ## Fluxo de Execução
     * 1. **Verificação de Sessão:** Garante que o usuário já possui um login ativo (leitura via `AuthUtil`).
     * 2. **Validação de Acesso:** Verifica se o usuário solicitado possui associação ativa com a empresa (`usuarioEmpresaRepo`).
     * 3. **Contextualização:** Monta um novo payload contendo os dados do usuário e os dados da empresa selecionada.
     * 4. **Token Rotation:** Gera um novo token JWT com os novos dados de contexto e define um tempo de expiração (60 minutos).
     * 5. **Persistência:** Registra a nova sessão no banco de dados (`usuario_session`) e atualiza o estado no cache (`CacheUtil`).
     *
     * ---
     * ## Observações Técnicas
     * - **Token Rotation:** O método invalida implicitamente o contexto anterior ao gerar um novo token baseado na nova seleção.
     * - **Atomicidade:** A operação ocorre dentro de uma transação, garantindo que se o token não for gerado ou a sessão não for criada, nada é alterado.
     * - **Segurança:** O hash do token gerado é armazenado para permitir rastreabilidade e segurança.
     *
     * @param string $empresa_uuid O Identificador Único Universal da empresa que o usuário deseja selecionar.
     * @return array{response_code: int, message: string, output: array{token: string}} Resposta estruturada contendo o novo token de acesso com o contexto atualizado.
     * @throws AppException Caso a sessão seja inválida, a empresa não exista, o usuário não tenha permissão de acesso à empresa ou falhas de persistência ocorram.
     */
    public function selectCompany(string $empresa_uuid) {
        try {
            return $this->db->transaction(
                callback: function() use ($empresa_uuid) {
                    if (empty($empresa_uuid)) {
                        throw new AppException(message: 'UUID da empresa é obrigatório.', code: 400);
                    }

                    // Lê os dados da sessão atual para validar a existência de um login prévio e obter o UUID do usuário
                    $sessionData = AuthUtil::readInitSession();
                    if (!$sessionData || !isset($sessionData['user_data']['uuid'])) {
                        throw new AppException(message: 'Sessão de usuário não encontrada. Faça login novamente.', code: 401);
                    }

                    // Valida se o usuário tem acesso à empresa selecionada
                    $usuarioModel = $this->usuarioRepo->findByIdentifier(identifier: 'uuid', value: $sessionData['user_data']['uuid']);
                    if (!$usuarioModel) {
                        throw new AppException(message: 'Ocorreu um erro ao selecionar a empresa. Tente novamente mais tarde.', code: 404);
                    }

                    // Valida se a empresa existe e se o usuário tem associação com ela
                    $empresaModel = $this->empresaRepo->findByIdentifier(identifier: 'uuid', value: $empresa_uuid);
                    if (!$empresaModel || !$this->usuarioEmpresaRepo->checkAssociationByUsuarioIdAndEmpresaId(usuario_id: $usuarioModel->getId(), empresa_id: $empresaModel->getId())) {
                        throw new AppException(message: 'Empresa não encontrada.', code: 404);
                    }

                    // Monta o payload da sessão principal, incluindo os dados do usuário e da empresa selecionada
                    $payload = [
                        'user_data' => [
                            'initial_login' => false,
                            'uuid' => $usuarioModel->getUuid(),
                            'nome_completo' => $usuarioModel->getNomeCompleto(),
                            'nome_usuario' => $usuarioModel->getNomeUsuario(),
                            'email' => $usuarioModel->getEmail(),
                            'is_root' => $usuarioModel->getIsRoot() ? true : false
                        ],
                        'empresa_data' => [
                            'uuid' => $empresaModel->getUuid(),
                            'nome_empresarial' => $empresaModel->getNomeEmpresarial(),
                            'nome_fantasia' => $empresaModel->getNomeFantasia(),
                            'cnpj' => $empresaModel->getDocumentCnpj(),
                            'tipo' => ucfirst(string: strtolower(string: $empresaModel->getTipo()))
                        ],
                        'session_data' => [
                            'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
                            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
                        ],
                        'iat' => (new DateTimeImmutable())->getTimestamp(),
                        'exp' => (new DateTimeImmutable())->modify("+{$_ENV['JWT_EXPIRATION_TIME']} seconds")->getTimestamp(),
                    ];

                    $jwtUtil = new JwtUtil(
                        secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
                        ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
                    );
                    $jwtToken = $jwtUtil->generate(payload: $payload);
                    if (!$jwtToken) {
                        throw new AppException(message: 'Erro ao gerar token de acesso.', code: 500);
                    }

                    $usuarioSessionModel = $this->usuarioSessionRepo->insert(
                        new UsuarioSessionModel(
                            uuid: Uuid::uuid7()->toString(),
                            usuario_id: $usuarioModel->getId(),
                            token_hash: CypherUtil::hash(data: $jwtToken),
                            ip_address: $payload['session_data']['ip_address'],
                            user_agent: $payload['session_data']['user_agent'],
                            created_at: new DateTimeImmutable()
                        )
                    );
                    if (!$usuarioSessionModel) {
                        throw new AppException(message: 'Erro ao criar sessão de usuário.', code: 500);
                    }

                    if (!CacheUtil::set(key: "login:main:{$jwtToken}", ttl: $_ENV['JWT_EXPIRATION_TIME'], value: $payload)) {
                        throw new AppException(message: 'Erro ao armazenar sessão no cache.', code: 500);
                    }

                    $beforeToken = HttpUtil::getBearerToken();
                    if (!CacheUtil::delete(keys: ["login:init:{$beforeToken}"])) {
                        throw new AppException(message: 'Erro ao limpar sessão temporária do cache.', code: 500);
                    }

                    return [
                        'response_code' => 200,
                        'message' => 'Empresa selecionada com sucesso.',
                        'output' => [
                            'token' => $jwtToken
                        ]
                    ];
                }
            );
        } catch(AppException $e) {
            throw $e;
        }
    }
}