using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Stock;
using ERPCore.Infra.Persistence;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Services.Stock;

/// <summary>
/// FIFO cost-layer + ledger writer and valuation reader (docs/10 Part A.2).
/// Writes (layer/ledger creation) are added to the tracked context and persisted
/// by the caller's UoW transaction; they are never saved here.
/// </summary>
public sealed class FifoCostingService : IFifoCostingService
{
    public const string BaseCurrency = "LKR";
    public const string Method = "FIFO";

    // Phase-1 placeholder GL accounts (data only, no posting — FR-STK-13).
    private const string InventoryAccount = "1300";
    private const string ClearingAccount = "2100";

    private readonly IRepository<StockLayer> _layers;
    private readonly IRepository<StockLedger> _ledger;
    private readonly IRepository<JournalEntryStub> _journal;
    private readonly ErpDbContext _db;

    public FifoCostingService(
        IRepository<StockLayer> layers, IRepository<StockLedger> ledger,
        IRepository<JournalEntryStub> journal, ErpDbContext db)
    {
        _layers = layers;
        _ledger = ledger;
        _journal = journal;
        _db = db;
    }

    public async Task<StockLayer> CreateInboundLayerAsync(
        int itemId, int warehouseId, int? batchId, int? serialId, int? grnLineId,
        decimal qtyBase, decimal unitCost, DateTime receiptDate, CancellationToken ct = default)
    {
        var layer = new StockLayer
        {
            ItemId = itemId,
            WarehouseId = warehouseId,
            BatchId = batchId,
            SerialId = serialId,
            GrnLineId = grnLineId,
            QtyReceived = qtyBase,
            QtyRemaining = qtyBase,
            UnitCost = unitCost,
            ReceiptDate = receiptDate
        };
        await _layers.AddAsync(layer, ct);
        return layer;
    }

    public async Task<IReadOnlyList<ConsumedSegment>> ConsumeAsync(
        int itemId, int warehouseId, int? batchId, decimal qtyBase, CancellationToken ct = default)
    {
        var today = DateOnly.FromDateTime(DateTime.UtcNow);

        if (batchId is not null)
        {
            var expiry = await _db.Batches.AsNoTracking()
                .Where(b => b.BatchId == batchId).Select(b => b.ExpiryDate).FirstOrDefaultAsync(ct);
            if (expiry is not null && expiry < today)
                throw new DomainException(ErrorCodes.ExpiredBatchBlocked,
                    $"Batch {batchId} expired on {expiry:yyyy-MM-dd} and cannot be issued.", 409);
        }

        // Row-lock the issuable open layers oldest-first (SELECT … FOR UPDATE, NFR-02).
        // Excludes on-hold (grn line) and expired-batch stock. No LINQ is composed on
        // top of the raw SQL so the FOR UPDATE reaches the database intact.
        // {batchId}::bigint casts give Npgsql an explicit type for the (possibly null)
        // parameter — without it a null batch filter fails with 42P18.
        var layers = await _db.StockLayers
            .FromSqlInterpolated($"""
                SELECT * FROM stock_layers sl
                WHERE sl."ItemId" = {itemId} AND sl."WarehouseId" = {warehouseId} AND sl."QtyRemaining" > 0
                  AND ({batchId}::bigint IS NULL OR sl."BatchId" = {batchId}::bigint)
                  AND NOT EXISTS (SELECT 1 FROM grn_lines gl WHERE gl."GrnLineId" = sl."GrnLineId" AND gl."HoldStatus" = 'OnHold')
                  AND NOT EXISTS (SELECT 1 FROM batches b WHERE b."BatchId" = sl."BatchId" AND b."ExpiryDate" < {today})
                ORDER BY sl."ReceiptDate", sl."LayerId"
                FOR UPDATE
                """)
            .ToListAsync(ct);

        var available = layers.Sum(l => l.QtyRemaining);
        if (available < qtyBase)
        {
            var heldExists = await _db.StockLayers.AsNoTracking().AnyAsync(l =>
                l.ItemId == itemId && l.WarehouseId == warehouseId && l.QtyRemaining > 0
                && (batchId == null || l.BatchId == batchId)
                && l.GrnLine != null && l.GrnLine.HoldStatus == HoldStatus.OnHold, ct);
            if (heldExists)
                throw new DomainException(ErrorCodes.OnHoldNotIssuable,
                    $"Stock for item {itemId} at warehouse {warehouseId} is on inspection hold and cannot be issued.", 409);

            throw new DomainException(ErrorCodes.StockNegativeBlocked,
                $"Available {available} < requested {qtyBase} for item {itemId} at warehouse {warehouseId}.", 409);
        }

        var segments = new List<ConsumedSegment>();
        var remaining = qtyBase;
        foreach (var layer in layers)
        {
            if (remaining <= 0) break;
            var take = Math.Min(remaining, layer.QtyRemaining);
            layer.QtyRemaining -= take;
            remaining -= take;
            segments.Add(new ConsumedSegment(layer.LayerId, take, layer.UnitCost));
        }

        return segments;
    }

    public async Task<StockLedger> PostLedgerAsync(
        int itemId, int warehouseId, int? binId, int? batchId, int? serialId, int userId,
        Direction direction, decimal qtyBase, decimal unitCost, decimal runningBalance,
        string sourceDocType, int sourceDocId, DateTime createdAt, CancellationToken ct = default,
        decimal? valueOverride = null)
    {
        var entry = new StockLedger
        {
            ItemId = itemId,
            WarehouseId = warehouseId,
            BinId = binId,
            BatchId = batchId,
            SerialId = serialId,
            UserId = userId,
            Direction = direction,
            QtyBase = qtyBase,
            UnitCost = unitCost,
            Value = valueOverride is not null
                ? Math.Round(valueOverride.Value, 4, MidpointRounding.AwayFromZero)
                : Math.Round(qtyBase * unitCost, 4, MidpointRounding.AwayFromZero),
            RunningBalance = runningBalance,
            SourceDocType = sourceDocType,
            SourceDocId = sourceDocId,
            CreatedAt = createdAt
        };
        await _ledger.AddAsync(entry, ct);

        // GL-ready journal entry per movement (FR-STK-13; data only, no posting).
        // Inbound debits Inventory / credits Clearing; outbound reverses.
        var (debit, credit) = direction == Direction.In
            ? (InventoryAccount, ClearingAccount)
            : (ClearingAccount, InventoryAccount);
        await _journal.AddAsync(new JournalEntryStub
        {
            SourceDocType = sourceDocType,
            SourceDocId = sourceDocId,
            DebitAccount = debit,
            CreditAccount = credit,
            Amount = entry.Value
        }, ct);

        return entry;
    }

    public async Task<decimal> GetOnHandAsync(int itemId, int warehouseId, CancellationToken ct = default)
        => await _layers.Query().AsNoTracking()
            .Where(l => l.ItemId == itemId && l.WarehouseId == warehouseId)
            .SumAsync(l => (decimal?)l.QtyRemaining, ct) ?? 0m;

    public async Task<StockValuationDto> GetValuationAsync(int itemId, int warehouseId, CancellationToken ct = default)
    {
        var open = await _layers.Query().AsNoTracking()
            .Where(l => l.ItemId == itemId && l.WarehouseId == warehouseId && l.QtyRemaining > 0)
            .OrderBy(l => l.ReceiptDate).ThenBy(l => l.LayerId)
            .Select(l => new { l.LayerId, l.QtyRemaining, l.UnitCost, l.ReceiptDate })
            .ToListAsync(ct);

        var layers = open.Select(l => new StockValuationLayerDto(
            l.LayerId, l.QtyRemaining, l.UnitCost,
            Math.Round(l.QtyRemaining * l.UnitCost, 4, MidpointRounding.AwayFromZero), l.ReceiptDate)).ToList();

        return new StockValuationDto(
            itemId, warehouseId, layers,
            layers.Sum(l => l.QtyRemaining), layers.Sum(l => l.Value), BaseCurrency, Method);
    }
}
