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

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class CollectionsWorkbenchRepository : ICollectionsWorkbenchRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public CollectionsWorkbenchSummaryView GetSummary(DateOnly asOfDate)
    {
        var queue = BuildQueue(asOfDate, null, null, false);
        return new CollectionsWorkbenchSummaryView(
            asOfDate,
            queue.Count,
            queue.Select(item => item.CustomerId).Distinct().Count(),
            queue.Sum(item => item.OutstandingAmount),
            queue.Count(item => item.ActionRequired),
            queue.Count(item => item.PromiseToPayDate.HasValue),
            queue.Count(item =>
                item.PromiseToPayDate.HasValue &&
                item.PromiseToPayDate.Value < asOfDate &&
                item.OutstandingAmount > 0m));
    }

    public IReadOnlyCollection<CollectionsQueueItemView> GetQueue(DateOnly asOfDate, long? customerId = null, string? currencyCode = null, bool actionRequiredOnly = false) =>
        BuildQueue(asOfDate, customerId, currencyCode, actionRequiredOnly);

    public IReadOnlyCollection<CollectionsFollowUpActionView> GetFollowUpActions(long accountsReceivableInvoiceId) =>
        _dbContext.CollectionsFollowUpActions
            .AsNoTracking()
            .Where(action => action.AccountsReceivableInvoiceId == accountsReceivableInvoiceId)
            .OrderByDescending(action => action.ActionDate)
            .ThenByDescending(action => action.Id)
            .ToArray()
            .Select(MapAction)
            .ToArray();

    public CollectionsCustomerSummaryView GetCustomerSummary(long customerId, DateOnly asOfDate, string? currencyCode = null)
    {
        var queue = BuildQueue(asOfDate, customerId, currencyCode, false);
        var customer = _dbContext.BusinessParties
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == customerId && entry.IsActive);

        if (customer is null)
        {
            throw new InvalidOperationException("Collections customer summary requires an existing active customer.");
        }

        var invoiceIds = queue.Select(item => item.AccountsReceivableInvoiceId).ToArray();
        var recentActions = _dbContext.CollectionsFollowUpActions
            .AsNoTracking()
            .ToArray()
            .Where(action => invoiceIds.Contains(action.AccountsReceivableInvoiceId))
            .OrderByDescending(action => action.ActionDate)
            .ThenByDescending(action => action.Id)
            .Take(10)
            .Select(MapAction)
            .ToArray();

        var brokenPromiseCount = queue.Count(item =>
            item.PromiseToPayDate.HasValue &&
            item.PromiseToPayDate.Value < asOfDate &&
            item.OutstandingAmount > 0m);
        var nextFollowUpDueCount = queue.Count(item =>
            item.NextFollowUpDate.HasValue &&
            item.NextFollowUpDate.Value <= asOfDate &&
            item.OutstandingAmount > 0m);
        var resolvedCurrencyCode = string.IsNullOrWhiteSpace(currencyCode)
            ? queue.Select(item => item.CurrencyCode).Distinct().Take(2).Count() > 1 ? "MIXED" : queue.FirstOrDefault()?.CurrencyCode ?? "EGP"
            : currencyCode.Trim().ToUpperInvariant();

        return new CollectionsCustomerSummaryView(
            asOfDate,
            customer.Id,
            customer.PartyCode,
            customer.PartyName,
            resolvedCurrencyCode,
            queue.Count,
            queue.Sum(item => item.OutstandingAmount),
            queue.Count(item => item.ActionRequired),
            queue.Count(item => item.PromiseToPayDate.HasValue),
            brokenPromiseCount,
            nextFollowUpDueCount,
            queue,
            recentActions);
    }

    public CollectionsFollowUpActionView RecordFollowUp(long accountsReceivableInvoiceId, long actorUserId, RecordCollectionsFollowUpCommand command)
    {
        var invoice = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == accountsReceivableInvoiceId);

        if (invoice is null)
        {
            throw new InvalidOperationException("Collections follow-up requires an existing AR invoice.");
        }

        if (invoice.StatusId != WorkflowStatusSeedIds.Posted)
        {
            throw new InvalidOperationException("Collections follow-up requires a posted AR invoice.");
        }

        if (!invoice.DueDate.HasValue)
        {
            throw new InvalidOperationException("Collections follow-up requires an AR invoice with a due date.");
        }

        var outstandingAmount = GetOutstandingAmountsByInvoiceId(invoice.CustomerId, invoice.CurrencyCode)
            .GetValueOrDefault(invoice.Id, 0m);

        if (outstandingAmount <= 0m)
        {
            throw new InvalidOperationException("Collections follow-up requires an AR invoice with outstanding balance.");
        }

        var action = new CollectionsFollowUpAction(
            accountsReceivableInvoiceId,
            invoice.CustomerId,
            command.ActionDate,
            NormalizeCode(command.ActionTypeCode, "collections action type"),
            NormalizeCode(command.OutcomeCode, "collections outcome code"),
            command.PromiseToPayDate,
            command.NextFollowUpDate,
            NormalizeOptionalText(command.Notes),
            actorUserId);

        _dbContext.CollectionsFollowUpActions.Add(action);
        _dbContext.SaveChanges();
        return GetFollowUpActions(accountsReceivableInvoiceId).Single(entry => entry.CollectionsFollowUpActionId == action.Id);
    }

    private IReadOnlyCollection<CollectionsQueueItemView> BuildQueue(DateOnly asOfDate, long? customerId, string? currencyCode, bool actionRequiredOnly)
    {
        var normalizedCurrencyCode = string.IsNullOrWhiteSpace(currencyCode) ? null : currencyCode.Trim().ToUpperInvariant();

        var invoices = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Where(invoice =>
                invoice.StatusId == WorkflowStatusSeedIds.Posted &&
                invoice.DueDate.HasValue &&
                invoice.DueDate.Value < asOfDate &&
                (!customerId.HasValue || invoice.CustomerId == customerId.Value) &&
                (normalizedCurrencyCode == null || invoice.CurrencyCode == normalizedCurrencyCode))
            .OrderBy(invoice => invoice.DueDate)
            .ThenBy(invoice => invoice.InvoiceDate)
            .ThenBy(invoice => invoice.Id)
            .ToArray();

        if (invoices.Length == 0)
        {
            return [];
        }

        var customerIds = invoices.Select(invoice => invoice.CustomerId).Distinct().ToArray();
        var customersById = _dbContext.BusinessParties
            .AsNoTracking()
            .ToArray()
            .Where(party => customerIds.Contains(party.Id))
            .ToDictionary(party => party.Id);

        var salesOrderIds = invoices.Where(invoice => invoice.SalesOrderId.HasValue).Select(invoice => invoice.SalesOrderId!.Value).Distinct().ToArray();
        var salesOrderBranches = _dbContext.SalesOrders
            .AsNoTracking()
            .ToArray()
            .Where(order => salesOrderIds.Contains(order.Id))
            .ToDictionary(order => order.Id, order => (long?)order.BranchId);

        var invoiceIds = invoices.Select(invoice => invoice.Id).ToArray();
        var outstandingByInvoiceId = invoices
            .Select(invoice => new
            {
                invoice.Id,
                OutstandingAmount = GetOutstandingAmountsByInvoiceId(invoice.CustomerId, invoice.CurrencyCode).GetValueOrDefault(invoice.Id, 0m)
            })
            .Where(entry => entry.OutstandingAmount > 0m)
            .ToDictionary(entry => entry.Id, entry => entry.OutstandingAmount);

        var actionGroups = _dbContext.CollectionsFollowUpActions
            .AsNoTracking()
            .ToArray()
            .Where(action => invoiceIds.Contains(action.AccountsReceivableInvoiceId))
            .GroupBy(action => action.AccountsReceivableInvoiceId)
            .ToDictionary(
                group => group.Key,
                group => group.OrderByDescending(action => action.ActionDate).ThenByDescending(action => action.Id).ToArray());

        var queue = new List<CollectionsQueueItemView>();

        foreach (var invoice in invoices)
        {
            if (!outstandingByInvoiceId.TryGetValue(invoice.Id, out var outstandingAmount))
            {
                continue;
            }

            if (!customersById.TryGetValue(invoice.CustomerId, out var customer))
            {
                continue;
            }

            var overdueDays = Math.Max(1, invoice.DueDate!.Value.DayNumber - invoice.DueDate.Value.DayNumber + (asOfDate.DayNumber - invoice.DueDate.Value.DayNumber));
            actionGroups.TryGetValue(invoice.Id, out var actions);
            var latestAction = actions?.FirstOrDefault();

            var brokenPromise = latestAction?.PromiseToPayDate is { } promiseToPayDate && promiseToPayDate < asOfDate;
            var nextFollowUpDue = latestAction?.NextFollowUpDate is { } nextFollowUpDate && nextFollowUpDate <= asOfDate;
            var staleFollowUp = latestAction is not null && !latestAction.NextFollowUpDate.HasValue && latestAction.ActionDate.AddDays(7) <= asOfDate;
            var actionRequired = latestAction is null || brokenPromise || nextFollowUpDue || staleFollowUp;

            if (actionRequiredOnly && !actionRequired)
            {
                continue;
            }

            var summary = latestAction is null
                ? $"Overdue receivable with no recorded collections action as of {asOfDate:yyyy-MM-dd}."
                : $"Latest collections action {latestAction.ActionTypeCode} on {latestAction.ActionDate:yyyy-MM-dd} with outcome {latestAction.OutcomeCode}.";

            queue.Add(new CollectionsQueueItemView(
                invoice.Id,
                invoice.InvoiceNumber,
                customer.Id,
                customer.PartyCode,
                customer.PartyName,
                invoice.SalesOrderId.HasValue ? salesOrderBranches.GetValueOrDefault(invoice.SalesOrderId.Value) : null,
                invoice.InvoiceDate,
                invoice.DueDate.Value,
                invoice.CurrencyCode,
                invoice.TotalAmount,
                outstandingAmount,
                asOfDate.DayNumber - invoice.DueDate.Value.DayNumber,
                GetAgingBucket(asOfDate.DayNumber - invoice.DueDate.Value.DayNumber),
                latestAction?.ActionDate,
                latestAction?.ActionTypeCode,
                latestAction?.OutcomeCode,
                latestAction?.PromiseToPayDate,
                latestAction?.NextFollowUpDate,
                actionRequired,
                summary));
        }

        return queue
            .OrderByDescending(item => item.ActionRequired)
            .ThenByDescending(item => item.OverdueDays)
            .ThenBy(item => item.CustomerCode)
            .ThenBy(item => item.InvoiceNumber)
            .ToArray();
    }

    private Dictionary<long, decimal> GetOutstandingAmountsByInvoiceId(long customerId, string currencyCode)
    {
        var normalizedCurrencyCode = currencyCode.Trim().ToUpperInvariant();
        var postedReceipts = _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .Where(receipt =>
                receipt.CustomerId == customerId &&
                receipt.StatusId == WorkflowStatusSeedIds.Posted &&
                receipt.CurrencyCode == normalizedCurrencyCode)
            .ToArray();

        var postedReceiptIds = postedReceipts.Select(receipt => receipt.Id).ToArray();
        var allocationsByInvoiceId = _dbContext.AccountsReceivableReceiptAllocations
            .AsNoTracking()
            .ToArray()
            .Where(allocation => postedReceiptIds.Contains(allocation.AccountsReceivableReceiptId))
            .GroupBy(allocation => allocation.AccountsReceivableInvoiceId)
            .ToDictionary(group => group.Key, group => group.Sum(allocation => allocation.AllocationAmount));

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

        return invoices.ToDictionary(
            invoice => invoice.Id,
            invoice => Math.Max(0m, invoice.TotalAmount - allocationsByInvoiceId.GetValueOrDefault(invoice.Id, 0m)));
    }

    private static CollectionsFollowUpActionView MapAction(CollectionsFollowUpAction action) =>
        new(
            action.Id,
            action.AccountsReceivableInvoiceId,
            action.CustomerId,
            action.ActionDate,
            action.ActionTypeCode,
            action.OutcomeCode,
            action.PromiseToPayDate,
            action.NextFollowUpDate,
            action.Notes,
            action.CreatedByUserId,
            action.CreatedAtUtc);

    private static string GetAgingBucket(int overdueDays) =>
        overdueDays switch
        {
            <= 30 => "1_30",
            <= 60 => "31_60",
            <= 90 => "61_90",
            _ => "90_PLUS"
        };

    private static string NormalizeCode(string? value, string fieldName)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new InvalidOperationException($"{fieldName} is required.");
        }

        return value.Trim().ToUpperInvariant();
    }

    private static string? NormalizeOptionalText(string? value) =>
        string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
