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

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class AccountsPayableBillRepository : IAccountsPayableBillRepository
{
    private const decimal MatchTolerance = 0.01m;

    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<AccountsPayableBillDetailView> GetBills() =>
        _dbContext.AccountsPayableBills
            .AsNoTracking()
            .Include(bill => bill.Lines)
            .OrderByDescending(bill => bill.BillDate)
            .ThenByDescending(bill => bill.Id)
            .ToArray()
            .Select(Map)
            .ToArray();

    public AccountsPayableBillDetailView? GetBill(long accountsPayableBillId)
    {
        var bill = _dbContext.AccountsPayableBills
            .AsNoTracking()
            .Include(existingBill => existingBill.Lines)
            .SingleOrDefault(existingBill => existingBill.Id == accountsPayableBillId);

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

    public AccountsPayableBillDetailView Create(CreateAccountsPayableBillCommand command)
    {
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, command.BillDate, "AP bill");

        var nextSequence = (_dbContext.AccountsPayableBills.Max(bill => (long?)bill.Id) ?? 0L) + 1L;
        var bill = new AccountsPayableBill(
            billNumber: $"BILL-{nextSequence:D6}",
            supplierId: command.SupplierId,
            purchaseOrderId: command.PurchaseOrderId,
            goodsReceiptId: command.GoodsReceiptId,
            purchaseReturnId: command.PurchaseReturnId,
            billType: command.BillType,
            billDate: command.BillDate,
            dueDate: command.DueDate,
            currencyCode: command.CurrencyCode);

        var lineNumber = 1;
        foreach (var line in command.Lines)
        {
            bill.AddLine(
                lineNumber++,
                line.PurchaseOrderLineId,
                line.ItemId,
                line.QuantityBilled,
                line.UnitOfMeasureCode,
                line.UnitPrice,
                line.TaxCode);
        }

        _dbContext.AccountsPayableBills.Add(bill);
        _dbContext.SaveChanges();

        return GetBill(bill.Id)!;
    }

    public AccountsPayableBillDetailView Submit(long accountsPayableBillId, long actorUserId)
    {
        var bill = _dbContext.AccountsPayableBills
            .Include(existingBill => existingBill.Lines)
            .Single(existingBill => existingBill.Id == accountsPayableBillId);

        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, bill.BillDate, "AP bill");
        bill.Submit(actorUserId);
        _dbContext.SaveChanges();
        return GetBill(bill.Id)!;
    }

    public AccountsPayableBillMatchView GetBillMatch(long accountsPayableBillId)
    {
        var bill = _dbContext.AccountsPayableBills
            .AsNoTracking()
            .Include(existingBill => existingBill.Lines)
            .Single(existingBill => existingBill.Id == accountsPayableBillId);

        return BuildMatchView(bill);
    }

    public AccountsPayableBillMatchView ApproveMatch(long accountsPayableBillId, long actorUserId)
    {
        var bill = _dbContext.AccountsPayableBills
            .Include(existingBill => existingBill.Lines)
            .Single(existingBill => existingBill.Id == accountsPayableBillId);

        if (bill.IsOnHold)
        {
            throw new InvalidOperationException("AP bill cannot complete finance match approval while supplier dispute hold is active.");
        }

        var match = BuildMatchView(bill);
        var isMatched = match.BlockingIssueCount == 0;
        bill.RecordMatchReview(actorUserId, isMatched, match.TotalVarianceAmount, match.Summary);

        if (match.IsEligibleForApproval)
        {
            bill.ApproveForPosting(actorUserId);
        }

        _dbContext.SaveChanges();
        return GetBillMatch(bill.Id);
    }

    public AccountsPayableBillDetailView PlaceOnHold(long accountsPayableBillId, long actorUserId, PlaceAccountsPayableBillOnHoldCommand command)
    {
        var bill = _dbContext.AccountsPayableBills
            .Include(existingBill => existingBill.Lines)
            .Single(existingBill => existingBill.Id == accountsPayableBillId);

        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, bill.BillDate, "AP bill");
        bill.PlaceOnHold(
            actorUserId,
            NormalizeCode(command.ReasonCode, "Supplier dispute reason code"),
            NormalizeOptionalNotes(command.Notes));

        _dbContext.SaveChanges();
        return GetBill(bill.Id)!;
    }

    public AccountsPayableBillDetailView ResolveDispute(long accountsPayableBillId, long actorUserId, ResolveAccountsPayableBillDisputeCommand command)
    {
        var bill = _dbContext.AccountsPayableBills
            .Include(existingBill => existingBill.Lines)
            .Single(existingBill => existingBill.Id == accountsPayableBillId);

        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, bill.BillDate, "AP bill");
        bill.ResolveDispute(
            actorUserId,
            NormalizeCode(command.DecisionCode, "Supplier dispute resolution decision"),
            NormalizeOptionalNotes(command.Notes));

        _dbContext.SaveChanges();
        return GetBill(bill.Id)!;
    }

    public AccountsPayableBillDetailView Post(long accountsPayableBillId, long actorUserId)
    {
        var bill = _dbContext.AccountsPayableBills
            .Include(existingBill => existingBill.Lines)
            .Single(existingBill => existingBill.Id == accountsPayableBillId);

        if (bill.StatusId == WorkflowStatusSeedIds.Posted)
        {
            return GetBill(bill.Id)!;
        }

        var postingPeriodId = AccountingPeriodLockGuard.RequireOpenPeriod(_dbContext, bill.BillDate, "AP bill");
        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: bill.IsDebitNote ? "AP_DEBIT_NOTE" : "AP_BILL",
            branchId: null,
            periodId: postingPeriodId,
            descriptionText: $"AP bill posting for {bill.BillNumber}",
            sourceModule: "FINANCE",
            sourceDocumentId: bill.Id);

        var journal = new GeneralLedgerJournal(
            generalLedgerBatchId: batch.Id,
            journalNumber: $"JRNL-{nextBatchSequence:D6}",
            journalType: bill.IsDebitNote ? "AP_DEBIT_NOTE" : "AP_BILL",
            postingDate: bill.BillDate,
            taxDate: bill.BillDate,
            documentDate: bill.BillDate,
            descriptionText: $"Supplier bill {bill.BillNumber}");

        if (bill.IsDebitNote)
        {
            journal.AddLine(
                lineNumber: 1,
                generalLedgerAccountId: 2001L,
                branchId: null,
                departmentId: null,
                partyId: bill.SupplierId,
                itemId: null,
                debitAmount: bill.TotalAmount,
                creditAmount: 0m,
                currencyCode: bill.CurrencyCode,
                fxRate: 1m,
                lineDescription: "Accounts payable reversal for supplier debit note");

            journal.AddLine(
                lineNumber: 2,
                generalLedgerAccountId: 1401L,
                branchId: null,
                departmentId: null,
                partyId: bill.SupplierId,
                itemId: null,
                debitAmount: 0m,
                creditAmount: bill.TotalAmount,
                currencyCode: bill.CurrencyCode,
                fxRate: 1m,
                lineDescription: "Inventory / expense reversal for supplier debit note");
        }
        else
        {
            journal.AddLine(
                lineNumber: 1,
                generalLedgerAccountId: 2001L,
                branchId: null,
                departmentId: null,
                partyId: bill.SupplierId,
                itemId: null,
                debitAmount: 0m,
                creditAmount: bill.TotalAmount,
                currencyCode: bill.CurrencyCode,
                fxRate: 1m,
                lineDescription: "Accounts payable recognition");

            journal.AddLine(
                lineNumber: 2,
                generalLedgerAccountId: 1401L,
                branchId: null,
                departmentId: null,
                partyId: bill.SupplierId,
                itemId: null,
                debitAmount: bill.TotalAmount,
                creditAmount: 0m,
                currencyCode: bill.CurrencyCode,
                fxRate: 1m,
                lineDescription: "Inventory / expense interim recognition");
        }

        batch.AddJournal(journal);
        _dbContext.GeneralLedgerBatches.Add(batch);
        bill.Post(actorUserId, batch.Id);
        _dbContext.SaveChanges();
        transaction?.Commit();
        return GetBill(bill.Id)!;
    }

    private static AccountsPayableBillDetailView Map(AccountsPayableBill bill) =>
        new(
            bill.Id,
            bill.BillNumber,
            bill.SupplierId,
            bill.PurchaseOrderId,
            bill.GoodsReceiptId,
            bill.PurchaseReturnId,
            bill.BillType,
            bill.BillDate,
            bill.DueDate,
            bill.CurrencyCode,
            WorkflowStatusSeedIds.ToCode(bill.StatusId),
            bill.ThreeWayMatchStatus,
            bill.DisputeStatusCode,
            bill.DisputeReasonCode,
            bill.DisputeNotes,
            bill.ResolutionDecisionCode,
            bill.ResolutionNotes,
            bill.MatchVarianceAmount,
            bill.MatchNotes,
            bill.GeneralLedgerBatchId,
            bill.SubtotalAmount,
            bill.TaxAmount,
            bill.TotalAmount,
            bill.Lines.OrderBy(line => line.LineNumber).Select(line => new AccountsPayableBillLineView(
                line.Id,
                line.LineNumber,
                line.PurchaseOrderLineId,
                line.ItemId,
                line.QuantityBilled,
                line.UnitOfMeasureCode,
                line.UnitPrice,
                line.TaxCode,
                line.LineNetAmount,
                line.LineTaxAmount,
                line.LineTotalAmount)).ToArray());

    private AccountsPayableBillMatchView BuildMatchView(AccountsPayableBill bill)
    {
        if (bill.IsDebitNote)
        {
            return BuildDebitNoteMatchView(bill);
        }

        var purchaseOrderLines = bill.PurchaseOrderId is long purchaseOrderId
            ? _dbContext.PurchaseOrderLines
                .AsNoTracking()
                .Where(line => line.PurchaseOrderId == purchaseOrderId)
                .ToArray()
            : Array.Empty<PurchaseOrderLine>();

        long[] goodsReceiptIds;
        if (bill.GoodsReceiptId is long goodsReceiptId)
        {
            goodsReceiptIds = [goodsReceiptId];
        }
        else if (bill.PurchaseOrderId is long fallbackPurchaseOrderId)
        {
            goodsReceiptIds = _dbContext.GoodsReceipts
                .AsNoTracking()
                .Where(receipt => receipt.PurchaseOrderId == fallbackPurchaseOrderId)
                .Select(receipt => receipt.Id)
                .ToArray();
        }
        else
        {
            goodsReceiptIds = Array.Empty<long>();
        }

        var goodsReceiptLines = goodsReceiptIds.Length == 0
            ? Array.Empty<GoodsReceiptLine>()
            : _dbContext.GoodsReceiptLines
                .AsNoTracking()
                .ToArray()
                .Where(line => goodsReceiptIds.Contains(line.GoodsReceiptId))
                .ToArray();

        var matchLines = bill.Lines
            .OrderBy(line => line.LineNumber)
            .Select(line => BuildMatchLine(line, purchaseOrderLines, goodsReceiptLines))
            .ToArray();

        var blockingIssueCount = matchLines.Count(line => !string.Equals(line.MatchStatus, "MATCHED", StringComparison.OrdinalIgnoreCase));
        var totalVarianceAmount = matchLines.Sum(line => Math.Abs(line.AmountVariance));
        var billStatusCode = WorkflowStatusSeedIds.ToCode(bill.StatusId);
        var isEligibleForApproval =
            bill.StatusId == WorkflowStatusSeedIds.Submitted &&
            bill.Lines.Count > 0 &&
            !bill.IsOnHold &&
            blockingIssueCount == 0 &&
            (bill.IsDebitNote
                ? bill.PurchaseReturnId.HasValue
                : bill.PurchaseOrderId.HasValue && bill.GoodsReceiptId.HasValue);
        var isEligibleForPosting =
            bill.StatusId == WorkflowStatusSeedIds.Approved &&
            !bill.IsOnHold &&
            string.Equals(bill.ThreeWayMatchStatus, "MATCHED", StringComparison.OrdinalIgnoreCase) &&
            bill.GeneralLedgerBatchId is null;

        var summary = bill.IsOnHold
            ? $"Supplier invoice dispute hold is active with reason {bill.DisputeReasonCode ?? "UNSPECIFIED"}."
            : blockingIssueCount == 0
            ? bill.IsDebitNote
                ? "Supplier debit note aligned to the posted purchase return."
                : "Three-way match cleared against purchase order and accepted receipt quantities."
            : $"{blockingIssueCount} blocking variance(s) found across PO, GRN, and AP bill lines.";

        return new AccountsPayableBillMatchView(
            bill.Id,
            bill.BillNumber,
            billStatusCode,
            bill.ThreeWayMatchStatus,
            isEligibleForApproval,
            isEligibleForPosting,
            blockingIssueCount,
            totalVarianceAmount,
            summary,
            matchLines);
    }

    private AccountsPayableBillMatchView BuildDebitNoteMatchView(AccountsPayableBill bill)
    {
        var purchaseReturnLines = bill.PurchaseReturnId is long purchaseReturnId
            ? _dbContext.Set<PurchaseReturnLine>()
                .AsNoTracking()
                .Where(line => line.PurchaseReturnId == purchaseReturnId)
                .ToArray()
            : Array.Empty<PurchaseReturnLine>();

        var purchaseOrderLines = bill.PurchaseOrderId is long purchaseOrderId
            ? _dbContext.PurchaseOrderLines
                .AsNoTracking()
                .Where(line => line.PurchaseOrderId == purchaseOrderId)
                .ToArray()
            : Array.Empty<PurchaseOrderLine>();

        var matchLines = bill.Lines
            .OrderBy(line => line.LineNumber)
            .Select(line => BuildDebitNoteMatchLine(line, purchaseOrderLines, purchaseReturnLines))
            .ToArray();

        var blockingIssueCount = matchLines.Count(line => !string.Equals(line.MatchStatus, "MATCHED", StringComparison.OrdinalIgnoreCase));
        var totalVarianceAmount = matchLines.Sum(line => Math.Abs(line.AmountVariance));
        var billStatusCode = WorkflowStatusSeedIds.ToCode(bill.StatusId);
        var isEligibleForApproval =
            bill.StatusId == WorkflowStatusSeedIds.Submitted &&
            bill.PurchaseReturnId.HasValue &&
            bill.Lines.Count > 0 &&
            !bill.IsOnHold &&
            blockingIssueCount == 0;
        var isEligibleForPosting =
            bill.StatusId == WorkflowStatusSeedIds.Approved &&
            !bill.IsOnHold &&
            string.Equals(bill.ThreeWayMatchStatus, "MATCHED", StringComparison.OrdinalIgnoreCase) &&
            bill.GeneralLedgerBatchId is null;

        var summary = bill.IsOnHold
            ? $"Supplier invoice dispute hold is active with reason {bill.DisputeReasonCode ?? "UNSPECIFIED"}."
            : blockingIssueCount == 0
            ? "Supplier debit note aligned to the posted purchase return."
            : $"{blockingIssueCount} blocking variance(s) found across purchase return and supplier debit-note lines.";

        return new AccountsPayableBillMatchView(
            bill.Id,
            bill.BillNumber,
            billStatusCode,
            bill.ThreeWayMatchStatus,
            isEligibleForApproval,
            isEligibleForPosting,
            blockingIssueCount,
            totalVarianceAmount,
            summary,
            matchLines);
    }

    private static AccountsPayableBillMatchLineView BuildMatchLine(
        AccountsPayableBillLine billLine,
        IReadOnlyCollection<PurchaseOrderLine> purchaseOrderLines,
        IReadOnlyCollection<GoodsReceiptLine> goodsReceiptLines)
    {
        var purchaseOrderLine = billLine.PurchaseOrderLineId.HasValue
            ? purchaseOrderLines.SingleOrDefault(line => line.Id == billLine.PurchaseOrderLineId.Value)
            : purchaseOrderLines.SingleOrDefault(line => line.ItemId == billLine.ItemId);

        var acceptedQuantity = goodsReceiptLines
            .Where(line =>
                billLine.PurchaseOrderLineId.HasValue
                    ? line.PurchaseOrderLineId == billLine.PurchaseOrderLineId.Value
                    : billLine.ItemId.HasValue && line.ItemId == billLine.ItemId.Value)
            .Sum(line => line.QuantityAccepted);

        var billedQuantity = billLine.QuantityBilled ?? 0m;
        var purchaseOrderUnitPrice = purchaseOrderLine?.UnitPrice;
        var orderedQuantity = purchaseOrderLine?.OrderedQuantity;
        var quantityVariance = billedQuantity - acceptedQuantity;
        var unitPriceVariance = billLine.UnitPrice - (purchaseOrderUnitPrice ?? billLine.UnitPrice);
        var billableQuantity = purchaseOrderUnitPrice.HasValue
            ? Math.Min(billedQuantity, acceptedQuantity)
            : billedQuantity;
        var expectedAmount = purchaseOrderUnitPrice.HasValue
            ? billableQuantity * purchaseOrderUnitPrice.Value
            : billLine.LineNetAmount;
        var amountVariance = billLine.LineNetAmount - expectedAmount;

        var matchStatus = "MATCHED";
        string? notes = null;

        if (purchaseOrderLine is null)
        {
            matchStatus = "EXCEPTION";
            notes = "No linked purchase order line found for this bill line.";
        }
        else if (acceptedQuantity <= MatchTolerance)
        {
            matchStatus = "EXCEPTION";
            notes = "No accepted receipt quantity found for this bill line.";
        }
        else if (quantityVariance > MatchTolerance)
        {
            matchStatus = "EXCEPTION";
            notes = "Billed quantity exceeds accepted receipt quantity.";
        }
        else if (Math.Abs(unitPriceVariance) > MatchTolerance)
        {
            matchStatus = "EXCEPTION";
            notes = "Billed unit price differs from the awarded purchase order.";
        }
        else if (billedQuantity < acceptedQuantity - MatchTolerance)
        {
            notes = "Partial billing against accepted receipt quantity.";
        }

        return new AccountsPayableBillMatchLineView(
            billLine.Id,
            billLine.LineNumber,
            billLine.PurchaseOrderLineId,
            billLine.ItemId,
            orderedQuantity,
            acceptedQuantity,
            billLine.QuantityBilled,
            purchaseOrderUnitPrice,
            billLine.UnitPrice,
            quantityVariance,
            unitPriceVariance,
            amountVariance,
            matchStatus,
            notes);
    }

    private static AccountsPayableBillMatchLineView BuildDebitNoteMatchLine(
        AccountsPayableBillLine billLine,
        IReadOnlyCollection<PurchaseOrderLine> purchaseOrderLines,
        IReadOnlyCollection<PurchaseReturnLine> purchaseReturnLines)
    {
        var purchaseOrderLine = billLine.PurchaseOrderLineId.HasValue
            ? purchaseOrderLines.SingleOrDefault(line => line.Id == billLine.PurchaseOrderLineId.Value)
            : purchaseOrderLines.SingleOrDefault(line => line.ItemId == billLine.ItemId);

        var returnedQuantity = purchaseReturnLines
            .Where(line =>
                billLine.PurchaseOrderLineId.HasValue
                    ? line.PurchaseOrderLineId == billLine.PurchaseOrderLineId.Value
                    : billLine.ItemId.HasValue && line.ItemId == billLine.ItemId.Value)
            .Sum(line => line.QuantityReturned);

        var billedQuantity = billLine.QuantityBilled ?? 0m;
        var purchaseOrderUnitPrice = purchaseOrderLine?.UnitPrice;
        var orderedQuantity = purchaseOrderLine?.OrderedQuantity;
        var quantityVariance = billedQuantity - returnedQuantity;
        var unitPriceVariance = billLine.UnitPrice - (purchaseOrderUnitPrice ?? billLine.UnitPrice);
        var expectedAmount = purchaseOrderUnitPrice.HasValue
            ? billedQuantity * purchaseOrderUnitPrice.Value
            : billLine.LineNetAmount;
        var amountVariance = billLine.LineNetAmount - expectedAmount;

        var matchStatus = "MATCHED";
        string? notes = null;

        if (purchaseOrderLine is null)
        {
            matchStatus = "EXCEPTION";
            notes = "No linked purchase order line found for this debit-note line.";
        }
        else if (returnedQuantity <= MatchTolerance)
        {
            matchStatus = "EXCEPTION";
            notes = "No posted purchase return quantity found for this debit-note line.";
        }
        else if (quantityVariance > MatchTolerance)
        {
            matchStatus = "EXCEPTION";
            notes = "Debit-note quantity exceeds posted purchase return quantity.";
        }
        else if (Math.Abs(unitPriceVariance) > MatchTolerance)
        {
            matchStatus = "EXCEPTION";
            notes = "Debit-note unit price differs from the source purchase order.";
        }
        else if (billedQuantity < returnedQuantity - MatchTolerance)
        {
            notes = "Partial supplier debit note against the posted purchase return quantity.";
        }

        return new AccountsPayableBillMatchLineView(
            billLine.Id,
            billLine.LineNumber,
            billLine.PurchaseOrderLineId,
            billLine.ItemId,
            orderedQuantity,
            returnedQuantity,
            billLine.QuantityBilled,
            purchaseOrderUnitPrice,
            billLine.UnitPrice,
            quantityVariance,
            unitPriceVariance,
            amountVariance,
            matchStatus,
            notes);
    }

    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? NormalizeOptionalNotes(string? notes)
    {
        if (string.IsNullOrWhiteSpace(notes))
        {
            return null;
        }

        return notes.Trim();
    }
}
