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

namespace ERPCore.Services;

public sealed class SalesPostingService : ISalesPostingService
{
    private readonly IRepository<SalesInvoice> _invoices;
    private readonly IRepository<SalesSlip> _slips;
    private readonly IRepository<BundleSale> _bundles;
    private readonly IRepository<Item> _items;
    private readonly IFifoCostingService _fifo;
    private readonly ISalesDomainService _sales;
    private readonly ICurrentUser _currentUser;
    private readonly IUnitOfWork _uow;

    public SalesPostingService(
        IRepository<SalesInvoice> invoices,
        IRepository<SalesSlip> slips,
        IRepository<BundleSale> bundles,
        IRepository<Item> items,
        IFifoCostingService fifo,
        ISalesDomainService sales,
        ICurrentUser currentUser,
        IUnitOfWork uow)
    {
        _invoices = invoices;
        _slips = slips;
        _bundles = bundles;
        _items = items;
        _fifo = fifo;
        _sales = sales;
        _currentUser = currentUser;
        _uow = uow;
    }

    public async Task<SalesInvoicePostingCheckDto> CheckInvoiceAsync(int salesInvoiceId, CancellationToken ct = default)
    {
        var invoice = await _invoices.Query().AsNoTracking().Include(x => x.Lines)
            .FirstOrDefaultAsync(x => x.SalesInvoiceId == salesInvoiceId, ct)
            ?? throw new NotFoundException($"Sales invoice {salesInvoiceId} was not found.");

        if (invoice.Status != SalesInvoiceStatus.Draft)
            return new SalesInvoicePostingCheckDto(invoice.SalesInvoiceId, invoice.InvoiceNo, invoice.Status, false, Array.Empty<SalesInvoicePostingIssueDto>());

        var issues = new List<SalesInvoicePostingIssueDto>();
        foreach (var line in invoice.Lines)
        {
            if (!await _sales.IsStockedItemAsync(line.ItemId, ct))
                continue;
            var requestedQty = line.Qty + line.FreeQty;
            var available = await _fifo.GetOnHandAsync(line.ItemId, line.WarehouseId, ct);
            if (available >= requestedQty) continue;

            var item = await _items.Query().AsNoTracking()
                .Where(x => x.ItemId == line.ItemId)
                .Select(x => new { x.Sku, x.Name })
                .FirstAsync(ct);

            issues.Add(new SalesInvoicePostingIssueDto(
                line.SalesInvoiceLineId, line.ItemId, item.Sku, item.Name, line.WarehouseId,
                requestedQty, available, requestedQty - available, line.IsFreeIssue));
        }

        return new SalesInvoicePostingCheckDto(invoice.SalesInvoiceId, invoice.InvoiceNo, invoice.Status, issues.Count == 0, issues);
    }

    public async Task<SalesSlipPostingCheckDto> CheckSlipAsync(int salesSlipId, CancellationToken ct = default)
    {
        var slip = await _slips.Query().AsNoTracking().Include(x => x.Lines)
            .FirstOrDefaultAsync(x => x.SalesSlipId == salesSlipId, ct)
            ?? throw new NotFoundException($"Sales slip {salesSlipId} was not found.");

        if (slip.Status != SalesSlipStatus.Draft)
            return new SalesSlipPostingCheckDto(slip.SalesSlipId, slip.SlipNo, slip.Status, false, Array.Empty<SalesSlipPostingIssueDto>());

        var issues = new List<SalesSlipPostingIssueDto>();
        foreach (var line in slip.Lines)
        {
            if (!await _sales.IsStockedItemAsync(line.ItemId, ct))
                continue;
            var requestedQty = line.Qty + line.FreeQty;
            var available = await _fifo.GetOnHandAsync(line.ItemId, line.WarehouseId, ct);
            if (available >= requestedQty) continue;

            var item = await _items.Query().AsNoTracking()
                .Where(x => x.ItemId == line.ItemId)
                .Select(x => new { x.Sku, x.Name })
                .FirstAsync(ct);

            issues.Add(new SalesSlipPostingIssueDto(
                line.SalesSlipLineId, line.ItemId, item.Sku, item.Name, line.WarehouseId,
                requestedQty, available, requestedQty - available, line.IsFreeIssue));
        }

        return new SalesSlipPostingCheckDto(slip.SalesSlipId, slip.SlipNo, slip.Status, issues.Count == 0, issues);
    }

    public async Task<BundleSalePostingCheckDto> CheckBundleAsync(int bundleSaleId, CancellationToken ct = default)
    {
        var bundle = await _bundles.Query().AsNoTracking().Include(x => x.Lines)
            .FirstOrDefaultAsync(x => x.BundleSaleId == bundleSaleId, ct)
            ?? throw new NotFoundException($"Bundle sale {bundleSaleId} was not found.");

        if (bundle.Status != BundleSaleStatus.Draft)
            return new BundleSalePostingCheckDto(bundle.BundleSaleId, bundle.BundleNo, bundle.Status, false, Array.Empty<BundleSalePostingIssueDto>());

        var issues = new List<BundleSalePostingIssueDto>();
        foreach (var line in bundle.Lines.Where(l => l.IncludeInBundle))
        {
            if (!await _sales.IsStockedItemAsync(line.ItemId, ct))
                continue;

            var item = await _items.Query().AsNoTracking()
                .FirstAsync(x => x.ItemId == line.ItemId, ct);
            var available = await _fifo.GetOnHandAsync(line.ItemId, line.WarehouseId, ct);
            if (available >= line.Qty) continue;

            issues.Add(new BundleSalePostingIssueDto(
                line.BundleSaleLineId, line.ItemId, item.Sku, item.Name, line.WarehouseId, line.Qty, available, line.Qty - available));
        }

        return new BundleSalePostingCheckDto(bundle.BundleSaleId, bundle.BundleNo, bundle.Status, issues.Count == 0, issues);
    }

    public Task PostInvoiceAsync(int salesInvoiceId, CancellationToken ct = default)
        => PostAsync(
            load: () => _invoices.Query().Include(x => x.Lines).FirstOrDefaultAsync(x => x.SalesInvoiceId == salesInvoiceId, ct),
            notFoundMessage: $"Sales invoice {salesInvoiceId} was not found.",
            statusSelector: x => x.Status,
            ensureDraftMessage: x => $"Sales invoice {x.SalesInvoiceId} is {x.Status} and cannot be posted.",
            getLines: x => x.Lines.Select(l => new PostingLine(l.ItemId, l.WarehouseId, l.Qty + l.FreeQty, l.Qty, l.FreeQty)),
            setPosted: x => x.Status = SalesInvoiceStatus.Posted,
            setUpdated: x => x.UpdatedAt = DateTime.UtcNow,
            sourceDocType: DocumentTypes.SalesInvoice,
            getDocId: x => x.SalesInvoiceId,
            ct: ct);

    public Task PostSlipAsync(int salesSlipId, CancellationToken ct = default)
        => PostAsync(
            load: () => _slips.Query().Include(x => x.Lines).FirstOrDefaultAsync(x => x.SalesSlipId == salesSlipId, ct),
            notFoundMessage: $"Sales slip {salesSlipId} was not found.",
            statusSelector: x => x.Status,
            ensureDraftMessage: x => $"Sales slip {x.SalesSlipId} is {x.Status} and cannot be posted.",
            getLines: x => x.Lines.Select(l => new PostingLine(l.ItemId, l.WarehouseId, l.Qty + l.FreeQty, l.Qty, l.FreeQty)),
            setPosted: x => x.Status = SalesSlipStatus.Posted,
            setUpdated: x => x.UpdatedAt = DateTime.UtcNow,
            sourceDocType: DocumentTypes.SalesSlip,
            getDocId: x => x.SalesSlipId,
            ct: ct);

    public Task PostBundleAsync(int bundleSaleId, CancellationToken ct = default)
        => PostAsync(
            load: () => _bundles.Query().Include(x => x.Lines).FirstOrDefaultAsync(x => x.BundleSaleId == bundleSaleId, ct),
            notFoundMessage: $"Bundle sale {bundleSaleId} was not found.",
            statusSelector: x => x.Status,
            ensureDraftMessage: x => $"Bundle sale {x.BundleSaleId} is {x.Status} and cannot be posted.",
            getLines: x => x.Lines.Where(l => l.IncludeInBundle).Select(l => new PostingLine(l.ItemId, l.WarehouseId, l.Qty, l.Qty, 0m)),
            setPosted: x => x.Status = BundleSaleStatus.Posted,
            setUpdated: x => x.UpdatedAt = DateTime.UtcNow,
            sourceDocType: DocumentTypes.BundleSale,
            getDocId: x => x.BundleSaleId,
            ct: ct);

    private async Task PostAsync<T>(
        Func<Task<T?>> load,
        string notFoundMessage,
        Func<T, object> statusSelector,
        Func<T, string> ensureDraftMessage,
        Func<T, IEnumerable<PostingLine>> getLines,
        Action<T> setPosted,
        Action<T> setUpdated,
        string sourceDocType,
        Func<T, int> getDocId,
        CancellationToken ct)
        where T : class
    {
        var doc = await load() ?? throw new NotFoundException(notFoundMessage);
        var status = statusSelector(doc);
        var statusValue = status?.ToString() ?? string.Empty;
        if (!string.Equals(statusValue, "Draft", StringComparison.Ordinal))
            throw new ConflictException(ensureDraftMessage(doc));

        await _uow.ExecuteInTransactionAsync(async token =>
        {
            foreach (var line in getLines(doc))
            {
                if (line.Qty <= 0) continue;
                if (!await _sales.IsStockedItemAsync(line.ItemId, token))
                    continue;

                // Line quantities are already a count of the item's base UOM — sales documents
                // carry no unit of their own — so this is the quantity FIFO consumes verbatim.
                var consumed = await _fifo.ConsumeAsync(line.ItemId, line.WarehouseId, null, line.Qty, token);
                var cost = consumed.Count == 0 ? 0m : consumed.Sum(x => x.Qty * x.UnitCost) / consumed.Sum(x => x.Qty);
                await _fifo.PostLedgerAsync(line.ItemId, line.WarehouseId, null, null, null, _currentUser.AuditUserId,
                    Direction.Out, line.Qty, cost, 0m, sourceDocType, getDocId(doc), DateTime.UtcNow, token);
            }

            setPosted(doc);
            setUpdated(doc);
        }, ct);
    }

    private sealed record PostingLine(int ItemId, int WarehouseId, decimal Qty, decimal PaidQty, decimal FreeQty);
}
