using SIGCM2.Application.Abstractions;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Abstractions.Security;
using SIGCM2.Application.Audit;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;
using SIGCM2.Domain.Security;

namespace SIGCM2.Application.Auth.Refresh;

public sealed class RefreshCommandHandler : ICommandHandler<RefreshCommand, RefreshResponseDto>
{
    private readonly IRefreshTokenRepository _refreshRepo;
    private readonly IUsuarioRepository _usuarioRepo;
    private readonly IJwtService _jwt;
    private readonly IRefreshTokenGenerator _refreshGenerator;
    private readonly IClientContext _clientCtx;
    private readonly AuthOptions _authOptions;
    private readonly ISecurityEventLogger _security;
    private readonly TimeProvider _timeProvider;

    public RefreshCommandHandler(
        IRefreshTokenRepository refreshRepo,
        IUsuarioRepository usuarioRepo,
        IJwtService jwt,
        IRefreshTokenGenerator refreshGenerator,
        IClientContext clientCtx,
        AuthOptions authOptions,
        ISecurityEventLogger security,
        TimeProvider timeProvider)
    {
        _refreshRepo = refreshRepo;
        _usuarioRepo = usuarioRepo;
        _jwt = jwt;
        _refreshGenerator = refreshGenerator;
        _clientCtx = clientCtx;
        _authOptions = authOptions;
        _security = security;
        _timeProvider = timeProvider;
    }

    public async Task<RefreshResponseDto> Handle(RefreshCommand command)
    {
        // 1. Validate access token signature (lifetime=false) and extract sub
        System.Security.Claims.ClaimsPrincipal principal;
        try
        {
            principal = _jwt.GetPrincipalFromExpiredToken(command.AccessToken);
        }
        catch
        {
            throw new InvalidRefreshTokenException();
        }

        if (!int.TryParse(principal.FindFirst("sub")?.Value, out var accessUserId))
            throw new InvalidRefreshTokenException();

        // 2. Hash the refresh token to look it up
        var hash = TokenHasher.Sha256Base64Url(command.RefreshToken);

        // 3. Look up in DB (returns record regardless of revoked/expired status)
        var stored = await _refreshRepo.GetByHashAsync(hash);
        if (stored is null)
            throw new InvalidRefreshTokenException();

        var now = _timeProvider.GetUtcNow().UtcDateTime;

        // 4. Reuse detection: already revoked → chain revocation and throw
        if (stored.IsRevoked)
        {
            await _refreshRepo.RevokeFamilyAsync(stored.FamilyId, now);
            await _security.LogAsync("refresh.reuse_detected", "failure",
                actorUserId: stored.UsuarioId,
                failureReason: "token_reused",
                metadata: new { familyId = stored.FamilyId });
            throw new TokenReuseDetectedException();
        }

        // 5. Absolute expiration check
        if (stored.IsExpired(now))
        {
            await _security.LogAsync("refresh.issue", "failure",
                actorUserId: stored.UsuarioId, failureReason: "token_expired");
            throw new InvalidRefreshTokenException();
        }

        // 6. UsuarioId must match access token's sub claim
        if (stored.UsuarioId != accessUserId)
        {
            await _security.LogAsync("refresh.issue", "failure",
                actorUserId: stored.UsuarioId, failureReason: "sub_mismatch");
            throw new InvalidRefreshTokenException();
        }

        // 7. Load current user (so access token has up-to-date claims)
        var usuario = await _usuarioRepo.GetByIdAsync(stored.UsuarioId);
        if (usuario is null)
        {
            await _security.LogAsync("refresh.issue", "failure",
                actorUserId: stored.UsuarioId, failureReason: "user_not_found");
            throw new InvalidRefreshTokenException();
        }

        if (!usuario.Activo)
        {
            await _security.LogAsync("refresh.issue", "failure",
                actorUserId: usuario.Id, failureReason: "user_inactive");
            throw new InvalidRefreshTokenException();
        }

        // 8. Rotate: create new token, persist, then revoke old
        var newRaw = _refreshGenerator.Generate();
        var newHash = TokenHasher.Sha256Base64Url(newRaw);
        var rotated = RefreshToken.IssueRotation(stored, newHash, now, _clientCtx.Ip, _clientCtx.UserAgent);

        var newId = await _refreshRepo.AddAsync(rotated);
        await _refreshRepo.RevokeAsync(stored.Id, replacedById: newId, revokedAt: now);

        // 9. Issue new access token
        var newAccess = _jwt.GenerateAccessToken(usuario);

        await _security.LogAsync("refresh.issue", "success", actorUserId: usuario.Id);

        return new RefreshResponseDto(newAccess, newRaw, _authOptions.AccessTokenMinutes * 60);
    }
}
