using System.Transactions;
using SIGCM2.Application.Abstractions;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Audit;
using SIGCM2.Application.Common;
using SIGCM2.Application.Usuarios.GetById;
using SIGCM2.Domain.Exceptions;

namespace SIGCM2.Application.Usuarios.Update;

public sealed class UpdateUsuarioCommandHandler : ICommandHandler<UpdateUsuarioCommand, UsuarioDetailDto>
{
    private readonly IUsuarioRepository _repository;
    private readonly IRolRepository _rolRepository;
    private readonly IRefreshTokenRepository _refreshTokenRepository;
    private readonly IAuditLogger _audit;
    private readonly TimeProvider _timeProvider;

    public UpdateUsuarioCommandHandler(
        IUsuarioRepository repository,
        IRolRepository rolRepository,
        IRefreshTokenRepository refreshTokenRepository,
        IAuditLogger audit,
        TimeProvider timeProvider)
    {
        _repository = repository;
        _rolRepository = rolRepository;
        _refreshTokenRepository = refreshTokenRepository;
        _audit = audit;
        _timeProvider = timeProvider;
    }

    public async Task<UsuarioDetailDto> Handle(UpdateUsuarioCommand cmd)
    {
        var target = await _repository.GetByIdAsync(cmd.Id)
            ?? throw new UsuarioNotFoundException(cmd.Id);

        // Guard: validate rol exists and is active
        var rolExists = await _rolRepository.ExistsActiveByCodigoAsync(cmd.Rol);
        if (!rolExists)
            throw new FluentValidation.ValidationException(
                [new FluentValidation.Results.ValidationFailure("Rol", $"El rol '{cmd.Rol}' no existe o está inactivo.")]);

        // Guard: anti-lockout — cannot remove last active admin
        if (target.Rol == "admin" && target.Activo)
        {
            var isChangingRol = !string.Equals(cmd.Rol, "admin", StringComparison.Ordinal);
            var isDeactivating = !cmd.Activo;

            if ((isChangingRol || isDeactivating)
                && await _repository.CountActiveAdminsAsync() <= 1)
            {
                throw new LastAdminLockoutException();
            }
        }

        var fields = new UpdateUsuarioFields(cmd.Nombre, cmd.Apellido, cmd.Email, cmd.Rol, cmd.Activo);
        var now = _timeProvider.GetUtcNow().UtcDateTime;

        using (var tx = new TransactionScope(
            TransactionScopeOption.Required,
            new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted },
            TransactionScopeAsyncFlowOption.Enabled))
        {
            await _repository.UpdateAsync(cmd.Id, fields, now);

            // Revoke refresh tokens if rol changed or user deactivated
            var rolChanged = !string.Equals(target.Rol, cmd.Rol, StringComparison.Ordinal);
            var justDeactivated = target.Activo && !cmd.Activo;
            if (rolChanged || justDeactivated)
            {
                await _refreshTokenRepository.RevokeAllActiveForUserAsync(cmd.Id, now);
            }

            await _audit.LogAsync(
                action: "usuario.update",
                targetType: "Usuario",
                targetId: cmd.Id.ToString(),
                metadata: new
                {
                    before = new { target.Nombre, target.Apellido, target.Email, target.Rol, target.Activo },
                    after = new { cmd.Nombre, cmd.Apellido, cmd.Email, cmd.Rol, cmd.Activo },
                });

            tx.Complete();
        }

        // Post-commit read: outside the scope so SqlClient does not try to enlist a completed transaction.
        var updated = await _repository.GetDetailAsync(cmd.Id)
            ?? throw new UsuarioNotFoundException(cmd.Id);

        return new UsuarioDetailDto(
            updated.Id, updated.Username, updated.Nombre, updated.Apellido,
            updated.Email, updated.Rol, updated.Activo, updated.MustChangePassword,
            updated.UltimoLogin, updated.FechaModificacion);
    }
}
