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

namespace SIGCM2.Infrastructure.Persistence;

/// <summary>
/// Dapper implementation of <see cref="IIngresosBrutosRepository"/>.
/// Provincia is persisted as the enum member name (PascalCase, e.g. "BuenosAires") via ToString().
/// On read, it is parsed back via Enum.Parse&lt;ProvinciaArgentina&gt; (strict PascalCase only).
/// Alicuota and Provincia are NEVER updated by cosmetic methods.
/// GetHistorialAsync uses a recursive CTE to walk the PredecesorId chain.
/// Note: As of V014 T700 cleanup, seed values are stored in PascalCase — legacy UPPER_SNAKE_CASE
/// support (LegacySeedMap / NormalizeUpperSnakeToPascal) has been removed.
/// </summary>
public sealed class IngresosBrutosRepository : IIngresosBrutosRepository
{
    private readonly SqlConnectionFactory _connectionFactory;

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

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

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

        try
        {
            return await connection.ExecuteScalarAsync<int>(sql, new
            {
                Provincia = entity.Provincia.ToString(),
                entity.Descripcion,
                entity.Alicuota,
                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 DuplicateProvinciaException(entity.Provincia);
        }
    }

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

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

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

    public async Task<bool> UpdateCosmeticoAsync(
        int id, string descripcion, bool activo,
        CancellationToken ct = default)
    {
        // NOTE: Alicuota and Provincia are intentionally EXCLUDED — they are IMMUTABLE.
        const string sql = """
            UPDATE dbo.IngresosBrutos
            SET
                Descripcion       = @Descripcion,
                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, Descripcion = descripcion, 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.IngresosBrutos
            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.IngresosBrutos
            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<IibbEntity>> ListAsync(IngresosBrutosQuery 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 (query.Provincia.HasValue)
        {
            where.Append(" AND Provincia = @Provincia");
            parameters.Add("Provincia", query.Provincia.Value.ToString());
        }

        var sql = $"""
            SELECT
                Id, Provincia, Descripcion, Alicuota, Activo,
                VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, FechaModificacion,
                COUNT(*) OVER() AS TotalCount
            FROM dbo.IngresosBrutos
            {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<IibbPagedRow>(sql, parameters);
        var list = rows.ToList();

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

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

    public async Task<IReadOnlyList<IibbEntity>> 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, Provincia, Descripcion, Alicuota, Activo,
                    VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, FechaModificacion,
                    0 AS NivelDesdeActual
                FROM dbo.IngresosBrutos
                WHERE Id = @Id

                UNION ALL

                SELECT
                    t.Id, t.Provincia, t.Descripcion, t.Alicuota, t.Activo,
                    t.VigenciaDesde, t.VigenciaHasta, t.PredecesorId, t.FechaCreacion, t.FechaModificacion,
                    c.NivelDesdeActual + 1
                FROM dbo.IngresosBrutos t
                INNER JOIN Cadena c ON t.Id = c.PredecesorId
            )
            SELECT
                Id, Provincia, Descripcion, Alicuota, 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<IibbRow>(sql, new { Id = id });
        return rows.Select(MapRow).ToList().AsReadOnly();
    }

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

    private static IibbEntity MapRow(IibbRow r)
        => IibbEntity.FromDb(
            id: r.Id,
            provincia: ParseProvincia(r.Provincia),
            descripcion: r.Descripcion,
            alicuota: r.Alicuota,
            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 IibbEntity MapPagedRow(IibbPagedRow r)
        => IibbEntity.FromDb(
            id: r.Id,
            provincia: ParseProvincia(r.Provincia),
            descripcion: r.Descripcion,
            alicuota: r.Alicuota,
            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);

    /// <summary>
    /// Parses a Provincia string from DB to ProvinciaArgentina enum.
    /// Since T700 cleanup, the seed stores PascalCase values matching enum.ToString().
    /// All values written by this repo are also PascalCase.
    /// </summary>
    private static ProvinciaArgentina ParseProvincia(string value)
    {
        if (Enum.TryParse<ProvinciaArgentina>(value, ignoreCase: false, out var result))
            return result;

        throw new ArgumentException(
            $"Cannot parse '{value}' as ProvinciaArgentina. " +
            $"Expected PascalCase enum name (e.g. 'BuenosAires', 'CiudadAutonomaDeBuenosAires').");
    }

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

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

    private sealed record IibbRow(
        int Id,
        string Provincia,
        string Descripcion,
        decimal Alicuota,
        bool Activo,
        DateTime VigenciaDesde,
        DateTime? VigenciaHasta,
        int? PredecesorId,
        DateTime FechaCreacion,
        DateTime? FechaModificacion);

    private sealed record IibbPagedRow(
        int Id,
        string Provincia,
        string Descripcion,
        decimal Alicuota,
        bool Activo,
        DateTime VigenciaDesde,
        DateTime? VigenciaHasta,
        int? PredecesorId,
        DateTime FechaCreacion,
        DateTime? FechaModificacion,
        int TotalCount);
}
