using System.Text;
using Dapper;
using Microsoft.Data.SqlClient;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Common;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;

namespace SIGCM2.Infrastructure.Persistence;

/// <summary>
/// Dapper implementation of <see cref="ITipoDeIvaRepository"/>.
/// All SQL is inline. Porcentaje and vigencia dates are NEVER updated by cosmetic methods.
/// GetHistorialAsync uses a recursive CTE to walk the PredecesorId chain.
/// </summary>
public sealed class TipoDeIvaRepository : ITipoDeIvaRepository
{
    private readonly SqlConnectionFactory _connectionFactory;

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

    public async Task<int> InsertAsync(TipoDeIva entity, CancellationToken ct = default)
    {
        const string sql = """
            INSERT INTO dbo.TipoDeIva
                (Codigo, Descripcion, Porcentaje, AplicaIVA, Activo, VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, FechaModificacion)
            OUTPUT INSERTED.Id
            VALUES
                (@Codigo, @Descripcion, @Porcentaje, @AplicaIVA, @Activo, @VigenciaDesde, @VigenciaHasta, @PredecesorId, SYSUTCDATETIME(), SYSUTCDATETIME())
            """;

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

        try
        {
            return await connection.ExecuteScalarAsync<int>(sql, new
            {
                entity.Codigo,
                entity.Descripcion,
                entity.Porcentaje,
                entity.AplicaIVA,
                entity.Activo,
                VigenciaDesde = entity.VigenciaDesde.ToDateTime(TimeOnly.MinValue),
                VigenciaHasta = entity.VigenciaHasta.HasValue
                    ? (object)entity.VigenciaHasta.Value.ToDateTime(TimeOnly.MinValue)
                    : DBNull.Value,
                PredecesorId = entity.PredecesorId.HasValue ? (object)entity.PredecesorId.Value : DBNull.Value,
            });
        }
        catch (SqlException ex) when (IsUniqueViolation(ex))
        {
            throw new DuplicateCodigoException(entity.Codigo);
        }
    }

    public async Task<TipoDeIva?> GetByIdAsync(int id, CancellationToken ct = default)
    {
        const string sql = """
            SELECT
                Id, Codigo, Descripcion, Porcentaje, AplicaIVA, Activo,
                VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, FechaModificacion
            FROM dbo.TipoDeIva
            WHERE Id = @Id
            """;

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

        var row = await connection.QuerySingleOrDefaultAsync<TipoDeIvaRow>(sql, new { Id = id });
        return row is null ? null : MapRow(row);
    }

    public async Task<bool> UpdateCosmeticoAsync(
        int id, string codigo, string descripcion, bool aplicaIVA, bool activo,
        CancellationToken ct = default)
    {
        // NOTE: Porcentaje, VigenciaDesde, VigenciaHasta, PredecesorId are intentionally EXCLUDED.
        const string sql = """
            UPDATE dbo.TipoDeIva
            SET
                Codigo            = @Codigo,
                Descripcion       = @Descripcion,
                AplicaIVA         = @AplicaIVA,
                Activo            = @Activo,
                FechaModificacion = SYSUTCDATETIME()
            WHERE Id = @Id
            """;

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

        var rows = await connection.ExecuteAsync(sql, new { Id = id, Codigo = codigo, Descripcion = descripcion, AplicaIVA = aplicaIVA, Activo = activo });
        return rows > 0;
    }

    public async Task<bool> UpdateCierreVigenciaAsync(
        int id, DateOnly vigenciaHasta,
        CancellationToken ct = default)
    {
        // Optimistic guard: only update if row is still open (VigenciaHasta IS NULL AND Activo = 1).
        // Returns false when 0 rows affected (already closed — race condition detected).
        const string sql = """
            UPDATE dbo.TipoDeIva
            SET
                VigenciaHasta     = @VigenciaHasta,
                FechaModificacion = SYSUTCDATETIME()
            WHERE Id = @Id
              AND VigenciaHasta IS NULL
              AND Activo = 1
            """;

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

        var rows = await connection.ExecuteAsync(sql, new
        {
            Id = id,
            VigenciaHasta = vigenciaHasta.ToDateTime(TimeOnly.MinValue),
        });
        return rows > 0;
    }

    public async Task<bool> SetActivoAsync(int id, bool activo, CancellationToken ct = default)
    {
        const string sql = """
            UPDATE dbo.TipoDeIva
            SET
                Activo            = @Activo,
                FechaModificacion = SYSUTCDATETIME()
            WHERE Id = @Id
            """;

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

        var rows = await connection.ExecuteAsync(sql, new { Id = id, Activo = activo });
        return rows > 0;
    }

    public async Task<PagedResult<TipoDeIva>> ListAsync(TiposDeIvaQuery query, CancellationToken ct = default)
    {
        var page = Math.Max(1, query.Page);
        var pageSize = Math.Clamp(query.PageSize, 1, 100);
        var offset = (page - 1) * pageSize;

        var where = new StringBuilder("WHERE 1=1");
        var parameters = new DynamicParameters();
        parameters.Add("PageSize", pageSize);
        parameters.Add("Offset", offset);

        if (query.Activo.HasValue)
        {
            where.Append(" AND Activo = @Activo");
            parameters.Add("Activo", query.Activo.Value ? 1 : 0);
        }

        if (!string.IsNullOrWhiteSpace(query.Codigo))
        {
            where.Append(" AND Codigo LIKE @Codigo + '%'");
            parameters.Add("Codigo", query.Codigo);
        }

        var sql = $"""
            SELECT
                Id, Codigo, Descripcion, Porcentaje, AplicaIVA, Activo,
                VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, FechaModificacion,
                COUNT(*) OVER() AS TotalCount
            FROM dbo.TipoDeIva
            {where}
            ORDER BY Id DESC
            OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
            """;

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

        var rows = await connection.QueryAsync<TipoDeIvaPagedRow>(sql, parameters);
        var list = rows.ToList();

        var total = list.Count > 0 ? list[0].TotalCount : 0;
        var items = list.Select(MapPagedRow).ToList();

        return new PagedResult<TipoDeIva>(items, page, pageSize, total);
    }

    public async Task<IReadOnlyList<TipoDeIva>> GetHistorialAsync(int id, CancellationToken ct = default)
    {
        // Recursive CTE: starts at @Id and walks PredecesorId upward to root,
        // then orders by VigenciaDesde ASC so root comes first.
        const string sql = """
            WITH Cadena AS (
                SELECT
                    Id, Codigo, Descripcion, Porcentaje, AplicaIVA, Activo,
                    VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, FechaModificacion,
                    0 AS NivelDesdeActual
                FROM dbo.TipoDeIva
                WHERE Id = @Id

                UNION ALL

                SELECT
                    t.Id, t.Codigo, t.Descripcion, t.Porcentaje, t.AplicaIVA, t.Activo,
                    t.VigenciaDesde, t.VigenciaHasta, t.PredecesorId, t.FechaCreacion, t.FechaModificacion,
                    c.NivelDesdeActual + 1
                FROM dbo.TipoDeIva t
                INNER JOIN Cadena c ON t.Id = c.PredecesorId
            )
            SELECT
                Id, Codigo, Descripcion, Porcentaje, AplicaIVA, Activo,
                VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, FechaModificacion
            FROM Cadena
            ORDER BY VigenciaDesde ASC
            OPTION (MAXRECURSION 100)
            """;

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

        var rows = await connection.QueryAsync<TipoDeIvaRow>(sql, new { Id = id });
        return rows.Select(MapRow).ToList().AsReadOnly();
    }

    // ── Private helpers ───────────────────────────────────────────────────────

    private static TipoDeIva MapRow(TipoDeIvaRow r)
        => TipoDeIva.FromDb(
            id: r.Id,
            codigo: r.Codigo,
            descripcion: r.Descripcion,
            porcentaje: r.Porcentaje,
            aplicaIVA: r.AplicaIVA,
            activo: r.Activo,
            vigenciaDesde: DateOnly.FromDateTime(r.VigenciaDesde),
            vigenciaHasta: r.VigenciaHasta.HasValue ? DateOnly.FromDateTime(r.VigenciaHasta.Value) : null,
            predecesorId: r.PredecesorId,
            fechaCreacion: r.FechaCreacion,
            fechaModificacion: r.FechaModificacion);

    private static TipoDeIva MapPagedRow(TipoDeIvaPagedRow r)
        => TipoDeIva.FromDb(
            id: r.Id,
            codigo: r.Codigo,
            descripcion: r.Descripcion,
            porcentaje: r.Porcentaje,
            aplicaIVA: r.AplicaIVA,
            activo: r.Activo,
            vigenciaDesde: DateOnly.FromDateTime(r.VigenciaDesde),
            vigenciaHasta: r.VigenciaHasta.HasValue ? DateOnly.FromDateTime(r.VigenciaHasta.Value) : null,
            predecesorId: r.PredecesorId,
            fechaCreacion: r.FechaCreacion,
            fechaModificacion: r.FechaModificacion);

    private static bool IsUniqueViolation(SqlException ex)
        => ex.Number is 2627 or 2601;

    // ── Private row records ───────────────────────────────────────────────────

    private sealed record TipoDeIvaRow(
        int Id,
        string Codigo,
        string Descripcion,
        decimal Porcentaje,
        bool AplicaIVA,
        bool Activo,
        DateTime VigenciaDesde,
        DateTime? VigenciaHasta,
        int? PredecesorId,
        DateTime FechaCreacion,
        DateTime? FechaModificacion);

    private sealed record TipoDeIvaPagedRow(
        int Id,
        string Codigo,
        string Descripcion,
        decimal Porcentaje,
        bool AplicaIVA,
        bool Activo,
        DateTime VigenciaDesde,
        DateTime? VigenciaHasta,
        int? PredecesorId,
        DateTime FechaCreacion,
        DateTime? FechaModificacion,
        int TotalCount);
}
