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

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class BankTransferBatchRepository : IBankTransferBatchRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<BankTransferBatchDetailView> GetBatches() =>
        _dbContext.Set<BankTransferBatch>()
            .AsNoTracking()
            .Include(batch => batch.Lines)
            .OrderByDescending(batch => batch.TransferDate)
            .ThenByDescending(batch => batch.Id)
            .ToArray()
            .Select(Map)
            .ToArray();

    public BankTransferBatchDetailView? GetBatch(long bankTransferBatchId)
    {
        var batch = _dbContext.Set<BankTransferBatch>()
            .AsNoTracking()
            .Include(existingBatch => existingBatch.Lines)
            .SingleOrDefault(existingBatch => existingBatch.Id == bankTransferBatchId);

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

    public BankTransferBatchDetailView Create(CreateBankTransferBatchCommand command)
    {
        ValidateCreateCommand(command);
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, command.TransferDate, "bank transfer batch");

        var payments = BuildEligiblePayments(command.CurrencyCode, command.AccountsPayablePaymentIds);
        var nextSequence = (_dbContext.Set<BankTransferBatch>().Max(batch => (long?)batch.Id) ?? 0L) + 1L;
        var batch = new BankTransferBatch(
            batchNumber: $"BTB-{nextSequence:D6}",
            bankAccountCode: NormalizeCode(command.BankAccountCode, "Bank account code"),
            transferDate: command.TransferDate,
            currencyCode: NormalizeCode(command.CurrencyCode, "Currency code"));

        foreach (var payment in payments.OrderBy(payment => payment.Id))
        {
            batch.AddPayment(payment.Id, payment.PaymentNumber, payment.SupplierId, payment.AmountAmount);
        }

        _dbContext.Add(batch);
        _dbContext.SaveChanges();
        return GetBatch(batch.Id)!;
    }

    public BankTransferBatchDetailView Submit(long bankTransferBatchId, long actorUserId)
    {
        var batch = _dbContext.Set<BankTransferBatch>()
            .Include(existingBatch => existingBatch.Lines)
            .Single(existingBatch => existingBatch.Id == bankTransferBatchId);

        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, batch.TransferDate, "bank transfer batch");
        batch.Submit(actorUserId);

        var workflowInstance = new WorkflowInstance(
            moduleCode: "FINANCE",
            documentType: "BANK_TRANSFER_BATCH",
            documentId: batch.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "Bank transfer batch submitted for checker approval.");

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetBatch(batch.Id)!;
    }

    public BankTransferBatchDetailView Approve(long bankTransferBatchId, long actorUserId)
    {
        var batch = _dbContext.Set<BankTransferBatch>()
            .Include(existingBatch => existingBatch.Lines)
            .Single(existingBatch => existingBatch.Id == bankTransferBatchId);

        batch.Approve(actorUserId);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(instance => instance.Actions)
            .SingleOrDefault(instance =>
                instance.DocumentType == "BANK_TRANSFER_BATCH" &&
                instance.DocumentId == batch.Id);

        workflowInstance?.Approve(actorUserId, "Bank transfer batch approved by checker for controlled posting.");

        _dbContext.SaveChanges();
        return GetBatch(batch.Id)!;
    }

    public BankTransferBatchDetailView Post(long bankTransferBatchId, long actorUserId)
    {
        var batch = _dbContext.Set<BankTransferBatch>()
            .Include(existingBatch => existingBatch.Lines)
            .Single(existingBatch => existingBatch.Id == bankTransferBatchId);

        if (batch.StatusId == WorkflowStatusSeedIds.Posted)
        {
            return GetBatch(batch.Id)!;
        }

        var postingPeriodId = AccountingPeriodLockGuard.RequireOpenPeriod(_dbContext, batch.TransferDate, "bank transfer batch");
        using var transaction = _dbContext.Database.IsRelational()
            ? _dbContext.Database.BeginTransaction()
            : null;

        var payments = _dbContext.Set<AccountsPayablePayment>()
            .Include(payment => payment.Allocations)
            .Where(payment => batch.Lines.Select(line => line.AccountsPayablePaymentId).Contains(payment.Id))
            .ToArray()
            .ToDictionary(payment => payment.Id);

        foreach (var line in batch.Lines)
        {
            if (!payments.TryGetValue(line.AccountsPayablePaymentId, out var payment))
            {
                throw new InvalidOperationException($"Bank transfer batch line references unknown AP payment {line.AccountsPayablePaymentId}.");
            }

            if (!string.Equals(payment.PaymentMethod, "BANK_TRANSFER", StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException($"AP payment {payment.PaymentNumber} is not eligible for bank transfer batch posting.");
            }

            if (payment.StatusId != WorkflowStatusSeedIds.Approved)
            {
                throw new InvalidOperationException($"AP payment {payment.PaymentNumber} must be approved and unposted before bank transfer batch posting.");
            }

            if (payment.GeneralLedgerBatchId is not null)
            {
                throw new InvalidOperationException($"AP payment {payment.PaymentNumber} is already posted and cannot be posted again through a bank transfer batch.");
            }

            PostApprovedPayment(payment, postingPeriodId, actorUserId, batch.TransferDate);
        }

        batch.Post(actorUserId);
        _dbContext.SaveChanges();
        transaction?.Commit();
        return GetBatch(batch.Id)!;
    }

    private void ValidateCreateCommand(CreateBankTransferBatchCommand command)
    {
        if (command.AccountsPayablePaymentIds.Count == 0)
        {
            throw new InvalidOperationException("Bank transfer batch must contain at least one AP payment.");
        }

        var duplicatePaymentIds = command.AccountsPayablePaymentIds
            .GroupBy(paymentId => paymentId)
            .Where(group => group.Count() > 1)
            .Select(group => group.Key)
            .ToArray();

        if (duplicatePaymentIds.Length > 0)
        {
            throw new InvalidOperationException($"Bank transfer batch cannot contain duplicate AP payment IDs: {string.Join(", ", duplicatePaymentIds)}.");
        }
    }

    private AccountsPayablePayment[] BuildEligiblePayments(string currencyCode, IReadOnlyCollection<long> paymentIds)
    {
        var normalizedCurrencyCode = NormalizeCode(currencyCode, "Currency code");
        var payments = _dbContext.Set<AccountsPayablePayment>()
            .AsNoTracking()
            .ToArray()
            .Where(payment => paymentIds.Contains(payment.Id))
            .ToArray();

        foreach (var paymentId in paymentIds)
        {
            if (!payments.Any(payment => payment.Id == paymentId))
            {
                throw new InvalidOperationException($"Bank transfer batch references unknown AP payment {paymentId}.");
            }
        }

        var existingBatchPaymentIds = _dbContext.Set<BankTransferBatchLine>()
            .AsNoTracking()
            .Where(line => paymentIds.Contains(line.AccountsPayablePaymentId))
            .Select(line => line.AccountsPayablePaymentId)
            .Distinct()
            .ToArray();

        if (existingBatchPaymentIds.Length > 0)
        {
            throw new InvalidOperationException($"AP payment IDs already belong to a bank transfer batch: {string.Join(", ", existingBatchPaymentIds)}.");
        }

        foreach (var payment in payments)
        {
            if (!string.Equals(payment.PaymentMethod, "BANK_TRANSFER", StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException($"AP payment {payment.PaymentNumber} must use BANK_TRANSFER to join a bank transfer batch.");
            }

            if (!string.Equals(payment.CurrencyCode, normalizedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException($"AP payment {payment.PaymentNumber} uses currency {payment.CurrencyCode}, which does not match bank transfer batch currency {normalizedCurrencyCode}.");
            }

            if (payment.StatusId != WorkflowStatusSeedIds.Approved)
            {
                throw new InvalidOperationException($"AP payment {payment.PaymentNumber} must be approved and unposted before batch creation.");
            }

            if (payment.GeneralLedgerBatchId is not null)
            {
                throw new InvalidOperationException($"AP payment {payment.PaymentNumber} is already posted and cannot join a bank transfer batch.");
            }
        }

        return payments;
    }

    private void PostApprovedPayment(AccountsPayablePayment payment, long postingPeriodId, long actorUserId, DateOnly postingDate)
    {
        var nextBatchSequence = (_dbContext.GeneralLedgerBatches.Max(batch => (long?)batch.Id) ?? 0L) + 1L;
        var batch = new GeneralLedgerBatch(
            batchNumber: $"GLB-{nextBatchSequence:D6}",
            batchType: "AP_PAYMENT",
            branchId: null,
            periodId: postingPeriodId,
            descriptionText: $"AP payment posting for {payment.PaymentNumber}",
            sourceModule: "FINANCE",
            sourceDocumentId: payment.Id);

        var journal = new GeneralLedgerJournal(
            generalLedgerBatchId: batch.Id,
            journalNumber: $"JRNL-{nextBatchSequence:D6}",
            journalType: "AP_PAYMENT",
            postingDate: postingDate,
            taxDate: postingDate,
            documentDate: payment.PaymentDate,
            descriptionText: $"Supplier payment {payment.PaymentNumber}");

        journal.AddLine(
            lineNumber: 1,
            generalLedgerAccountId: 2001L,
            branchId: null,
            departmentId: null,
            partyId: payment.SupplierId,
            itemId: null,
            debitAmount: payment.AmountAmount,
            creditAmount: 0m,
            currencyCode: payment.CurrencyCode,
            fxRate: 1m,
            lineDescription: "Accounts payable settlement");

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

        batch.AddJournal(journal);
        _dbContext.GeneralLedgerBatches.Add(batch);
        payment.Post(actorUserId, batch.Id);
    }

    private static BankTransferBatchDetailView Map(BankTransferBatch batch) =>
        new(
            batch.Id,
            batch.BatchNumber,
            batch.BankAccountCode,
            batch.TransferDate,
            batch.CurrencyCode,
            WorkflowStatusSeedIds.ToCode(batch.StatusId),
            batch.TotalPaymentCount,
            batch.TotalAmount,
            batch.SubmittedAtUtc,
            batch.SubmittedByUserId,
            batch.ApprovedAtUtc,
            batch.ApprovedByUserId,
            batch.PostedAtUtc,
            batch.PostedByUserId,
            batch.Lines
                .OrderBy(line => line.Id)
                .Select(line => new BankTransferBatchLineView(
                    line.Id,
                    line.AccountsPayablePaymentId,
                    line.PaymentNumber,
                    line.SupplierId,
                    line.AmountAmount))
                .ToArray());

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

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