using System;
using System.Collections.Generic;
using System.Linq;
using Gebhardt.DbAccess.Base;
using Gebhardt.Shared;
using Gebhardt.Shared.DbAccess;
using Gebhardt.StoreWare.Wcs.Common.Application.TransportHandling.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Factories;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model.Enums;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;

public class OrdersHost : AutoIncrementEntity
{
    private Le _le;

    private ICollection<OrdersConveyor> _ordersConveyor;

    private ICollection<OrdersMiniload> _ordersMiniload;

    private ICollection<ResourceList> _resources;

    internal OrdersHost(TransportOrderType type, string leNo, string source, string destination, int priority, int? idOrderWms, int? idOrderWmsHead, int? idOrderWmsPos, OrderTypeWms? typeWms)
    {
        Type = type;
        LeNo = leNo;
        Source = source;
        Destination = destination;
        Priority = priority;
        IdOrderWms = idOrderWms;
        IdOrderWmsHead = idOrderWmsHead;
        IdOrderWmsPos = idOrderWmsPos;
        TypeWms = typeWms;
        OrdersConveyor = new List<OrdersConveyor>();
        OrdersMiniload = new List<OrdersMiniload>();
        Resources = new List<ResourceList>();
    }

    private OrdersHost()
    {
    }

    private ILazyLoader LazyLoader { get; set; }

    public string LeNo { get; private set; }

    public Le Le
    {
        get => LazyLoader.Load(this, ref _le);
        private set => _le = value;
    }

    public TransportOrderType Type { get; private set; }

    public string Source { get; set; }

    /// <summary>
    ///   the (possibly gross) destination for the Le: a storage area, a work place, etc.
    /// </summary>
    public string Destination { get; set; }

    public string HostDestination { get; set; }

    public TransportOrderStatus Status { get; private set; } = TransportOrderStatus.Initial;

    public int? IdOrderWmsHead { get; private set; }

    public int? IdOrderWms { get; private set; }

    public int? IdOrderWmsPos { get; private set; }

    // Currently not used at ETRA
    public int Priority { get; set; }

    public DateTime? PriorityDate { get; set; }

    public DateTime? StartTime { get; private set; }

    public string Info { get; set; }

    public OrderTypeWms? TypeWms { get; }

    public string Error { get; internal set; }

    public bool IsEmptyLeRequest { get; set; }

    // Used by sequencer to know what order to send out boxes in.
    public int? SequenceWms { get; set; }

    public bool? IsDirectPicking { get; set; }

    //Used in deadlock situations when the same box is used in multiple Sequencers
    public bool? IsStolen { get; set; }

    public DateTime? SequencerRetrievalTime { get; set; }

    public ICollection<OrdersMiniload> OrdersMiniload
    {
        get => LazyLoader.Load(this, ref _ordersMiniload);
        set => _ordersMiniload = value;
    }

    public ICollection<OrdersConveyor> OrdersConveyor
    {
        get => LazyLoader.Load(this, ref _ordersConveyor);
        set => _ordersConveyor = value;
    }

    public ICollection<ResourceList> Resources
    {
        get => LazyLoader.Load(this, ref _resources);
        set => _resources = value;
    }

    //Todo-Job: I don't like that the order is directly transmitted and no Tord is sent.
    public OrdersConveyor StartConveyorOrderToNextDestination(IDestinationProperties newDestination = null, string reasonforredirect = null)
    {
        if (Status == TransportOrderStatus.InDestinationZone && newDestination is { BuffersEmptyLe: true })
        {
            throw new InvalidOperationException($"Cannot start {nameof(OrdersConveyor)} to next destination ({newDestination.Name}) as the {nameof(OrdersHost)} has already arrived.");
        }
        OrdersConveyor openOrder = OrdersConveyor.SingleOrDefault(oc => TransportOrderStatusGroups.Open.Contains(oc.Status));
        if (openOrder != null)
        {
            openOrder.HandleFinished(newDestination);
        }

        OrdersConveyor followingOrder = OrdersConveyorFactory.GetInstance(this).InitialForOrdersHost(openOrder?.Destination, newDestination?.Name);

        followingOrder.Transmit(reasonforredirect);
        return followingOrder;
    }

    public void Redirect(IDestinationProperties newDestination = null, string reasonForRedirect = null)
    {
        if (newDestination != null && newDestination.Name != Destination)
        {
            OrdersConveyor openOrder = OrdersConveyor.SingleOrDefault(oc => TransportOrderStatusGroups.Open.Contains(oc.Status));
            if (openOrder != null)
            {
                openOrder.Cancel($"Redirected to {newDestination.Name}");
                UpdateResources(TransportOrderStatus.Cancelled, null);
            }
            Destination = newDestination.Name;
            Info = reasonForRedirect;
            OrdersConveyor followingOrder = OrdersConveyorFactory.GetInstance(this).InitialForOrdersHost(openOrder?.Destination, newDestination.Name);
            followingOrder.Transmit();
        }
    }

    public OrdersHost Reinitialize(string info)
    {
        Status = TransportOrderStatus.Initial;
        Info = info;
        StartTime = null;
        return this;
    }

    public OrdersHost Postpone(IDestinationProperties destination, string info)
    {
        if (!TransportOrderStatusGroups.Waiting.Contains(Status))
        {
            throw new InvalidOperationException("Only orders that have not started yet can be postponed.");
        }
        if (!destination.BuffersEmptyLe)
        {
            Reinitialize(info);
        }
        else
        {
            Cancel(string.Empty, info);
        }
        return this;
    }

    public OrdersConveyor TransportToNio(string error)
    {
        Error = error;
        OrdersConveyor nioTransport = OrdersConveyor?.FirstOrDefault();
        if (nioTransport == null)
        {
            nioTransport = OrdersConveyorFactory.GetInstance(this).PendingForOrdersHost(error: error);
        }
        nioTransport.Transmit(error);
        return nioTransport;
    }

    public OrdersHost Schedule(string source = null, string info = null)
    {
        if (TransportOrderStatusGroups.Waiting.Contains(Status))
        {
            Status = TransportOrderStatus.Pending;
            Info = info;
            if (source != null)
            {
                Source = source;
            }
        }
        else
        {
            Log.Write(LogLevel.Error, $"cannot schedule due to Status={Status}; {this} ");
        }
        return this;
    }

    /// <summary>
    ///   set the OrdersHost to initial - if it's Status is  InProgress or Transmitted
    /// </summary>
    /// <param name="info">informal reason</param>
    /// <returns>true if rescheduled</returns>
    public bool Reschedule(string info = null, string source = null)
    {
        if (Status != TransportOrderStatus.InProgress && Status != TransportOrderStatus.Transmitted)
        {
            Log.Write(LogLevel.Error, $"cannot reschedule due to Status={Status}; {this} ");
            return false;
        }
        Status = TransportOrderStatus.Initial;
        Info = info;
        if (source != null) { Source = source; }
        Log.Write(LogLevel.Info, $"Rescheduled {this}");
        return true;
    }

    public OrdersHost Start()
    {
        if (TransportOrderStatusGroups.Waiting.Contains(Status))
        {
            Status = TransportOrderStatus.InProgress;
            StartTime = DBDateTime.Now;
        }
        return this;
    }

    public void ManageResources()
    {
        if (!(Resources ??= new List<ResourceList>()).All(r => r.Status == TransportOrderStatus.Cancelled || r.Status == TransportOrderStatus.Finished) &&
                Resources.Count != 0)
        {
            return;
        }
        // Is there an old resource that we need to reuse? (Primary key).
        ResourceList oldResource = Resources.FirstOrDefault(r => r.OrdersHostId == Id && r.Name == Destination);
        if (oldResource != null)
        {
            oldResource.Status = Status;
        }
        // If not create new.
        else
        {
            Resources.Add(new ResourceList
            {
                OrdersHostId = Id,
                LeNo = LeNo,
                Name = Destination,
                Status = Status
            });
        }
    }

    public void AddResourceEntry(TransportOrderStatus status, string destination)
    {
        // Is there an old resource that we need to reuse? (Primary key).
        ResourceList oldResource = Resources.FirstOrDefault(r => r.OrdersHostId == Id && r.Name == destination);
        if (oldResource != null)
        {
            oldResource.Status = status;
        }
        // If not create new.
        else
        {
            Resources.Add(new ResourceList
            {
                OrdersHostId = Id,
                LeNo = LeNo,
                Name = destination,
                Status = status
            });
        }
    }

    public void FinishResources(string destination)
    {
        // Is there an old resource that we need to reuse? (Primary key)
        List<ResourceList> oldResource = Resources.Where(r => r.Name == destination).ToList();
        foreach (ResourceList res in oldResource)
        {
            res.Status = TransportOrderStatus.Finished;
        }
    }

    public void CancelResources(string destination)
    {
        List<ResourceList> oldResource = Resources.Where(r => r.Name == destination).ToList();
        foreach (ResourceList res in oldResource)
        {
            Log.Write(LogLevel.Info, $"Cancelling previously assigned resource for Le {LeNo} to destination {destination}");
            res.Status = TransportOrderStatus.Cancelled;
        }
    }

    public OrdersHost EnterZone(IDestinationProperties destination)
    {
        if (!(TransportOrderStatusGroups.Active.Contains(Status) || Status == TransportOrderStatus.InSequencer))
        {
            return this;
        }
        Status = TransportOrderStatus.InDestinationZone;
        UpdateResources(TransportOrderStatus.InDestinationZone, null);
        return this;
    }

    /// <summary>
    ///   Set the status to <code>InDestinationZone</code>, update the resources and register the Le at the destination (if
    ///   known)
    /// </summary>
    /// <param name="destinationService"></param>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    public OrdersHost Arrive(IDestinationService destinationService)
    {
        if (TransportOrderStatusGroups.Waiting.Contains(Status))
        {
            throw new InvalidOperationException("Order has to start before it can arrive.");
        }

        if (TransportOrderStatusGroups.Complete.Contains(Status))
        {
            throw new InvalidOperationException("Order can not arrive if it is already finished.");
        }

        if (TransportOrderStatusGroups.Active.Contains(Status))
        {
            Status = TransportOrderStatus.InDestinationZone;
            destinationService?.SetArrived(LeNo, Destination);

            UpdateResources(TransportOrderStatus.InDestinationZone, null);
        }

        return this;
    }

    /// <summary>
    ///   finish this order, related OrdersMiniload, Orders Conveyor and clear the resources
    /// </summary>
    public void Finish()
    {
        OrdersConveyor.ToList().ForEach(oc =>
        {
            if (TransportOrderStatusGroups.Open.Contains(oc.Status))
            {
                oc.HandleFinished();
                using IWcsDbContext db = new WcsDbContextFactory().GetDbContext();
                ConveyorTelegrams.SendTordDeleteEtra(db, oc.LeNo);
                db.SaveChanges();
            }
        });
        OrdersMiniload.ToList().ForEach(om =>
        {
            if (TransportOrderStatusGroups.Open.Contains(om.Status))
            {
                om.HandleFinished();
            }
        });
        Status = TransportOrderStatus.Finished;
        UpdateResources(TransportOrderStatus.Finished, null);
    }

    /// <summary>
    ///   Cancel this order, related OrdersMiniload, Orders Conveyor and clear the resources
    /// </summary>
    public void Cancel(string error, string info, bool cancelOrdersMiniload = true)
    {
        OrdersConveyor.ToList().ForEach(oc =>
        {
            if (TransportOrderStatusGroups.Open.Contains(oc.Status))
            {
                oc.Cancel(error);
            }
        });
        if (cancelOrdersMiniload)
        {
            OrdersMiniload.ToList().ForEach(om =>
            {
                if (TransportOrderStatusGroups.Open.Contains(om.Status))
                {
                    om.Cancel(error);
                }
            });
        }
        Status = TransportOrderStatus.Cancelled;
        Error = error;
        Info = info;
        UpdateResources(TransportOrderStatus.Cancelled, null);
    }

    public OrdersMiniload GetPendingOrdersMiniload()
    {
        return OrdersMiniload?.SingleOrDefault(o => o.Status == TransportOrderStatus.Pending);
    }

    public OrdersMiniload GetOpenOrdersMiniload()
    {
        return OrdersMiniload?.FirstOrDefault(o => TransportOrderStatusGroups.Open.Contains(o.Status));
    }

    public OrdersMiniload GetActiveOrdersMiniload()
    {
        return OrdersMiniload?.SingleOrDefault(o => TransportOrderStatusGroups.Active.Contains(o.Status));
    }

    public OrdersConveyor GetActiveOrdersConveyor()
    {
        return OrdersConveyor?.SingleOrDefault(o => TransportOrderStatusGroups.Active.Contains(o.Status));
    }

    public void UpdateResources(TransportOrderStatus resourceListStatus, List<ResourceList> occupiedBySameLeButDifferentOrder)
    {
        //If a OrdersHost is Finished or Cancelled all ResourceList for this OH are affected, for other status only entries with same Destination
        Resources
            .Where(r => !TransportOrderStatusGroups.Complete.Contains(r.Status) && (r.Name == Destination || resourceListStatus == TransportOrderStatus.Finished || resourceListStatus == TransportOrderStatus.Cancelled))
            .ToList()
            .ForEach(r => r.Status = resourceListStatus);
        occupiedBySameLeButDifferentOrder?.ForEach(r => r.Status = TransportOrderStatus.Cancelled);
    }

    public void UpdateSpecificResource(TransportOrderStatus resourceListStatus, string destination)
    {
        ResourceList oldResource = Resources.FirstOrDefault(r => r.OrdersHostId == Id && r.Name == destination);
        if (oldResource == null)
        {
            return;
        }
        else
        {
            oldResource.Status = resourceListStatus;
        }
    }
    public OrdersHost ReplaceNextEmptyLe(Le le)
    {
        if (!LeType.EmptyLeTypeNames.Contains(Le.Type))
        {
            throw new ArgumentException($"Reassigning an LE is only allowed to replace any of {nameof(LeType.EmptyLeTypeNames)}. Current LE: {le.LeNo}");
        }

        Le = le;
        ResourceList resource = Resources.SingleOrDefault(r => r.Name == Destination);
        if (resource != null)
        {
            resource.Le = le;
        }
        return this;
    }

    public override string ToString()
    {
        return $"{nameof(OrdersHost)}[{nameof(Id)}: {Id}, {nameof(LeNo)}: {LeNo}, {nameof(Type)}: {Type}, {nameof(Source)}: {Source}, {nameof(Destination)}: {Destination}, {nameof(Status)}: {Status}, {nameof(IdOrderWmsHead)}: {IdOrderWmsHead}, {nameof(IdOrderWms)}: {IdOrderWms}, {nameof(IdOrderWmsPos)}: {IdOrderWmsPos}]";
    }

    /// <summary>
    /// Set the status to InDestinationZone regardless of the previous status. This is ONLY to be used for creating an orders host dummy when we get a no read on the outputs. Otherwise we cannot send an UnloadHuToAgv telegram to unload the board to an AGV.
    /// </summary>
    /// <returns></returns>
    public OrdersHost ForceSetStatusInDestinationZone()
    {
        Status = TransportOrderStatus.InDestinationZone;
        return this;
    }

    public OrdersHost ForceSetStatusInSequencer()
    {
        Status = TransportOrderStatus.InSequencer;
        return this;
    }

    public OrdersHost ForceSetStatusInProgress()
    {
        Status = TransportOrderStatus.InProgress;
        return this;
    }

    public OrdersHost ForceSetStatusTransmitted()
    {
        Status = TransportOrderStatus.Transmitted;
        return this;
    }

    public bool IsSequencerCancelOrder()
    {
        return Source.Contains(WcsNames.SEQ) && Destination == Common.Constants.MfcAllDestinations.StorageLoop2;
    }
}