using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Sales;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Services;

public sealed class SalesReportService : ISalesReportService
{
    private static readonly SalesReportDefinitionDto[] ReportDefinitions =
    [
        new("daily-summary", "Daily Summary", "Aggregated sales by day across posted invoices and slips.", ["from", "to"]),
        new("item-summary", "Item Summary", "Aggregated sales by item across posted invoices and slips.", ["from", "to", "itemId", "warehouseId"]),
        new("customer-summary", "Customer Summary", "Aggregated sales by customer across posted invoices and slips.", ["from", "to", "customerId"]),
        new("warehouse-summary", "Warehouse Summary", "Aggregated sales by warehouse across posted invoices and slips.", ["from", "to", "warehouseId"]),
        new("discount-summary", "Discount Summary", "Documents with discounts applied.", ["from", "to"]),
        new("free-issue-summary", "Free Issue Summary", "Lines with free quantities issued.", ["from", "to"])
    ];

    private readonly IRepository<SalesInvoice> _invoices;
    private readonly IRepository<SalesSlip> _slips;

    public SalesReportService(IRepository<SalesInvoice> invoices, IRepository<SalesSlip> slips)
    {
        _invoices = invoices;
        _slips = slips;
    }

    public IReadOnlyList<SalesReportDefinitionDto> ListReports() => ReportDefinitions;

    public SalesReportDefinitionDto? GetReport(string reportId)
    {
        var normalized = reportId.Trim().ToLowerInvariant();
        return ReportDefinitions.FirstOrDefault(r => r.Id == normalized);
    }

    public async Task<IReadOnlyList<object>> QueryAsync(string reportType, DateOnly from, DateOnly to, int? itemId, int? customerId, int? warehouseId, CancellationToken ct = default)
    {
        var normalized = reportType.Trim().ToLowerInvariant();
        ValidateFilters(normalized, itemId, customerId, warehouseId);

        return normalized switch
        {
            "daily" or "daily-summary" => (await DailySummaryAsync(from, to, ct)).Cast<object>().ToList(),
            "item" or "item-summary" or "item-wise" => (await ItemSummaryAsync(from, to, itemId, warehouseId, ct)).Cast<object>().ToList(),
            "customer" or "customer-summary" or "customer-wise" => (await CustomerSummaryAsync(from, to, customerId, ct)).Cast<object>().ToList(),
            "warehouse" or "warehouse-summary" or "warehouse-wise" => (await WarehouseSummaryAsync(from, to, warehouseId, ct)).Cast<object>().ToList(),
            "discount" or "discount-summary" => (await DiscountSummaryAsync(from, to, ct)).Cast<object>().ToList(),
            "free-issue" or "free-issue-summary" => (await FreeIssueSummaryAsync(from, to, ct)).Cast<object>().ToList(),
            _ => throw new DomainException("INVALID_REPORT_TYPE", $"Unsupported sales report type '{reportType}'.", 400)
        };
    }

    private static void ValidateFilters(string reportType, int? itemId, int? customerId, int? warehouseId)
    {
        var allowed = reportType switch
        {
            "daily" or "daily-summary" => new FilterSet(false, false, false),
            "item" or "item-summary" or "item-wise" => new FilterSet(true, false, true),
            "customer" or "customer-summary" or "customer-wise" => new FilterSet(false, true, false),
            "warehouse" or "warehouse-summary" or "warehouse-wise" => new FilterSet(false, false, true),
            "discount" or "discount-summary" => new FilterSet(false, false, false),
            "free-issue" or "free-issue-summary" => new FilterSet(false, false, false),
            _ => throw new DomainException("INVALID_REPORT_TYPE", $"Unsupported sales report type '{reportType}'.", 400)
        };

        if (!allowed.Item && itemId is not null)
            throw new DomainException("INVALID_REPORT_FILTER", $"Filter 'itemId' is not valid for report type '{reportType}'.", 400);
        if (!allowed.Customer && customerId is not null)
            throw new DomainException("INVALID_REPORT_FILTER", $"Filter 'customerId' is not valid for report type '{reportType}'.", 400);
        if (!allowed.Warehouse && warehouseId is not null)
            throw new DomainException("INVALID_REPORT_FILTER", $"Filter 'warehouseId' is not valid for report type '{reportType}'.", 400);
    }

    private readonly record struct FilterSet(bool Item, bool Customer, bool Warehouse);

    public async Task<IReadOnlyList<SalesDailySummaryRowDto>> DailySummaryAsync(DateOnly from, DateOnly to, CancellationToken ct = default)
    {
        var invoiceRows = await _invoices.Query().AsNoTracking()
            .Where(x => x.Status == SalesInvoiceStatus.Posted && x.InvoiceDate >= from.ToDateTime(TimeOnly.MinValue) && x.InvoiceDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue))
            .GroupBy(x => DateOnly.FromDateTime(x.InvoiceDate))
            .Select(g => new
            {
                Date = g.Key,
                InvoiceCount = g.Count(),
                SlipCount = 0,
                InvoiceSubtotal = g.Sum(x => x.Subtotal),
                SlipSubtotal = 0m,
                DiscountTotal = g.Sum(x => x.DiscountTotal),
                FreeQtyTotal = g.Sum(x => x.Lines.Sum(l => l.FreeQty)),
                TaxTotal = g.Sum(x => x.TaxTotal),
                GrandTotal = g.Sum(x => x.GrandTotal)
            })
            .ToListAsync(ct);

        var slipRows = await _slips.Query().AsNoTracking()
            .Where(x => x.Status == SalesSlipStatus.Posted && x.SlipDate >= from.ToDateTime(TimeOnly.MinValue) && x.SlipDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue))
            .GroupBy(x => DateOnly.FromDateTime(x.SlipDate))
            .Select(g => new
            {
                Date = g.Key,
                InvoiceCount = 0,
                SlipCount = g.Count(),
                InvoiceSubtotal = 0m,
                SlipSubtotal = g.Sum(x => x.Subtotal),
                DiscountTotal = g.Sum(x => x.DiscountTotal),
                FreeQtyTotal = g.Sum(x => x.Lines.Sum(l => l.FreeQty)),
                TaxTotal = g.Sum(x => x.TaxTotal),
                GrandTotal = g.Sum(x => x.GrandTotal)
            })
            .ToListAsync(ct);

        return invoiceRows.Concat(slipRows)
            .GroupBy(x => x.Date)
            .OrderBy(x => x.Key)
            .Select(g => new SalesDailySummaryRowDto(
                g.Key,
                g.Sum(x => x.InvoiceCount),
                g.Sum(x => x.SlipCount),
                g.Sum(x => x.InvoiceSubtotal),
                g.Sum(x => x.SlipSubtotal),
                g.Sum(x => x.DiscountTotal),
                g.Sum(x => x.FreeQtyTotal),
                g.Sum(x => x.TaxTotal),
                g.Sum(x => x.GrandTotal)))
            .ToList();
    }

    public async Task<IReadOnlyList<SalesItemSummaryRowDto>> ItemSummaryAsync(DateOnly from, DateOnly to, int? itemId, int? warehouseId, CancellationToken ct = default)
    {
        var invoiceRows = await _invoices.Query().AsNoTracking()
            .Where(x => x.Status == SalesInvoiceStatus.Posted && x.InvoiceDate >= from.ToDateTime(TimeOnly.MinValue) && x.InvoiceDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue))
            .SelectMany(x => x.Lines.Select(l => new
            {
                l.ItemId,
                l.Description,
                l.Qty,
                l.FreeQty,
                Gross = l.Qty * l.UnitPrice,
                l.DiscountAmount,
                l.TaxAmount,
                l.LineTotal,
                x.WarehouseId
            }))
            .ToListAsync(ct);

        if (warehouseId is not null)
            invoiceRows = invoiceRows.Where(x => x.WarehouseId == warehouseId).ToList();
        if (itemId is not null)
            invoiceRows = invoiceRows.Where(x => x.ItemId == itemId).ToList();

        var slipRows = await _slips.Query().AsNoTracking()
            .Where(x => x.Status == SalesSlipStatus.Posted && x.SlipDate >= from.ToDateTime(TimeOnly.MinValue) && x.SlipDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue))
            .SelectMany(x => x.Lines.Select(l => new
            {
                l.ItemId,
                l.Description,
                l.Qty,
                l.FreeQty,
                Gross = l.Qty * l.UnitPrice,
                l.DiscountAmount,
                l.TaxAmount,
                l.LineTotal,
                x.WarehouseId
            }))
            .ToListAsync(ct);

        if (warehouseId is not null)
            slipRows = slipRows.Where(x => x.WarehouseId == warehouseId).ToList();
        if (itemId is not null)
            slipRows = slipRows.Where(x => x.ItemId == itemId).ToList();

        return invoiceRows.Concat(slipRows)
            .GroupBy(x => new { x.ItemId, x.Description })
            .OrderByDescending(g => g.Sum(x => x.LineTotal) + g.Sum(x => x.TaxAmount))
            .Select(g => new SalesItemSummaryRowDto(
                g.Key.ItemId,
                g.Key.Description,
                g.Sum(x => x.Qty),
                g.Sum(x => x.FreeQty),
                g.Sum(x => x.Gross),
                g.Sum(x => x.DiscountAmount),
                g.Sum(x => x.TaxAmount),
                g.Sum(x => x.LineTotal) + g.Sum(x => x.TaxAmount)))
            .ToList();
    }

    public async Task<IReadOnlyList<SalesCustomerSummaryRowDto>> CustomerSummaryAsync(DateOnly from, DateOnly to, int? customerId, CancellationToken ct = default)
    {
        var invoiceQuery = _invoices.Query().AsNoTracking()
            .Where(x => x.Status == SalesInvoiceStatus.Posted && x.InvoiceDate >= from.ToDateTime(TimeOnly.MinValue) && x.InvoiceDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue));
        if (customerId is not null) invoiceQuery = invoiceQuery.Where(x => x.CustomerId == customerId);

        var slipQuery = _slips.Query().AsNoTracking()
            .Where(x => x.Status == SalesSlipStatus.Posted && x.SlipDate >= from.ToDateTime(TimeOnly.MinValue) && x.SlipDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue));
        if (customerId is not null) slipQuery = slipQuery.Where(x => x.CustomerId == customerId);

        var invoiceRows = await invoiceQuery
            .GroupBy(x => new { x.CustomerId, x.CustomerSnapshotName })
            .Select(g => new
            {
                g.Key.CustomerId,
                CustomerName = g.Key.CustomerSnapshotName,
                InvoiceCount = g.Count(),
                SlipCount = 0,
                SoldQty = g.Sum(x => x.Lines.Sum(l => l.Qty)),
                FreeQty = g.Sum(x => x.Lines.Sum(l => l.FreeQty)),
                GrossAmount = g.Sum(x => x.Subtotal),
                DiscountTotal = g.Sum(x => x.DiscountTotal),
                TaxTotal = g.Sum(x => x.TaxTotal),
                NetAmount = g.Sum(x => x.GrandTotal)
            })
            .ToListAsync(ct);

        var slipRows = await slipQuery
            .GroupBy(x => new { x.CustomerId, x.CustomerSnapshotName })
            .Select(g => new
            {
                g.Key.CustomerId,
                CustomerName = g.Key.CustomerSnapshotName,
                InvoiceCount = 0,
                SlipCount = g.Count(),
                SoldQty = g.Sum(x => x.Lines.Sum(l => l.Qty)),
                FreeQty = g.Sum(x => x.Lines.Sum(l => l.FreeQty)),
                GrossAmount = g.Sum(x => x.Subtotal),
                DiscountTotal = g.Sum(x => x.DiscountTotal),
                TaxTotal = g.Sum(x => x.TaxTotal),
                NetAmount = g.Sum(x => x.GrandTotal)
            })
            .ToListAsync(ct);

        return invoiceRows.Concat(slipRows)
            .GroupBy(x => new { x.CustomerId, x.CustomerName })
            .OrderByDescending(g => g.Sum(x => x.NetAmount))
            .Select(g => new SalesCustomerSummaryRowDto(
                g.Key.CustomerId,
                g.Key.CustomerName,
                g.Sum(x => x.InvoiceCount),
                g.Sum(x => x.SlipCount),
                g.Sum(x => x.SoldQty),
                g.Sum(x => x.FreeQty),
                g.Sum(x => x.GrossAmount),
                g.Sum(x => x.DiscountTotal),
                g.Sum(x => x.TaxTotal),
                g.Sum(x => x.NetAmount)))
            .ToList();
    }

    public async Task<IReadOnlyList<SalesWarehouseSummaryRowDto>> WarehouseSummaryAsync(DateOnly from, DateOnly to, int? warehouseId, CancellationToken ct = default)
    {
        var invoiceQuery = _invoices.Query().AsNoTracking()
            .Where(x => x.Status == SalesInvoiceStatus.Posted && x.InvoiceDate >= from.ToDateTime(TimeOnly.MinValue) && x.InvoiceDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue));
        if (warehouseId is not null) invoiceQuery = invoiceQuery.Where(x => x.WarehouseId == warehouseId);

        var slipQuery = _slips.Query().AsNoTracking()
            .Where(x => x.Status == SalesSlipStatus.Posted && x.SlipDate >= from.ToDateTime(TimeOnly.MinValue) && x.SlipDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue));
        if (warehouseId is not null) slipQuery = slipQuery.Where(x => x.WarehouseId == warehouseId);

        var invoiceRows = await invoiceQuery
            .GroupBy(x => new { x.WarehouseId, x.Warehouse!.Name })
            .Select(g => new
            {
                g.Key.WarehouseId,
                WarehouseName = g.Key.Name,
                InvoiceCount = g.Count(),
                SlipCount = 0,
                SoldQty = g.Sum(x => x.Lines.Sum(l => l.Qty)),
                FreeQty = g.Sum(x => x.Lines.Sum(l => l.FreeQty)),
                GrossAmount = g.Sum(x => x.Subtotal),
                DiscountTotal = g.Sum(x => x.DiscountTotal),
                TaxTotal = g.Sum(x => x.TaxTotal),
                NetAmount = g.Sum(x => x.GrandTotal)
            })
            .ToListAsync(ct);

        var slipRows = await slipQuery
            .GroupBy(x => new { x.WarehouseId, x.Warehouse!.Name })
            .Select(g => new
            {
                g.Key.WarehouseId,
                WarehouseName = g.Key.Name,
                InvoiceCount = 0,
                SlipCount = g.Count(),
                SoldQty = g.Sum(x => x.Lines.Sum(l => l.Qty)),
                FreeQty = g.Sum(x => x.Lines.Sum(l => l.FreeQty)),
                GrossAmount = g.Sum(x => x.Subtotal),
                DiscountTotal = g.Sum(x => x.DiscountTotal),
                TaxTotal = g.Sum(x => x.TaxTotal),
                NetAmount = g.Sum(x => x.GrandTotal)
            })
            .ToListAsync(ct);

        return invoiceRows.Concat(slipRows)
            .GroupBy(x => new { x.WarehouseId, x.WarehouseName })
            .OrderByDescending(g => g.Sum(x => x.NetAmount))
            .Select(g => new SalesWarehouseSummaryRowDto(
                g.Key.WarehouseId,
                g.Key.WarehouseName,
                g.Sum(x => x.InvoiceCount),
                g.Sum(x => x.SlipCount),
                g.Sum(x => x.SoldQty),
                g.Sum(x => x.FreeQty),
                g.Sum(x => x.GrossAmount),
                g.Sum(x => x.DiscountTotal),
                g.Sum(x => x.TaxTotal),
                g.Sum(x => x.NetAmount)))
            .ToList();
    }

    public async Task<IReadOnlyList<SalesDiscountSummaryRowDto>> DiscountSummaryAsync(DateOnly from, DateOnly to, CancellationToken ct = default)
    {
        var invoices = await _invoices.Query().AsNoTracking()
            .Where(x => x.Status == SalesInvoiceStatus.Posted && x.InvoiceDate >= from.ToDateTime(TimeOnly.MinValue) && x.InvoiceDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue) && x.DiscountTotal > 0m)
            .Select(x => new SalesDiscountSummaryRowDto("Invoice", x.InvoiceNo, x.InvoiceDate, x.CustomerSnapshotName, x.Subtotal, x.DiscountTotal, x.TaxTotal, x.GrandTotal))
            .ToListAsync(ct);

        var slips = await _slips.Query().AsNoTracking()
            .Where(x => x.Status == SalesSlipStatus.Posted && x.SlipDate >= from.ToDateTime(TimeOnly.MinValue) && x.SlipDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue) && x.DiscountTotal > 0m)
            .Select(x => new SalesDiscountSummaryRowDto("Slip", x.SlipNo, x.SlipDate, x.CustomerSnapshotName, x.Subtotal, x.DiscountTotal, x.TaxTotal, x.GrandTotal))
            .ToListAsync(ct);

        return invoices.Concat(slips).OrderByDescending(x => x.DiscountTotal).ToList();
    }

    public async Task<IReadOnlyList<SalesFreeIssueSummaryRowDto>> FreeIssueSummaryAsync(DateOnly from, DateOnly to, CancellationToken ct = default)
    {
        var invoiceRows = await _invoices.Query().AsNoTracking()
            .Where(x => x.Status == SalesInvoiceStatus.Posted && x.InvoiceDate >= from.ToDateTime(TimeOnly.MinValue) && x.InvoiceDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue))
            .SelectMany(x => x.Lines.Where(l => l.FreeQty > 0m).Select(l => new SalesFreeIssueSummaryRowDto(
                "Invoice",
                x.InvoiceNo,
                x.InvoiceDate,
                x.CustomerSnapshotName,
                l.ItemId,
                l.Description,
                l.FreeQty,
                l.FreeQty * l.UnitPrice,
                l.WarehouseId,
                x.Warehouse!.Name)))
            .ToListAsync(ct);

        var slipRows = await _slips.Query().AsNoTracking()
            .Where(x => x.Status == SalesSlipStatus.Posted && x.SlipDate >= from.ToDateTime(TimeOnly.MinValue) && x.SlipDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue))
            .SelectMany(x => x.Lines.Where(l => l.FreeQty > 0m).Select(l => new SalesFreeIssueSummaryRowDto(
                "Slip",
                x.SlipNo,
                x.SlipDate,
                x.CustomerSnapshotName,
                l.ItemId,
                l.Description,
                l.FreeQty,
                l.FreeQty * l.UnitPrice,
                l.WarehouseId,
                x.Warehouse!.Name)))
            .ToListAsync(ct);

        return invoiceRows.Concat(slipRows).OrderByDescending(x => x.FreeQty).ToList();
    }
}
