using Dapper;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Domain.Entities;

namespace SIGCM2.Infrastructure.Persistence;

public sealed class RefreshTokenRepository : IRefreshTokenRepository
{
    private readonly SqlConnectionFactory _connectionFactory;

    public RefreshTokenRepository(SqlConnectionFactory connectionFactory)
    {
        _connectionFactory = connectionFactory;
    }

    public async Task<RefreshToken?> GetByHashAsync(string tokenHash, CancellationToken ct = default)
    {
        const string sql = """
            SELECT Id, UsuarioId, TokenHash, FamilyId,
                   IssuedAt, ExpiresAt, RevokedAt, ReplacedById,
                   CreatedByIp, UserAgent
            FROM dbo.RefreshToken
            WHERE TokenHash = @TokenHash
            """;

        await using var connection = _connectionFactory.CreateConnection();
        await connection.OpenAsync(ct);

        var row = await connection.QuerySingleOrDefaultAsync<RefreshTokenRow>(sql, new { TokenHash = tokenHash });
        return row is null ? null : MapRow(row);
    }

    public async Task<int> AddAsync(RefreshToken token, CancellationToken ct = default)
    {
        const string sql = """
            INSERT INTO dbo.RefreshToken
                (UsuarioId, TokenHash, FamilyId, IssuedAt, ExpiresAt, CreatedByIp, UserAgent)
            VALUES
                (@UsuarioId, @TokenHash, @FamilyId, @IssuedAt, @ExpiresAt, @CreatedByIp, @UserAgent);
            SELECT CAST(SCOPE_IDENTITY() AS INT);
            """;

        await using var connection = _connectionFactory.CreateConnection();
        await connection.OpenAsync(ct);

        return await connection.QuerySingleAsync<int>(sql, new
        {
            token.UsuarioId,
            token.TokenHash,
            token.FamilyId,
            token.IssuedAt,
            token.ExpiresAt,
            token.CreatedByIp,
            token.UserAgent,
        });
    }

    public async Task RevokeAsync(int id, int? replacedById, DateTime revokedAt, CancellationToken ct = default)
    {
        const string sql = """
            UPDATE dbo.RefreshToken
            SET RevokedAt = @RevokedAt, ReplacedById = @ReplacedById
            WHERE Id = @Id AND RevokedAt IS NULL
            """;

        await using var connection = _connectionFactory.CreateConnection();
        await connection.OpenAsync(ct);

        await connection.ExecuteAsync(sql, new { Id = id, ReplacedById = replacedById, RevokedAt = revokedAt });
    }

    public async Task<int> RevokeFamilyAsync(Guid familyId, DateTime revokedAt, CancellationToken ct = default)
    {
        const string sql = """
            UPDATE dbo.RefreshToken
            SET RevokedAt = @RevokedAt
            WHERE FamilyId = @FamilyId AND RevokedAt IS NULL;
            SELECT @@ROWCOUNT;
            """;

        await using var connection = _connectionFactory.CreateConnection();
        await connection.OpenAsync(ct);

        return await connection.QuerySingleAsync<int>(sql, new { FamilyId = familyId, RevokedAt = revokedAt });
    }

    public async Task<int> RevokeAllActiveForUserAsync(int usuarioId, DateTime revokedAt, CancellationToken ct = default)
    {
        const string sql = """
            UPDATE dbo.RefreshToken
            SET RevokedAt = @RevokedAt
            WHERE UsuarioId = @UsuarioId AND RevokedAt IS NULL;
            SELECT @@ROWCOUNT;
            """;

        await using var connection = _connectionFactory.CreateConnection();
        await connection.OpenAsync(ct);

        return await connection.QuerySingleAsync<int>(sql, new { UsuarioId = usuarioId, RevokedAt = revokedAt });
    }

    private static RefreshToken MapRow(RefreshTokenRow row) => RefreshTokenRow.Reconstruct(row);

    // Flat Dapper DTO
    private sealed record RefreshTokenRow(
        int Id,
        int UsuarioId,
        string TokenHash,
        Guid FamilyId,
        DateTime IssuedAt,
        DateTime ExpiresAt,
        DateTime? RevokedAt,
        int? ReplacedById,
        string CreatedByIp,
        string? UserAgent)
    {
        public static RefreshToken Reconstruct(RefreshTokenRow r)
        {
            // Build an empty token using the rotation factory from a dummy parent,
            // then we manually set fields via the available setters.
            // Since RefreshToken uses init-only properties, we use object initializer.
            var token = new RefreshToken
            {
                Id = r.Id,
                UsuarioId = r.UsuarioId,
                TokenHash = r.TokenHash,
                FamilyId = r.FamilyId,
                IssuedAt = r.IssuedAt,
                ExpiresAt = r.ExpiresAt,
                CreatedByIp = r.CreatedByIp,
                UserAgent = r.UserAgent,
            };

            if (r.RevokedAt.HasValue)
                token.MarkAsPersistedRevocation(r.RevokedAt.Value, r.ReplacedById);

            return token;
        }
    }
}
