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

namespace SIGCM2.Infrastructure.Persistence;

public sealed class RubroRepository : IRubroRepository
{
    private readonly SqlConnectionFactory _factory;

    public RubroRepository(SqlConnectionFactory factory)
    {
        _factory = factory;
    }

    public async Task<int> AddAsync(Rubro rubro, CancellationToken ct = default)
    {
        // DF handles: Activo (1), FechaCreacion (SYSUTCDATETIME()), Orden (0 default — overridden if provided).
        const string sql = """
            INSERT INTO dbo.Rubro (ParentId, Nombre, Orden, Activo, TarifarioBaseId)
            OUTPUT INSERTED.Id
            VALUES (@ParentId, @Nombre, @Orden, @Activo, @TarifarioBaseId)
            """;

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

        return await connection.ExecuteScalarAsync<int>(sql, new
        {
            rubro.ParentId,
            rubro.Nombre,
            rubro.Orden,
            Activo = rubro.Activo ? 1 : 0,
            rubro.TarifarioBaseId,
        });
    }

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

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

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

    public async Task<IReadOnlyList<Rubro>> GetAllAsync(bool incluirInactivos, CancellationToken ct = default)
    {
        var sql = incluirInactivos
            ? """
              SELECT Id, ParentId, Nombre, Orden, Activo, TarifarioBaseId, FechaCreacion, FechaModificacion
              FROM dbo.Rubro
              ORDER BY ParentId, Orden
              """
            : """
              SELECT Id, ParentId, Nombre, Orden, Activo, TarifarioBaseId, FechaCreacion, FechaModificacion
              FROM dbo.Rubro
              WHERE Activo = 1
              ORDER BY ParentId, Orden
              """;

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

        var rows = await connection.QueryAsync<RubroRow>(sql);
        return rows.Select(MapRow).ToList();
    }

    public async Task<IReadOnlyList<Rubro>> GetDescendantsAsync(int rootId, CancellationToken ct = default)
    {
        const string sql = """
            WITH Descendants AS (
                SELECT Id, ParentId, Nombre, Orden, Activo, TarifarioBaseId, FechaCreacion, FechaModificacion
                FROM dbo.Rubro
                WHERE ParentId = @RootId
                UNION ALL
                SELECT r.Id, r.ParentId, r.Nombre, r.Orden, r.Activo, r.TarifarioBaseId, r.FechaCreacion, r.FechaModificacion
                FROM dbo.Rubro r
                INNER JOIN Descendants d ON r.ParentId = d.Id
            )
            SELECT * FROM Descendants
            """;

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

        var rows = await connection.QueryAsync<RubroRow>(sql, new { RootId = rootId });
        return rows.Select(MapRow).ToList();
    }

    public async Task UpdateAsync(Rubro rubro, CancellationToken ct = default)
    {
        const string sql = """
            UPDATE dbo.Rubro
            SET Nombre              = @Nombre,
                ParentId            = @ParentId,
                Orden               = @Orden,
                Activo              = @Activo,
                TarifarioBaseId     = @TarifarioBaseId,
                FechaModificacion   = @FechaModificacion
            WHERE Id = @Id
            """;

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

        await connection.ExecuteAsync(sql, new
        {
            rubro.Nombre,
            rubro.ParentId,
            rubro.Orden,
            Activo = rubro.Activo ? 1 : 0,
            rubro.TarifarioBaseId,
            rubro.FechaModificacion,
            rubro.Id,
        });
    }

    public async Task<int> CountActiveChildrenAsync(int id, CancellationToken ct = default)
    {
        const string sql = """
            SELECT COUNT(1) FROM dbo.Rubro
            WHERE ParentId = @Id AND Activo = 1
            """;

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

        return await connection.ExecuteScalarAsync<int>(sql, new { Id = id });
    }

    public async Task<int> GetMaxOrdenAsync(int? parentId, CancellationToken ct = default)
    {
        // Returns MAX(Orden) + 1 among siblings, or 0 if no siblings exist.
        // Handler uses return value directly as the orden for the new Rubro.
        const string sql = """
            SELECT ISNULL(MAX(Orden) + 1, 0)
            FROM dbo.Rubro
            WHERE (@ParentId IS NULL AND ParentId IS NULL)
               OR ParentId = @ParentId
            """;

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

        return await connection.ExecuteScalarAsync<int>(sql, new { ParentId = parentId });
    }

    public async Task<bool> ExistsByNombreUnderParentAsync(
        int? parentId,
        string nombre,
        int? excludeId,
        CancellationToken ct = default)
    {
        // Use UPPER() for explicit case-insensitive comparison.
        // DB collation is SQL_Latin1_General_CP1_CI_AI on Nombre column (already CI),
        // but UPPER() makes intent explicit and works regardless of collation.
        // The WHERE clause handles both root (NULL parent) and non-root cases.
        const string sql = """
            SELECT COUNT(1)
            FROM dbo.Rubro
            WHERE ((@ParentId IS NULL AND ParentId IS NULL) OR ParentId = @ParentId)
              AND UPPER(Nombre) = UPPER(@Nombre)
              AND Activo = 1
              AND (@ExcludeId IS NULL OR Id <> @ExcludeId)
            """;

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

        var count = await connection.ExecuteScalarAsync<int>(sql, new
        {
            ParentId = parentId,
            Nombre = nombre,
            ExcludeId = excludeId,
        });

        return count > 0;
    }

    public async Task<int> GetDepthAsync(int? parentId, CancellationToken ct = default)
    {
        // If parentId is null, depth is 0 (creating a root node).
        if (!parentId.HasValue)
            return 0;

        // CTE walks up the ancestor chain from parentId to root, counting levels.
        // Each UNION ALL step goes one level up, so the count of rows = depth of parentId.
        const string sql = """
            WITH Ancestors AS (
                SELECT Id, ParentId, 1 AS Depth
                FROM dbo.Rubro
                WHERE Id = @ParentId
                UNION ALL
                SELECT r.Id, r.ParentId, a.Depth + 1
                FROM dbo.Rubro r
                INNER JOIN Ancestors a ON r.Id = a.ParentId
            )
            SELECT ISNULL(MAX(Depth), 0) FROM Ancestors
            """;

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

        return await connection.ExecuteScalarAsync<int>(sql, new { ParentId = parentId.Value });
    }

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

    private static Rubro MapRow(RubroRow r)
        => new(
            id: r.Id,
            parentId: r.ParentId,
            nombre: r.Nombre,
            orden: r.Orden,
            activo: r.Activo,
            tarifarioBaseId: r.TarifarioBaseId,
            fechaCreacion: r.FechaCreacion,
            fechaModificacion: r.FechaModificacion);

    private sealed record RubroRow(
        int Id,
        int? ParentId,
        string Nombre,
        int Orden,
        bool Activo,
        int? TarifarioBaseId,
        DateTime FechaCreacion,
        DateTime? FechaModificacion);
}
