using CashLineERP.Application.Sales;
using CashLineERP.Domain.Sales;
using CashLineERP.Domain.Workflow;
using CashLineERP.SharedKernel.Workflow;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class SalesOrderRepository : ISalesOrderRepository
{
    private readonly CashLineErpDbContext _dbContext;

    public SalesOrderRepository(CashLineErpDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IReadOnlyCollection<SalesOrderDetailView> GetSalesOrders() =>
        _dbContext.SalesOrders
            .AsNoTracking()
            .Include(order => order.Lines)
            .OrderByDescending(order => order.OrderDate)
            .ThenByDescending(order => order.Id)
            .ToArray()
            .Select(Map)
            .ToArray();

    public SalesOrderDetailView? GetSalesOrder(long salesOrderId)
    {
        var salesOrder = _dbContext.SalesOrders
            .AsNoTracking()
            .Include(order => order.Lines)
            .SingleOrDefault(order => order.Id == salesOrderId);

        return salesOrder is null ? null : Map(salesOrder);
    }

    public SalesOrderDetailView Create(CreateSalesOrderCommand command)
    {
        var nextSequence = (_dbContext.SalesOrders.Max(order => (long?)order.Id) ?? 0L) + 1L;
        var salesOrder = new SalesOrder(
            salesOrderNumber: $"SO-{nextSequence:D6}",
            branchId: command.BranchId,
            customerId: command.CustomerId,
            orderDate: command.OrderDate,
            currencyCode: command.CurrencyCode,
            exchangeRate: command.ExchangeRate);

        var lineNumber = 1;
        foreach (var line in command.Lines)
        {
            var resolvedPricing = ResolvePricing(
                command.BranchId,
                command.CustomerId,
                command.OrderDate,
                command.CurrencyCode,
                line.ItemId,
                line.OrderedQuantity,
                line.UnitOfMeasureCode,
                line.UnitPrice);

            salesOrder.AddLine(
                lineNumber++,
                line.ItemId,
                line.WarehouseId,
                line.OrderedQuantity,
                line.UnitOfMeasureCode,
                resolvedPricing.UnitPrice,
                resolvedPricing.UnitPrice,
                resolvedPricing.PricingRuleId,
                resolvedPricing.PricingRuleCode,
                resolvedPricing.PricingSourceCode,
                line.TaxCode);
        }

        _dbContext.SalesOrders.Add(salesOrder);
        _dbContext.SaveChanges();
        return GetSalesOrder(salesOrder.Id)!;
    }

    public SalesOrderDetailView Submit(long salesOrderId, long actorUserId)
    {
        var salesOrder = _dbContext.SalesOrders
            .Include(order => order.Lines)
            .Single(order => order.Id == salesOrderId);

        var conversion = CurrencyConversionContext.Create(_dbContext);
        var evaluation = EvaluateCustomerCreditControl(salesOrder.CustomerId, salesOrder.CurrencyCode, salesOrder.OrderDate, salesOrder.TotalAmount, salesOrder.Id);
        var projectedExposureBaseAmount = evaluation.CurrentExposureBaseAmount + conversion.ConvertToBase(salesOrder.CurrencyCode, salesOrder.TotalAmount, salesOrder.OrderDate);

        if (evaluation.ShouldHoldNewOrders)
        {
            salesOrder.SubmitOnCreditHold(
                actorUserId,
                evaluation.HoldReason ?? "Customer credit control hold applied.",
                evaluation.CreditLimitAmount,
                projectedExposureBaseAmount,
                evaluation.OverdueInvoiceCount);
        }
        else
        {
            salesOrder.Submit(
                actorUserId,
                evaluation.CreditLimitAmount,
                projectedExposureBaseAmount,
                evaluation.OverdueInvoiceCount);
        }

        var workflowInstance = new WorkflowInstance(
            moduleCode: "SALES",
            documentType: "SALES_ORDER",
            documentId: salesOrder.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "Sales order submitted from order-to-cash workspace.");

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetSalesOrder(salesOrder.Id)!;
    }

    public SalesOrderDetailView ReleaseCreditHold(long salesOrderId, long actorUserId, string decisionCode, string notes)
    {
        var salesOrder = _dbContext.SalesOrders
            .Include(order => order.Lines)
            .Single(order => order.Id == salesOrderId);

        var normalizedDecisionCode = decisionCode.Trim().ToUpperInvariant();
        if (string.IsNullOrWhiteSpace(notes))
        {
            throw new InvalidOperationException("Credit release decision requires finance notes.");
        }

        switch (normalizedDecisionCode)
        {
            case "RELEASE":
            case "RELEASED":
                salesOrder.ReleaseCreditHold(actorUserId, notes);
                break;
            case "REJECT":
            case "REJECTED":
                salesOrder.RejectCreditRelease(actorUserId, notes);
                break;
            default:
                throw new InvalidOperationException($"Unsupported credit decision '{decisionCode}'.");
        }

        _dbContext.SaveChanges();
        return GetSalesOrder(salesOrder.Id)!;
    }

    public IReadOnlyCollection<SalesDiscountOverrideDetailView> GetDiscountOverrides(long salesOrderId) =>
        _dbContext.SalesDiscountOverrides
            .AsNoTracking()
            .Where(entry => entry.SalesOrderId == salesOrderId)
            .OrderByDescending(entry => entry.SubmittedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .ToArray()
            .Select(Map)
            .ToArray();

    public SalesDiscountOverrideDetailView CreateDiscountOverride(long salesOrderId, long actorUserId, CreateSalesDiscountOverrideCommand command)
    {
        var salesOrder = _dbContext.SalesOrders
            .Include(order => order.Lines)
            .Single(order => order.Id == salesOrderId);

        if (salesOrder.StatusId != WorkflowStatusSeedIds.Draft)
        {
            throw new InvalidOperationException("Discount overrides can only be requested while the sales order is draft.");
        }

        var salesOrderLine = salesOrder.Lines.SingleOrDefault(line => line.Id == command.SalesOrderLineId)
            ?? throw new InvalidOperationException("Sales order line was not found for discount override.");

        if (salesOrderLine.DiscountOverrideId.HasValue)
        {
            throw new InvalidOperationException("Sales order line already has an approved discount override.");
        }

        var hasPendingOverride = _dbContext.SalesDiscountOverrides.Any(entry =>
            entry.SalesOrderLineId == command.SalesOrderLineId
            && entry.StatusId == WorkflowStatusSeedIds.Submitted);
        if (hasPendingOverride)
        {
            throw new InvalidOperationException("Sales order line already has a submitted discount override awaiting checker review.");
        }

        var nextSequence = (_dbContext.SalesDiscountOverrides.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var discountOverride = new SalesDiscountOverride(
            overrideNumber: $"SDO-{nextSequence:D6}",
            salesOrderId: salesOrder.Id,
            salesOrderLineId: salesOrderLine.Id,
            baseUnitPrice: salesOrderLine.BaseUnitPrice,
            requestedUnitPrice: command.RequestedUnitPrice,
            reasonCode: command.ReasonCode.Trim().ToUpperInvariant(),
            notes: command.Notes,
            actorUserId: actorUserId,
            orderedQuantity: salesOrderLine.OrderedQuantity);

        _dbContext.SalesDiscountOverrides.Add(discountOverride);
        _dbContext.SaveChanges();

        var workflowInstance = new WorkflowInstance(
            moduleCode: "SALES",
            documentType: "DISCOUNT_OVERRIDE",
            documentId: discountOverride.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "Sales discount override submitted for checker approval.");

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetDiscountOverrides(salesOrderId).Single(entry => entry.SalesDiscountOverrideId == discountOverride.Id);
    }

    public SalesDiscountOverrideDetailView ApproveDiscountOverride(long salesOrderId, long salesDiscountOverrideId, long actorUserId, ReviewSalesDiscountOverrideCommand command)
    {
        var discountOverride = _dbContext.SalesDiscountOverrides.Single(entry => entry.Id == salesDiscountOverrideId);
        if (discountOverride.SalesOrderId != salesOrderId)
        {
            throw new InvalidOperationException("Discount override does not belong to the requested sales order.");
        }

        var salesOrder = _dbContext.SalesOrders
            .Include(order => order.Lines)
            .Single(order => order.Id == salesOrderId);

        discountOverride.Approve(actorUserId, command.DecisionNotes);
        salesOrder.ApplyApprovedDiscountOverride(discountOverride.SalesOrderLineId, discountOverride.Id, discountOverride.RequestedUnitPrice);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(entry => entry.Actions)
            .SingleOrDefault(entry => entry.DocumentType == "DISCOUNT_OVERRIDE" && entry.DocumentId == salesDiscountOverrideId);
        workflowInstance?.Approve(actorUserId, "Sales discount override approved by checker.");

        _dbContext.SaveChanges();
        return GetDiscountOverrides(salesOrderId).Single(entry => entry.SalesDiscountOverrideId == discountOverride.Id);
    }

    public SalesDiscountOverrideDetailView RejectDiscountOverride(long salesOrderId, long salesDiscountOverrideId, long actorUserId, ReviewSalesDiscountOverrideCommand command)
    {
        var discountOverride = _dbContext.SalesDiscountOverrides.Single(entry => entry.Id == salesDiscountOverrideId);
        if (discountOverride.SalesOrderId != salesOrderId)
        {
            throw new InvalidOperationException("Discount override does not belong to the requested sales order.");
        }

        discountOverride.Reject(actorUserId, command.DecisionNotes);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(entry => entry.Actions)
            .SingleOrDefault(entry => entry.DocumentType == "DISCOUNT_OVERRIDE" && entry.DocumentId == salesDiscountOverrideId);
        workflowInstance?.Reject(actorUserId, "Sales discount override rejected by checker.");

        _dbContext.SaveChanges();
        return GetDiscountOverrides(salesOrderId).Single(entry => entry.SalesDiscountOverrideId == discountOverride.Id);
    }

    public CustomerCreditControlView GetCustomerCreditControl(long customerId, string currencyCode, DateOnly asOfDate)
    {
        var evaluation = EvaluateCustomerCreditControl(customerId, currencyCode, asOfDate, 0m, excludeSalesOrderId: null);
        return new CustomerCreditControlView(
            evaluation.CustomerId,
            evaluation.CustomerCode,
            evaluation.CustomerName,
            evaluation.CurrencyCode,
            evaluation.BaseCurrencyCode,
            evaluation.CreditLimitAmount,
            evaluation.CreditLimitBaseAmount,
            evaluation.CreditTermsDays,
            evaluation.IsAccountCreditHoldEnabled,
            evaluation.AccountCreditHoldReason,
            evaluation.OutstandingReceivablesAmount,
            evaluation.OutstandingReceivablesBaseAmount,
            evaluation.OpenSalesOrderAmount,
            evaluation.OpenSalesOrderBaseAmount,
            evaluation.CurrentExposureAmount,
            evaluation.CurrentExposureBaseAmount,
            evaluation.OverdueInvoiceCount,
            evaluation.OldestOverdueInvoiceDate,
            evaluation.ExposureIncludesMultipleCurrencies,
            evaluation.ShouldHoldNewOrders,
            evaluation.HoldReason);
    }

    private static SalesOrderDetailView Map(SalesOrder salesOrder) =>
        new(
            salesOrder.Id,
            salesOrder.SalesOrderNumber,
            salesOrder.BranchId,
            salesOrder.CustomerId,
            salesOrder.OrderDate,
            salesOrder.CurrencyCode,
            salesOrder.ExchangeRate,
            WorkflowStatusSeedIds.ToCode(salesOrder.StatusId),
            salesOrder.CreditStatusCode,
            salesOrder.CreditHoldReason,
            salesOrder.CreditLimitAmountAtCheck,
            salesOrder.CreditExposureAmountAtCheck,
            salesOrder.OverdueInvoiceCountAtCheck,
            salesOrder.CreditCheckedAtUtc,
            salesOrder.CreditReleaseDecisionCode,
            salesOrder.CreditReleaseNotes,
            salesOrder.CreditReleasedAtUtc,
            salesOrder.CreditReleasedByUserId,
            salesOrder.SubtotalAmount,
            salesOrder.TaxAmount,
            salesOrder.TotalAmount,
            salesOrder.Lines.OrderBy(line => line.LineNumber).Select(line => new SalesOrderLineView(
                line.Id,
                line.LineNumber,
                line.ItemId,
                line.WarehouseId,
                line.OrderedQuantity,
                line.AllocatedQuantity,
                line.DeliveredQuantity,
                line.UnitOfMeasureCode,
                line.UnitPrice,
                line.BaseUnitPrice,
                line.PricingRuleId,
                line.PricingRuleCode,
                line.PricingSourceCode,
                line.DiscountAmount,
                line.DiscountPercent,
                line.DiscountOverrideId,
                line.DiscountSourceCode,
                line.TaxCode,
                line.LineNetAmount,
                line.LineTaxAmount,
                line.LineTotalAmount)).ToArray());

    private static SalesDiscountOverrideDetailView Map(SalesDiscountOverride discountOverride) =>
        new(
            discountOverride.Id,
            discountOverride.OverrideNumber,
            discountOverride.SalesOrderId,
            discountOverride.SalesOrderLineId,
            discountOverride.BaseUnitPrice,
            discountOverride.RequestedUnitPrice,
            discountOverride.RequestedDiscountAmount,
            discountOverride.RequestedDiscountPercent,
            discountOverride.ReasonCode,
            discountOverride.Notes,
            WorkflowStatusSeedIds.ToCode(discountOverride.StatusId),
            discountOverride.SubmittedAtUtc,
            discountOverride.SubmittedByUserId,
            discountOverride.DecidedAtUtc,
            discountOverride.DecidedByUserId,
            discountOverride.DecisionCode,
            discountOverride.DecisionNotes);

    private ResolvedPricing ResolvePricing(
        long branchId,
        long customerId,
        DateOnly orderDate,
        string currencyCode,
        long itemId,
        decimal orderedQuantity,
        string unitOfMeasureCode,
        decimal requestedUnitPrice)
    {
        if (requestedUnitPrice > 0m)
        {
            return new ResolvedPricing(requestedUnitPrice, null, null, "MANUAL");
        }

        var normalizedCurrencyCode = currencyCode.Trim().ToUpperInvariant();
        var normalizedUnitOfMeasureCode = unitOfMeasureCode.Trim().ToUpperInvariant();

        var matchedRule = _dbContext.PricingRules
            .AsNoTracking()
            .ToArray()
            .Where(rule => rule.AppliesTo(
                branchId,
                customerId,
                itemId,
                normalizedCurrencyCode,
                normalizedUnitOfMeasureCode,
                orderedQuantity,
                orderDate))
            .OrderByDescending(rule => rule.Priority)
            .ThenByDescending(rule => rule.GetSpecificityScore())
            .ThenByDescending(rule => rule.EffectiveFromDate)
            .ThenBy(rule => rule.Id)
            .FirstOrDefault();

        if (matchedRule is null)
        {
            throw new InvalidOperationException(
                $"No active pricing rule matched item {itemId} for customer {customerId} in {normalizedCurrencyCode}.");
        }

        return new ResolvedPricing(
            matchedRule.FixedUnitPrice,
            matchedRule.Id,
            matchedRule.RuleCode,
            "RULE");
    }

    private CreditControlEvaluation EvaluateCustomerCreditControl(long customerId, string currencyCode, DateOnly asOfDate, decimal pendingOrderAmount, long? excludeSalesOrderId)
    {
        var normalizedCurrencyCode = currencyCode.Trim().ToUpperInvariant();
        var conversion = CurrencyConversionContext.Create(_dbContext);
        var customer = _dbContext.BusinessParties.SingleOrDefault(party => party.Id == customerId && party.IsActive);
        if (customer is null || !string.Equals(customer.PartyType, "CUSTOMER", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Sales order credit control requires an active customer party.");
        }

        var postedInvoices = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Where(invoice =>
                invoice.CustomerId == customerId
                && invoice.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray();

        var postedReceiptAllocations = _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .Where(receipt =>
                receipt.CustomerId == customerId
                && receipt.StatusId == WorkflowStatusSeedIds.Posted)
            .Join(
                _dbContext.AccountsReceivableReceiptAllocations.AsNoTracking(),
                receipt => receipt.Id,
                allocation => allocation.AccountsReceivableReceiptId,
                (receipt, allocation) => allocation)
            .ToArray();

        var allocationsByInvoiceId = postedReceiptAllocations
            .GroupBy(allocation => allocation.AccountsReceivableInvoiceId)
            .ToDictionary(group => group.Key, group => group.Sum(allocation => allocation.AllocationAmount));

        var outstandingInvoices = postedInvoices
            .Select(invoice => new
            {
                Invoice = invoice,
                OutstandingAmount = invoice.TotalAmount - allocationsByInvoiceId.GetValueOrDefault(invoice.Id, 0m),
                OutstandingBaseAmount = conversion.ConvertToBase(invoice.CurrencyCode, invoice.TotalAmount - allocationsByInvoiceId.GetValueOrDefault(invoice.Id, 0m), asOfDate)
            })
            .Where(entry => entry.OutstandingAmount > 0m)
            .ToArray();

        var outstandingReceivablesAmount = outstandingInvoices
            .Where(entry => string.Equals(entry.Invoice.CurrencyCode, normalizedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .Sum(entry => entry.OutstandingAmount);
        var outstandingReceivablesBaseAmount = outstandingInvoices.Sum(entry => entry.OutstandingBaseAmount);
        var overdueInvoices = outstandingInvoices
            .Where(entry => entry.Invoice.DueDate.HasValue && entry.Invoice.DueDate.Value < asOfDate)
            .ToArray();

        var openSalesOrders = _dbContext.SalesOrders
            .AsNoTracking()
            .Where(order =>
                order.CustomerId == customerId
                && order.StatusId == WorkflowStatusSeedIds.Submitted
                && order.CreditStatusCode != "REJECTED"
                && (!excludeSalesOrderId.HasValue || order.Id != excludeSalesOrderId.Value))
            .ToArray();

        var openSalesOrderAmount = openSalesOrders
            .Where(order => string.Equals(order.CurrencyCode, normalizedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .Sum(order => order.TotalAmount);
        var openSalesOrderBaseAmount = openSalesOrders.Sum(order => conversion.ConvertToBase(order.CurrencyCode, order.TotalAmount, asOfDate));
        var currentExposureAmount = outstandingReceivablesAmount + openSalesOrderAmount;
        var currentExposureBaseAmount = outstandingReceivablesBaseAmount + openSalesOrderBaseAmount;
        var pendingOrderBaseAmount = conversion.ConvertToBase(normalizedCurrencyCode, pendingOrderAmount, asOfDate);
        var exposureIncludesMultipleCurrencies = conversion.HasMixedCurrencies(
            outstandingInvoices.Select(entry => entry.Invoice.CurrencyCode)
                .Concat(openSalesOrders.Select(order => order.CurrencyCode))
                .Append(normalizedCurrencyCode));

        string? holdReason = null;
        if (customer.IsCreditHoldEnabled)
        {
            holdReason = string.IsNullOrWhiteSpace(customer.CreditHoldReason)
                ? "Customer account is on credit hold."
                : customer.CreditHoldReason;
        }
        else if (customer.CreditLimitAmount.HasValue && currentExposureBaseAmount + pendingOrderBaseAmount > customer.CreditLimitAmount.Value)
        {
            holdReason =
                $"Projected customer exposure {currentExposureBaseAmount + pendingOrderBaseAmount:0.00} {conversion.BaseCurrencyCode} exceeds the configured credit limit {customer.CreditLimitAmount.Value:0.00} {conversion.BaseCurrencyCode}.";
        }
        else if (customer.CreditTermsDays.HasValue && overdueInvoices.Length > 0)
        {
            holdReason = $"Customer has {overdueInvoices.Length} overdue receivable item(s) beyond the configured credit terms.";
        }

        return new CreditControlEvaluation(
            customer.Id,
            customer.PartyCode,
            customer.PartyName,
            normalizedCurrencyCode,
            conversion.BaseCurrencyCode,
            customer.CreditLimitAmount,
            customer.CreditLimitAmount,
            customer.CreditTermsDays,
            customer.IsCreditHoldEnabled,
            customer.CreditHoldReason,
            outstandingReceivablesAmount,
            outstandingReceivablesBaseAmount,
            openSalesOrderAmount,
            openSalesOrderBaseAmount,
            currentExposureAmount,
            currentExposureBaseAmount,
            overdueInvoices.Length,
            overdueInvoices.OrderBy(entry => entry.Invoice.DueDate).Select(entry => entry.Invoice.DueDate).FirstOrDefault(),
            exposureIncludesMultipleCurrencies,
            holdReason is not null,
            holdReason);
    }

    private sealed record ResolvedPricing(
        decimal UnitPrice,
        long? PricingRuleId,
        string? PricingRuleCode,
        string PricingSourceCode);
}
