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

namespace ERPCore.Services;

/// <summary>
/// Inter-warehouse transfer service (FR-STK-05/06). Dispatch consumes source FIFO
/// layers (row-locked; negative-stock blocked) and records the value-weighted cost
/// on the line; receive recreates the destination layer at that cost
/// (cost-preserving — no revaluation). Both run in a single UoW transaction.
///
/// Deliberately posts no GL journal entry: <see cref="Warehouse"/> carries no GL
/// account of its own (unlike a GL "location"/cost-center dimension), so every
/// warehouse's stock sits in the same Inventory account — a transfer would debit and
/// credit that identical account for the identical amount, a no-op journal entry that
/// exists only to say nothing. The per-warehouse movement is still fully recorded in
/// <see cref="StockLedger"/>/<see cref="StockLayer"/>, which is what On-Hand-by-warehouse
/// reporting actually reads from.
/// </summary>
public sealed class TransferService : ITransferService
{
    private readonly IRepository<StockTransfer> _transfers;
    private readonly IRepository<Warehouse> _warehouses;
    private readonly IRepository<Item> _items;
    private readonly IFifoCostingService _fifo;
    private readonly INumberSequenceService _numbers;
    private readonly ICurrentUser _currentUser;
    private readonly IUnitOfWork _uow;

    public TransferService(
        IRepository<StockTransfer> transfers, IRepository<Warehouse> warehouses, IRepository<Item> items,
        IFifoCostingService fifo, INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow)
    {
        _transfers = transfers;
        _warehouses = warehouses;
        _items = items;
        _fifo = fifo;
        _numbers = numbers;
        _currentUser = currentUser;
        _uow = uow;
    }

    public async Task<PagedResponse<TransferSummaryDto>> ListAsync(
        PageQuery query, TransferStatus? status, int? srcWarehouseId, int? destWarehouseId, CancellationToken ct = default)
    {
        var q = _transfers.Query().AsNoTracking();

        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(t => EF.Functions.ILike(t.DocNo, $"%{term}%"));
        }
        if (status is not null) q = q.Where(t => t.Status == status);
        if (srcWarehouseId is not null) q = q.Where(t => t.SrcWarehouseId == srcWarehouseId);
        if (destWarehouseId is not null) q = q.Where(t => t.DestWarehouseId == destWarehouseId);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(t => t.TransferId)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(t => new TransferSummaryDto(
                t.TransferId, t.DocNo, t.SrcWarehouseId, t.DestWarehouseId, t.Status,
                t.CreatedBy, t.CreatedAt, t.Lines.Count))
            .ToListAsync(ct);

        return PagedResponse<TransferSummaryDto>.Create(rows, query.Page, query.PageSize, total);
    }

    public async Task<TransferDto?> GetAsync(int transferId, CancellationToken ct = default)
    {
        var t = await _transfers.Query().AsNoTracking().Include(x => x.Lines)
            .FirstOrDefaultAsync(x => x.TransferId == transferId, ct);
        return t is null ? null : Map(t);
    }

    public async Task<TransferDto> CreateAsync(CreateTransferRequest request, CancellationToken ct = default)
    {
        if (request.SrcWarehouseId == request.DestWarehouseId)
            throw new DomainException(ErrorCodes.Validation, "destWarehouseId must differ from srcWarehouseId.", 422);
        await EnsureWarehouseAsync(request.SrcWarehouseId, ct);
        await EnsureWarehouseAsync(request.DestWarehouseId, ct);
        foreach (var line in request.Lines)
            if (!await _items.Query().AnyAsync(i => i.ItemId == line.ItemId, ct))
                throw new DomainException(ErrorCodes.Validation, $"Item {line.ItemId} does not exist.", 422);

        var actor = _currentUser.AuditUserId;
        var transfer = await _uow.ExecuteInTransactionAsync(async token =>
        {
            var docNo = await _numbers.NextAsync(DocumentTypes.Transfer, token);
            var entity = new StockTransfer
            {
                DocNo = docNo,
                SrcWarehouseId = request.SrcWarehouseId,
                DestWarehouseId = request.DestWarehouseId,
                Status = TransferStatus.Draft,
                CreatedBy = actor,
                CreatedAt = DateTime.UtcNow,
                Lines = request.Lines.Select(l => new StockTransferLine
                {
                    ItemId = l.ItemId,
                    SrcBinId = l.SrcBinId,
                    DestBinId = l.DestBinId,
                    BatchId = l.BatchId,
                    Qty = l.Qty
                }).ToList()
            };
            await _transfers.AddAsync(entity, token);
            return entity;
        }, ct);

        return Map(transfer);
    }

    public async Task<DispatchResultDto> DispatchAsync(int transferId, CancellationToken ct = default)
    {
        var transfer = await _transfers.Query().Include(t => t.Lines)
            .FirstOrDefaultAsync(t => t.TransferId == transferId, ct)
            ?? throw new NotFoundException($"Transfer {transferId} was not found.");
        if (transfer.Status != TransferStatus.Draft)
            throw new ConflictException($"Transfer {transferId} is {transfer.Status}; only a Draft can be dispatched.");

        var actor = _currentUser.AuditUserId;
        var now = DateTime.UtcNow;
        var consumed = new List<ConsumedLayerDto>();
        var ledgerRefs = new List<StockLedger>();
        var balances = new Dictionary<int, decimal>();

        await _uow.ExecuteInTransactionAsync(async token =>
        {
            foreach (var line in transfer.Lines.OrderBy(l => l.TransferLineId))
            {
                var segments = await _fifo.ConsumeAsync(line.ItemId, transfer.SrcWarehouseId, line.BatchId, line.Qty, token);
                var value = segments.Sum(s => s.Qty * s.UnitCost);
                line.UnitCost = value / line.Qty; // value-weighted cost, preserved to receive

                if (!balances.TryGetValue(line.ItemId, out var bal))
                    bal = await _fifo.GetOnHandAsync(line.ItemId, transfer.SrcWarehouseId, token);
                bal -= line.Qty;
                balances[line.ItemId] = bal;

                var entry = await _fifo.PostLedgerAsync(
                    line.ItemId, transfer.SrcWarehouseId, line.SrcBinId, line.BatchId, null, actor,
                    Direction.Out, line.Qty, line.UnitCost.Value, bal, DocumentTypes.Transfer, transfer.TransferId, now, token);
                ledgerRefs.Add(entry);

                consumed.AddRange(segments.Select(s => new ConsumedLayerDto(s.LayerId, s.Qty, s.UnitCost)));
            }

            transfer.Status = TransferStatus.InTransit;
            return 0;
        }, ct);

        return new DispatchResultDto(transfer.TransferId, transfer.Status, consumed, ledgerRefs.Select(l => l.LedgerId).ToList());
    }

    public async Task<ReceiveResultDto> ReceiveAsync(int transferId, ReceiveTransferRequest request, CancellationToken ct = default)
    {
        var transfer = await _transfers.Query().Include(t => t.Lines)
            .FirstOrDefaultAsync(t => t.TransferId == transferId, ct)
            ?? throw new NotFoundException($"Transfer {transferId} was not found.");
        if (transfer.Status != TransferStatus.InTransit)
            throw new ConflictException($"Transfer {transferId} is {transfer.Status}; only an InTransit transfer can be received.");

        var actor = _currentUser.AuditUserId;
        var now = DateTime.UtcNow;
        var createdLayers = new List<StockLayer>();
        var ledgerRefs = new List<StockLedger>();
        var balances = new Dictionary<int, decimal>();

        await _uow.ExecuteInTransactionAsync(async token =>
        {
            foreach (var input in request.Lines)
            {
                var line = transfer.Lines.FirstOrDefault(l => l.TransferLineId == input.TransferLineId)
                    ?? throw new DomainException(ErrorCodes.Validation, $"Transfer line {input.TransferLineId} is not on transfer {transferId}.", 422);

                var outstanding = line.Qty - line.QtyReceived;
                if (input.Qty > outstanding)
                    throw new DomainException(ErrorCodes.Validation,
                        $"Receiving {input.Qty} exceeds the outstanding in-transit {outstanding} on line {input.TransferLineId}.", 422);

                var unitCost = line.UnitCost ?? 0m;
                var layer = await _fifo.CreateInboundLayerAsync(
                    line.ItemId, transfer.DestWarehouseId, line.BatchId, null, null, input.Qty, unitCost, now, token);
                createdLayers.Add(layer);

                if (!balances.TryGetValue(line.ItemId, out var bal))
                    bal = await _fifo.GetOnHandAsync(line.ItemId, transfer.DestWarehouseId, token);
                bal += input.Qty;
                balances[line.ItemId] = bal;

                var entry = await _fifo.PostLedgerAsync(
                    line.ItemId, transfer.DestWarehouseId, line.DestBinId, line.BatchId, null, actor,
                    Direction.In, input.Qty, unitCost, bal, DocumentTypes.Transfer, transfer.TransferId, now, token);
                ledgerRefs.Add(entry);

                line.QtyReceived += input.Qty;
            }

            if (transfer.Lines.All(l => l.QtyReceived >= l.Qty))
                transfer.Status = TransferStatus.Received;
            return 0;
        }, ct);

        // Map after commit so layer ids are populated.
        var created = createdLayers
            .Select(l => new TransferCreatedLayerDto(l.LayerId, l.WarehouseId, l.QtyReceived, l.UnitCost))
            .ToList();
        return new ReceiveResultDto(transfer.TransferId, transfer.Status, created, ledgerRefs.Select(l => l.LedgerId).ToList());
    }

    private async Task EnsureWarehouseAsync(int warehouseId, CancellationToken ct)
    {
        if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == warehouseId, ct))
            throw new DomainException(ErrorCodes.Validation, $"Warehouse {warehouseId} does not exist.", 422);
    }

    private static TransferDto Map(StockTransfer t) => new(
        t.TransferId, t.DocNo, t.SrcWarehouseId, t.DestWarehouseId, t.Status, t.CreatedBy, t.CreatedAt,
        t.Lines.OrderBy(l => l.TransferLineId).Select(l => new TransferLineDto(
            l.TransferLineId, l.ItemId, l.SrcBinId, l.DestBinId, l.BatchId, l.Qty, l.QtyReceived)).ToList());
}
