using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Infra.Auth;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services.Interfaces;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Services.Stock;

/// <summary>
/// Applies signed stock deltas for a document and posts the ledger (see
/// <see cref="IStockMutator"/>). Running balances are tracked in-memory per
/// (item, warehouse), seeded from current on-hand, so multiple lines for the same
/// item chain correctly within the transaction.
/// </summary>
public sealed class StockMutator : IStockMutator
{
    private readonly IFifoCostingService _fifo;
    private readonly IRepository<StockLayer> _layers;
    private readonly ICurrentUser _currentUser;

    public StockMutator(IFifoCostingService fifo, IRepository<StockLayer> layers, ICurrentUser currentUser)
    {
        _fifo = fifo;
        _layers = layers;
        _currentUser = currentUser;
    }

    public async Task<IReadOnlyList<StockLedger>> ApplyAsync(
        int warehouseId, string sourceDocType, int sourceDocId, DateTime now,
        IReadOnlyList<StockDelta> deltas, CancellationToken ct = default)
    {
        var actor = _currentUser.AuditUserId;
        var balances = new Dictionary<int, decimal>();
        var entries = new List<StockLedger>();

        foreach (var d in deltas)
        {
            if (d.QtyDelta == 0) continue;

            if (!balances.TryGetValue(d.ItemId, out var bal))
                bal = await _fifo.GetOnHandAsync(d.ItemId, warehouseId, ct);

            StockLedger entry;
            if (d.QtyDelta < 0)
            {
                var qty = -d.QtyDelta;
                var segments = await _fifo.ConsumeAsync(d.ItemId, warehouseId, d.BatchId, qty, ct);
                var unitCost = segments.Sum(s => s.Qty * s.UnitCost) / qty;
                bal -= qty;
                entry = await _fifo.PostLedgerAsync(
                    d.ItemId, warehouseId, d.BinId, d.BatchId, null, actor,
                    Direction.Out, qty, unitCost, bal, sourceDocType, sourceDocId, now, ct);
            }
            else
            {
                var unitCost = await LastCostAsync(d.ItemId, warehouseId, ct);
                await _fifo.CreateInboundLayerAsync(d.ItemId, warehouseId, d.BatchId, null, null, d.QtyDelta, unitCost, now, ct);
                bal += d.QtyDelta;
                entry = await _fifo.PostLedgerAsync(
                    d.ItemId, warehouseId, d.BinId, d.BatchId, null, actor,
                    Direction.In, d.QtyDelta, unitCost, bal, sourceDocType, sourceDocId, now, ct);
            }

            balances[d.ItemId] = bal;
            entries.Add(entry);
        }

        return entries;
    }

    private async Task<decimal> LastCostAsync(int itemId, int warehouseId, CancellationToken ct)
        => await _layers.Query().AsNoTracking()
            .Where(l => l.ItemId == itemId && l.WarehouseId == warehouseId)
            .OrderByDescending(l => l.ReceiptDate).ThenByDescending(l => l.LayerId)
            .Select(l => (decimal?)l.UnitCost).FirstOrDefaultAsync(ct) ?? 0m;
}
