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

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class CustomerStatementRepository : ICustomerStatementRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<CustomerStatementSummaryView> GetStatements(long? customerId = null) =>
        _dbContext.CustomerStatements
            .AsNoTracking()
            .Include(entry => entry.Lines)
            .Include(entry => entry.DeliveryLogs)
            .Where(entry => !customerId.HasValue || entry.CustomerId == customerId.Value)
            .OrderByDescending(entry => entry.GeneratedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .ToArray()
            .Select(MapSummary)
            .ToArray();

    public CustomerStatementDetailView? GetStatement(long customerStatementId)
    {
        var statement = _dbContext.CustomerStatements
            .AsNoTracking()
            .Include(entry => entry.Lines)
            .Include(entry => entry.DeliveryLogs)
            .SingleOrDefault(entry => entry.Id == customerStatementId);

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

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

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

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

        var statementLines = BuildCustomerStatementLines(command.CustomerId);
        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 outstandingByInvoice = GetOutstandingInvoiceViews(command.CustomerId, resolvedCurrencyCode, asOfDate);

        var statement = new CustomerStatement(
            statementNumber: $"CSTMT-{_dbContext.CustomerStatements.Count() + 1:000000}",
            customerId: customer.Id,
            customerCode: customer.PartyCode,
            customerName: customer.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,
            openInvoiceAmount: outstandingByInvoice.Where(entry => entry.OutstandingAmount > 0m).Sum(entry => entry.OutstandingAmount),
            overdueInvoiceAmount: outstandingByInvoice.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.CustomerStatements.Add(statement);
        _dbContext.SaveChanges();

        return MapDetail(statement);
    }

    public IReadOnlyCollection<CustomerStatementDeliveryLogView> GetDeliveryLogs(long customerStatementId)
    {
        EnsureStatementExists(customerStatementId);

        return _dbContext.CustomerStatementDeliveryLogs
            .AsNoTracking()
            .Where(entry => entry.CustomerStatementId == customerStatementId)
            .OrderByDescending(entry => entry.DeliveredAtUtc)
            .ThenByDescending(entry => entry.Id)
            .ToArray()
            .Select(Map)
            .ToArray();
    }

    public CustomerStatementDeliveryLogView RecordDelivery(long customerStatementId, long actorUserId, RecordCustomerStatementDeliveryCommand command)
    {
        var statement = _dbContext.CustomerStatements
            .Include(entry => entry.DeliveryLogs)
            .SingleOrDefault(entry => entry.Id == customerStatementId)
            ?? throw new InvalidOperationException($"Customer statement '{customerStatementId}' was not found.");

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

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

    private IReadOnlyCollection<StatementLineDraft> BuildCustomerStatementLines(long customerId)
    {
        var invoiceLines = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Where(entry => entry.CustomerId == customerId && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray()
            .Select(entry =>
            {
                var signedTotal = GetCustomerInvoiceSignedAmount(entry.InvoiceType, entry.TotalAmount);
                return new StatementLineDraft(
                    "INVOICE",
                    entry.InvoiceType,
                    entry.Id,
                    entry.InvoiceNumber,
                    entry.InvoiceDate,
                    entry.DueDate,
                    entry.CurrencyCode,
                    signedTotal > 0m ? signedTotal : 0m,
                    signedTotal < 0m ? Math.Abs(signedTotal) : 0m,
                    0m,
                    WorkflowStatusSeedIds.ToCode(entry.StatusId),
                    entry.EtaUuid ?? entry.DispatchOrderId?.ToString() ?? entry.SalesOrderId?.ToString());
            });

        var receiptLines = _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .Where(entry => entry.CustomerId == customerId && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray()
            .Select(entry => new StatementLineDraft(
                "PAYMENT",
                "AR_RECEIPT",
                entry.Id,
                entry.ReceiptNumber,
                entry.ReceiptDate,
                null,
                entry.CurrencyCode,
                0m,
                entry.AmountAmount,
                0m,
                WorkflowStatusSeedIds.ToCode(entry.StatusId),
                entry.ReferenceNumber));

        var refundLines = _dbContext.CustomerRefunds
            .AsNoTracking()
            .Where(entry => entry.CustomerId == customerId && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray()
            .Select(entry => new StatementLineDraft(
                "ADJUSTMENT",
                "CUSTOMER_REFUND",
                entry.Id,
                entry.RefundNumber,
                entry.RefundDate,
                null,
                entry.CurrencyCode,
                entry.AmountAmount,
                0m,
                0m,
                WorkflowStatusSeedIds.ToCode(entry.StatusId),
                entry.ReferenceNumber));

        return invoiceLines.Concat(receiptLines).Concat(refundLines).ToArray();
    }

    private IReadOnlyCollection<OutstandingInvoiceView> GetOutstandingInvoiceViews(long customerId, string currencyCode, DateOnly asOfDate)
    {
        var postedReceiptIds = _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .Where(entry => entry.CustomerId == customerId && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .Select(entry => entry.Id)
            .ToArray();

        var allocationsByInvoiceId = _dbContext.AccountsReceivableReceiptAllocations
            .AsNoTracking()
            .ToArray()
            .Where(entry => postedReceiptIds.Contains(entry.AccountsReceivableReceiptId) && entry.AllocationDate <= asOfDate)
            .GroupBy(entry => entry.AccountsReceivableInvoiceId)
            .ToDictionary(group => group.Key, group => group.Sum(item => item.AllocationAmount));

        return _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Where(entry =>
                entry.CustomerId == customerId &&
                entry.StatusId == WorkflowStatusSeedIds.Posted &&
                string.Equals(entry.CurrencyCode, currencyCode, StringComparison.OrdinalIgnoreCase) &&
                entry.InvoiceDate <= asOfDate)
            .ToArray()
            .Select(entry =>
            {
                var signedTotal = GetCustomerInvoiceSignedAmount(entry.InvoiceType, entry.TotalAmount);
                var allocatedAmount = allocationsByInvoiceId.GetValueOrDefault(entry.Id);
                var outstandingAmount = signedTotal > 0m ? Math.Max(0m, signedTotal - allocatedAmount) : signedTotal;
                return new OutstandingInvoiceView(entry.Id, entry.DueDate, outstandingAmount);
            })
            .ToArray();
    }

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

    private static decimal GetCustomerInvoiceSignedAmount(string invoiceType, decimal amount) =>
        invoiceType switch
        {
            "CREDIT" or "CREDIT_NOTE" or "CREDIT_MEMO" => -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("Customer statement generation requires currencyCode when multiple document currencies exist for the selected customer range.")
        };
    }

    private static CustomerStatementSummaryView MapSummary(CustomerStatement statement) =>
        new(
            statement.Id,
            statement.StatementNumber,
            statement.CustomerId,
            statement.CustomerCode,
            statement.CustomerName,
            statement.DateFrom,
            statement.DateTo,
            statement.CurrencyCode,
            statement.OpeningBalanceAmount,
            statement.TotalChargeAmount,
            statement.TotalSettlementAmount,
            statement.ClosingBalanceAmount,
            statement.OpenInvoiceAmount,
            statement.OverdueInvoiceAmount,
            statement.Lines.Count,
            statement.DeliveryLogs.Count,
            statement.GeneratedAtUtc,
            statement.GeneratedByUserId);

    private static CustomerStatementDetailView MapDetail(CustomerStatement statement) =>
        new(
            statement.Id,
            statement.StatementNumber,
            statement.CustomerId,
            statement.CustomerCode,
            statement.CustomerName,
            statement.DateFrom,
            statement.DateTo,
            statement.CurrencyCode,
            statement.OpeningBalanceAmount,
            statement.TotalChargeAmount,
            statement.TotalSettlementAmount,
            statement.ClosingBalanceAmount,
            statement.OpenInvoiceAmount,
            statement.OverdueInvoiceAmount,
            statement.GeneratedAtUtc,
            statement.GeneratedByUserId,
            statement.Lines
                .OrderBy(entry => entry.BusinessDate)
                .ThenBy(entry => entry.DocumentNumber)
                .Select(entry => new CustomerStatementLineView(
                    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(),
            statement.DeliveryLogs
                .OrderByDescending(entry => entry.DeliveredAtUtc)
                .ThenByDescending(entry => entry.Id)
                .Select(Map)
                .ToArray());

    private static CustomerStatementDeliveryLogView Map(CustomerStatementDeliveryLog log) =>
        new(
            log.Id,
            log.CustomerStatementId,
            log.DeliveredAtUtc,
            log.ChannelCode,
            log.RecipientAddress,
            log.DeliveryStatusCode,
            log.Notes,
            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 OutstandingInvoiceView(long AccountsReceivableInvoiceId, DateOnly? DueDate, decimal OutstandingAmount);
}
