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;
using Microsoft.Extensions.Configuration;

namespace ERPCore.Services;

/// <summary>
/// Cashier day-end close-out. A day's <see cref="SalesSlip"/>s and <see cref="BundleSale"/>s —
/// both cashier documents with the same Draft→Posted lifecycle, just priced differently — are
/// rung up individually through the day with no GL impact of their own; <see cref="CloseAsync"/>
/// is the single point where that day's sales become real accounting entries — one consolidated
/// journal entry per cashier per day, mirroring how <see cref="GrnService"/> posts per receipt.
/// Every document is treated as a cash sale (Debit Cash for GrandTotal): SalesSlip's
/// PaidAmount/BalanceAmount fields exist for a future partial-payment flow but nothing
/// sets them today (SalesSlipService.CreateAsync always leaves PaidAmount at 0), so
/// they aren't a usable signal here — see docs/14-BACKEND-SALES-API.md if that changes.
/// A bundle sale's net revenue is always exactly <c>BundlePrice</c> (<c>GrandTotal - TaxTotal</c>
/// by construction, docs/15) regardless of whether the bundle sold at a discount or a premium to
/// its component subtotal, so it folds into Subtotal/DiscountTotal the same way a slip's net
/// (Subtotal - DiscountTotal) does, without a separate "bundle discount" GL line.
/// </summary>
public sealed class SalesDayEndService : ISalesDayEndService
{
    private readonly IRepository<SalesDayEnd> _dayEnds;
    private readonly IRepository<SalesSlip> _slips;
    private readonly IRepository<BundleSale> _bundles;
    private readonly IRepository<StockLedger> _ledger;
    private readonly IRepository<Item> _items;
    private readonly INumberSequenceService _numbers;
    private readonly ICurrentUser _currentUser;
    private readonly IUnitOfWork _uow;
    private readonly IGeneralLedgerService _gl;
    private readonly string _glCashAccountCode;
    private readonly string _glSalesRevenueAccountCode;
    private readonly string _glSalesDiscountAccountCode;
    private readonly string _glTaxPayableAccountCode;
    private readonly string _glCogsAccountCode;
    private readonly string _glInventoryAccountCode;

    public SalesDayEndService(
        IRepository<SalesDayEnd> dayEnds, IRepository<SalesSlip> slips, IRepository<BundleSale> bundles,
        IRepository<StockLedger> ledger, IRepository<Item> items, INumberSequenceService numbers,
        ICurrentUser currentUser, IUnitOfWork uow, IGeneralLedgerService gl, IConfiguration configuration)
    {
        _dayEnds = dayEnds;
        _slips = slips;
        _bundles = bundles;
        _ledger = ledger;
        _items = items;
        _numbers = numbers;
        _currentUser = currentUser;
        _uow = uow;
        _gl = gl;
        _glCashAccountCode = configuration["SalesDayEnd:GlCashAccountCode"] ?? string.Empty;
        _glSalesRevenueAccountCode = configuration["SalesDayEnd:GlSalesRevenueAccountCode"] ?? string.Empty;
        _glSalesDiscountAccountCode = configuration["SalesDayEnd:GlSalesDiscountAccountCode"] ?? string.Empty;
        _glTaxPayableAccountCode = configuration["SalesDayEnd:GlTaxPayableAccountCode"] ?? string.Empty;
        _glCogsAccountCode = configuration["SalesDayEnd:GlCogsAccountCode"] ?? string.Empty;
        _glInventoryAccountCode = configuration["SalesDayEnd:GlInventoryAccountCode"] ?? string.Empty;
    }

    public async Task<SalesDayEndPreviewDto> PreviewAsync(int cashierUserId, DateOnly businessDate, CancellationToken ct = default)
    {
        var existing = await _dayEnds.Query().AsNoTracking()
            .FirstOrDefaultAsync(x => x.CashierUserId == cashierUserId && x.BusinessDate == businessDate, ct);
        if (existing is not null)
        {
            var closedSlipNumbers = await _slips.Query().AsNoTracking()
                .Where(x => x.DayEndId == existing.SalesDayEndId)
                .OrderBy(x => x.SlipNo)
                .Select(x => x.SlipNo)
                .ToListAsync(ct);
            var closedBundleNumbers = await _bundles.Query().AsNoTracking()
                .Where(x => x.DayEndId == existing.SalesDayEndId)
                .OrderBy(x => x.BundleNo)
                .Select(x => x.BundleNo)
                .ToListAsync(ct);
            return new SalesDayEndPreviewDto(
                cashierUserId, businessDate, true,
                existing.SlipCount, existing.BundleCount, existing.Subtotal, existing.DiscountTotal, existing.TaxTotal, existing.GrandTotal,
                closedSlipNumbers, closedBundleNumbers,
                Array.Empty<DraftSlipBlockingCloseDto>(), Array.Empty<DraftBundleBlockingCloseDto>());
        }

        var openSlips = await LoadOpenSlipsAsync(cashierUserId, businessDate, ct);
        var postedSlips = openSlips.Where(x => x.Status == SalesSlipStatus.Posted).ToList();
        var draftSlips = openSlips.Where(x => x.Status == SalesSlipStatus.Draft).ToList();

        var openBundles = await LoadOpenBundleSalesAsync(cashierUserId, businessDate, ct);
        var postedBundles = openBundles.Where(x => x.Status == BundleSaleStatus.Posted).ToList();
        var draftBundles = openBundles.Where(x => x.Status == BundleSaleStatus.Draft).ToList();

        var subtotal = postedSlips.Sum(x => x.Subtotal) + postedBundles.Sum(x => x.BundlePrice);
        var discountTotal = postedSlips.Sum(x => x.DiscountTotal);
        var taxTotal = postedSlips.Sum(x => x.TaxTotal) + postedBundles.Sum(x => x.TaxTotal);
        var grandTotal = postedSlips.Sum(x => x.GrandTotal) + postedBundles.Sum(x => x.GrandTotal);

        return new SalesDayEndPreviewDto(
            cashierUserId, businessDate, false,
            postedSlips.Count, postedBundles.Count, subtotal, discountTotal, taxTotal, grandTotal,
            postedSlips.OrderBy(x => x.SlipNo).Select(x => x.SlipNo).ToList(),
            postedBundles.OrderBy(x => x.BundleNo).Select(x => x.BundleNo).ToList(),
            draftSlips.OrderBy(x => x.SlipNo).Select(x => new DraftSlipBlockingCloseDto(x.SalesSlipId, x.SlipNo, x.GrandTotal)).ToList(),
            draftBundles.OrderBy(x => x.BundleNo).Select(x => new DraftBundleBlockingCloseDto(x.BundleSaleId, x.BundleNo, x.GrandTotal)).ToList());
    }

    public async Task<SalesDayEndDto> CloseAsync(CreateSalesDayEndRequest request, CancellationToken ct = default)
    {
        var businessDate = request.BusinessDate ?? DateOnly.FromDateTime(DateTime.UtcNow);

        // Idempotent replay (same pattern as GrnService.ConfirmAsync): closing an
        // already-closed date returns the existing record instead of erroring.
        var existing = await _dayEnds.Query()
            .FirstOrDefaultAsync(x => x.CashierUserId == request.CashierUserId && x.BusinessDate == businessDate, ct);
        if (existing is not null)
            return await BuildDtoAsync(existing, ct);

        var openSlips = await LoadOpenSlipsAsync(request.CashierUserId, businessDate, ct);
        var draftSlips = openSlips.Where(x => x.Status == SalesSlipStatus.Draft).ToList();
        var openBundles = await LoadOpenBundleSalesAsync(request.CashierUserId, businessDate, ct);
        var draftBundles = openBundles.Where(x => x.Status == BundleSaleStatus.Draft).ToList();

        if (draftSlips.Count > 0 || draftBundles.Count > 0)
        {
            var parts = new List<string>();
            if (draftSlips.Count > 0)
                parts.Add($"{draftSlips.Count} sales slip(s): " + string.Join(", ", draftSlips.OrderBy(x => x.SlipNo).Select(x => x.SlipNo)));
            if (draftBundles.Count > 0)
                parts.Add($"{draftBundles.Count} bundle sale(s): " + string.Join(", ", draftBundles.OrderBy(x => x.BundleNo).Select(x => x.BundleNo)));
            throw new ConflictException(
                "Still Draft for this cashier/date — post or cancel them before closing the day: " + string.Join("; ", parts));
        }

        var postedSlips = openSlips.Where(x => x.Status == SalesSlipStatus.Posted).ToList();
        var settledSlips = openSlips.Where(x => x.Status != SalesSlipStatus.Draft).ToList(); // Posted + Cancelled — everything gets frozen against this close
        var postedBundles = openBundles.Where(x => x.Status == BundleSaleStatus.Posted).ToList();
        var settledBundles = openBundles.Where(x => x.Status != BundleSaleStatus.Draft).ToList();

        var subtotal = postedSlips.Sum(x => x.Subtotal) + postedBundles.Sum(x => x.BundlePrice);
        var discountTotal = postedSlips.Sum(x => x.DiscountTotal);
        var taxTotal = postedSlips.Sum(x => x.TaxTotal) + postedBundles.Sum(x => x.TaxTotal);
        var grandTotal = postedSlips.Sum(x => x.GrandTotal) + postedBundles.Sum(x => x.GrandTotal);

        var slipIds = postedSlips.Select(x => x.SalesSlipId).ToList();
        var bundleIds = postedBundles.Select(x => x.BundleSaleId).ToList();
        var slipCogs = slipIds.Count == 0
            ? 0m
            : await _ledger.Query().AsNoTracking()
                .Where(l => l.SourceDocType == DocumentTypes.SalesSlip && slipIds.Contains(l.SourceDocId) && l.Direction == Direction.Out)
                .SumAsync(l => l.Value, ct);
        var bundleCogs = bundleIds.Count == 0
            ? 0m
            : await _ledger.Query().AsNoTracking()
                .Where(l => l.SourceDocType == DocumentTypes.BundleSale && bundleIds.Contains(l.SourceDocId) && l.Direction == Direction.Out)
                .SumAsync(l => l.Value, ct);
        var cogs = slipCogs + bundleCogs;

        var dayEnd = await _uow.ExecuteInTransactionAsync(async token =>
        {
            var now = DateTime.UtcNow;
            var docNo = await _numbers.NextAsync(DocumentTypes.SalesDayEnd, token);
            var entity = new SalesDayEnd
            {
                DocNo = docNo,
                CashierUserId = request.CashierUserId,
                BusinessDate = businessDate,
                SlipCount = postedSlips.Count,
                BundleCount = postedBundles.Count,
                Subtotal = subtotal,
                DiscountTotal = discountTotal,
                TaxTotal = taxTotal,
                GrandTotal = grandTotal,
                CostOfGoodsSold = cogs,
                ClosedBy = _currentUser.AuditUserId,
                ClosedAt = now
            };

            if (grandTotal > 0 || cogs > 0)
            {
                var period = await _gl.GetPeriodByDateAsync(businessDate, token);
                var glLines = new List<GlJournalEntryLineRequest>();
                if (grandTotal > 0)
                {
                    glLines.Add(new GlJournalEntryLineRequest(_glCashAccountCode, grandTotal, 0m, $"Day-end {businessDate:yyyy-MM-dd} cashier {request.CashierUserId} — cash collected"));
                    if (discountTotal > 0)
                        glLines.Add(new GlJournalEntryLineRequest(_glSalesDiscountAccountCode, discountTotal, 0m, "Sales discount"));
                    glLines.Add(new GlJournalEntryLineRequest(_glSalesRevenueAccountCode, 0m, subtotal, "Sales revenue"));
                    if (taxTotal > 0)
                        glLines.Add(new GlJournalEntryLineRequest(_glTaxPayableAccountCode, 0m, taxTotal, "Output tax payable"));
                }
                if (cogs > 0)
                {
                    glLines.Add(new GlJournalEntryLineRequest(_glCogsAccountCode, cogs, 0m, "Cost of goods sold"));
                    glLines.Add(new GlJournalEntryLineRequest(_glInventoryAccountCode, 0m, cogs, "Inventory reduction"));
                }

                var glResult = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest
                {
                    PeriodId = period.PeriodId,
                    EntryDate = businessDate,
                    SourceModule = "SALES-DAYEND",
                    Reference = docNo,
                    Narration = $"Cashier day-end {docNo} — {businessDate:yyyy-MM-dd}",
                    Lines = glLines
                }, token);
                entity.GlJournalNo = glResult.JournalNo;
                entity.GlPostedAt = now;
            }

            // Relationship fixup, not a direct FK assignment: `entity` has no real id yet
            // (assigned by SaveChangesAsync inside ExecuteInTransactionAsync) — EF resolves
            // the FK on these already-tracked rows from the navigation once it does, so the
            // day-end row and every slip's/bundle's DayEndId commit together in one transaction.
            foreach (var slip in settledSlips) slip.DayEnd = entity;
            foreach (var bundle in settledBundles) bundle.DayEnd = entity;

            await _dayEnds.AddAsync(entity, token);
            return entity;
        }, ct);

        return await BuildDtoAsync(dayEnd, ct);
    }

    public async Task<PagedResponse<SalesDayEndSummaryDto>> ListAsync(PageQuery query, int? cashierUserId, CancellationToken ct = default)
    {
        var q = _dayEnds.Query().AsNoTracking();
        if (cashierUserId is not null) q = q.Where(x => x.CashierUserId == cashierUserId);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(x => x.SalesDayEndId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct);
        var items = rows.Select(x => new SalesDayEndSummaryDto(
            x.SalesDayEndId, x.DocNo, x.CashierUserId, x.BusinessDate, x.SlipCount, x.BundleCount, x.GrandTotal, x.GlJournalNo, x.ClosedAt)).ToList();
        return PagedResponse<SalesDayEndSummaryDto>.Create(items, query.Page, query.PageSize, total);
    }

    public async Task<SalesDayEndDto?> GetAsync(int salesDayEndId, CancellationToken ct = default)
    {
        var entity = await _dayEnds.Query().AsNoTracking().FirstOrDefaultAsync(x => x.SalesDayEndId == salesDayEndId, ct);
        return entity is null ? null : await BuildDtoAsync(entity, ct);
    }

    private async Task<List<SalesSlip>> LoadOpenSlipsAsync(int cashierUserId, DateOnly businessDate, CancellationToken ct)
    {
        var (dayStart, dayEnd) = DayRangeUtc(businessDate);
        return await _slips.Query()
            .Where(x => x.CashierUserId == cashierUserId && x.DayEndId == null && x.SlipDate >= dayStart && x.SlipDate < dayEnd)
            .ToListAsync(ct);
    }

    private async Task<List<BundleSale>> LoadOpenBundleSalesAsync(int cashierUserId, DateOnly businessDate, CancellationToken ct)
    {
        var (dayStart, dayEnd) = DayRangeUtc(businessDate);
        return await _bundles.Query()
            .Where(x => x.CashierUserId == cashierUserId && x.DayEndId == null && x.BundleDate >= dayStart && x.BundleDate < dayEnd)
            .ToListAsync(ct);
    }

    // SlipDate/BundleDate are stored as DateTime.UtcNow (Kind=Utc) throughout — Npgsql requires
    // a matching Kind=Utc here too, or comparisons against the `timestamptz` column throw.
    private static (DateTime Start, DateTime End) DayRangeUtc(DateOnly businessDate)
    {
        var start = DateTime.SpecifyKind(businessDate.ToDateTime(TimeOnly.MinValue), DateTimeKind.Utc);
        return (start, start.AddDays(1));
    }

    private async Task<SalesDayEndDto> BuildDtoAsync(SalesDayEnd entity, CancellationToken ct)
    {
        var postedSlips = await _slips.Query().AsNoTracking().Include(x => x.Lines)
            .Where(x => x.DayEndId == entity.SalesDayEndId && x.Status == SalesSlipStatus.Posted)
            .ToListAsync(ct);
        var postedBundles = await _bundles.Query().AsNoTracking().Include(x => x.Lines)
            .Where(x => x.DayEndId == entity.SalesDayEndId && x.Status == BundleSaleStatus.Posted)
            .ToListAsync(ct);

        var slipNumbers = postedSlips.OrderBy(x => x.SlipNo).Select(x => x.SlipNo).ToList();
        var bundleNumbers = postedBundles.OrderBy(x => x.BundleNo).Select(x => x.BundleNo).ToList();

        var slipQtyRevenue = postedSlips.SelectMany(x => x.Lines)
            .GroupBy(l => l.ItemId)
            .Select(g => new { ItemId = g.Key, Qty = g.Sum(l => l.Qty + l.FreeQty), Revenue = g.Sum(l => l.LineTotal) });
        var bundleQtyRevenue = postedBundles.SelectMany(x => x.Lines)
            .GroupBy(l => l.ItemId)
            .Select(g => new { ItemId = g.Key, Qty = g.Sum(l => l.Qty), Revenue = g.Sum(l => l.LineTotal) });

        var grouped = slipQtyRevenue.Concat(bundleQtyRevenue)
            .GroupBy(x => x.ItemId)
            .Select(g => new { ItemId = g.Key, Qty = g.Sum(x => x.Qty), Revenue = g.Sum(x => x.Revenue) })
            .ToList();
        var itemIds = grouped.Select(g => g.ItemId).ToList();
        var itemInfo = await _items.Query().AsNoTracking()
            .Where(i => itemIds.Contains(i.ItemId))
            .Select(i => new { i.ItemId, i.Sku, i.Name })
            .ToListAsync(ct);
        var breakdown = grouped
            .Select(g =>
            {
                var info = itemInfo.FirstOrDefault(i => i.ItemId == g.ItemId);
                return new SalesDayEndItemLineDto(g.ItemId, info?.Sku ?? $"SKU-{g.ItemId}", info?.Name ?? "—", g.Qty, g.Revenue);
            })
            .OrderByDescending(x => x.Revenue)
            .ToList();

        return new SalesDayEndDto(
            entity.SalesDayEndId, entity.DocNo, entity.CashierUserId, entity.BusinessDate,
            entity.SlipCount, entity.BundleCount, entity.Subtotal, entity.DiscountTotal, entity.TaxTotal, entity.GrandTotal,
            entity.CostOfGoodsSold, entity.GlJournalNo, entity.GlPostedAt, entity.ClosedBy, entity.ClosedAt,
            slipNumbers, bundleNumbers, breakdown);
    }
}
