using CashLineERP.Application.Finance;
using CashLineERP.SharedKernel.Workflow;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class SupplierAccountReconciliationRepository : ISupplierAccountReconciliationRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<SupplierAccountReconciliationSummaryView> GetSummaries(DateOnly asOfDate, long? supplierId = null, string? currencyCode = null)
    {
        var suppliers = _dbContext.BusinessParties
            .AsNoTracking()
            .Where(entry => entry.IsActive && string.Equals(entry.PartyType, "SUPPLIER", StringComparison.OrdinalIgnoreCase))
            .Where(entry => !supplierId.HasValue || entry.Id == supplierId.Value)
            .Select(entry => new SupplierContext(entry.Id, entry.PartyCode, entry.PartyName))
            .ToArray();

        return suppliers
            .Select(supplier => BuildSummary(supplier, asOfDate, NormalizeOptionalCurrencyCode(currencyCode)))
            .Where(summary => summary.CurrentBalanceAmount != 0m || summary.LastStatementId.HasValue || summary.OnHoldBillCount > 0)
            .OrderBy(summary => summary.SupplierCode)
            .ToArray();
    }

    public SupplierAccountReconciliationDetailView GetDetail(long supplierId, DateOnly asOfDate, string? currencyCode = null)
    {
        var supplier = _dbContext.BusinessParties
            .AsNoTracking()
            .Where(entry => entry.Id == supplierId && entry.IsActive && string.Equals(entry.PartyType, "SUPPLIER", StringComparison.OrdinalIgnoreCase))
            .Select(entry => new SupplierContext(entry.Id, entry.PartyCode, entry.PartyName))
            .SingleOrDefault()
            ?? throw new InvalidOperationException($"Active supplier '{supplierId}' was not found.");

        var normalizedCurrencyCode = NormalizeOptionalCurrencyCode(currencyCode);
        var detail = BuildDetail(supplier, asOfDate, normalizedCurrencyCode);
        return detail;
    }

    private SupplierAccountReconciliationSummaryView BuildSummary(SupplierContext supplier, DateOnly asOfDate, string? requestedCurrencyCode)
    {
        var detail = BuildDetail(supplier, asOfDate, requestedCurrencyCode);
        return new SupplierAccountReconciliationSummaryView(
            detail.SupplierId,
            detail.SupplierCode,
            detail.SupplierName,
            detail.AsOfDate,
            detail.CurrencyCode,
            detail.LastStatementId,
            detail.LastStatementNumber,
            detail.LastStatementGeneratedAtUtc,
            detail.LastStatementClosingBalanceAmount,
            detail.CurrentBalanceAmount,
            detail.OpenPayableAmount,
            detail.OverduePayableAmount,
            detail.UnappliedPaymentAmount,
            detail.OnHoldBillCount,
            detail.DifferenceAmount);
    }

    private SupplierAccountReconciliationDetailView BuildDetail(SupplierContext supplier, DateOnly asOfDate, string? requestedCurrencyCode)
    {
        var statementLines = BuildSupplierStatementLines(supplier.Id, asOfDate);
        var resolvedCurrencyCode = ResolveCurrencyCode(
            requestedCurrencyCode,
            statementLines.Select(line => line.CurrencyCode));

        var filteredLines = statementLines
            .Where(line => string.Equals(line.CurrencyCode, resolvedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .OrderBy(line => line.BusinessDate)
            .ThenBy(line => line.DocumentNumber)
            .ToArray();

        var runningBalance = 0m;
        var normalizedLines = new List<SupplierAccountReconciliationLineView>(filteredLines.Length);
        foreach (var line in filteredLines)
        {
            runningBalance += line.ChargeAmount - line.SettlementAmount;
            normalizedLines.Add(line with { RunningBalanceAmount = runningBalance });
        }

        var postedPaymentIds = _dbContext.AccountsPayablePayments
            .AsNoTracking()
            .Where(entry =>
                entry.SupplierId == supplier.Id &&
                entry.StatusId == WorkflowStatusSeedIds.Posted &&
                entry.PaymentDate <= asOfDate &&
                string.Equals(entry.CurrencyCode, resolvedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .Select(entry => entry.Id)
            .ToArray();

        var allocationsByBillId = _dbContext.AccountsPayablePaymentAllocations
            .AsNoTracking()
            .ToArray()
            .Where(entry => postedPaymentIds.Contains(entry.AccountsPayablePaymentId) && entry.AllocationDate <= asOfDate)
            .GroupBy(entry => entry.AccountsPayableBillId)
            .ToDictionary(group => group.Key, group => group.Sum(item => item.AllocationAmount));

        var postedBills = _dbContext.AccountsPayableBills
            .AsNoTracking()
            .Where(entry =>
                entry.SupplierId == supplier.Id &&
                entry.StatusId == WorkflowStatusSeedIds.Posted &&
                entry.BillDate <= asOfDate &&
                string.Equals(entry.CurrencyCode, resolvedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .ToArray();

        var openPayableAmount = 0m;
        var overduePayableAmount = 0m;
        foreach (var bill in postedBills)
        {
            var signedTotal = GetSupplierSignedAmount(bill.BillType, bill.TotalAmount);
            var allocatedAmount = allocationsByBillId.GetValueOrDefault(bill.Id);
            var outstandingAmount = signedTotal > 0m ? Math.Max(0m, signedTotal - allocatedAmount) : signedTotal;

            if (outstandingAmount > 0m)
            {
                openPayableAmount += outstandingAmount;
                if (bill.DueDate.HasValue && bill.DueDate.Value < asOfDate)
                {
                    overduePayableAmount += outstandingAmount;
                }
            }
        }

        var unappliedPaymentAmount = _dbContext.AccountsPayablePayments
            .AsNoTracking()
            .Where(entry =>
                entry.SupplierId == supplier.Id &&
                entry.StatusId == WorkflowStatusSeedIds.Posted &&
                entry.PaymentDate <= asOfDate &&
                string.Equals(entry.CurrencyCode, resolvedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .Sum(entry => entry.UnappliedAmount);

        var onHoldBillCount = _dbContext.AccountsPayableBills
            .AsNoTracking()
            .Count(entry =>
                entry.SupplierId == supplier.Id &&
                entry.DisputeStatusCode == "ON_HOLD" &&
                string.Equals(entry.CurrencyCode, resolvedCurrencyCode, StringComparison.OrdinalIgnoreCase));

        var lastStatement = _dbContext.SupplierStatements
            .AsNoTracking()
            .Where(entry =>
                entry.SupplierId == supplier.Id &&
                string.Equals(entry.CurrencyCode, resolvedCurrencyCode, StringComparison.OrdinalIgnoreCase) &&
                (!entry.DateTo.HasValue || entry.DateTo.Value <= asOfDate))
            .OrderByDescending(entry => entry.GeneratedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .FirstOrDefault();

        var lastStatementClosingBalanceAmount = lastStatement?.ClosingBalanceAmount ?? 0m;
        var differenceAmount = runningBalance - lastStatementClosingBalanceAmount;

        return new SupplierAccountReconciliationDetailView(
            supplier.Id,
            supplier.SupplierCode,
            supplier.SupplierName,
            asOfDate,
            resolvedCurrencyCode,
            lastStatement?.Id,
            lastStatement?.StatementNumber,
            lastStatement?.GeneratedAtUtc,
            lastStatementClosingBalanceAmount,
            runningBalance,
            openPayableAmount,
            overduePayableAmount,
            unappliedPaymentAmount,
            onHoldBillCount,
            differenceAmount,
            normalizedLines);
    }

    private IReadOnlyCollection<SupplierAccountReconciliationLineView> BuildSupplierStatementLines(long supplierId, DateOnly asOfDate)
    {
        var billLines = _dbContext.AccountsPayableBills
            .AsNoTracking()
            .Where(entry => entry.SupplierId == supplierId && entry.StatusId == WorkflowStatusSeedIds.Posted && entry.BillDate <= asOfDate)
            .ToArray()
            .Select(entry =>
            {
                var signedTotal = GetSupplierSignedAmount(entry.BillType, entry.TotalAmount);
                return new SupplierAccountReconciliationLineView(
                    "INVOICE",
                    entry.BillType,
                    entry.Id,
                    entry.BillNumber,
                    entry.BillDate,
                    entry.DueDate,
                    entry.CurrencyCode,
                    signedTotal > 0m ? signedTotal : 0m,
                    signedTotal < 0m ? Math.Abs(signedTotal) : 0m,
                    0m,
                    WorkflowStatusSeedIds.ToCode(entry.StatusId),
                    entry.DisputeStatusCode,
                    entry.PurchaseOrderId?.ToString() ?? entry.PurchaseReturnId?.ToString());
            });

        var paymentLines = _dbContext.AccountsPayablePayments
            .AsNoTracking()
            .Where(entry => entry.SupplierId == supplierId && entry.StatusId == WorkflowStatusSeedIds.Posted && entry.PaymentDate <= asOfDate)
            .ToArray()
            .Select(entry => new SupplierAccountReconciliationLineView(
                "PAYMENT",
                "AP_PAYMENT",
                entry.Id,
                entry.PaymentNumber,
                entry.PaymentDate,
                null,
                entry.CurrencyCode,
                0m,
                entry.AmountAmount,
                0m,
                WorkflowStatusSeedIds.ToCode(entry.StatusId),
                entry.PaymentMethod,
                entry.ReferenceNumber));

        return billLines.Concat(paymentLines).ToArray();
    }

    private static decimal GetSupplierSignedAmount(string billType, decimal amount) =>
        string.Equals(billType, "DEBIT_NOTE", StringComparison.OrdinalIgnoreCase) ? -amount : amount;

    private static string? NormalizeOptionalCurrencyCode(string? currencyCode) =>
        string.IsNullOrWhiteSpace(currencyCode) ? null : currencyCode.Trim().ToUpperInvariant();

    private static string ResolveCurrencyCode(string? requestedCurrencyCode, IEnumerable<string> currencies)
    {
        if (!string.IsNullOrWhiteSpace(requestedCurrencyCode))
        {
            return requestedCurrencyCode;
        }

        var distinctCurrencies = currencies
            .Where(currency => !string.IsNullOrWhiteSpace(currency))
            .Select(currency => currency.Trim().ToUpperInvariant())
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        return distinctCurrencies.Length switch
        {
            0 => "EGP",
            1 => distinctCurrencies[0],
            _ => throw new InvalidOperationException("Supplier account reconciliation requires currencyCode when multiple document currencies exist for the selected supplier scope.")
        };
    }

    private sealed record SupplierContext(long Id, string SupplierCode, string SupplierName);
}
