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

namespace ERPCore.Services;

/// <summary>
/// Sales-return service. Auto-posts with a mandatory Return reason code and
/// generates an inbound stock movement via the shared <see cref="IStockMutator"/>
/// (positive delta — creates an inbound FIFO layer at last cost). Single UoW
/// transaction, mirroring <see cref="PurchaseReturnService"/> with the direction
/// reversed. Also posts a real GL journal entry reversing Inventory/COGS for the
/// stock movement's value (see <see cref="SalesReturn.GlJournalNo"/> for why revenue/tax
/// aren't part of it) — reuses the Sales Day End accounts since that's the module
/// whose COGS this reverses.
/// </summary>
public sealed class SalesReturnService : ISalesReturnService
{
    private readonly IRepository<SalesReturn> _returns;
    private readonly IRepository<Customer> _customers;
    private readonly IRepository<Warehouse> _warehouses;
    private readonly IRepository<Item> _items;
    private readonly IRepository<ReasonCode> _reasonCodes;
    private readonly IRepository<SalesInvoiceLine> _salesInvoiceLines;
    private readonly IRepository<SalesReturnLine> _returnLines;
    private readonly IRepository<StockLedger> _ledger;
    private readonly IStockMutator _mutator;
    private readonly INumberSequenceService _numbers;
    private readonly ICurrentUser _currentUser;
    private readonly IUnitOfWork _uow;
    private readonly IGeneralLedgerService _gl;
    private readonly string _glInventoryAccountCode;
    private readonly string _glCogsAccountCode;

    public SalesReturnService(
        IRepository<SalesReturn> returns, IRepository<Customer> customers, IRepository<Warehouse> warehouses,
        IRepository<Item> items, IRepository<ReasonCode> reasonCodes, IRepository<SalesInvoiceLine> salesInvoiceLines,
        IRepository<SalesReturnLine> returnLines, IRepository<StockLedger> ledger, IStockMutator mutator,
        INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow,
        IGeneralLedgerService gl, IConfiguration configuration)
    {
        _returns = returns;
        _customers = customers;
        _warehouses = warehouses;
        _items = items;
        _reasonCodes = reasonCodes;
        _salesInvoiceLines = salesInvoiceLines;
        _returnLines = returnLines;
        _ledger = ledger;
        _mutator = mutator;
        _numbers = numbers;
        _currentUser = currentUser;
        _uow = uow;
        _gl = gl;
        _glInventoryAccountCode = configuration["SalesDayEnd:GlInventoryAccountCode"] ?? string.Empty;
        _glCogsAccountCode = configuration["SalesDayEnd:GlCogsAccountCode"] ?? string.Empty;
    }

    public async Task<PagedResponse<SalesReturnSummaryDto>> ListAsync(
        PageQuery query, int? customerId, int? warehouseId, CancellationToken ct = default)
    {
        var q = _returns.Query().AsNoTracking();

        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(r => EF.Functions.ILike(r.DocNo, $"%{term}%"));
        }
        if (customerId is not null) q = q.Where(r => r.CustomerId == customerId);
        if (warehouseId is not null) q = q.Where(r => r.WarehouseId == warehouseId);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(r => r.ReturnId)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(r => new SalesReturnSummaryDto(
                r.ReturnId, r.DocNo, r.CustomerId, r.WarehouseId, r.ReasonCodeId, r.Status,
                r.CreatedBy, r.CreatedAt, r.Lines.Count, r.Lines.Sum(l => l.Qty), r.GlJournalNo))
            .ToListAsync(ct);

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

    public async Task<SalesReturnDto?> GetAsync(int returnId, CancellationToken ct = default)
    {
        var ret = await _returns.Query().AsNoTracking()
            .Include(r => r.Lines)
            .FirstOrDefaultAsync(r => r.ReturnId == returnId, ct);
        if (ret is null) return null;

        // Polymorphic ledger reference — recovered by source-doc lookup.
        var ledgerRefs = await _ledger.Query().AsNoTracking()
            .Where(l => l.SourceDocType == DocumentTypes.SalesReturn && l.SourceDocId == returnId)
            .OrderBy(l => l.LedgerId)
            .Select(l => l.LedgerId)
            .ToListAsync(ct);

        return ToDto(ret, ledgerRefs);
    }

    public async Task<SalesReturnDto> CreateAsync(CreateSalesReturnRequest request, CancellationToken ct = default)
    {
        if (request.ReasonCodeId is null)
            throw new DomainException(ErrorCodes.ReasonCodeRequired, "A reason code is required for a sales return.", 400);

        if (!await _customers.Query().AnyAsync(c => c.CustomerId == request.CustomerId, ct))
            throw new DomainException(ErrorCodes.Validation, $"Customer {request.CustomerId} does not exist.", 422);
        if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.WarehouseId, ct))
            throw new DomainException(ErrorCodes.Validation, $"Warehouse {request.WarehouseId} does not exist.", 422);

        var reason = await _reasonCodes.Query().AsNoTracking().FirstOrDefaultAsync(r => r.ReasonCodeId == request.ReasonCodeId, ct)
            ?? throw new DomainException(ErrorCodes.Validation, $"Reason code {request.ReasonCodeId} does not exist.", 422);
        if (reason.Context != ReasonContext.Return)
            throw new DomainException(ErrorCodes.Validation, $"Reason code {request.ReasonCodeId} is not a Return reason.", 422);

        // Original sold qty is never mutated — "remaining returnable" is computed from
        // return history instead, so the invoice keeps recording what was actually sold.
        var pendingByInvoiceLine = new Dictionary<int, decimal>();

        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);
            if (line.SalesInvoiceLineId is not null)
            {
                var invoiceLineId = line.SalesInvoiceLineId.Value;
                var invoiceLine = await _salesInvoiceLines.Query().AsNoTracking().FirstOrDefaultAsync(l => l.SalesInvoiceLineId == invoiceLineId, ct)
                    ?? throw new DomainException(ErrorCodes.Validation, $"Sales invoice line {invoiceLineId} does not exist.", 422);
                if (invoiceLine.ItemId != line.ItemId)
                    throw new DomainException(ErrorCodes.Validation, $"Sales invoice line {invoiceLineId} is for a different item.", 422);

                var alreadyReturned = await _returnLines.Query().AsNoTracking()
                    .Where(l => l.SalesInvoiceLineId == invoiceLineId)
                    .SumAsync(l => (decimal?)l.Qty, ct) ?? 0m;
                pendingByInvoiceLine.TryGetValue(invoiceLineId, out var pending);
                var remaining = invoiceLine.Qty - alreadyReturned - pending;

                if (line.Qty > remaining)
                    throw new DomainException(ErrorCodes.Validation, $"Insufficient quantity — only {remaining} remain returnable on sales invoice line {invoiceLineId} (requested {line.Qty}).", 422);
                pendingByInvoiceLine[invoiceLineId] = pending + line.Qty;
            }
        }

        var now = DateTime.UtcNow;
        var deltas = request.Lines.Select(l => new StockDelta(l.ItemId, null, null, l.Qty)).ToList();

        var (entity, ledgerEntries) = await _uow.ExecuteInTransactionAsync(async token =>
        {
            var docNo = await _numbers.NextAsync(DocumentTypes.SalesReturn, token);
            var ret = new SalesReturn
            {
                DocNo = docNo,
                CustomerId = request.CustomerId,
                WarehouseId = request.WarehouseId,
                ReasonCodeId = request.ReasonCodeId.Value,
                Status = ReturnStatus.Posted,
                CreatedBy = _currentUser.AuditUserId,
                CreatedAt = now,
                Lines = request.Lines.Select(l => new SalesReturnLine
                {
                    SalesInvoiceLineId = l.SalesInvoiceLineId,
                    ItemId = l.ItemId,
                    Qty = l.Qty
                }).ToList()
            };
            await _returns.AddAsync(ret, token);
            await _uow.SaveChangesAsync(token); // flush for a valid ledger sourceDocId

            var refs = await _mutator.ApplyAsync(request.WarehouseId, DocumentTypes.SalesReturn, ret.ReturnId, now, deltas, token);

            var totalValue = refs.Sum(r => r.Value);
            if (totalValue > 0)
            {
                var period = await _gl.GetPeriodByDateAsync(DateOnly.FromDateTime(now), token);
                var glResult = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest
                {
                    PeriodId = period.PeriodId,
                    EntryDate = DateOnly.FromDateTime(now),
                    SourceModule = "SALES-RETURN",
                    Reference = docNo,
                    Narration = $"Sales return {docNo} — stock restocked",
                    Lines = new List<GlJournalEntryLineRequest>
                    {
                        new(_glInventoryAccountCode, totalValue, 0m, $"Sales return {docNo}"),
                        new(_glCogsAccountCode, 0m, totalValue, $"Sales return {docNo} — COGS reversal")
                    }
                }, token);
                ret.GlJournalNo = glResult.JournalNo;
                ret.GlPostedAt = now;
            }

            return (ret, refs);
        }, ct);

        // Map ledger ids after commit so they are populated.
        return ToDto(entity, ledgerEntries.Select(r => r.LedgerId).ToList());
    }

    public async Task<IReadOnlyList<SalesInvoiceLineRemainingDto>> GetRemainingByInvoiceAsync(int salesInvoiceId, CancellationToken ct = default)
    {
        var lines = await _salesInvoiceLines.Query().AsNoTracking()
            .Where(l => l.SalesInvoiceId == salesInvoiceId)
            .Select(l => new { l.SalesInvoiceLineId, l.Qty })
            .ToListAsync(ct);

        var lineIds = lines.Select(l => l.SalesInvoiceLineId).ToList();
        var returnedByLine = await _returnLines.Query().AsNoTracking()
            .Where(l => l.SalesInvoiceLineId != null && lineIds.Contains(l.SalesInvoiceLineId.Value))
            .GroupBy(l => l.SalesInvoiceLineId!.Value)
            .Select(g => new { SalesInvoiceLineId = g.Key, Returned = g.Sum(x => x.Qty) })
            .ToDictionaryAsync(x => x.SalesInvoiceLineId, x => x.Returned, ct);

        return lines
            .Select(l => new SalesInvoiceLineRemainingDto(
                l.SalesInvoiceLineId,
                l.Qty - (returnedByLine.TryGetValue(l.SalesInvoiceLineId, out var returned) ? returned : 0m)))
            .ToList();
    }

    private static SalesReturnDto ToDto(SalesReturn r, IReadOnlyList<int> ledgerRefs) => new(
        r.ReturnId, r.DocNo, r.CustomerId, r.WarehouseId, r.ReasonCodeId, r.Status, r.CreatedBy, r.CreatedAt,
        r.GlJournalNo, r.GlPostedAt,
        r.Lines.OrderBy(l => l.ReturnLineId)
            .Select(l => new SalesReturnLineDto(l.ReturnLineId, l.SalesInvoiceLineId, l.ItemId, l.Qty)).ToList(),
        ledgerRefs);
}
