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 BundleSaleService : IBundleSaleService
{
    private readonly IRepository<BundleSaleTemplate> _templates;
    private readonly IRepository<BundleSale> _bundles;
    private readonly IRepository<SalesDayEnd> _dayEnds;
    private readonly IRepository<Customer> _customers;
    private readonly IRepository<Item> _items;
    private readonly IRepository<Warehouse> _warehouses;
    private readonly IRepository<User> _users;
    private readonly ISalesDomainService _sales;
    private readonly ISalesPostingService _posting;
    private readonly ICurrentUser _currentUser;
    private readonly INumberSequenceService _numbers;
    private readonly IUnitOfWork _uow;

    public BundleSaleService(
        IRepository<BundleSale> bundles,
        IRepository<BundleSaleTemplate> templates,
        IRepository<SalesDayEnd> dayEnds,
        IRepository<Customer> customers,
        IRepository<Item> items,
        IRepository<Warehouse> warehouses,
        IRepository<User> users,
        ISalesDomainService sales,
        ISalesPostingService posting,
        ICurrentUser currentUser,
        INumberSequenceService numbers,
        IUnitOfWork uow)
    {
        _templates = templates;
        _bundles = bundles;
        _dayEnds = dayEnds;
        _customers = customers;
        _items = items;
        _warehouses = warehouses;
        _users = users;
        _sales = sales;
        _posting = posting;
        _currentUser = currentUser;
        _numbers = numbers;
        _uow = uow;
    }

    public async Task<PagedResponse<BundleSaleTemplateSummaryDto>> ListTemplatesAsync(PageQuery query, CancellationToken ct = default)
    {
        IQueryable<BundleSaleTemplate> q = _templates.Query().AsNoTracking().Include(x => x.Lines);
        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(x => EF.Functions.ILike(x.TemplateCode, $"%{term}%") || EF.Functions.ILike(x.TemplateName, $"%{term}%") || EF.Functions.ILike(x.Description ?? "", $"%{term}%"));
        }
        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(x => x.BundleSaleTemplateId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct);
        return PagedResponse<BundleSaleTemplateSummaryDto>.Create(rows.Select(x => new BundleSaleTemplateSummaryDto(
            x.BundleSaleTemplateId, x.TemplateCode, x.TemplateName, x.Description, x.Status, x.Lines.Count, x.CreatedAt, x.UpdatedAt)).ToList(), query.Page, query.PageSize, total);
    }

    public async Task<BundleSaleTemplateDto?> GetTemplateAsync(int bundleSaleTemplateId, CancellationToken ct = default)
    {
        var template = await _templates.Query().AsNoTracking().Include(x => x.Lines).FirstOrDefaultAsync(x => x.BundleSaleTemplateId == bundleSaleTemplateId, ct);
        return template is null ? null : new BundleSaleTemplateDto(
            template.BundleSaleTemplateId,
            template.TemplateCode,
            template.TemplateName,
            template.Description,
            template.Status,
            template.CreatedAt,
            template.UpdatedAt,
            template.Lines.OrderBy(x => x.SortOrder).Select(x => new BundleSaleTemplateLineDto(
                x.BundleSaleTemplateLineId, x.ItemId, x.WarehouseId, x.Qty, x.UnitPrice, x.IncludeInBundle, x.SortOrder)).ToList());
    }

    public async Task<PagedResponse<BundleSaleSummaryDto>> ListAsync(PageQuery query, BundleSaleStatus? status, int? customerId, int? warehouseId, CancellationToken ct = default)
    {
        IQueryable<BundleSale> q = _bundles.Query().AsNoTracking().Include(x => x.Lines);
        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(x => EF.Functions.ILike(x.BundleNo, $"%{term}%") || EF.Functions.ILike(x.BundleName, $"%{term}%") || EF.Functions.ILike(x.BundleCode, $"%{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.BundleSaleId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct);
        return PagedResponse<BundleSaleSummaryDto>.Create(rows.Select(MapSummary).ToList(), query.Page, query.PageSize, total);
    }

    public async Task<BundleSaleDto?> GetAsync(int bundleSaleId, CancellationToken ct = default)
    {
        var bundle = await _bundles.Query().AsNoTracking().Include(x => x.Lines).FirstOrDefaultAsync(x => x.BundleSaleId == bundleSaleId, ct);
        return bundle is null ? null : Map(bundle);
    }

    public Task<BundleSalePostingCheckDto> CheckPostingAsync(int bundleSaleId, CancellationToken ct = default)
        => _posting.CheckBundleAsync(bundleSaleId, ct);

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

        // Same guard as SalesSlipService.CreateAsync — a bundle sale is a cashier document
        // exactly like a sales slip, so it's blocked by the same closed day (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 template = await _templates.Query().AsNoTracking().Include(x => x.Lines)
            .FirstAsync(x => x.BundleSaleTemplateId == request.BundleSaleTemplateId, ct);
        var bundle = new BundleSale
        {
            BundleNo = await _numbers.NextAsync(DocumentTypes.BundleSale, ct),
            BundleDate = DateTime.UtcNow,
            CustomerId = request.CustomerId,
            WarehouseId = request.WarehouseId,
            CashierUserId = request.CashierUserId,
            BundleSaleTemplateId = request.BundleSaleTemplateId,
            BundleName = request.BundleName,
            BundleCode = string.Empty,
            Status = BundleSaleStatus.Draft,
            CreatedAt = DateTime.UtcNow
        };
        bundle.CustomerSnapshotName = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.DisplayName ?? x.Name).FirstAsync(ct);
        bundle.Lines = await BuildLinesAsync(template, request.WarehouseId, request.Lines, ct);
        Recalculate(bundle, request.BundlePrice);
        bundle.BundleCode = $"{bundle.BundleNo}-B";
        await _bundles.AddAsync(bundle, ct);
        bundle.ConcurrencyStamp = 1;
        await _uow.SaveChangesAsync(ct);
        return Map(bundle);
    }

    public async Task<BundleSaleDto> UpdateAsync(int bundleSaleId, UpdateBundleSaleRequest request, CancellationToken ct = default)
    {
        var bundle = await _bundles.Query().Include(x => x.Lines).FirstOrDefaultAsync(x => x.BundleSaleId == bundleSaleId, ct)
            ?? throw new NotFoundException($"Bundle sale {bundleSaleId} was not found.");
        if (bundle.Status != BundleSaleStatus.Draft)
            throw new ConflictException($"Bundle sale {bundleSaleId} is {bundle.Status} and cannot be edited.");

        await _sales.ValidateSalesHeaderAsync(request.CustomerId, request.WarehouseId, request.CashierUserId, true, ct);
        var template = await _templates.Query().AsNoTracking().Include(x => x.Lines)
            .FirstAsync(x => x.BundleSaleTemplateId == request.BundleSaleTemplateId, ct);
        bundle.CustomerId = request.CustomerId;
        bundle.WarehouseId = request.WarehouseId;
        bundle.CashierUserId = request.CashierUserId;
        bundle.BundleSaleTemplateId = request.BundleSaleTemplateId;
        bundle.BundleName = request.BundleName;
        bundle.CustomerSnapshotName = await _customers.Query().Where(x => x.CustomerId == request.CustomerId).Select(x => x.DisplayName ?? x.Name).FirstAsync(ct);
        bundle.Lines.Clear();
        foreach (var line in await BuildLinesAsync(template, request.WarehouseId, request.Lines, ct)) bundle.Lines.Add(line);
        Recalculate(bundle, request.BundlePrice);
        bundle.UpdatedAt = DateTime.UtcNow;
        bundle.ConcurrencyStamp++;
        await _uow.SaveChangesAsync(ct);
        return Map(bundle);
    }

    public async Task<BundleSaleDto> PostAsync(int bundleSaleId, CancellationToken ct = default)
        {
            await _posting.PostBundleAsync(bundleSaleId, ct);
            var bundle = await _bundles.Query().AsNoTracking().Include(x => x.Lines)
                .FirstAsync(x => x.BundleSaleId == bundleSaleId, ct);
            return Map(bundle);
        }

    public async Task<BundleSaleDto> CancelAsync(int bundleSaleId, CancellationToken ct = default)
    {
        var bundle = await _bundles.Query().Include(x => x.Lines).FirstOrDefaultAsync(x => x.BundleSaleId == bundleSaleId, ct)
            ?? throw new NotFoundException($"Bundle sale {bundleSaleId} was not found.");
        if (bundle.Status != BundleSaleStatus.Draft)
            throw new ConflictException($"Bundle sale {bundleSaleId} is {bundle.Status} and cannot be cancelled.");
        bundle.Status = BundleSaleStatus.Cancelled;
        bundle.UpdatedAt = DateTime.UtcNow;
        bundle.ConcurrencyStamp++;
        await _uow.SaveChangesAsync(ct);
        return Map(bundle);
    }

    private async Task<List<BundleSaleLine>> BuildLinesAsync(
        BundleSaleTemplate template, int warehouseId, IReadOnlyList<CreateBundleSaleTemplateLineRequest> requestLines, CancellationToken ct)
    {
        var lines = new List<BundleSaleLine>();
        var sourceLines = requestLines.Count > 0
            ? requestLines.OrderBy(x => x.SortOrder).ToList()
            : template.Lines.OrderBy(x => x.SortOrder).Select(x => new CreateBundleSaleTemplateLineRequest
            {
                ItemId = x.ItemId,
                WarehouseId = x.WarehouseId,
                Qty = x.Qty,
                UnitPrice = x.UnitPrice,
                IncludeInBundle = x.IncludeInBundle,
                SortOrder = x.SortOrder
            }).ToList();

        foreach (var r in sourceLines)
        {
            if (r.Qty <= 0)
                throw new DomainException(ErrorCodes.Validation, "Bundle line quantity must be greater than zero.", 422);

            // Bundle sales use the header warehouse as the source of truth for stock and pricing.
            // Keep any per-line warehouse input from drifting away from the header.
            var lineWarehouseId = warehouseId;

            var item = await _items.Query().AsNoTracking().FirstAsync(x => x.ItemId == r.ItemId, ct);
            await _sales.ValidateSalesLineAsync(warehouseId, r.ItemId, lineWarehouseId, r.Qty, 0m, null, ct);
            var calc = _sales.ComputeLine(r.Qty, 0m, r.UnitPrice, SalesDiscountMode.Amount, 0m, 0m, 0m, 0m, false);
            lines.Add(new BundleSaleLine
            {
                ItemId = r.ItemId,
                Description = item.Name,
                Qty = r.Qty,
                WarehouseId = lineWarehouseId,
                UnitPrice = r.UnitPrice,
                LineTotal = calc.LineTotal,
                IncludeInBundle = r.IncludeInBundle,
                IsComponent = true,
                ParentLineId = null
            });
        }
        return lines;
    }

    private static void Recalculate(BundleSale bundle, decimal bundlePrice)
    {
        bundle.ComponentSubtotal = bundle.Lines.Where(x => x.IncludeInBundle).Sum(x => x.LineTotal);
        bundle.BundlePrice = bundlePrice;
        bundle.MarginAmount = bundle.BundlePrice - bundle.ComponentSubtotal;
        bundle.DiscountTotal = Math.Max(0m, bundle.ComponentSubtotal - bundle.BundlePrice);
        bundle.TaxTotal = 0m;
        bundle.GrandTotal = bundle.BundlePrice + bundle.TaxTotal;
    }

    private static BundleSaleSummaryDto MapSummary(BundleSale x) => new(
        x.BundleSaleId, x.BundleNo, x.BundleDate, x.CustomerId, x.CustomerSnapshotName, x.WarehouseId, x.BundleName, x.BundleCode, x.Status, x.ComponentSubtotal, x.BundlePrice, x.GrandTotal, x.CreatedAt);

    private static BundleSaleDto Map(BundleSale x) => new(
        x.BundleSaleId, x.BundleNo, x.BundleDate, x.CustomerId, x.CustomerSnapshotName, x.WarehouseId, x.CashierUserId, x.BundleSaleTemplateId,
        x.BundleName, x.BundleCode, x.Status, x.ComponentSubtotal, x.BundlePrice, x.MarginAmount, x.DiscountTotal, x.TaxTotal, x.GrandTotal,
        x.CreatedAt, x.UpdatedAt,
        x.Lines.Select(l => new BundleSaleLineDto(l.BundleSaleLineId, l.ItemId, l.Description, l.Qty, l.WarehouseId, l.UnitPrice, l.LineTotal, l.IncludeInBundle, l.IsComponent, l.ParentLineId)).ToList());

}
