using System.Transactions;
using SIGCM2.Application.Abstractions;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Audit;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;

namespace SIGCM2.Application.Products.Prices.AddPrice;

/// <summary>
/// PRD-003 — Handler del comando AddProductPrice.
/// Flujo: verifica producto activo → abre TransactionScope (AsyncFlow) →
///        AddAsync (SP usp_AddProductPrice) → IAuditLogger.LogAsync (fail-closed) →
///        tx.Complete() → construye response con GetByProductIdAsync.
/// </summary>
public sealed class AddProductPriceCommandHandler
    : ICommandHandler<AddProductPriceCommand, AddProductPriceResponse>
{
    private readonly IProductPriceRepository _pricesRepo;
    private readonly IProductRepository _productsRepo;
    private readonly IAuditLogger _audit;
    private readonly TimeProvider _timeProvider;

    public AddProductPriceCommandHandler(
        IProductPriceRepository pricesRepo,
        IProductRepository productsRepo,
        IAuditLogger audit,
        TimeProvider timeProvider)
    {
        _pricesRepo = pricesRepo;
        _productsRepo = productsRepo;
        _audit = audit;
        _timeProvider = timeProvider;
    }

    public async Task<AddProductPriceResponse> Handle(AddProductPriceCommand command)
    {
        // 1. Producto debe existir Y estar activo (defensa Application — el SP también valida en BD).
        var product = await _productsRepo.GetByIdAsync(command.ProductId)
            ?? throw new ProductNotFoundException(command.ProductId);

        if (!product.IsActive)
            throw new ProductNotFoundException(command.ProductId); // inactivo = invisible para clientes

        // 2. TX + SP + audit (fail-closed).
        //    El audit.LogAsync enlista en el mismo TransactionScope — si falla, rollback total.
        //    GetByProductIdAsync se ejecuta FUERA del scope (post-commit) para evitar
        //    "TransactionScope is already complete" al abrir una nueva conexión dentro del using.
        long newId;
        long? closedId;
        using (var tx = new TransactionScope(
            TransactionScopeOption.Required,
            new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted },
            TransactionScopeAsyncFlowOption.Enabled))
        {
            (newId, closedId) = await _pricesRepo.AddAsync(
                command.ProductId, command.Price, command.PriceValidFrom);

            await _audit.LogAsync(
                action: "product_price.created",
                targetType: "ProductPrice",
                targetId: newId.ToString(),
                metadata: new
                {
                    after = new
                    {
                        command.ProductId,
                        command.Price,
                        priceValidFrom = command.PriceValidFrom.ToString("yyyy-MM-dd"),
                    },
                    closedPriceId = closedId
                });

            tx.Complete();
        } // TX disposed (committed) here — BEFORE the post-commit read below.

        // 3. Compongo la respuesta post-commit con lectura de historial actualizado.
        // La primera página (pageSize=2) es suficiente: solo necesitamos el nuevo y el cerrado,
        // que son siempre los más recientes (ORDER BY PriceValidFrom DESC).
        var pricesPage = await _pricesRepo.GetByProductIdAsync(command.ProductId, page: 1, pageSize: 2);
        var prices = pricesPage.Items;
        var created = prices.Single(p => p.Id == newId);
        var closed  = closedId.HasValue
            ? prices.SingleOrDefault(p => p.Id == closedId.Value)
            : null;

        return new AddProductPriceResponse(ToDto(created), closed is null ? null : ToDto(closed));
    }

    private static ProductPriceDto ToDto(ProductPrice p)
        => new(p.Id, p.ProductId, p.Price, p.PriceValidFrom, p.PriceValidTo, p.IsActive);
}
