using CashLineERP.Application.IslamicFinance;
using CashLineERP.Application.IslamicFinance.ReceivablesFinance;
using CashLineERP.Domain.Finance;
using CashLineERP.Domain.IslamicFinance.Facilities;
using CashLineERP.Domain.IslamicFinance.ProductProfiles;
using CashLineERP.Domain.IslamicFinance.ReceivablesFinance;
using CashLineERP.SharedKernel.IslamicFinance;
using CashLineERP.SharedKernel.Workflow;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories.IslamicFinance;

public sealed class ReceivablesFinanceRepository : IReceivablesFinanceRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<IslamicFinanceReceivablesCaseDetailView> GetCases(
        string? statusCode = null,
        long? customerPartyId = null,
        long? partnerLenderId = null)
    {
        var normalizedStatusCode = string.IsNullOrWhiteSpace(statusCode)
            ? null
            : IslamicFinanceWorkflowCatalog.NormalizeStatusCode(statusCode);

        return _dbContext.IslamicFinanceReceivablesCases
            .AsNoTracking()
            .Where(entry => normalizedStatusCode == null || entry.StatusCode == normalizedStatusCode)
            .Where(entry => !customerPartyId.HasValue || entry.CustomerPartyId == customerPartyId.Value)
            .Where(entry => !partnerLenderId.HasValue || entry.PartnerLenderId == partnerLenderId.Value)
            .OrderBy(entry => entry.CaseNumber)
            .ToArray()
            .Select(MapCase)
            .ToArray();
    }

    public IslamicFinanceReceivablesCaseDetailView? GetCase(long receivablesCaseId)
    {
        var entry = _dbContext.IslamicFinanceReceivablesCases
            .AsNoTracking()
            .SingleOrDefault(caseEntry => caseEntry.Id == receivablesCaseId);

        return entry is null ? null : MapCase(entry);
    }

    public IslamicFinanceReceivablesCaseDetailView Create(CreateIslamicFinanceReceivablesCaseCommand command)
    {
        if (command.Lines.Count == 0)
        {
            throw new InvalidOperationException("Islamic Finance receivables case must contain at least one invoice line.");
        }

        var request = _dbContext.IslamicFinanceRequests
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == command.FinancingRequestId)
            ?? throw new InvalidOperationException("Islamic Finance receivables case requires a valid financing request.");

        if (!string.Equals(request.StatusCode, IslamicFinanceWorkflowCatalog.Assigned, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Islamic Finance receivables case can only be created from an assigned financing request.");
        }

        if (!request.AssignedPartnerLenderId.HasValue)
        {
            throw new InvalidOperationException("Islamic Finance receivables case requires a partner-lender-assigned financing request.");
        }

        var partnerLender = _dbContext.IslamicFinancePartnerLenders
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == request.AssignedPartnerLenderId.Value)
            ?? throw new InvalidOperationException("Islamic Finance receivables case requires a valid assigned partner lender.");

        if (!partnerLender.IsActive || !partnerLender.SupportsReceivablesFinance)
        {
            throw new InvalidOperationException("Islamic Finance receivables case requires an active partner lender that supports receivables finance.");
        }

        var productProfile = _dbContext.IslamicFinanceProductProfiles
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == request.ProductProfileId)
            ?? throw new InvalidOperationException("Islamic Finance receivables case requires a valid product profile.");

        if (!productProfile.IsActive
            || productProfile.ProductTypeValue != IslamicFinanceProductType.ReceivablesFinance)
        {
            throw new InvalidOperationException("Islamic Finance receivables case requires an active receivables-finance product profile.");
        }

        IslamicFinanceFacility? facility = null;
        if (request.FacilityId.HasValue)
        {
            facility = _dbContext.IslamicFinanceFacilities
                .SingleOrDefault(entry => entry.Id == request.FacilityId.Value)
                ?? throw new InvalidOperationException("Islamic Finance receivables case requires a valid facility when the financing request references one.");

            if (!facility.IsActive
                || !string.Equals(facility.StatusCode, IslamicFinanceWorkflowCatalog.Active, StringComparison.OrdinalIgnoreCase)
                || facility.CustomerPartyId != request.CustomerPartyId
                || facility.PartnerLenderId != partnerLender.Id
                || facility.ProductProfileId != request.ProductProfileId
                || !string.Equals(facility.CurrencyCode, request.TransactionCurrencyCode, StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException("Islamic Finance receivables case facility must be active and aligned to the financing request.");
            }
        }

        var normalizedCaseNumber = command.CaseNumber.Trim().ToUpperInvariant();
        if (_dbContext.IslamicFinanceReceivablesCases.AsNoTracking().Any(entry => entry.CaseNumber == normalizedCaseNumber))
        {
            throw new InvalidOperationException("Islamic Finance receivables case number already exists.");
        }

        var invoiceIds = command.Lines.Select(entry => entry.AccountsReceivableInvoiceId).Distinct().ToArray();
        if (invoiceIds.Length != command.Lines.Count)
        {
            throw new InvalidOperationException("Islamic Finance receivables case cannot contain duplicate invoice rows.");
        }

        var invoices = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .ToArray()
            .Where(entry => invoiceIds.Contains(entry.Id))
            .ToArray();

        if (invoices.Length != invoiceIds.Length)
        {
            throw new InvalidOperationException("Islamic Finance receivables case references an unknown AR invoice.");
        }

        var linkedInvoiceIds = _dbContext.IslamicFinanceReceivablesCaseLines
            .AsNoTracking()
            .ToArray()
            .Where(entry => invoiceIds.Contains(entry.AccountsReceivableInvoiceId))
            .Select(entry => entry.AccountsReceivableInvoiceId)
            .Distinct()
            .ToArray();

        if (linkedInvoiceIds.Length > 0)
        {
            throw new InvalidOperationException($"Islamic Finance receivables case cannot relink invoices already financed: {string.Join(", ", linkedInvoiceIds.Order())}.");
        }

        var outstandingByInvoiceId = GetOutstandingAmountsByInvoiceId(request.CustomerPartyId, request.TransactionCurrencyCode);
        var invoiceMap = invoices.ToDictionary(entry => entry.Id);
        var lineBlueprints = new List<(long InvoiceId, decimal ReceivableAmount, decimal FinancedAmount)>();

        foreach (var line in command.Lines)
        {
            var invoice = invoiceMap[line.AccountsReceivableInvoiceId];
            if (invoice.StatusId != WorkflowStatusSeedIds.Posted)
            {
                throw new InvalidOperationException("Islamic Finance receivables case requires posted AR invoices.");
            }

            if (invoice.CustomerId != request.CustomerPartyId
                || !string.Equals(invoice.CurrencyCode, request.TransactionCurrencyCode, StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException("Islamic Finance receivables case invoices must align to the financing request customer and currency.");
            }

            var outstandingAmount = outstandingByInvoiceId.GetValueOrDefault(invoice.Id);
            if (outstandingAmount <= 0m)
            {
                throw new InvalidOperationException("Islamic Finance receivables case requires invoices with outstanding receivable amounts.");
            }

            if (line.FinancedAmount <= 0m || line.FinancedAmount > outstandingAmount)
            {
                throw new InvalidOperationException("Islamic Finance receivables financed amount must be positive and cannot exceed the invoice outstanding amount.");
            }

            lineBlueprints.Add((invoice.Id, outstandingAmount, line.FinancedAmount));
        }

        var totalReceivableAmount = lineBlueprints.Sum(entry => entry.ReceivableAmount);
        var financedAmount = lineBlueprints.Sum(entry => entry.FinancedAmount);
        if (facility is not null && financedAmount > facility.AvailableAmount())
        {
            throw new InvalidOperationException("Islamic Finance receivables financed amount cannot exceed available facility amount.");
        }

        var advanceRatePercent = Math.Round(financedAmount / totalReceivableAmount * 100m, 2, MidpointRounding.AwayFromZero);

        var entry = new IslamicFinanceReceivablesCase(
            normalizedCaseNumber,
            request.Id,
            request.CustomerPartyId,
            partnerLender.Id,
            facility?.Id,
            request.ProductProfileId,
            request.TransactionCurrencyCode,
            totalReceivableAmount,
            financedAmount,
            advanceRatePercent);

        _dbContext.IslamicFinanceReceivablesCases.Add(entry);
        _dbContext.SaveChanges();

        var caseLines = lineBlueprints
            .Select(line => new IslamicFinanceReceivablesCaseLine(
                entry.Id,
                line.InvoiceId,
                line.ReceivableAmount,
                line.FinancedAmount,
                entry.CreatedByUserId))
            .ToArray();
        _dbContext.IslamicFinanceReceivablesCaseLines.AddRange(caseLines);
        _dbContext.SaveChanges();

        return GetCase(entry.Id)!;
    }

    public IslamicFinanceReceivablesCaseDetailView Submit(long receivablesCaseId, long actorUserId)
    {
        var entry = _dbContext.IslamicFinanceReceivablesCases.Single(caseEntry => caseEntry.Id == receivablesCaseId);
        entry.Submit(actorUserId);
        _dbContext.SaveChanges();
        return GetCase(entry.Id)!;
    }

    public IslamicFinanceReceivablesCaseDetailView Approve(long receivablesCaseId, long actorUserId)
    {
        var entry = _dbContext.IslamicFinanceReceivablesCases.Single(caseEntry => caseEntry.Id == receivablesCaseId);
        entry.Approve(actorUserId);
        _dbContext.SaveChanges();
        return GetCase(entry.Id)!;
    }

    public IslamicFinanceReceivablesCaseDetailView Activate(long receivablesCaseId, long actorUserId)
    {
        var entry = _dbContext.IslamicFinanceReceivablesCases.Single(caseEntry => caseEntry.Id == receivablesCaseId);
        if (entry.FacilityId.HasValue)
        {
            var facility = _dbContext.IslamicFinanceFacilities.Single(facilityEntry => facilityEntry.Id == entry.FacilityId.Value);
            facility.Allocate(entry.FinancedAmount, actorUserId);
        }

        var lines = _dbContext.IslamicFinanceReceivablesCaseLines
            .Where(line => line.IslamicFinanceReceivablesCaseId == receivablesCaseId)
            .ToArray();
        foreach (var line in lines)
        {
            line.Activate(actorUserId);
        }

        entry.Activate(actorUserId);
        _dbContext.SaveChanges();
        return GetCase(entry.Id)!;
    }

    public IReadOnlyCollection<IslamicFinanceLifecycleHistoryEntryView> GetCaseHistory(long receivablesCaseId)
    {
        var receivablesCase = _dbContext.IslamicFinanceReceivablesCases
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == receivablesCaseId);

        if (receivablesCase is null)
        {
            return Array.Empty<IslamicFinanceLifecycleHistoryEntryView>();
        }

        var events = new List<IslamicFinanceLifecycleHistoryEntryView>
        {
            new(
                receivablesCase.Id,
                "RECEIVABLES_CASE_CREATED",
                "RECEIVABLES_CASE",
                receivablesCase.Id,
                receivablesCase.CreatedByUserId,
                receivablesCase.CaseNumber,
                receivablesCase.CreatedAtUtc)
        };

        var lines = _dbContext.IslamicFinanceReceivablesCaseLines
            .AsNoTracking()
            .Where(entry => entry.IslamicFinanceReceivablesCaseId == receivablesCaseId)
            .OrderBy(entry => entry.CreatedAtUtc)
            .ToArray();

        foreach (var line in lines)
        {
            events.Add(new IslamicFinanceLifecycleHistoryEntryView(
                receivablesCase.Id,
                "RECEIVABLE_INVOICE_LINKED",
                "RECEIVABLES_CASE_LINE",
                line.Id,
                line.CreatedByUserId,
                $"Invoice {line.AccountsReceivableInvoiceId}",
                line.CreatedAtUtc));

            if (line.UpdatedAtUtc.HasValue)
            {
                events.Add(new IslamicFinanceLifecycleHistoryEntryView(
                    receivablesCase.Id,
                    "RECEIVABLE_INVOICE_ACTIVATED",
                    "RECEIVABLES_CASE_LINE",
                    line.Id,
                    line.UpdatedByUserId,
                    $"Invoice {line.AccountsReceivableInvoiceId}",
                    line.UpdatedAtUtc.Value));
            }
        }

        AddIfPresent(events, receivablesCase.Id, "RECEIVABLES_CASE_SUBMITTED", "RECEIVABLES_CASE", receivablesCase.Id, receivablesCase.SubmittedByUserId, null, receivablesCase.SubmittedAtUtc);
        AddIfPresent(events, receivablesCase.Id, "RECEIVABLES_CASE_APPROVED", "RECEIVABLES_CASE", receivablesCase.Id, receivablesCase.ApprovedByUserId, null, receivablesCase.ApprovedAtUtc);
        AddIfPresent(events, receivablesCase.Id, "RECEIVABLES_CASE_ACTIVATED", "RECEIVABLES_CASE", receivablesCase.Id, receivablesCase.ActivatedByUserId, null, receivablesCase.ActivatedAtUtc);

        return events.OrderBy(entry => entry.OccurredAtUtc).ThenBy(entry => entry.EventCode).ToArray();
    }

    public IslamicFinanceReceivablesCaseAuditView? GetCaseAudit(long receivablesCaseId)
    {
        var receivablesCase = GetCase(receivablesCaseId);
        return receivablesCase is null
            ? null
            : new IslamicFinanceReceivablesCaseAuditView(receivablesCase, GetCaseHistory(receivablesCaseId));
    }

    public IReadOnlyCollection<IslamicFinanceReceivablesEligibilityInvoiceView> GetEligibleInvoices(long customerPartyId, string? currencyCode, DateOnly asOfDate)
    {
        var normalizedCurrencyCode = string.IsNullOrWhiteSpace(currencyCode) ? null : currencyCode.Trim().ToUpperInvariant();
        var invoices = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Where(entry => entry.CustomerId == customerPartyId
                && entry.StatusId == WorkflowStatusSeedIds.Posted
                && (normalizedCurrencyCode == null || entry.CurrencyCode == normalizedCurrencyCode))
            .OrderBy(entry => entry.DueDate ?? entry.InvoiceDate)
            .ThenBy(entry => entry.InvoiceNumber)
            .ToArray();

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

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

        var invoiceIds = invoices.Select(entry => entry.Id).ToArray();
        var linkedInvoiceIdSet = _dbContext.IslamicFinanceReceivablesCaseLines
            .AsNoTracking()
            .ToArray()
            .Where(entry => invoiceIds.Contains(entry.AccountsReceivableInvoiceId))
            .Select(entry => entry.AccountsReceivableInvoiceId)
            .ToHashSet();

        return invoices
            .Where(invoice => outstandingByInvoiceId.ContainsKey(invoice.Id))
            .Select(invoice => new IslamicFinanceReceivablesEligibilityInvoiceView(
                invoice.Id,
                invoice.InvoiceNumber,
                invoice.CustomerId,
                invoice.SalesOrderId,
                invoice.DispatchOrderId,
                invoice.InvoiceDate,
                invoice.DueDate,
                invoice.CurrencyCode,
                invoice.TotalAmount,
                outstandingByInvoiceId[invoice.Id],
                invoice.DueDate.HasValue ? Math.Max(0, asOfDate.DayNumber - invoice.DueDate.Value.DayNumber) : 0,
                linkedInvoiceIdSet.Contains(invoice.Id)))
            .ToArray();
    }

    private IslamicFinanceReceivablesCaseDetailView MapCase(IslamicFinanceReceivablesCase entry)
    {
        var lines = _dbContext.IslamicFinanceReceivablesCaseLines
            .AsNoTracking()
            .Where(line => line.IslamicFinanceReceivablesCaseId == entry.Id)
            .OrderBy(line => line.Id)
            .ToArray();

        var lineInvoiceIds = lines.Select(line => line.AccountsReceivableInvoiceId).ToArray();
        var invoicesById = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .ToArray()
            .Where(invoice => lineInvoiceIds.Contains(invoice.Id))
            .ToArray()
            .ToDictionary(invoice => invoice.Id);

        return new IslamicFinanceReceivablesCaseDetailView(
            entry.Id,
            entry.CaseNumber,
            entry.FinancingRequestId,
            entry.CustomerPartyId,
            entry.PartnerLenderId,
            entry.FacilityId,
            entry.ProductProfileId,
            entry.TransactionCurrencyCode,
            entry.TotalReceivableAmount,
            entry.FinancedAmount,
            entry.AdvanceRatePercent,
            entry.StatusCode,
            entry.WorkflowInstanceId,
            entry.CreatedAtUtc,
            entry.CreatedByUserId,
            entry.SubmittedAtUtc,
            entry.SubmittedByUserId,
            entry.ApprovedAtUtc,
            entry.ApprovedByUserId,
            entry.ActivatedAtUtc,
            entry.ActivatedByUserId,
            entry.UpdatedAtUtc,
            entry.UpdatedByUserId,
            lines.Select(line =>
            {
                var invoice = invoicesById[line.AccountsReceivableInvoiceId];
                return new IslamicFinanceReceivablesCaseLineView(
                    line.Id,
                    line.IslamicFinanceReceivablesCaseId,
                    line.AccountsReceivableInvoiceId,
                    invoice.InvoiceNumber,
                    invoice.InvoiceDate,
                    invoice.DueDate,
                    line.ReceivableAmount,
                    line.FinancedAmount,
                    line.StatusCode,
                    line.CreatedAtUtc,
                    line.CreatedByUserId,
                    line.UpdatedAtUtc,
                    line.UpdatedByUserId);
            }).ToArray());
    }

    private Dictionary<long, decimal> GetOutstandingAmountsByInvoiceId(long customerId, string currencyCode)
    {
        var postedInvoices = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Where(entry => entry.CustomerId == customerId
                && entry.CurrencyCode == currencyCode
                && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray();

        var invoiceIds = postedInvoices.Select(entry => entry.Id).ToArray();
        if (invoiceIds.Length == 0)
        {
            return [];
        }

        var postedReceiptIds = _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .Where(entry => entry.CustomerId == customerId
                && entry.CurrencyCode == currencyCode
                && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .Select(entry => entry.Id)
            .ToArray();

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

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

    private static decimal GetCustomerSignedAmount(string invoiceType, decimal totalAmount) =>
        string.Equals(invoiceType, "CREDIT_NOTE", StringComparison.OrdinalIgnoreCase)
            ? -Math.Abs(totalAmount)
            : totalAmount;

    private static void AddIfPresent(
        ICollection<IslamicFinanceLifecycleHistoryEntryView> events,
        long entityId,
        string eventCode,
        string sourceType,
        long? sourceId,
        long? actorUserId,
        string? notes,
        DateTime? occurredAtUtc)
    {
        if (!occurredAtUtc.HasValue)
        {
            return;
        }

        events.Add(new IslamicFinanceLifecycleHistoryEntryView(
            entityId,
            eventCode,
            sourceType,
            sourceId,
            actorUserId,
            notes,
            occurredAtUtc.Value));
    }
}
