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 SalesInvoiceService : ISalesInvoiceService
{
    private readonly IRepository<SalesInvoice> _invoices;
    private readonly IRepository<Customer> _customers;
    private readonly IRepository<Item> _items;
    private readonly IRepository<Uom> _uoms;
    private readonly IRepository<Warehouse> _warehouses;
    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 SalesInvoiceService(
        IRepository<SalesInvoice> invoices, IRepository<Customer> customers, IRepository<Item> items,
        IRepository<Uom> uoms, IRepository<Warehouse> warehouses, ISalesDomainService sales, ISalesPostingService posting, ISalesMappingService mapping,
        ISalesDocumentWorkflowService workflow,
        ICurrentUser currentUser, INumberSequenceService numbers, IUnitOfWork uow)
    {
        _invoices = invoices;
        _customers = customers;
        _items = items;
        _uoms = uoms;
        _warehouses = warehouses;
        _sales = sales;
        _posting = posting;
        _mapping = mapping;
        _workflow = workflow;
        _currentUser = currentUser;
        _numbers = numbers;
        _uow = uow;
    }

    public async Task<PagedResponse<SalesInvoiceSummaryDto>> ListAsync(PageQuery query, SalesInvoiceStatus? status, int? customerId, int? warehouseId, CancellationToken ct = default)
    {
        IQueryable<SalesInvoice> q = _invoices.Query().AsNoTracking().Include(x => x.Lines);
        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(x => EF.Functions.ILike(x.InvoiceNo, $"%{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.SalesInvoiceId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct);

        return PagedResponse<SalesInvoiceSummaryDto>.Create(rows.Select(MapSummary).ToList(), query.Page, query.PageSize, total);
    }

    public async Task<ETagged<SalesInvoiceDto>?> GetAsync(int salesInvoiceId, CancellationToken ct = default)
    {
        var invoice = await _invoices.Query().AsNoTracking().Include(x => x.Lines)
            .FirstOrDefaultAsync(x => x.SalesInvoiceId == salesInvoiceId, ct);
        return invoice is null ? null : new ETagged<SalesInvoiceDto>(_mapping.MapInvoice(invoice), invoice.RowVersion);
    }

    public Task<SalesInvoicePostingCheckDto> CheckPostingAsync(int salesInvoiceId, CancellationToken ct = default)
        => _posting.CheckInvoiceAsync(salesInvoiceId, ct);

    public async Task<ETagged<SalesInvoiceDto>> CreateAsync(CreateSalesInvoiceRequest request, CancellationToken ct = default)
    {
        await _sales.ValidateSalesHeaderAsync(request.CustomerId, request.WarehouseId, null, false, ct);

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

        var invoice = await _uow.ExecuteInTransactionAsync(async token =>
        {
            var entity = new SalesInvoice
            {
                InvoiceNo = await _numbers.NextAsync(DocumentTypes.SalesInvoice, token),
                InvoiceDate = DateTime.UtcNow,
                CustomerId = request.CustomerId,
                WarehouseId = request.WarehouseId,
                InvoiceType = request.InvoiceType,
                Status = SalesInvoiceStatus.Draft,
                CreatedBy = _currentUser.AuditUserId,
                CreatedAt = DateTime.UtcNow,
                CustomerSnapshotName = customerSnapshotName,
                CustomerSnapshotTaxNo = customerSnapshotTaxNo,
                Lines = lines
            };
            Recalculate(entity);
            await _invoices.AddAsync(entity, token);
            return entity;
        }, ct);

        return new ETagged<SalesInvoiceDto>(_mapping.MapInvoice(invoice), invoice.RowVersion);
    }

    public async Task<ETagged<SalesInvoiceDto>> UpdateAsync(int salesInvoiceId, UpdateSalesInvoiceRequest request, uint expectedRowVersion, CancellationToken ct = default)
    {
        var invoice = await _workflow.LoadEditableInvoiceAsync(salesInvoiceId, expectedRowVersion, ct);

        await _sales.ValidateSalesHeaderAsync(request.CustomerId, request.WarehouseId, null, false, ct);
        invoice.CustomerId = request.CustomerId;
        invoice.WarehouseId = request.WarehouseId;
        invoice.InvoiceType = request.InvoiceType;
        invoice.CustomerSnapshotName = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.DisplayName ?? x.Name).FirstAsync(ct);
        invoice.CustomerSnapshotTaxNo = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.TaxRegistrationNo).FirstAsync(ct);
        invoice.Lines.Clear();
        foreach (var line in await BuildLinesAsync(request.WarehouseId, request.Lines, ct)) invoice.Lines.Add(line);
        Recalculate(invoice);
        invoice.UpdatedAt = DateTime.UtcNow;
        await _uow.SaveChangesAsync(ct);
        return new ETagged<SalesInvoiceDto>(_mapping.MapInvoice(invoice), invoice.RowVersion);
    }

    public async Task<SalesInvoiceDto> PostAsync(int salesInvoiceId, CancellationToken ct = default)
        {
            await _posting.PostInvoiceAsync(salesInvoiceId, ct);
            var invoice = await _invoices.Query().AsNoTracking().Include(x => x.Lines)
                .FirstAsync(x => x.SalesInvoiceId == salesInvoiceId, ct);
            return _mapping.MapInvoice(invoice);
        }

    public async Task<SalesInvoiceDto> CancelAsync(int salesInvoiceId, CancellationToken ct = default)
    {
        var invoice = await _invoices.Query().Include(x => x.Lines).FirstOrDefaultAsync(x => x.SalesInvoiceId == salesInvoiceId, ct)
            ?? throw new NotFoundException($"Sales invoice {salesInvoiceId} was not found.");
        if (invoice.Status != SalesInvoiceStatus.Draft)
            throw new ConflictException($"Sales invoice {salesInvoiceId} is {invoice.Status} and cannot be cancelled.");
        invoice.Status = SalesInvoiceStatus.Cancelled;
        invoice.UpdatedAt = DateTime.UtcNow;
        await _uow.SaveChangesAsync(ct);
        return _mapping.MapInvoice(invoice);
    }

    private async Task<List<SalesInvoiceLine>> BuildLinesAsync(int headerWarehouseId, List<CreateSalesInvoiceLineRequest> requests, CancellationToken ct)
    {
        var lines = new List<SalesInvoiceLine>();
        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 SalesInvoiceLine
            {
                ItemId = r.ItemId,
                Description = item.Name,
                Qty = r.Qty,
                FreeQty = r.FreeQty,
                WarehouseId = r.WarehouseId,
                UnitPrice = unitPrice,
                BaseCost = unitPrice,
                PriceSource = priceSource,
                DiscountPct = r.DiscountPct,
                DiscountAmount = calc.DiscountTotal,
                DiscountMode = r.DiscountMode,
                NetUnitPrice = calc.NetUnitPrice,
                LineTotal = calc.LineTotal,
                TaxPct = r.TaxPct,
                TaxAmount = calc.TaxAmount,
                IsFreeIssue = r.IsFreeIssue,
                ParentLineId = r.ParentLineId
            });
        }
        return lines;
    }

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

    private SalesInvoiceSummaryDto MapSummary(SalesInvoice x) => new(
        x.SalesInvoiceId, x.InvoiceNo, x.InvoiceDate, x.CustomerId, x.CustomerSnapshotName,
        x.WarehouseId, x.InvoiceType, x.Status, _mapping.MapInvoiceTotals(x), x.CreatedAt, x.GlJournalNo);
}
