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;

public sealed class PuntoDeVentaRepository : IPuntoDeVentaRepository
{
    private readonly SqlConnectionFactory _connectionFactory;

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

    public async Task<int> AddAsync(PuntoDeVenta pdv, CancellationToken ct = default)
    {
        // DF handles: Activo (1), FechaCreacion (SYSUTCDATETIME()).
        const string sql = """
            INSERT INTO dbo.PuntoDeVenta (MedioId, NumeroAFIP, Nombre, Descripcion)
            OUTPUT INSERTED.Id
            VALUES (@MedioId, @NumeroAFIP, @Nombre, @Descripcion)
            """;

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

        try
        {
            return await connection.ExecuteScalarAsync<int>(sql, new
            {
                pdv.MedioId,
                pdv.NumeroAFIP,
                pdv.Nombre,
                pdv.Descripcion,
            });
        }
        catch (SqlException ex) when (IsUniqueViolation(ex) && ex.Message.Contains("UQ_PuntoDeVenta_Medio_AFIP"))
        {
            throw new NumeroAFIPDuplicadoException(pdv.MedioId, pdv.NumeroAFIP);
        }
    }

    public async Task<PuntoDeVenta?> GetByIdAsync(int id, CancellationToken ct = default)
    {
        const string sql = """
            SELECT Id, MedioId, NumeroAFIP, Nombre, Descripcion, Activo, FechaCreacion, FechaModificacion
            FROM dbo.PuntoDeVenta
            WHERE Id = @Id
            """;

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

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

    public async Task<bool> ExistsByNumeroAFIPInMedioAsync(int medioId, short numeroAFIP, int? excludeId = null, CancellationToken ct = default)
    {
        var sql = excludeId.HasValue
            ? "SELECT COUNT(1) FROM dbo.PuntoDeVenta WHERE MedioId = @MedioId AND NumeroAFIP = @NumeroAFIP AND Id <> @ExcludeId"
            : "SELECT COUNT(1) FROM dbo.PuntoDeVenta WHERE MedioId = @MedioId AND NumeroAFIP = @NumeroAFIP";

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

        var count = await connection.ExecuteScalarAsync<int>(sql, new { MedioId = medioId, NumeroAFIP = numeroAFIP, ExcludeId = excludeId });
        return count > 0;
    }

    public async Task UpdateAsync(PuntoDeVenta pdv, CancellationToken ct = default)
    {
        const string sql = """
            UPDATE dbo.PuntoDeVenta
            SET NumeroAFIP        = @NumeroAFIP,
                Nombre            = @Nombre,
                Descripcion       = @Descripcion,
                Activo            = @Activo,
                FechaModificacion = @FechaModificacion
            WHERE Id = @Id
            """;

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

        try
        {
            await connection.ExecuteAsync(sql, new
            {
                pdv.NumeroAFIP,
                pdv.Nombre,
                pdv.Descripcion,
                pdv.Activo,
                FechaModificacion = pdv.FechaModificacion,
                pdv.Id,
            });
        }
        catch (SqlException ex) when (IsUniqueViolation(ex) && ex.Message.Contains("UQ_PuntoDeVenta_Medio_AFIP"))
        {
            throw new NumeroAFIPDuplicadoException(pdv.MedioId, pdv.NumeroAFIP);
        }
    }

    public async Task<PagedResult<PuntoDeVenta>> GetPagedAsync(PuntosDeVentaQuery q, CancellationToken ct = default)
    {
        var page = Math.Max(1, q.Page);
        var pageSize = Math.Clamp(q.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 (q.MedioId.HasValue)
        {
            where.Append(" AND MedioId = @MedioId");
            parameters.Add("MedioId", q.MedioId.Value);
        }

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

        var sql = $"""
            SELECT
                Id, MedioId, NumeroAFIP, Nombre, Descripcion, Activo, FechaCreacion, FechaModificacion,
                COUNT(*) OVER() AS TotalCount
            FROM dbo.PuntoDeVenta
            {where}
            ORDER BY MedioId, NumeroAFIP
            OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
            """;

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

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

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

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

    // ── mapping ───────────────────────────────────────────────────────────────

    private static PuntoDeVenta MapRow(PdvRow r)
        => new(
            id: r.Id,
            medioId: r.MedioId,
            numeroAFIP: r.NumeroAFIP,
            nombre: r.Nombre,
            descripcion: r.Descripcion,
            activo: r.Activo,
            fechaCreacion: r.FechaCreacion,
            fechaModificacion: r.FechaModificacion);

    private static PuntoDeVenta MapRow(PdvPagedRow r)
        => new(
            id: r.Id,
            medioId: r.MedioId,
            numeroAFIP: r.NumeroAFIP,
            nombre: r.Nombre,
            descripcion: r.Descripcion,
            activo: r.Activo,
            fechaCreacion: r.FechaCreacion,
            fechaModificacion: r.FechaModificacion);

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

    // ── private rows ──────────────────────────────────────────────────────────

    private sealed record PdvRow(
        int Id,
        int MedioId,
        short NumeroAFIP,
        string Nombre,
        string? Descripcion,
        bool Activo,
        DateTime FechaCreacion,
        DateTime? FechaModificacion);

    private sealed record PdvPagedRow(
        int Id,
        int MedioId,
        short NumeroAFIP,
        string Nombre,
        string? Descripcion,
        bool Activo,
        DateTime FechaCreacion,
        DateTime? FechaModificacion,
        int TotalCount);
}
