using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Stock;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services.Interfaces;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Services.Stock;

public sealed class StockService : IStockService
{
    private readonly IFifoCostingService _fifo;
    private readonly IRepository<StockLayer> _layers;
    private readonly IRepository<StockLedger> _ledger;
    private readonly IRepository<StockTransferLine> _transferLines;

    public StockService(
        IFifoCostingService fifo, IRepository<StockLayer> layers,
        IRepository<StockLedger> ledger, IRepository<StockTransferLine> transferLines)
    {
        _fifo = fifo;
        _layers = layers;
        _ledger = ledger;
        _transferLines = transferLines;
    }

    public async Task<StockOnHandDto> GetOnHandAsync(int itemId, int warehouseId, CancellationToken ct = default)
    {
        var onHand = await _fifo.GetOnHandAsync(itemId, warehouseId, ct);

        // On-hold stock is on-hand but not issuable — sourced from GRN lines still OnHold.
        var onHold = await _layers.Query().AsNoTracking()
            .Where(l => l.ItemId == itemId && l.WarehouseId == warehouseId
                        && l.GrnLine != null && l.GrnLine.HoldStatus == HoldStatus.OnHold)
            .SumAsync(l => (decimal?)l.QtyRemaining, ct) ?? 0m;

        // Outbound in-transit: dispatched from this warehouse, not yet received at the
        // destination. Dispatch already consumed the source layers, so this stock has
        // left onHand — it is reported for visibility (AR-05) but is NOT re-subtracted
        // from available (that would double-count). reserved (sales) stays a stub.
        var inTransit = await _transferLines.Query().AsNoTracking()
            .Where(l => l.ItemId == itemId
                        && l.Transfer!.SrcWarehouseId == warehouseId
                        && l.Transfer.Status == TransferStatus.InTransit)
            .SumAsync(l => (decimal?)(l.Qty - l.QtyReceived), ct) ?? 0m;
        const decimal reserved = 0m;
        var available = onHand - onHold - reserved;

        return new StockOnHandDto(itemId, warehouseId, onHand, available, onHold, inTransit, reserved, DateTime.UtcNow);
    }

    /// <summary>
    /// On-hand across every (item, warehouse) pair that holds stock — backs the Stock
    /// Enquiry list. Deliberately set-based: four grouped queries regardless of page size,
    /// rather than calling <see cref="GetOnHandAsync"/> per row (which would be N+1).
    /// Pairs are sourced from <c>StockLayer</c>, so an item that never had a receipt in a
    /// warehouse simply does not appear.
    /// </summary>
    public async Task<PagedResponse<StockOnHandDto>> GetOnHandListAsync(
        int? itemId, int? warehouseId, PageQuery query, CancellationToken ct = default)
    {
        var layers = _layers.Query().AsNoTracking();
        if (itemId is not null) layers = layers.Where(l => l.ItemId == itemId);
        if (warehouseId is not null) layers = layers.Where(l => l.WarehouseId == warehouseId);

        var grouped = layers
            .GroupBy(l => new { l.ItemId, l.WarehouseId })
            .Select(g => new { g.Key.ItemId, g.Key.WarehouseId, OnHand = g.Sum(x => x.QtyRemaining) });

        var total = await grouped.CountAsync(ct);
        var page = await grouped
            .OrderBy(x => x.ItemId).ThenBy(x => x.WarehouseId)
            .Skip(query.Skip).Take(query.PageSize)
            .ToListAsync(ct);

        if (page.Count == 0)
            return PagedResponse<StockOnHandDto>.Create([], query.Page, query.PageSize, total);

        // Filtering by the page's ids gives a superset (the cross-product of both lists);
        // the join below narrows it back to the actual pairs.
        var itemIds = page.Select(p => p.ItemId).Distinct().ToList();
        var warehouseIds = page.Select(p => p.WarehouseId).Distinct().ToList();

        var onHold = (await _layers.Query().AsNoTracking()
            .Where(l => itemIds.Contains(l.ItemId) && warehouseIds.Contains(l.WarehouseId)
                        && l.GrnLine != null && l.GrnLine.HoldStatus == HoldStatus.OnHold)
            .GroupBy(l => new { l.ItemId, l.WarehouseId })
            .Select(g => new { g.Key.ItemId, g.Key.WarehouseId, Qty = g.Sum(x => x.QtyRemaining) })
            .ToListAsync(ct))
            .ToDictionary(x => (x.ItemId, x.WarehouseId), x => x.Qty);

        var inTransit = (await _transferLines.Query().AsNoTracking()
            .Where(l => itemIds.Contains(l.ItemId)
                        && l.Transfer != null
                        && warehouseIds.Contains(l.Transfer.SrcWarehouseId)
                        && l.Transfer.Status == TransferStatus.InTransit)
            .GroupBy(l => new { l.ItemId, WarehouseId = l.Transfer!.SrcWarehouseId })
            .Select(g => new { g.Key.ItemId, g.Key.WarehouseId, Qty = g.Sum(x => x.Qty - x.QtyReceived) })
            .ToListAsync(ct))
            .ToDictionary(x => (x.ItemId, x.WarehouseId), x => x.Qty);

        var asOf = DateTime.UtcNow;
        var rows = page.Select(p =>
        {
            var key = (p.ItemId, p.WarehouseId);
            var hold = onHold.GetValueOrDefault(key);
            var transit = inTransit.GetValueOrDefault(key);
            const decimal reserved = 0m;
            // Same formula as GetOnHandAsync: in-transit is reported, not re-subtracted.
            return new StockOnHandDto(
                p.ItemId, p.WarehouseId, p.OnHand, p.OnHand - hold - reserved, hold, transit, reserved, asOf);
        }).ToList();

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

    public async Task<PagedResponse<StockLedgerRowDto>> GetLedgerAsync(
        int? itemId, int? warehouseId, DateOnly? from, DateOnly? to,
        string? sourceDocType, int? sourceDocId, PageQuery query, CancellationToken ct = default)
    {
        var q = _ledger.Query().AsNoTracking();
        if (itemId is not null) q = q.Where(l => l.ItemId == itemId);
        if (warehouseId is not null) q = q.Where(l => l.WarehouseId == warehouseId);
        if (from is not null) q = q.Where(l => l.CreatedAt >= from.Value.ToDateTime(TimeOnly.MinValue));
        if (to is not null) q = q.Where(l => l.CreatedAt < to.Value.AddDays(1).ToDateTime(TimeOnly.MinValue));
        // Source-doc filter: the ledger references its originating document polymorphically
        // (docs/10 C.9), so this is the only way to ask "what did document X post?" —
        // needed by any screen that reports on a document's costed movements.
        if (!string.IsNullOrWhiteSpace(sourceDocType)) q = q.Where(l => l.SourceDocType == sourceDocType);
        if (sourceDocId is not null) q = q.Where(l => l.SourceDocId == sourceDocId);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(l => l.LedgerId)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(l => new StockLedgerRowDto(
                l.LedgerId, l.ItemId, l.WarehouseId, l.BinId, l.BatchId, l.SerialId,
                l.Direction, l.QtyBase, l.UnitCost, l.Value, l.RunningBalance,
                l.SourceDocType, l.SourceDocId, l.UserId, l.CreatedAt))
            .ToListAsync(ct);

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

    public Task<StockValuationDto> GetValuationAsync(int itemId, int warehouseId, CancellationToken ct = default)
        => _fifo.GetValuationAsync(itemId, warehouseId, ct);
}
