using CashLineERP.Application.Sales;
using CashLineERP.Domain.Inventory;
using CashLineERP.Domain.Sales;
using CashLineERP.SharedKernel.Inventory;
using CashLineERP.SharedKernel.Workflow;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class DispatchOrderRepository : IDispatchOrderRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<DispatchOrderDetailView> GetDispatchOrders() =>
        _dbContext.DispatchOrders
            .AsNoTracking()
            .Include(dispatch => dispatch.Lines)
            .OrderByDescending(dispatch => dispatch.Id)
            .ToArray()
            .Select(Map)
            .ToArray();

    public DispatchOrderDetailView? GetDispatchOrder(long dispatchOrderId)
    {
        var dispatchOrder = _dbContext.DispatchOrders
            .AsNoTracking()
            .Include(dispatch => dispatch.Lines)
            .SingleOrDefault(dispatch => dispatch.Id == dispatchOrderId);

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

    public DispatchOrderDetailView Create(CreateDispatchOrderCommand command)
    {
        var salesOrder = _dbContext.SalesOrders
            .Include(order => order.Lines)
            .SingleOrDefault(order => order.Id == command.SalesOrderId);
        if (salesOrder is null)
        {
            throw new InvalidOperationException("Dispatch creation requires a valid sales order.");
        }

        if (string.Equals(salesOrder.CreditStatusCode, "ON_HOLD", StringComparison.OrdinalIgnoreCase)
            || string.Equals(salesOrder.CreditStatusCode, "REJECTED", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Dispatch creation is blocked while the sales order remains on customer credit hold.");
        }

        if (command.Lines.Count == 0)
        {
            throw new InvalidOperationException("Dispatch creation requires at least one dispatch line.");
        }

        if (command.BranchId != salesOrder.BranchId)
        {
            throw new InvalidOperationException("Dispatch branch must match the source sales order branch.");
        }

        var nextSequence = (_dbContext.DispatchOrders.Max(dispatch => (long?)dispatch.Id) ?? 0L) + 1L;
        var dispatchOrder = new DispatchOrder(
            dispatchNumber: $"DSP-{nextSequence:D6}",
            branchId: command.BranchId,
            salesOrderId: command.SalesOrderId,
            dispatchWarehouseId: command.DispatchWarehouseId,
            plannedShipAtUtc: command.PlannedShipAtUtc);

        var lineNumber = 1;
        foreach (var line in command.Lines)
        {
            if (line.QuantityDispatched <= 0m)
            {
                throw new InvalidOperationException("Dispatch quantity must be greater than zero.");
            }

            var salesOrderLine = salesOrder.Lines.SingleOrDefault(existingLine => existingLine.Id == line.SalesOrderLineId);
            if (salesOrderLine is null)
            {
                throw new InvalidOperationException("Dispatch line must reference a sales order line from the source sales order.");
            }

            if (salesOrderLine.ItemId != line.ItemId)
            {
                throw new InvalidOperationException("Dispatch line item must match the source sales order line item.");
            }

            var remainingAllocatableQuantity = salesOrderLine.OrderedQuantity - salesOrderLine.AllocatedQuantity;
            if (line.QuantityDispatched > remainingAllocatableQuantity)
            {
                throw new InvalidOperationException("Dispatch quantity cannot exceed the remaining unallocated sales order quantity.");
            }

            dispatchOrder.AddLine(
                lineNumber++,
                line.SalesOrderLineId,
                line.ItemId,
                line.QuantityDispatched,
                line.WarehouseLocationId,
                line.BatchNumber,
                line.SerialNumber);

            salesOrderLine.Allocate(line.QuantityDispatched);
        }

        _dbContext.DispatchOrders.Add(dispatchOrder);
        _dbContext.SaveChanges();
        return GetDispatchOrder(dispatchOrder.Id)!;
    }

    public DispatchOrderDetailView Post(long dispatchOrderId, long actorUserId, DateTime shippedAtUtc, DateTime? deliveredAtUtc, string? proofOfDeliveryReceiver)
    {
        var dispatchOrder = _dbContext.DispatchOrders
            .Include(dispatch => dispatch.Lines)
            .Single(dispatch => dispatch.Id == dispatchOrderId);

        dispatchOrder.Post(actorUserId, shippedAtUtc, deliveredAtUtc, proofOfDeliveryReceiver);

        var nextSequence = (_dbContext.InventoryMovements.Max(movement => (long?)movement.Id) ?? 0L) + 1L;
        foreach (var line in dispatchOrder.Lines.OrderBy(line => line.LineNumber))
        {
            var salesOrderLine = _dbContext.SalesOrderLines.SingleOrDefault(existingLine => existingLine.Id == line.SalesOrderLineId);
            if (deliveredAtUtc.HasValue)
            {
                salesOrderLine?.RegisterDelivery(line.QuantityDelivered);
            }

            var movement = new InventoryMovement(
                movementNumber: $"IM-{nextSequence + line.LineNumber - 1:D6}",
                movementType: "DISPATCH_ISSUE",
                branchId: dispatchOrder.BranchId,
                itemId: line.ItemId,
                fromWarehouseId: dispatchOrder.DispatchWarehouseId,
                fromWarehouseLocationId: line.WarehouseLocationId,
                toWarehouseId: null,
                toWarehouseLocationId: null,
                batchNumber: line.BatchNumber,
                serialNumber: line.SerialNumber,
                stockStatus: "AVAILABLE",
                quantityMoved: line.QuantityDispatched,
                unitCost: null,
                movedAtUtc: shippedAtUtc);

            _dbContext.InventoryMovements.Add(movement);
        }

        _dbContext.SaveChanges();
        return GetDispatchOrder(dispatchOrder.Id)!;
    }

    public DispatchOrderDetailView RecordProofOfDelivery(
        long dispatchOrderId,
        long actorUserId,
        DateTime deliveredAtUtc,
        string proofOfDeliveryReceiver,
        string deliveryConditionCode,
        string? proofOfDeliveryReference,
        string? notes)
    {
        var dispatchOrder = _dbContext.DispatchOrders
            .Include(dispatch => dispatch.Lines)
            .Single(dispatch => dispatch.Id == dispatchOrderId);

        dispatchOrder.RecordProofOfDelivery(
            actorUserId,
            deliveredAtUtc,
            proofOfDeliveryReceiver,
            deliveryConditionCode,
            proofOfDeliveryReference,
            notes);

        var salesOrderLines = _dbContext.SalesOrderLines
            .Where(line => dispatchOrder.Lines.Select(dispatchLine => dispatchLine.SalesOrderLineId).Contains(line.Id))
            .ToDictionary(line => line.Id);

        foreach (var line in dispatchOrder.Lines)
        {
            if (salesOrderLines.TryGetValue(line.SalesOrderLineId, out var salesOrderLine))
            {
                salesOrderLine.RegisterDelivery(line.QuantityDelivered);
            }
        }

        _dbContext.SaveChanges();
        return GetDispatchOrder(dispatchOrder.Id)!;
    }

    public DispatchOrderDetailView RecordDeliveryException(
        long dispatchOrderId,
        long actorUserId,
        DateTime deliveredAtUtc,
        string deliveryConditionCode,
        string deliveryExceptionCode,
        string? proofOfDeliveryReceiver,
        string? proofOfDeliveryReference,
        string? notes,
        IReadOnlyCollection<(long DispatchOrderLineId, decimal QuantityFailed)> failedLines)
    {
        if (failedLines.Count == 0)
        {
            throw new InvalidOperationException("Delivery exception requires at least one failed-delivery line.");
        }

        var dispatchOrder = _dbContext.DispatchOrders
            .Include(dispatch => dispatch.Lines)
            .Single(dispatch => dispatch.Id == dispatchOrderId);

        var failedQuantityByLineId = failedLines
            .GroupBy(line => line.DispatchOrderLineId)
            .ToDictionary(group => group.Key, group => group.Sum(entry => entry.QuantityFailed));

        var dispatchLineIds = dispatchOrder.Lines.Select(line => line.Id).ToHashSet();
        if (failedQuantityByLineId.Keys.Any(lineId => !dispatchLineIds.Contains(lineId)))
        {
            throw new InvalidOperationException("Delivery exception lines must reference dispatch lines from the source dispatch.");
        }

        if (failedQuantityByLineId.Values.Sum() <= 0m)
        {
            throw new InvalidOperationException("Delivery exception requires at least one positive failed-delivery quantity.");
        }

        dispatchOrder.RecordDeliveryException(
            actorUserId,
            deliveredAtUtc,
            deliveryConditionCode,
            deliveryExceptionCode,
            proofOfDeliveryReceiver,
            proofOfDeliveryReference,
            notes);

        var salesOrderLines = _dbContext.SalesOrderLines
            .Where(line => dispatchOrder.Lines.Select(dispatchLine => dispatchLine.SalesOrderLineId).Contains(line.Id))
            .ToDictionary(line => line.Id);

        foreach (var line in dispatchOrder.Lines)
        {
            var quantityFailed = failedQuantityByLineId.GetValueOrDefault(line.Id);
            line.RecordFailedQuantity(quantityFailed);

            if (!salesOrderLines.TryGetValue(line.SalesOrderLineId, out var salesOrderLine))
            {
                continue;
            }

            if (line.QuantityDelivered > 0m)
            {
                salesOrderLine.RegisterDelivery(line.QuantityDelivered);
            }
        }

        _dbContext.SaveChanges();
        return GetDispatchOrder(dispatchOrder.Id)!;
    }

    public DispatchOrderDetailView RecoverFailedDelivery(
        long dispatchOrderId,
        long actorUserId,
        DateTime recoveredAtUtc,
        string? notes,
        IReadOnlyCollection<(long DispatchOrderLineId, decimal QuantityRecovered, long? WarehouseLocationId, string RecoveredStockStatusCode)> recoveredLines)
    {
        if (recoveredLines.Count == 0)
        {
            throw new InvalidOperationException("Failed-delivery recovery requires at least one recovery line.");
        }

        var dispatchOrder = _dbContext.DispatchOrders
            .Include(dispatch => dispatch.Lines)
            .Single(dispatch => dispatch.Id == dispatchOrderId);

        var recoveredLinesById = recoveredLines
            .GroupBy(line => line.DispatchOrderLineId)
            .ToDictionary(group => group.Key, group => group.ToArray());

        var dispatchLineIds = dispatchOrder.Lines.Select(line => line.Id).ToHashSet();
        if (recoveredLinesById.Keys.Any(lineId => !dispatchLineIds.Contains(lineId)))
        {
            throw new InvalidOperationException("Failed-delivery recovery lines must reference dispatch lines from the source dispatch.");
        }

        var nextSequence = (_dbContext.InventoryMovements.Max(movement => (long?)movement.Id) ?? 0L) + 1L;
        var movementOffset = 0;

        foreach (var line in dispatchOrder.Lines)
        {
            if (!recoveredLinesById.TryGetValue(line.Id, out var recoveryEntries))
            {
                continue;
            }

            foreach (var recoveryEntry in recoveryEntries)
            {
                line.RecoverFailedQuantity(recoveryEntry.QuantityRecovered);

                var movement = new InventoryMovement(
                    movementNumber: $"IM-{nextSequence + movementOffset:D6}",
                    movementType: "FAILED_DELIVERY_RECOVERY",
                    branchId: dispatchOrder.BranchId,
                    itemId: line.ItemId,
                    fromWarehouseId: null,
                    fromWarehouseLocationId: null,
                    toWarehouseId: dispatchOrder.DispatchWarehouseId,
                    toWarehouseLocationId: recoveryEntry.WarehouseLocationId ?? line.WarehouseLocationId,
                    batchNumber: line.BatchNumber,
                    serialNumber: line.SerialNumber,
                    stockStatus: InventoryStockStatusPolicy.Normalize(recoveryEntry.RecoveredStockStatusCode),
                    quantityMoved: recoveryEntry.QuantityRecovered,
                    unitCost: null,
                    movedAtUtc: recoveredAtUtc);

                movementOffset++;
                _dbContext.InventoryMovements.Add(movement);
            }
        }

        var fullyRecovered = dispatchOrder.Lines.All(line => line.QuantityFailed <= line.QuantityRecovered);
        dispatchOrder.RecordDeliveryRecovery(actorUserId, recoveredAtUtc, notes, fullyRecovered);

        _dbContext.SaveChanges();
        return GetDispatchOrder(dispatchOrder.Id)!;
    }

    private static DispatchOrderDetailView Map(DispatchOrder dispatchOrder) =>
        new(
            dispatchOrder.Id,
            dispatchOrder.DispatchNumber,
            dispatchOrder.BranchId,
            dispatchOrder.SalesOrderId,
            dispatchOrder.DispatchWarehouseId,
            WorkflowStatusSeedIds.ToCode(dispatchOrder.StatusId),
            dispatchOrder.PlannedShipAtUtc,
            dispatchOrder.ShippedAtUtc,
            dispatchOrder.DeliveredAtUtc,
            dispatchOrder.ProofOfDeliveryReceiver,
            dispatchOrder.ProofOfDeliveryCapturedAtUtc,
            dispatchOrder.ProofOfDeliveryCapturedByUserId,
            dispatchOrder.DeliveryConditionCode,
            dispatchOrder.ProofOfDeliveryReference,
            dispatchOrder.ProofOfDeliveryNotes,
            dispatchOrder.DeliveryExceptionCode,
            dispatchOrder.DeliveryExceptionRecordedAtUtc,
            dispatchOrder.DeliveryExceptionRecordedByUserId,
            dispatchOrder.DeliveryExceptionNotes,
            dispatchOrder.DeliveryRecoveryStatusCode,
            dispatchOrder.DeliveryRecoveredAtUtc,
            dispatchOrder.DeliveryRecoveredByUserId,
            dispatchOrder.DeliveryRecoveryNotes,
            dispatchOrder.Lines.OrderBy(line => line.LineNumber).Select(line => new DispatchOrderLineView(
                line.Id,
                line.LineNumber,
                line.SalesOrderLineId,
                line.ItemId,
                line.QuantityDispatched,
                line.QuantityDelivered,
                line.QuantityFailed,
                line.QuantityRecovered,
                line.WarehouseLocationId,
                line.BatchNumber,
                line.SerialNumber)).ToArray());
}
