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

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class SupplierStatementRepository : ISupplierStatementRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<SupplierStatementSummaryView> GetStatements(long? supplierId = null) =>
        _dbContext.SupplierStatements
            .AsNoTracking()
            .Include(entry => entry.Lines)
            .Include(entry => entry.DeliveryLogs)
            .Where(entry => !supplierId.HasValue || entry.SupplierId == supplierId.Value)
            .OrderByDescending(entry => entry.GeneratedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .ToArray()
            .Select(MapSummary)
            .ToArray();

    public SupplierStatementDetailView? GetStatement(long supplierStatementId)
    {
        var statement = _dbContext.SupplierStatements
            .AsNoTracking()
            .Include(entry => entry.Lines)
            .Include(entry => entry.DeliveryLogs)
            .SingleOrDefault(entry => entry.Id == supplierStatementId);

        return statement is null ? null : MapDetail(statement);
    }

    public SupplierStatementDetailView GenerateStatement(long actorUserId, CreateSupplierStatementCommand command)
    {
        if (command.DateFrom.HasValue && command.DateTo.HasValue && command.DateFrom.Value > command.DateTo.Value)
        {
            throw new InvalidOperationException("Supplier statement date range is invalid because dateFrom is after dateTo.");
        }

        var supplier = _dbContext.BusinessParties
            .AsNoTracking()
            .Where(entry => entry.Id == command.SupplierId && entry.IsActive)
            .Select(entry => new
            {
                entry.Id,
                entry.PartyCode,
                entry.PartyName,
                entry.PartyType
            })
            .SingleOrDefault();

        if (supplier is null || !string.Equals(supplier.PartyType, "SUPPLIER", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException($"Active supplier '{command.SupplierId}' was not found.");
        }

        var statementLines = BuildSupplierStatementLines(command.SupplierId);
        var resolvedCurrencyCode = ResolveCurrencyCode(
            command.CurrencyCode,
            statementLines
                .Where(line => !command.DateTo.HasValue || line.BusinessDate <= command.DateTo.Value)
                .Select(line => line.CurrencyCode));

        var openingBalanceAmount = statementLines
            .Where(line =>
                string.Equals(line.CurrencyCode, resolvedCurrencyCode, StringComparison.OrdinalIgnoreCase) &&
                command.DateFrom.HasValue &&
                line.BusinessDate < command.DateFrom.Value)
            .Sum(line => line.ChargeAmount - line.SettlementAmount);

        var filteredLines = statementLines
            .Where(line =>
                string.Equals(line.CurrencyCode, resolvedCurrencyCode, StringComparison.OrdinalIgnoreCase) &&
                (!command.DateFrom.HasValue || line.BusinessDate >= command.DateFrom.Value) &&
                (!command.DateTo.HasValue || line.BusinessDate <= command.DateTo.Value))
            .OrderBy(line => line.BusinessDate)
            .ThenBy(line => line.DocumentNumber)
            .ToArray();

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

        var asOfDate = command.DateTo ?? DateOnly.FromDateTime(DateTime.UtcNow.Date);
        var outstandingByBill = GetOutstandingBillViews(command.SupplierId, resolvedCurrencyCode, asOfDate);

        var statement = new SupplierStatement(
            statementNumber: $"SSTMT-{_dbContext.SupplierStatements.Count() + 1:000000}",
            supplierId: supplier.Id,
            supplierCode: supplier.PartyCode,
            supplierName: supplier.PartyName,
            dateFrom: command.DateFrom,
            dateTo: command.DateTo,
            currencyCode: resolvedCurrencyCode,
            openingBalanceAmount: openingBalanceAmount,
            totalChargeAmount: normalizedLines.Sum(line => line.ChargeAmount),
            totalSettlementAmount: normalizedLines.Sum(line => line.SettlementAmount),
            closingBalanceAmount: runningBalance,
            openPayableAmount: outstandingByBill.Where(entry => entry.OutstandingAmount > 0m).Sum(entry => entry.OutstandingAmount),
            overduePayableAmount: outstandingByBill.Where(entry => entry.OutstandingAmount > 0m && entry.DueDate.HasValue && entry.DueDate.Value < asOfDate).Sum(entry => entry.OutstandingAmount),
            actorUserId: actorUserId);

        foreach (var line in normalizedLines)
        {
            statement.AddLine(
                line.EntryType,
                line.DocumentType,
                line.DocumentId,
                line.DocumentNumber,
                line.BusinessDate,
                line.DueDate,
                line.CurrencyCode,
                line.ChargeAmount,
                line.SettlementAmount,
                line.RunningBalanceAmount,
                line.StatusCode,
                line.ReferenceText);
        }

        _dbContext.SupplierStatements.Add(statement);
        _dbContext.SaveChanges();

        return MapDetail(statement);
    }

    public IReadOnlyCollection<SupplierStatementDeliveryLogView> GetDeliveryLogs(long supplierStatementId)
    {
        EnsureStatementExists(supplierStatementId);

        return _dbContext.SupplierStatementDeliveryLogs
            .AsNoTracking()
            .Where(entry => entry.SupplierStatementId == supplierStatementId)
            .OrderByDescending(entry => entry.DeliveredAtUtc)
            .ThenByDescending(entry => entry.Id)
            .ToArray()
            .Select(Map)
            .ToArray();
    }

    public SupplierStatementDeliveryLogView RecordDelivery(long supplierStatementId, long actorUserId, RecordSupplierStatementDeliveryCommand command)
    {
        var statement = _dbContext.SupplierStatements
            .Include(entry => entry.DeliveryLogs)
            .SingleOrDefault(entry => entry.Id == supplierStatementId)
            ?? throw new InvalidOperationException($"Supplier statement '{supplierStatementId}' was not found.");

        var log = statement.AddDeliveryLog(
            command.DeliveredAtUtc,
            command.ChannelCode,
            command.RecipientAddress,
            command.DeliveryStatusCode,
            command.Notes,
            actorUserId);

        _dbContext.SaveChanges();
        return Map(log);
    }

    public SupplierStatementDeliveryLogView AcknowledgeDelivery(long supplierStatementDeliveryLogId, long actorUserId, AcknowledgeSupplierStatementDeliveryCommand command)
    {
        var log = _dbContext.SupplierStatementDeliveryLogs
            .SingleOrDefault(entry => entry.Id == supplierStatementDeliveryLogId)
            ?? throw new InvalidOperationException($"Supplier statement delivery log '{supplierStatementDeliveryLogId}' was not found.");

        log.Acknowledge(actorUserId, command.AcknowledgmentStatusCode, command.Notes);
        _dbContext.SaveChanges();
        return Map(log);
    }

    private IReadOnlyCollection<StatementLineDraft> BuildSupplierStatementLines(long supplierId)
    {
        var billLines = _dbContext.AccountsPayableBills
            .AsNoTracking()
            .Where(entry => entry.SupplierId == supplierId && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray()
            .Select(entry =>
            {
                var signedTotal = GetSupplierSignedAmount(entry.BillType, entry.TotalAmount);
                return new StatementLineDraft(
                    "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.PurchaseOrderId?.ToString() ?? entry.PurchaseReturnId?.ToString());
            });

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

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

    private IReadOnlyCollection<OutstandingBillView> GetOutstandingBillViews(long supplierId, string currencyCode, DateOnly asOfDate)
    {
        var postedPaymentIds = _dbContext.AccountsPayablePayments
            .AsNoTracking()
            .Where(entry => entry.SupplierId == supplierId && entry.StatusId == WorkflowStatusSeedIds.Posted && entry.PaymentDate <= asOfDate)
            .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));

        return _dbContext.AccountsPayableBills
            .AsNoTracking()
            .Where(entry =>
                entry.SupplierId == supplierId &&
                entry.StatusId == WorkflowStatusSeedIds.Posted &&
                string.Equals(entry.CurrencyCode, currencyCode, StringComparison.OrdinalIgnoreCase) &&
                entry.BillDate <= asOfDate)
            .ToArray()
            .Select(entry =>
            {
                var signedTotal = GetSupplierSignedAmount(entry.BillType, entry.TotalAmount);
                var allocatedAmount = allocationsByBillId.GetValueOrDefault(entry.Id);
                var outstandingAmount = signedTotal > 0m ? Math.Max(0m, signedTotal - allocatedAmount) : signedTotal;
                return new OutstandingBillView(entry.Id, entry.DueDate, outstandingAmount);
            })
            .ToArray();
    }

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

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

        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 statement generation requires currencyCode when multiple document currencies exist for the selected supplier range.")
        };
    }

    private static SupplierStatementSummaryView MapSummary(SupplierStatement statement) =>
        new(
            statement.Id,
            statement.StatementNumber,
            statement.SupplierId,
            statement.SupplierCode,
            statement.SupplierName,
            statement.DateFrom,
            statement.DateTo,
            statement.CurrencyCode,
            statement.OpeningBalanceAmount,
            statement.TotalChargeAmount,
            statement.TotalSettlementAmount,
            statement.ClosingBalanceAmount,
            statement.OpenPayableAmount,
            statement.OverduePayableAmount,
            statement.Lines.Count,
            statement.DeliveryLogs.Count,
            statement.GeneratedAtUtc,
            statement.GeneratedByUserId);

    private static SupplierStatementDetailView MapDetail(SupplierStatement statement) =>
        new(
            statement.Id,
            statement.StatementNumber,
            statement.SupplierId,
            statement.SupplierCode,
            statement.SupplierName,
            statement.DateFrom,
            statement.DateTo,
            statement.CurrencyCode,
            statement.OpeningBalanceAmount,
            statement.TotalChargeAmount,
            statement.TotalSettlementAmount,
            statement.ClosingBalanceAmount,
            statement.OpenPayableAmount,
            statement.OverduePayableAmount,
            statement.GeneratedAtUtc,
            statement.GeneratedByUserId,
            statement.Lines
                .OrderBy(entry => entry.BusinessDate)
                .ThenBy(entry => entry.DocumentNumber)
                .Select(entry => new SupplierStatementLineView(
                    entry.EntryType,
                    entry.DocumentType,
                    entry.DocumentId,
                    entry.DocumentNumber,
                    entry.BusinessDate,
                    entry.DueDate,
                    entry.CurrencyCode,
                    entry.ChargeAmount,
                    entry.SettlementAmount,
                    entry.RunningBalanceAmount,
                    entry.StatusCode,
                    entry.ReferenceText))
                .ToArray());

    private void EnsureStatementExists(long supplierStatementId)
    {
        if (!_dbContext.SupplierStatements.AsNoTracking().Any(entry => entry.Id == supplierStatementId))
        {
            throw new InvalidOperationException($"Supplier statement '{supplierStatementId}' was not found.");
        }
    }

    private static SupplierStatementDeliveryLogView Map(SupplierStatementDeliveryLog log) =>
        new(
            log.Id,
            log.SupplierStatementId,
            log.DeliveredAtUtc,
            log.ChannelCode,
            log.RecipientAddress,
            log.DeliveryStatusCode,
            log.Notes,
            log.AcknowledgmentStatusCode,
            log.AcknowledgedAtUtc,
            log.AcknowledgedByUserId,
            log.CreatedByUserId,
            log.CreatedAtUtc);

    private sealed record StatementLineDraft(
        string EntryType,
        string DocumentType,
        long DocumentId,
        string DocumentNumber,
        DateOnly BusinessDate,
        DateOnly? DueDate,
        string CurrencyCode,
        decimal ChargeAmount,
        decimal SettlementAmount,
        decimal RunningBalanceAmount,
        string StatusCode,
        string? ReferenceText);

    private sealed record OutstandingBillView(long AccountsPayableBillId, DateOnly? DueDate, decimal OutstandingAmount);
}
