using System.Text.Json;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using SIGCM2.Api.Authorization;
using SIGCM2.Api.Contracts.Fiscal;
using SIGCM2.Application.Abstractions;
using SIGCM2.Application.Common;
using SIGCM2.Application.IngresosBrutos.Create;
using SIGCM2.Application.IngresosBrutos.Deactivate;
using SIGCM2.Application.IngresosBrutos.Dtos;
using SIGCM2.Application.IngresosBrutos.GetById;
using SIGCM2.Application.IngresosBrutos.GetHistorial;
using SIGCM2.Application.IngresosBrutos.List;
using SIGCM2.Application.IngresosBrutos.NuevaVersion;
using SIGCM2.Application.IngresosBrutos.Reactivate;
using SIGCM2.Application.IngresosBrutos.Update;
using SIGCM2.Application.TiposDeIva.Create;
using SIGCM2.Application.TiposDeIva.Deactivate;
using SIGCM2.Application.TiposDeIva.Dtos;
using SIGCM2.Application.TiposDeIva.GetById;
using SIGCM2.Application.TiposDeIva.GetHistorial;
using SIGCM2.Application.TiposDeIva.List;
using SIGCM2.Application.TiposDeIva.NuevaVersion;
using SIGCM2.Application.TiposDeIva.Reactivate;
using SIGCM2.Application.TiposDeIva.Update;
using SIGCM2.Domain.Exceptions;
using SIGCM2.Domain.Fiscal;

namespace SIGCM2.Api.Controllers;

/// <summary>
/// ADM-009: Tablas Fiscales — IVA + IngresosBrutos endpoints at /api/v1/admin/fiscal.
/// All endpoints require permission 'administracion:fiscal:gestionar'.
/// </summary>
[ApiController]
[Route("api/v1/admin/fiscal")]
public sealed class FiscalController : ControllerBase
{
    private readonly IDispatcher _dispatcher;
    private readonly IValidator<CreateTipoDeIvaCommand> _createIvaValidator;
    private readonly IValidator<UpdateTipoDeIvaCommand> _updateIvaValidator;
    private readonly IValidator<NuevaVersionTipoDeIvaCommand> _nuevaVersionIvaValidator;
    private readonly IValidator<CreateIngresosBrutosCommand> _createIibbValidator;
    private readonly IValidator<UpdateIngresosBrutosCommand> _updateIibbValidator;
    private readonly IValidator<NuevaVersionIngresosBrutosCommand> _nuevaVersionIibbValidator;

    public FiscalController(
        IDispatcher dispatcher,
        IValidator<CreateTipoDeIvaCommand> createIvaValidator,
        IValidator<UpdateTipoDeIvaCommand> updateIvaValidator,
        IValidator<NuevaVersionTipoDeIvaCommand> nuevaVersionIvaValidator,
        IValidator<CreateIngresosBrutosCommand> createIibbValidator,
        IValidator<UpdateIngresosBrutosCommand> updateIibbValidator,
        IValidator<NuevaVersionIngresosBrutosCommand> nuevaVersionIibbValidator)
    {
        _dispatcher = dispatcher;
        _createIvaValidator = createIvaValidator;
        _updateIvaValidator = updateIvaValidator;
        _nuevaVersionIvaValidator = nuevaVersionIvaValidator;
        _createIibbValidator = createIibbValidator;
        _updateIibbValidator = updateIibbValidator;
        _nuevaVersionIibbValidator = nuevaVersionIibbValidator;
    }

    // ══════════════════════════════════════════════════════════════════════════
    // IVA endpoints
    // ══════════════════════════════════════════════════════════════════════════

    /// <summary>Lists TiposDeIva with optional filters. Requires administracion:fiscal:gestionar.</summary>
    [HttpGet("iva")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    public async Task<IActionResult> ListIva(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        [FromQuery] bool? activo = null,
        [FromQuery] string? codigo = null)
    {
        if (page < 1) return BadRequest(new { error = "page must be >= 1" });
        if (pageSize < 1) return BadRequest(new { error = "pageSize must be >= 1" });

        var query = new ListTiposDeIvaQuery(page, pageSize, activo, codigo);
        var result = await _dispatcher.Send<ListTiposDeIvaQuery, PagedResult<TipoDeIvaDto>>(query);

        return Ok(new
        {
            Items = result.Items.Select(FiscalContractMapper.ToIvaResponse).ToList(),
            result.Page,
            result.PageSize,
            result.Total
        });
    }

    /// <summary>Gets a single TipoDeIva by id.</summary>
    [HttpGet("iva/{id:int}")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetIvaById([FromRoute] int id)
    {
        var query = new GetTipoDeIvaByIdQuery(id);
        var result = await _dispatcher.Send<GetTipoDeIvaByIdQuery, TipoDeIvaDto>(query);
        return Ok(FiscalContractMapper.ToIvaResponse(result));
    }

    /// <summary>Gets the full version chain for a TipoDeIva.</summary>
    [HttpGet("iva/{id:int}/historial")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    public async Task<IActionResult> GetHistorialIva([FromRoute] int id)
    {
        var query = new GetHistorialTipoDeIvaQuery(id);
        var result = await _dispatcher.Send<GetHistorialTipoDeIvaQuery, IReadOnlyList<HistorialCadenaDto>>(query);
        return Ok(result.Select(FiscalContractMapper.ToHistorialIvaResponse).ToList());
    }

    /// <summary>Creates a new TipoDeIva. Returns 201 on success.</summary>
    [HttpPost("iva")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<IActionResult> CreateIva([FromBody] CreateTipoDeIvaRequest request)
    {
        var vigenciaDesde = ParseDateOnly(request.VigenciaDesde, "vigenciaDesde");
        if (vigenciaDesde is null)
            return BadRequest(new { error = "vigenciaDesde must be a valid date (yyyy-MM-dd)" });

        DateOnly? vigenciaHasta = null;
        if (request.VigenciaHasta is not null)
        {
            vigenciaHasta = ParseDateOnly(request.VigenciaHasta, "vigenciaHasta");
            if (vigenciaHasta is null)
                return BadRequest(new { error = "vigenciaHasta must be a valid date (yyyy-MM-dd)" });
        }

        var command = new CreateTipoDeIvaCommand(
            Codigo: request.Codigo ?? string.Empty,
            Descripcion: request.Descripcion ?? string.Empty,
            Porcentaje: request.Porcentaje ?? 0m,
            AplicaIVA: request.AplicaIVA ?? false,
            VigenciaDesde: vigenciaDesde.Value,
            VigenciaHasta: vigenciaHasta);

        var validation = await _createIvaValidator.ValidateAsync(command);
        if (!validation.IsValid)
        {
            var errors = validation.Errors
                .GroupBy(e => e.PropertyName)
                .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
            return BadRequest(new { errors });
        }

        var result = await _dispatcher.Send<CreateTipoDeIvaCommand, TipoDeIvaDto>(command);
        return CreatedAtAction(nameof(GetIvaById), new { id = result.Id }, FiscalContractMapper.ToIvaResponse(result));
    }

    /// <summary>
    /// Updates cosmetic fields of a TipoDeIva (Codigo, Descripcion, AplicaIVA, Activo).
    /// IMPORTANT: if the raw body contains "porcentaje" (case-insensitive) → 409 inmutable_usar_nueva_version.
    /// </summary>
    [HttpPatch("iva/{id:int}")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<IActionResult> UpdateIva([FromRoute] int id)
    {
        // Read raw body to detect immutable-field tampering before deserialization
        Request.EnableBuffering();
        using var reader = new StreamReader(Request.Body, leaveOpen: true);
        var rawBody = await reader.ReadToEndAsync();
        Request.Body.Position = 0;

        // Defend against porcentaje in body — must return 409 before dispatch
        if (ContainsImmutableField(rawBody, "porcentaje"))
            throw new PorcentajeInmutableException();

        UpdateTipoDeIvaRequest? request;
        try
        {
            request = JsonSerializer.Deserialize<UpdateTipoDeIvaRequest>(rawBody,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
        }
        catch (JsonException)
        {
            return BadRequest(new { error = "Invalid JSON body" });
        }

        if (request is null)
            return BadRequest(new { error = "Request body is required" });

        var command = new UpdateTipoDeIvaCommand(
            Id: id,
            Codigo: request.Codigo ?? string.Empty,
            Descripcion: request.Descripcion ?? string.Empty,
            AplicaIVA: request.AplicaIVA ?? false,
            Activo: request.Activo ?? true);

        var validation = await _updateIvaValidator.ValidateAsync(command);
        if (!validation.IsValid)
        {
            var errors = validation.Errors
                .GroupBy(e => e.PropertyName)
                .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
            return BadRequest(new { errors });
        }

        var result = await _dispatcher.Send<UpdateTipoDeIvaCommand, TipoDeIvaDto>(command);
        return Ok(FiscalContractMapper.ToIvaResponse(result));
    }

    /// <summary>Creates a new version of a TipoDeIva (closes the predecessor). Returns 201.</summary>
    [HttpPost("iva/{id:int}/nueva-version")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<IActionResult> NuevaVersionIva(
        [FromRoute] int id,
        [FromBody] NuevaVersionTipoDeIvaRequest request)
    {
        var vigenciaDesde = ParseDateOnly(request.VigenciaDesde, "vigenciaDesde");
        if (vigenciaDesde is null)
            return BadRequest(new { error = "vigenciaDesde must be a valid date (yyyy-MM-dd)" });

        var command = new NuevaVersionTipoDeIvaCommand(
            PredecesoraId: id,
            NuevoPorcentaje: request.Porcentaje ?? 0m,
            VigenciaDesde: vigenciaDesde.Value);

        var validation = await _nuevaVersionIvaValidator.ValidateAsync(command);
        if (!validation.IsValid)
        {
            var errors = validation.Errors
                .GroupBy(e => e.PropertyName)
                .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
            return BadRequest(new { errors });
        }

        var result = await _dispatcher.Send<NuevaVersionTipoDeIvaCommand, SIGCM2.Application.TiposDeIva.Dtos.NuevaVersionResultDto>(command);
        return CreatedAtAction(
            nameof(GetIvaById),
            new { id = result.NuevaVersionId },
            new NuevaVersionResponse(result.PredecesoraId, result.NuevaVersionId));
    }

    /// <summary>Deactivates a TipoDeIva. Idempotent.</summary>
    [HttpPost("iva/{id:int}/deactivate")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> DeactivateIva([FromRoute] int id)
    {
        var command = new DeactivateTipoDeIvaCommand(id);
        var result = await _dispatcher.Send<DeactivateTipoDeIvaCommand, TipoDeIvaDto>(command);
        return Ok(FiscalContractMapper.ToIvaResponse(result));
    }

    /// <summary>Reactivates a TipoDeIva. Idempotent.</summary>
    [HttpPost("iva/{id:int}/reactivate")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> ReactivateIva([FromRoute] int id)
    {
        var command = new ReactivateTipoDeIvaCommand(id);
        var result = await _dispatcher.Send<ReactivateTipoDeIvaCommand, TipoDeIvaDto>(command);
        return Ok(FiscalContractMapper.ToIvaResponse(result));
    }

    // ══════════════════════════════════════════════════════════════════════════
    // IngresosBrutos endpoints
    // ══════════════════════════════════════════════════════════════════════════

    /// <summary>Lists IngresosBrutos with optional filters.</summary>
    [HttpGet("iibb")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    public async Task<IActionResult> ListIibb(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        [FromQuery] bool? activo = null,
        [FromQuery] string? provincia = null)
    {
        if (page < 1) return BadRequest(new { error = "page must be >= 1" });
        if (pageSize < 1) return BadRequest(new { error = "pageSize must be >= 1" });

        ProvinciaArgentina? provinciaEnum = null;
        if (provincia is not null)
        {
            if (!Enum.TryParse<ProvinciaArgentina>(provincia, ignoreCase: true, out var parsed))
                return BadRequest(new { error = $"'{provincia}' is not a valid ProvinciaArgentina value." });
            provinciaEnum = parsed;
        }

        var query = new ListIngresosBrutosQuery(page, pageSize, activo, provinciaEnum);
        var result = await _dispatcher.Send<ListIngresosBrutosQuery, PagedResult<IngresosBrutosDto>>(query);

        return Ok(new
        {
            Items = result.Items.Select(FiscalContractMapper.ToIibbResponse).ToList(),
            result.Page,
            result.PageSize,
            result.Total
        });
    }

    /// <summary>Gets a single IngresosBrutos by id.</summary>
    [HttpGet("iibb/{id:int}")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetIibbById([FromRoute] int id)
    {
        var query = new GetIngresosBrutosByIdQuery(id);
        var result = await _dispatcher.Send<GetIngresosBrutosByIdQuery, IngresosBrutosDto>(query);
        return Ok(FiscalContractMapper.ToIibbResponse(result));
    }

    /// <summary>Gets the full version chain for an IngresosBrutos entry.</summary>
    [HttpGet("iibb/{id:int}/historial")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    public async Task<IActionResult> GetHistorialIibb([FromRoute] int id)
    {
        var query = new GetHistorialIngresosBrutosQuery(id);
        var result = await _dispatcher.Send<GetHistorialIngresosBrutosQuery, IReadOnlyList<HistorialCadenaIibbDto>>(query);
        return Ok(result.Select(FiscalContractMapper.ToHistorialIibbResponse).ToList());
    }

    /// <summary>Creates a new IngresosBrutos entry. Returns 201 on success.</summary>
    [HttpPost("iibb")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<IActionResult> CreateIibb([FromBody] CreateIngresosBrutosRequest request)
    {
        if (request.Provincia is null)
            return BadRequest(new { error = "provincia is required" });

        // Accept enum name (PascalCase) or display string
        ProvinciaArgentina provinciaEnum;
        if (Enum.TryParse<ProvinciaArgentina>(request.Provincia, ignoreCase: true, out var parsedEnum))
        {
            provinciaEnum = parsedEnum;
        }
        else
        {
            try
            {
                provinciaEnum = ProvinciaArgentinaExtensions.FromDisplayString(request.Provincia);
            }
            catch (ArgumentException)
            {
                return BadRequest(new { error = $"'{request.Provincia}' is not a valid provincia. Use enum name or display string." });
            }
        }

        var vigenciaDesde = ParseDateOnly(request.VigenciaDesde, "vigenciaDesde");
        if (vigenciaDesde is null)
            return BadRequest(new { error = "vigenciaDesde must be a valid date (yyyy-MM-dd)" });

        DateOnly? vigenciaHasta = null;
        if (request.VigenciaHasta is not null)
        {
            vigenciaHasta = ParseDateOnly(request.VigenciaHasta, "vigenciaHasta");
            if (vigenciaHasta is null)
                return BadRequest(new { error = "vigenciaHasta must be a valid date (yyyy-MM-dd)" });
        }

        var command = new CreateIngresosBrutosCommand(
            Provincia: provinciaEnum,
            Descripcion: request.Descripcion ?? string.Empty,
            Alicuota: request.Alicuota ?? 0m,
            VigenciaDesde: vigenciaDesde.Value,
            VigenciaHasta: vigenciaHasta);

        var validation = await _createIibbValidator.ValidateAsync(command);
        if (!validation.IsValid)
        {
            var errors = validation.Errors
                .GroupBy(e => e.PropertyName)
                .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
            return BadRequest(new { errors });
        }

        var result = await _dispatcher.Send<CreateIngresosBrutosCommand, IngresosBrutosDto>(command);
        return CreatedAtAction(nameof(GetIibbById), new { id = result.Id }, FiscalContractMapper.ToIibbResponse(result));
    }

    /// <summary>
    /// Updates cosmetic fields of IngresosBrutos (Descripcion, Activo).
    /// IMPORTANT: if the raw body contains "alicuota" (case-insensitive) → 409 inmutable_usar_nueva_version.
    /// </summary>
    [HttpPatch("iibb/{id:int}")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<IActionResult> UpdateIibb([FromRoute] int id)
    {
        Request.EnableBuffering();
        using var reader = new StreamReader(Request.Body, leaveOpen: true);
        var rawBody = await reader.ReadToEndAsync();
        Request.Body.Position = 0;

        if (ContainsImmutableField(rawBody, "alicuota"))
            throw new AlicuotaInmutableException();

        UpdateIngresosBrutosRequest? request;
        try
        {
            request = JsonSerializer.Deserialize<UpdateIngresosBrutosRequest>(rawBody,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
        }
        catch (JsonException)
        {
            return BadRequest(new { error = "Invalid JSON body" });
        }

        if (request is null)
            return BadRequest(new { error = "Request body is required" });

        var command = new UpdateIngresosBrutosCommand(
            Id: id,
            Descripcion: request.Descripcion ?? string.Empty,
            Activo: request.Activo ?? true);

        var validation = await _updateIibbValidator.ValidateAsync(command);
        if (!validation.IsValid)
        {
            var errors = validation.Errors
                .GroupBy(e => e.PropertyName)
                .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
            return BadRequest(new { errors });
        }

        var result = await _dispatcher.Send<UpdateIngresosBrutosCommand, IngresosBrutosDto>(command);
        return Ok(FiscalContractMapper.ToIibbResponse(result));
    }

    /// <summary>Creates a new version of IngresosBrutos (closes the predecessor). Returns 201.</summary>
    [HttpPost("iibb/{id:int}/nueva-version")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<IActionResult> NuevaVersionIibb(
        [FromRoute] int id,
        [FromBody] NuevaVersionIngresosBrutosRequest request)
    {
        var vigenciaDesde = ParseDateOnly(request.VigenciaDesde, "vigenciaDesde");
        if (vigenciaDesde is null)
            return BadRequest(new { error = "vigenciaDesde must be a valid date (yyyy-MM-dd)" });

        var command = new NuevaVersionIngresosBrutosCommand(
            PredecesoraId: id,
            NuevaAlicuota: request.Alicuota ?? 0m,
            VigenciaDesde: vigenciaDesde.Value);

        var validation = await _nuevaVersionIibbValidator.ValidateAsync(command);
        if (!validation.IsValid)
        {
            var errors = validation.Errors
                .GroupBy(e => e.PropertyName)
                .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
            return BadRequest(new { errors });
        }

        var result = await _dispatcher.Send<NuevaVersionIngresosBrutosCommand, SIGCM2.Application.IngresosBrutos.Dtos.NuevaVersionIibbResultDto>(command);
        return CreatedAtAction(
            nameof(GetIibbById),
            new { id = result.NuevaVersionId },
            new NuevaVersionResponse(result.PredecesoraId, result.NuevaVersionId));
    }

    /// <summary>Deactivates an IngresosBrutos entry. Idempotent.</summary>
    [HttpPost("iibb/{id:int}/deactivate")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> DeactivateIibb([FromRoute] int id)
    {
        var command = new DeactivateIngresosBrutosCommand(id);
        var result = await _dispatcher.Send<DeactivateIngresosBrutosCommand, IngresosBrutosDto>(command);
        return Ok(FiscalContractMapper.ToIibbResponse(result));
    }

    /// <summary>Reactivates an IngresosBrutos entry. Idempotent.</summary>
    [HttpPost("iibb/{id:int}/reactivate")]
    [RequirePermission("administracion:fiscal:gestionar")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> ReactivateIibb([FromRoute] int id)
    {
        var command = new ReactivateIngresosBrutosCommand(id);
        var result = await _dispatcher.Send<ReactivateIngresosBrutosCommand, IngresosBrutosDto>(command);
        return Ok(FiscalContractMapper.ToIibbResponse(result));
    }

    // ══════════════════════════════════════════════════════════════════════════
    // Private helpers
    // ══════════════════════════════════════════════════════════════════════════

    /// <summary>
    /// Parses a date string "yyyy-MM-dd" to DateOnly. Returns null if invalid.
    /// </summary>
    private static DateOnly? ParseDateOnly(string? value, string fieldName)
    {
        if (value is null) return null;
        return DateOnly.TryParseExact(value, "yyyy-MM-dd",
            System.Globalization.CultureInfo.InvariantCulture,
            System.Globalization.DateTimeStyles.None,
            out var result)
            ? result
            : null;
    }

    /// <summary>
    /// Checks if a raw JSON string contains a given field name (case-insensitive).
    /// Used to detect immutable-field tampering before deserialization silently drops the field.
    /// </summary>
    private static bool ContainsImmutableField(string rawJson, string fieldName)
    {
        if (string.IsNullOrWhiteSpace(rawJson)) return false;
        try
        {
            using var doc = JsonDocument.Parse(rawJson);
            return doc.RootElement.ValueKind == JsonValueKind.Object &&
                   doc.RootElement.EnumerateObject()
                      .Any(p => string.Equals(p.Name, fieldName, StringComparison.OrdinalIgnoreCase));
        }
        catch (JsonException)
        {
            return false;
        }
    }
}
