using CashLineERP.Domain.Common;
using CashLineERP.SharedKernel.Workflow;

namespace CashLineERP.Domain.Procurement;

public sealed class AccountsPayableBill : Entity
{
    private const string PendingMatchStatus = "PENDING";
    private const string MatchedStatus = "MATCHED";

    public long Id { get; private set; }

    public string BillNumber { get; private set; } = string.Empty;

    public long SupplierId { get; private set; }

    public long? PurchaseOrderId { get; private set; }

    public long? GoodsReceiptId { get; private set; }

    public long? PurchaseReturnId { get; private set; }

    public string BillType { get; private set; } = "INVOICE";

    public DateOnly BillDate { get; private set; }

    public DateOnly? DueDate { get; private set; }

    public string CurrencyCode { get; private set; } = "EGP";

    public long StatusId { get; private set; } = WorkflowStatusSeedIds.Draft;

    public decimal SubtotalAmount { get; private set; }

    public decimal TaxAmount { get; private set; }

    public decimal TotalAmount { get; private set; }

    public string ThreeWayMatchStatus { get; private set; } = PendingMatchStatus;

    public decimal MatchVarianceAmount { get; private set; }

    public string? MatchNotes { get; private set; }

    public string DisputeStatusCode { get; private set; } = "CLEAR";

    public string? DisputeReasonCode { get; private set; }

    public string? DisputeNotes { get; private set; }

    public DateTime? HeldAtUtc { get; private set; }

    public long? HeldByUserId { get; private set; }

    public DateTime? ResolvedAtUtc { get; private set; }

    public long? ResolvedByUserId { get; private set; }

    public string? ResolutionDecisionCode { get; private set; }

    public string? ResolutionNotes { get; private set; }

    public DateTime? MatchedAtUtc { get; private set; }

    public long? MatchedByUserId { get; private set; }

    public DateTime? ApprovedAtUtc { get; private set; }

    public long? ApprovedByUserId { get; private set; }

    public DateTime? PostedAtUtc { get; private set; }

    public long? PostedByUserId { get; private set; }

    public long? GeneralLedgerBatchId { get; private set; }

    public List<AccountsPayableBillLine> Lines { get; private set; } = [];

    public AccountsPayableBill(
        string billNumber,
        long supplierId,
        long? purchaseOrderId,
        long? goodsReceiptId,
        long? purchaseReturnId,
        string billType,
        DateOnly billDate,
        DateOnly? dueDate,
        string currencyCode)
    {
        BillNumber = billNumber;
        SupplierId = supplierId;
        PurchaseOrderId = purchaseOrderId;
        GoodsReceiptId = goodsReceiptId;
        PurchaseReturnId = purchaseReturnId;
        BillType = billType;
        BillDate = billDate;
        DueDate = dueDate;
        CurrencyCode = currencyCode;
        ThreeWayMatchStatus = IsDebitNote ? MatchedStatus : PendingMatchStatus;
    }

    private AccountsPayableBill()
    {
    }

    public bool IsDebitNote =>
        string.Equals(BillType, "DEBIT_NOTE", StringComparison.OrdinalIgnoreCase);

    public bool IsOnHold =>
        string.Equals(DisputeStatusCode, "ON_HOLD", StringComparison.OrdinalIgnoreCase);

    public void AddLine(
        int lineNumber,
        long? purchaseOrderLineId,
        long? itemId,
        decimal? quantityBilled,
        string? unitOfMeasureCode,
        decimal unitPrice,
        string? taxCode)
    {
        Lines.Add(new AccountsPayableBillLine(
            accountsPayableBillId: Id,
            lineNumber: lineNumber,
            purchaseOrderLineId: purchaseOrderLineId,
            itemId: itemId,
            quantityBilled: quantityBilled,
            unitOfMeasureCode: unitOfMeasureCode,
            unitPrice: unitPrice,
            taxCode: taxCode));

        RecalculateTotals();
    }

    public void Submit(long actorUserId)
    {
        StatusId = WorkflowStatusSeedIds.Submitted;
        if (!IsDebitNote)
        {
            ThreeWayMatchStatus = PendingMatchStatus;
        }
        Touch(actorUserId);
    }

    public void RecordMatchReview(long actorUserId, bool isMatched, decimal varianceAmount, string? notes)
    {
        ThreeWayMatchStatus = isMatched ? MatchedStatus : "EXCEPTION";
        MatchVarianceAmount = varianceAmount;
        MatchNotes = notes;
        MatchedAtUtc = DateTime.UtcNow;
        MatchedByUserId = actorUserId;
        Touch(actorUserId);
    }

    public void ApproveForPosting(long actorUserId)
    {
        if (StatusId == WorkflowStatusSeedIds.Posted)
        {
            return;
        }

        if (StatusId != WorkflowStatusSeedIds.Submitted)
        {
            throw new InvalidOperationException("Only submitted AP bills can be approved for posting.");
        }

        if (!string.Equals(ThreeWayMatchStatus, MatchedStatus, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("AP bill must be fully matched before posting approval.");
        }

        if (IsOnHold)
        {
            throw new InvalidOperationException("AP bill cannot be approved for posting while supplier dispute hold is active.");
        }

        StatusId = WorkflowStatusSeedIds.Approved;
        ApprovedAtUtc = DateTime.UtcNow;
        ApprovedByUserId = actorUserId;
        Touch(actorUserId);
    }

    public void Post(long actorUserId, long generalLedgerBatchId)
    {
        if (StatusId == WorkflowStatusSeedIds.Posted)
        {
            GeneralLedgerBatchId ??= generalLedgerBatchId;
            return;
        }

        if (StatusId != WorkflowStatusSeedIds.Approved)
        {
            throw new InvalidOperationException("AP bill must be approved before finance posting.");
        }

        if (IsOnHold)
        {
            throw new InvalidOperationException("AP bill cannot be posted while supplier dispute hold is active.");
        }

        StatusId = WorkflowStatusSeedIds.Posted;
        PostedAtUtc = DateTime.UtcNow;
        PostedByUserId = actorUserId;
        GeneralLedgerBatchId = generalLedgerBatchId;
        Touch(actorUserId);
    }

    public void PlaceOnHold(long actorUserId, string reasonCode, string? notes)
    {
        if (StatusId == WorkflowStatusSeedIds.Posted)
        {
            throw new InvalidOperationException("Posted AP bills cannot enter supplier dispute hold.");
        }

        if (StatusId == WorkflowStatusSeedIds.Rejected || StatusId == WorkflowStatusSeedIds.Cancelled)
        {
            throw new InvalidOperationException("Rejected or cancelled AP bills cannot enter supplier dispute hold.");
        }

        if (StatusId == WorkflowStatusSeedIds.Draft)
        {
            throw new InvalidOperationException("Only submitted or approved AP bills can enter supplier dispute hold.");
        }

        DisputeStatusCode = "ON_HOLD";
        DisputeReasonCode = reasonCode;
        DisputeNotes = notes;
        HeldAtUtc = DateTime.UtcNow;
        HeldByUserId = actorUserId;
        ResolvedAtUtc = null;
        ResolvedByUserId = null;
        ResolutionDecisionCode = null;
        ResolutionNotes = null;
        Touch(actorUserId);
    }

    public void ResolveDispute(long actorUserId, string decisionCode, string? notes)
    {
        if (!IsOnHold)
        {
            throw new InvalidOperationException("AP bill does not have an active supplier dispute hold.");
        }

        if (string.Equals(decisionCode, "REJECT", StringComparison.OrdinalIgnoreCase))
        {
            StatusId = WorkflowStatusSeedIds.Rejected;
        }
        else if (string.Equals(decisionCode, "RETURN", StringComparison.OrdinalIgnoreCase))
        {
            StatusId = WorkflowStatusSeedIds.Returned;
        }
        else if (!string.Equals(decisionCode, "RELEASE", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Supplier invoice dispute resolution decision must be RELEASE, RETURN, or REJECT.");
        }

        DisputeStatusCode = "RESOLVED";
        ResolvedAtUtc = DateTime.UtcNow;
        ResolvedByUserId = actorUserId;
        ResolutionDecisionCode = decisionCode;
        ResolutionNotes = notes;
        Touch(actorUserId);
    }

    private void RecalculateTotals()
    {
        SubtotalAmount = Lines.Sum(line => line.LineNetAmount);
        TaxAmount = Lines.Sum(line => line.LineTaxAmount);
        TotalAmount = Lines.Sum(line => line.LineTotalAmount);
    }
}
