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

namespace CashLineERP.Domain.Sales;

public sealed class DispatchOrder : Entity
{
    public long Id { get; private set; }

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

    public long BranchId { get; private set; }

    public long SalesOrderId { get; private set; }

    public long DispatchWarehouseId { get; private set; }

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

    public DateTime? PlannedShipAtUtc { get; private set; }

    public DateTime? ShippedAtUtc { get; private set; }

    public DateTime? DeliveredAtUtc { get; private set; }

    public string? ProofOfDeliveryReceiver { get; private set; }

    public DateTime? ProofOfDeliveryCapturedAtUtc { get; private set; }

    public long? ProofOfDeliveryCapturedByUserId { get; private set; }

    public string? DeliveryConditionCode { get; private set; }

    public string? ProofOfDeliveryReference { get; private set; }

    public string? ProofOfDeliveryNotes { get; private set; }

    public string? DeliveryExceptionCode { get; private set; }

    public DateTime? DeliveryExceptionRecordedAtUtc { get; private set; }

    public long? DeliveryExceptionRecordedByUserId { get; private set; }

    public string? DeliveryExceptionNotes { get; private set; }

    public string? DeliveryRecoveryStatusCode { get; private set; }

    public DateTime? DeliveryRecoveredAtUtc { get; private set; }

    public long? DeliveryRecoveredByUserId { get; private set; }

    public string? DeliveryRecoveryNotes { get; private set; }

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

    public DispatchOrder(
        string dispatchNumber,
        long branchId,
        long salesOrderId,
        long dispatchWarehouseId,
        DateTime? plannedShipAtUtc)
    {
        DispatchNumber = dispatchNumber;
        BranchId = branchId;
        SalesOrderId = salesOrderId;
        DispatchWarehouseId = dispatchWarehouseId;
        PlannedShipAtUtc = plannedShipAtUtc;
    }

    private DispatchOrder()
    {
    }

    public void AddLine(
        int lineNumber,
        long salesOrderLineId,
        long itemId,
        decimal quantityDispatched,
        long? warehouseLocationId,
        string? batchNumber,
        string? serialNumber)
    {
        Lines.Add(new DispatchOrderLine(
            dispatchOrderId: Id,
            lineNumber: lineNumber,
            salesOrderLineId: salesOrderLineId,
            itemId: itemId,
            quantityDispatched: quantityDispatched,
            warehouseLocationId: warehouseLocationId,
            batchNumber: batchNumber,
            serialNumber: serialNumber));
    }

    public void Post(long actorUserId, DateTime shippedAtUtc, DateTime? deliveredAtUtc, string? proofOfDeliveryReceiver)
    {
        StatusId = WorkflowStatusSeedIds.Posted;
        ShippedAtUtc = shippedAtUtc;
        DeliveredAtUtc = deliveredAtUtc;
        ProofOfDeliveryReceiver = proofOfDeliveryReceiver;

        foreach (var line in Lines)
        {
            line.MarkDelivered(deliveredAtUtc.HasValue ? line.QuantityDispatched : 0m);
        }

        Touch(actorUserId);
    }

    public void RecordProofOfDelivery(
        long actorUserId,
        DateTime deliveredAtUtc,
        string proofOfDeliveryReceiver,
        string deliveryConditionCode,
        string? proofOfDeliveryReference,
        string? notes)
    {
        if (StatusId != WorkflowStatusSeedIds.Posted)
        {
            throw new InvalidOperationException("Proof of delivery can only be recorded for posted dispatches.");
        }

        if (ProofOfDeliveryCapturedAtUtc.HasValue || !string.IsNullOrWhiteSpace(DeliveryConditionCode))
        {
            throw new InvalidOperationException("Delivery outcome has already been recorded for this dispatch.");
        }

        if (Lines.Any(line => line.QuantityDelivered > 0m || line.QuantityFailed > 0m || line.QuantityRecovered > 0m))
        {
            throw new InvalidOperationException("Delivery outcome cannot be captured again after delivery quantities have already been registered.");
        }

        if (string.IsNullOrWhiteSpace(proofOfDeliveryReceiver))
        {
            throw new InvalidOperationException("Proof of delivery receiver is required.");
        }

        if (string.IsNullOrWhiteSpace(deliveryConditionCode))
        {
            throw new InvalidOperationException("Delivery condition code is required for proof of delivery.");
        }

        DeliveredAtUtc = deliveredAtUtc;
        ProofOfDeliveryReceiver = proofOfDeliveryReceiver.Trim();
        DeliveryConditionCode = deliveryConditionCode.Trim().ToUpperInvariant();
        ProofOfDeliveryReference = string.IsNullOrWhiteSpace(proofOfDeliveryReference) ? null : proofOfDeliveryReference.Trim();
        ProofOfDeliveryNotes = string.IsNullOrWhiteSpace(notes) ? null : notes.Trim();
        ProofOfDeliveryCapturedAtUtc = DateTime.UtcNow;
        ProofOfDeliveryCapturedByUserId = actorUserId;

        foreach (var line in Lines)
        {
            line.MarkDelivered(line.QuantityDispatched);
        }

        Touch(actorUserId);
    }

    public void RecordDeliveryException(
        long actorUserId,
        DateTime deliveredAtUtc,
        string deliveryConditionCode,
        string deliveryExceptionCode,
        string? proofOfDeliveryReceiver,
        string? proofOfDeliveryReference,
        string? notes)
    {
        if (StatusId != WorkflowStatusSeedIds.Posted)
        {
            throw new InvalidOperationException("Delivery exception can only be recorded for posted dispatches.");
        }

        if (ProofOfDeliveryCapturedAtUtc.HasValue || !string.IsNullOrWhiteSpace(DeliveryConditionCode))
        {
            throw new InvalidOperationException("Delivery outcome has already been recorded for this dispatch.");
        }

        if (Lines.Any(line => line.QuantityDelivered > 0m || line.QuantityFailed > 0m || line.QuantityRecovered > 0m))
        {
            throw new InvalidOperationException("Delivery outcome cannot be captured again after delivery quantities have already been registered.");
        }

        if (string.IsNullOrWhiteSpace(deliveryConditionCode))
        {
            throw new InvalidOperationException("Delivery condition code is required for delivery exceptions.");
        }

        if (string.IsNullOrWhiteSpace(deliveryExceptionCode))
        {
            throw new InvalidOperationException("Delivery exception code is required.");
        }

        DeliveredAtUtc = deliveredAtUtc;
        DeliveryConditionCode = deliveryConditionCode.Trim().ToUpperInvariant();
        DeliveryExceptionCode = deliveryExceptionCode.Trim().ToUpperInvariant();
        ProofOfDeliveryReceiver = string.IsNullOrWhiteSpace(proofOfDeliveryReceiver) ? null : proofOfDeliveryReceiver.Trim();
        ProofOfDeliveryReference = string.IsNullOrWhiteSpace(proofOfDeliveryReference) ? null : proofOfDeliveryReference.Trim();
        ProofOfDeliveryNotes = string.IsNullOrWhiteSpace(notes) ? null : notes.Trim();
        ProofOfDeliveryCapturedAtUtc = DateTime.UtcNow;
        ProofOfDeliveryCapturedByUserId = actorUserId;
        DeliveryExceptionRecordedAtUtc = DateTime.UtcNow;
        DeliveryExceptionRecordedByUserId = actorUserId;
        DeliveryExceptionNotes = string.IsNullOrWhiteSpace(notes) ? null : notes.Trim();
        DeliveryRecoveryStatusCode = "PENDING";
        DeliveryRecoveredAtUtc = null;
        DeliveryRecoveredByUserId = null;
        DeliveryRecoveryNotes = null;

        Touch(actorUserId);
    }

    public void RecordDeliveryRecovery(
        long actorUserId,
        DateTime recoveredAtUtc,
        string? notes,
        bool fullyRecovered)
    {
        if (!string.Equals(DeliveryRecoveryStatusCode, "PENDING", StringComparison.OrdinalIgnoreCase)
            && !string.Equals(DeliveryRecoveryStatusCode, "PARTIAL", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Delivery recovery requires a pending failed-delivery exception.");
        }

        DeliveryRecoveryStatusCode = fullyRecovered ? "RECOVERED" : "PARTIAL";
        DeliveryRecoveredAtUtc = recoveredAtUtc;
        DeliveryRecoveredByUserId = actorUserId;
        DeliveryRecoveryNotes = string.IsNullOrWhiteSpace(notes) ? null : notes.Trim();

        Touch(actorUserId);
    }
}
