using ERPCore.Common.Http;
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.Interfaces;
using ERPCore.Services.Stock;
using ERPCore.System.Errors;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Services;

public sealed class SalesSlipService : ISalesSlipService
{
    private readonly IRepository<SalesSlip> _slips;
    private readonly IRepository<SalesDayEnd> _dayEnds;
    private readonly IRepository<Customer> _customers;
    private readonly IRepository<Item> _items;
    private readonly IRepository<Uom> _uoms;
    private readonly IRepository<Warehouse> _warehouses;
    private readonly IRepository<User> _users;
    private readonly ISalesDomainService _sales;
    private readonly ISalesPostingService _posting;
    private readonly ISalesMappingService _mapping;
    private readonly ISalesDocumentWorkflowService _workflow;
    private readonly ICurrentUser _currentUser;
    private readonly INumberSequenceService _numbers;
    private readonly IUnitOfWork _uow;

    public SalesSlipService(
        IRepository<SalesSlip> slips, IRepository<SalesDayEnd> dayEnds, IRepository<Customer> customers, IRepository<Item> items,
        IRepository<Uom> uoms, IRepository<Warehouse> warehouses, IRepository<User> users, ISalesDomainService sales, ISalesPostingService posting, ISalesMappingService mapping,
        ISalesDocumentWorkflowService workflow,
        ICurrentUser currentUser, INumberSequenceService numbers, IUnitOfWork uow)
    {
        _slips = slips;
        _dayEnds = dayEnds;
        _customers = customers;
        _items = items;
        _uoms = uoms;
        _warehouses = warehouses;
        _users = users;
        _sales = sales;
        _posting = posting;
        _mapping = mapping;
        _workflow = workflow;
        _currentUser = currentUser;
        _numbers = numbers;
        _uow = uow;
    }

    public async Task<PagedResponse<SalesSlipSummaryDto>> ListAsync(PageQuery query, SalesSlipStatus? status, int? customerId, int? warehouseId, CancellationToken ct = default)
    {
        IQueryable<SalesSlip> q = _slips.Query().AsNoTracking().Include(x => x.Lines);
        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(x => EF.Functions.ILike(x.SlipNo, $"%{term}%") || EF.Functions.ILike(x.CustomerSnapshotName, $"%{term}%"));
        }
        if (status is not null) q = q.Where(x => x.Status == status);
        if (customerId is not null) q = q.Where(x => x.CustomerId == customerId);
        if (warehouseId is not null) q = q.Where(x => x.WarehouseId == warehouseId);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(x => x.SalesSlipId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct);
        return PagedResponse<SalesSlipSummaryDto>.Create(rows.Select(MapSummary).ToList(), query.Page, query.PageSize, total);
    }

    public async Task<ETagged<SalesSlipDto>?> GetAsync(int salesSlipId, CancellationToken ct = default)
    {
        var slip = await _slips.Query().AsNoTracking().Include(x => x.Lines)
            .FirstOrDefaultAsync(x => x.SalesSlipId == salesSlipId, ct);
        return slip is null ? null : new ETagged<SalesSlipDto>(_mapping.MapSlip(slip), slip.RowVersion);
    }

    public async Task<PagedResponse<FreeIssueSummaryDto>> ListFreeIssuesAsync(PageQuery query, SalesSlipStatus? status, int? customerId, int? warehouseId, CancellationToken ct = default)
    {
        IQueryable<SalesSlip> q = _slips.Query().AsNoTracking().Include(x => x.Lines);
        q = q.Where(x => x.Lines.Any(l => l.IsFreeIssue || l.FreeQty > 0m));
        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(x => EF.Functions.ILike(x.SlipNo, $"%{term}%") || EF.Functions.ILike(x.CustomerSnapshotName, $"%{term}%"));
        }
        if (status is not null) q = q.Where(x => x.Status == status);
        if (customerId is not null) q = q.Where(x => x.CustomerId == customerId);
        if (warehouseId is not null) q = q.Where(x => x.WarehouseId == warehouseId);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(x => x.SalesSlipId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct);
        return PagedResponse<FreeIssueSummaryDto>.Create(rows.Select(MapFreeIssueSummary).ToList(), query.Page, query.PageSize, total);
    }

    public async Task<ETagged<FreeIssueDto>?> GetFreeIssueAsync(int salesSlipId, CancellationToken ct = default)
    {
        var slip = await _slips.Query().AsNoTracking().Include(x => x.Lines)
            .FirstOrDefaultAsync(x => x.SalesSlipId == salesSlipId, ct);
        return slip is null ? null : new ETagged<FreeIssueDto>(MapFreeIssue(slip), slip.RowVersion);
    }

    public Task<SalesSlipPostingCheckDto> CheckPostingAsync(int salesSlipId, CancellationToken ct = default)
        => _posting.CheckSlipAsync(salesSlipId, ct);

    public async Task<ETagged<SalesSlipDto>> CreateAsync(CreateSalesSlipRequest request, CancellationToken ct = default)
    {
        await _sales.ValidateSalesHeaderAsync(request.CustomerId, request.WarehouseId, request.CashierUserId, true, ct);

        // A cashier can't ring up more sales for a business date they've already closed
        // (SalesDayEndService.CloseAsync) — otherwise the new slip would sit outside every
        // day-end's totals and GL posting forever (docs/14 Sales Day End).
        var today = DateOnly.FromDateTime(DateTime.UtcNow);
        var alreadyClosed = await _dayEnds.Query().AsNoTracking()
            .AnyAsync(x => x.CashierUserId == request.CashierUserId && x.BusinessDate == today, ct);
        if (alreadyClosed)
            throw new ConflictException($"Cashier {request.CashierUserId} already closed today's ({today:yyyy-MM-dd}) sales — day-end has been posted.");

        var customerSnapshotName = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.DisplayName ?? x.Name).FirstAsync(ct);
        var lines = await BuildLinesAsync(request.WarehouseId, request.Lines, ct);

        var slip = await _uow.ExecuteInTransactionAsync(async token =>
        {
            var entity = new SalesSlip
            {
                SlipNo = await _numbers.NextAsync(DocumentTypes.SalesSlip, token),
                SlipDate = DateTime.UtcNow,
                CustomerId = request.CustomerId,
                WarehouseId = request.WarehouseId,
                CashierUserId = request.CashierUserId,
                CustomerSnapshotName = customerSnapshotName,
                Status = SalesSlipStatus.Draft,
                CreatedAt = DateTime.UtcNow,
                Lines = lines
            };
            Recalculate(entity);
            await _slips.AddAsync(entity, token);
            return entity;
        }, ct);

        return new ETagged<SalesSlipDto>(_mapping.MapSlip(slip), slip.RowVersion);
    }

    public async Task<ETagged<SalesSlipDto>> UpdateAsync(int salesSlipId, UpdateSalesSlipRequest request, uint expectedRowVersion, CancellationToken ct = default)
    {
        var slip = await _workflow.LoadEditableSlipAsync(salesSlipId, expectedRowVersion, ct);

        await _sales.ValidateSalesHeaderAsync(request.CustomerId, request.WarehouseId, request.CashierUserId, true, ct);
        slip.CustomerId = request.CustomerId;
        slip.WarehouseId = request.WarehouseId;
        slip.CashierUserId = request.CashierUserId;
        slip.CustomerSnapshotName = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.DisplayName ?? x.Name).FirstAsync(ct);
        slip.Lines.Clear();
        foreach (var line in await BuildLinesAsync(request.WarehouseId, request.Lines, ct)) slip.Lines.Add(line);
        Recalculate(slip);
        slip.UpdatedAt = DateTime.UtcNow;
        await _uow.SaveChangesAsync(ct);
        return new ETagged<SalesSlipDto>(_mapping.MapSlip(slip), slip.RowVersion);
    }

    public async Task<SalesSlipDto> PostAsync(int salesSlipId, CancellationToken ct = default)
        {
            await _posting.PostSlipAsync(salesSlipId, ct);
            var slip = await _slips.Query().AsNoTracking().Include(x => x.Lines)
                .FirstAsync(x => x.SalesSlipId == salesSlipId, ct);
            return _mapping.MapSlip(slip);
        }

    public async Task<SalesSlipDto> CancelAsync(int salesSlipId, CancellationToken ct = default)
    {
        var slip = await _slips.Query().Include(x => x.Lines).FirstOrDefaultAsync(x => x.SalesSlipId == salesSlipId, ct)
            ?? throw new NotFoundException($"Sales slip {salesSlipId} was not found.");
        if (slip.Status != SalesSlipStatus.Draft)
            throw new ConflictException($"Sales slip {salesSlipId} is {slip.Status} and cannot be cancelled.");
        slip.Status = SalesSlipStatus.Cancelled;
        slip.UpdatedAt = DateTime.UtcNow;
        await _uow.SaveChangesAsync(ct);
        return _mapping.MapSlip(slip);
    }

    private async Task<List<SalesSlipLine>> BuildLinesAsync(int headerWarehouseId, List<CreateSalesSlipLineRequest> requests, CancellationToken ct)
    {
        var lines = new List<SalesSlipLine>();
        foreach (var r in requests)
        {
            var item = await _items.Query().AsNoTracking().FirstAsync(x => x.ItemId == r.ItemId, ct);
            await _sales.ValidateSalesLineAsync(headerWarehouseId, r.ItemId, r.WarehouseId, r.Qty, r.FreeQty, r.ParentLineId, ct);
            var resolved = await _sales.ResolveLinePriceAsync(r.ItemId, r.WarehouseId, r.UnitPrice, r.AllowManualPriceOverride, ct);
            var unitPrice = resolved.UnitPrice;
            var priceSource = resolved.PriceSource;

            var calc = _sales.ComputeLine(r.Qty, r.FreeQty, unitPrice, r.DiscountMode, r.DiscountPct, r.DiscountValue, r.DiscountAmount, r.TaxPct, r.IsFreeIssue);

            lines.Add(new SalesSlipLine
            {
                ItemId = r.ItemId,
                Description = item.Name,
                Qty = r.Qty,
                FreeQty = r.FreeQty,
                WarehouseId = r.WarehouseId,
                UnitPrice = unitPrice,
                BaseCost = unitPrice,
                PriceSource = priceSource,
                DiscountMode = r.DiscountMode,
                DiscountPct = r.DiscountPct,
                DiscountAmount = calc.DiscountTotal,
                NetUnitPrice = calc.NetUnitPrice,
                LineTotal = calc.LineTotal,
                TaxPct = r.TaxPct,
                TaxAmount = calc.TaxAmount,
                IsFreeIssue = r.IsFreeIssue,
                ParentLineId = r.ParentLineId
            });
        }
        return lines;
    }

    private static void Recalculate(SalesSlip slip)
    {
        slip.Subtotal = slip.Lines.Sum(x => x.Qty * x.UnitPrice);
        slip.DiscountTotal = slip.Lines.Sum(x => x.DiscountAmount);
        slip.TaxTotal = slip.Lines.Sum(x => x.TaxAmount);
        slip.GrandTotal = slip.Lines.Sum(x => x.LineTotal) + slip.TaxTotal;
        slip.PaidAmount = 0m;
        slip.BalanceAmount = slip.GrandTotal - slip.PaidAmount;
    }

    private SalesSlipSummaryDto MapSummary(SalesSlip x) => new(
        x.SalesSlipId, x.SlipNo, x.SlipDate, x.CustomerId, x.CustomerSnapshotName, x.WarehouseId, x.Status,
        _mapping.MapSlipTotals(x), x.CreatedAt);

    private FreeIssueSummaryDto MapFreeIssueSummary(SalesSlip x)
    {
        var line = x.Lines.FirstOrDefault();
        var item = line is null ? null : _items.Query().AsNoTracking()
            .Where(i => i.ItemId == line.ItemId)
            .Select(i => new { i.ItemId, i.Sku, i.Name, i.BaseUomId })
            .FirstOrDefault();
        // The line carries no unit of its own — its quantity is a count of the item's base
        // UOM — so the display name comes from there.
        var uom = item is null ? null : _uoms.Query().AsNoTracking()
            .Where(u => u.UomId == item.BaseUomId)
            .Select(u => new { u.UomId, u.Name })
            .FirstOrDefault();
        var warehouse = _warehouses.Query().AsNoTracking()
            .Where(w => w.WarehouseId == x.WarehouseId)
            .Select(w => new { w.WarehouseId, w.Name })
            .FirstOrDefault();
        return new FreeIssueSummaryDto(
            x.SalesSlipId,
            x.SlipNo,
            x.Status,
            x.CreatedAt,
            x.WarehouseId,
            warehouse?.Name ?? $"Warehouse {x.WarehouseId}",
            line?.ItemId ?? 0,
            item?.Sku ?? $"SKU-{line?.ItemId ?? 0}",
            item?.Name ?? line?.Description ?? "—",
            uom?.Name ?? $"UOM {item?.BaseUomId ?? 0}",
            line?.Qty ?? 0m,
            line?.FreeQty ?? 0m,
            line is null ? "No line" : $"Buy {line.Qty} Get {line.FreeQty}");
    }

    private FreeIssueDto MapFreeIssue(SalesSlip x)
    {
        var summary = MapFreeIssueSummary(x);
        return new FreeIssueDto(
            x.SalesSlipId,
            x.SlipNo,
            x.SlipDate,
            x.Status,
            x.CustomerId,
            x.CustomerSnapshotName,
            x.WarehouseId,
            summary.WarehouseName,
            x.CashierUserId,
            x.CreatedAt,
            x.UpdatedAt,
            summary,
            x.Lines.Select(l => new SalesSlipLineDto(l.SalesSlipLineId, l.ItemId, l.Description, l.Qty, l.FreeQty, l.WarehouseId, l.UnitPrice, l.BaseCost, l.PriceSource, l.DiscountPct, l.DiscountAmount, l.DiscountMode, l.NetUnitPrice, l.LineTotal, l.TaxPct, l.TaxAmount, l.IsFreeIssue, l.ParentLineId)).ToList());
    }

    private SalesSlipDto Map(SalesSlip x) => _mapping.MapSlip(x);
}
