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

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class CustomerRefundRepository : ICustomerRefundRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<CustomerRefundDetailView> GetRefunds(long? customerId = null) =>
        _dbContext.CustomerRefunds
            .AsNoTracking()
            .Where(entry => !customerId.HasValue || entry.CustomerId == customerId.Value)
            .OrderByDescending(entry => entry.RefundDate)
            .ThenByDescending(entry => entry.Id)
            .ToArray()
            .Select(Map)
            .ToArray();

    public CustomerRefundDetailView? GetRefund(long customerRefundId)
    {
        var refund = _dbContext.CustomerRefunds
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == customerRefundId);

        return refund is null ? null : Map(refund);
    }

    public CustomerRefundDetailView Create(CreateCustomerRefundCommand command)
    {
        ValidateCreateCommand(command);
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, command.RefundDate, "customer refund");

        var receipt = ResolveSourceReceipt(command.CustomerId, command.SourceAccountsReceivableReceiptId, command.CurrencyCode);
        var reservedAmount = GetReservedRefundAmount(receipt.Id);
        var availableRefundAmount = Math.Max(0m, receipt.UnappliedAmount - reservedAmount);
        if (command.AmountAmount > availableRefundAmount)
        {
            throw new InvalidOperationException($"Customer refund amount {command.AmountAmount} exceeds available refundable amount {availableRefundAmount} on receipt {receipt.ReceiptNumber}.");
        }

        if (command.PaymentChannelConfigurationId.HasValue)
        {
            ResolveRefundPaymentChannel(command.PaymentChannelConfigurationId.Value, command.CurrencyCode, requireActive: false);
        }

        var nextSequence = (_dbContext.CustomerRefunds.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var refund = new CustomerRefund(
            refundNumber: $"ARRFND-{nextSequence:D6}",
            customerId: command.CustomerId,
            sourceAccountsReceivableReceiptId: receipt.Id,
            refundDate: command.RefundDate,
            refundMethod: NormalizeCode(command.RefundMethod, "customer refund method"),
            currencyCode: NormalizeCode(command.CurrencyCode, "customer refund currency code"),
            amountAmount: command.AmountAmount,
            paymentChannelConfigurationId: command.PaymentChannelConfigurationId,
            referenceNumber: NormalizeOptionalText(command.ReferenceNumber),
            notes: NormalizeOptionalText(command.Notes));

        _dbContext.CustomerRefunds.Add(refund);
        _dbContext.SaveChanges();
        return GetRefund(refund.Id)!;
    }

    public CustomerRefundDetailView Submit(long customerRefundId, long actorUserId)
    {
        var refund = _dbContext.CustomerRefunds.Single(entry => entry.Id == customerRefundId);
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, refund.RefundDate, "customer refund");
        refund.Submit(actorUserId);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(entry => entry.Actions)
            .SingleOrDefault(entry => entry.DocumentType == "CUSTOMER_REFUND" && entry.DocumentId == refund.Id);
        if (workflowInstance is null)
        {
            workflowInstance = new WorkflowInstance(
                moduleCode: "FINANCE",
                documentType: "CUSTOMER_REFUND",
                documentId: refund.Id,
                makerUserId: actorUserId,
                workflowRuleId: 1L);
            workflowInstance.Submit(actorUserId, "Customer refund submitted for checker approval.");
            _dbContext.WorkflowInstances.Add(workflowInstance);
        }
        else
        {
            workflowInstance.Submit(actorUserId, "Customer refund resubmitted for checker approval.");
        }

        _dbContext.SaveChanges();
        return GetRefund(refund.Id)!;
    }

    public CustomerRefundDetailView Approve(long customerRefundId, long actorUserId)
    {
        var refund = _dbContext.CustomerRefunds.Single(entry => entry.Id == customerRefundId);
        refund.Approve(actorUserId);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(entry => entry.Actions)
            .SingleOrDefault(entry => entry.DocumentType == "CUSTOMER_REFUND" && entry.DocumentId == refund.Id);
        workflowInstance?.Approve(actorUserId, "Customer refund approved by checker.");

        _dbContext.SaveChanges();
        return GetRefund(refund.Id)!;
    }

    public CustomerRefundDetailView Return(long customerRefundId, long actorUserId, string reviewNotes)
    {
        var refund = _dbContext.CustomerRefunds.Single(entry => entry.Id == customerRefundId);
        refund.Return(actorUserId, reviewNotes);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(entry => entry.Actions)
            .SingleOrDefault(entry => entry.DocumentType == "CUSTOMER_REFUND" && entry.DocumentId == refund.Id);
        workflowInstance?.Return(actorUserId, "Customer refund returned for correction.");

        _dbContext.SaveChanges();
        return GetRefund(refund.Id)!;
    }

    public CustomerRefundDetailView Reject(long customerRefundId, long actorUserId, string reviewNotes)
    {
        var refund = _dbContext.CustomerRefunds.Single(entry => entry.Id == customerRefundId);
        refund.Reject(actorUserId, reviewNotes);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(entry => entry.Actions)
            .SingleOrDefault(entry => entry.DocumentType == "CUSTOMER_REFUND" && entry.DocumentId == refund.Id);
        workflowInstance?.Reject(actorUserId, "Customer refund rejected by checker.");

        _dbContext.SaveChanges();
        return GetRefund(refund.Id)!;
    }

    public CustomerRefundDetailView Post(long customerRefundId, long actorUserId)
    {
        var refund = _dbContext.CustomerRefunds.Single(entry => entry.Id == customerRefundId);
        if (refund.StatusId == WorkflowStatusSeedIds.Posted)
        {
            return GetRefund(refund.Id)!;
        }

        var sourceReceipt = ResolveSourceReceipt(refund.CustomerId, refund.SourceAccountsReceivableReceiptId, refund.CurrencyCode);
        var reservedAmount = GetReservedRefundAmount(refund.SourceAccountsReceivableReceiptId, excludeRefundId: refund.Id);
        var availableRefundAmount = Math.Max(0m, sourceReceipt.UnappliedAmount - reservedAmount);
        if (refund.AmountAmount > availableRefundAmount)
        {
            throw new InvalidOperationException($"Customer refund amount {refund.AmountAmount} exceeds remaining refundable amount {availableRefundAmount} on receipt {sourceReceipt.ReceiptNumber}.");
        }

        if (refund.PaymentChannelConfigurationId.HasValue)
        {
            ResolveRefundPaymentChannel(refund.PaymentChannelConfigurationId.Value, refund.CurrencyCode, requireActive: true);
        }

        var periodId = AccountingPeriodLockGuard.RequireOpenPeriod(_dbContext, refund.RefundDate, "customer refund");
        using var transaction = _dbContext.Database.IsRelational()
            ? _dbContext.Database.BeginTransaction()
            : null;

        var nextBatchSequence = (_dbContext.GeneralLedgerBatches.Max(batch => (long?)batch.Id) ?? 0L) + 1L;
        var batch = new GeneralLedgerBatch(
            batchNumber: $"GLB-{nextBatchSequence:D6}",
            batchType: "CUSTOMER_REFUND",
            branchId: null,
            periodId: periodId,
            descriptionText: $"Customer refund posting for {refund.RefundNumber}",
            sourceModule: "FINANCE",
            sourceDocumentId: refund.Id);

        var journal = new GeneralLedgerJournal(
            generalLedgerBatchId: batch.Id,
            journalNumber: $"JRNL-{nextBatchSequence:D6}",
            journalType: "CUSTOMER_REFUND",
            postingDate: refund.RefundDate,
            taxDate: refund.RefundDate,
            documentDate: refund.RefundDate,
            descriptionText: $"Customer refund {refund.RefundNumber}");

        journal.AddLine(
            lineNumber: 1,
            generalLedgerAccountId: 1101L,
            branchId: null,
            departmentId: null,
            partyId: refund.CustomerId,
            itemId: null,
            debitAmount: refund.AmountAmount,
            creditAmount: 0m,
            currencyCode: refund.CurrencyCode,
            fxRate: 1m,
            lineDescription: "Accounts receivable customer refund settlement");

        journal.AddLine(
            lineNumber: 2,
            generalLedgerAccountId: 1001L,
            branchId: null,
            departmentId: null,
            partyId: refund.CustomerId,
            itemId: null,
            debitAmount: 0m,
            creditAmount: refund.AmountAmount,
            currencyCode: refund.CurrencyCode,
            fxRate: 1m,
            lineDescription: "Cash / bank customer refund");

        batch.AddJournal(journal);
        _dbContext.GeneralLedgerBatches.Add(batch);

        refund.Post(actorUserId, batch.Id);
        OutboxEventWriter.Enqueue(
            _dbContext,
            sourceModuleCode: "FINANCE",
            aggregateType: "CUSTOMER_REFUND",
            aggregateId: refund.Id,
            eventType: "CUSTOMER_REFUND_POSTED",
            payload: new
            {
                refund.RefundNumber,
                refund.CustomerId,
                refund.SourceAccountsReceivableReceiptId,
                refund.CurrencyCode,
                refund.AmountAmount,
                refund.PaymentChannelConfigurationId,
                refund.ReferenceNumber
            },
            actorUserId: actorUserId,
            correlationKey: refund.RefundNumber);

        _dbContext.SaveChanges();
        transaction?.Commit();
        return GetRefund(refund.Id)!;
    }

    private AccountsReceivableReceipt ResolveSourceReceipt(long customerId, long receiptId, string currencyCode)
    {
        var receipt = _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == receiptId)
            ?? throw new InvalidOperationException("Customer refund requires a valid source AR receipt.");

        if (receipt.StatusId != WorkflowStatusSeedIds.Posted)
        {
            throw new InvalidOperationException("Customer refund requires a posted source AR receipt.");
        }

        if (receipt.CustomerId != customerId)
        {
            throw new InvalidOperationException("Customer refund source receipt belongs to a different customer.");
        }

        if (!string.Equals(receipt.CurrencyCode, currencyCode, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Customer refund currency must match the source receipt currency.");
        }

        return receipt;
    }

    private PaymentChannelConfiguration ResolveRefundPaymentChannel(long paymentChannelConfigurationId, string currencyCode, bool requireActive)
    {
        var channel = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == paymentChannelConfigurationId)
            ?? throw new InvalidOperationException("Customer refund payment channel was not found.");

        if (!string.Equals(channel.UsageCode, "CUSTOMER_REFUND", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Customer refund payment channel must be configured with CUSTOMER_REFUND usage.");
        }

        if (!string.Equals(channel.CurrencyCode, currencyCode, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Customer refund payment channel currency must match the refund currency.");
        }

        if (requireActive && !channel.IsActive)
        {
            throw new InvalidOperationException("Customer refund posting requires an active payment channel when channel routing is configured.");
        }

        return channel;
    }

    private decimal GetReservedRefundAmount(long sourceAccountsReceivableReceiptId, long? excludeRefundId = null) =>
        _dbContext.CustomerRefunds
            .AsNoTracking()
            .Where(entry =>
                entry.SourceAccountsReceivableReceiptId == sourceAccountsReceivableReceiptId
                && (!excludeRefundId.HasValue || entry.Id != excludeRefundId.Value))
            .Sum(entry => entry.AmountAmount);

    private static CustomerRefundDetailView Map(CustomerRefund refund) =>
        new(
            refund.Id,
            refund.RefundNumber,
            refund.CustomerId,
            refund.SourceAccountsReceivableReceiptId,
            refund.RefundDate,
            refund.RefundMethod,
            refund.CurrencyCode,
            refund.AmountAmount,
            refund.PaymentChannelConfigurationId,
            WorkflowStatusSeedIds.ToCode(refund.StatusId),
            refund.ReferenceNumber,
            refund.Notes,
            refund.SubmittedAtUtc,
            refund.SubmittedByUserId,
            refund.ApprovedAtUtc,
            refund.ApprovedByUserId,
            refund.ReturnedAtUtc,
            refund.ReturnedByUserId,
            refund.ReturnNotes,
            refund.RejectedAtUtc,
            refund.RejectedByUserId,
            refund.RejectionNotes,
            refund.PostedAtUtc,
            refund.PostedByUserId,
            refund.GeneralLedgerBatchId);

    private static void ValidateCreateCommand(CreateCustomerRefundCommand command)
    {
        if (command.AmountAmount <= 0m)
        {
            throw new InvalidOperationException("Customer refund amount must be greater than zero.");
        }
    }

    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();
}
