using CareFusion.Lib.StorageSystem.Helpers.Enums;
using CareFusion.Lib.StorageSystem.Input;
using CareFusion.Lib.StorageSystem.Logging;
using CareFusion.Lib.StorageSystem.Stock;
using CareFusion.Lib.StorageSystem.Wwks2.Types;
using Rowa.Mosaic.Shared.Converters;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Xml.Serialization;

namespace CareFusion.Lib.StorageSystem.Wwks2.Messages.Input
{
    /// <summary>
    /// Class which represents the WWKS 2.0 InfeedInputRequest message envelope.
    /// </summary>
    [XmlType(TypeName = "WWKS")]
    public class InfeedInputRequestEnvelope : EnvelopeBase
    {
        [XmlElement]
        public InfeedInputRequest InfeedInputRequest { get; set; }
    }

    /// <summary>
    /// Class which represents the WWKS 2.0 InfeedInputRequest message.
    /// <see cref="https://portalrowa.carefusion.com/Unternehmen/Entwicklung/TechCom/Writing/P-010-044-B-I-388-DEU.docx" />
    /// </summary>
    public class InfeedInputRequest : MessageBase, IInfeedInputRequest
    {
        #region Constants

        /// <summary>
        /// Timeout in milliseconds to wait for a response from the storage system.
        /// </summary>
        private const int WaitForResponseTimeoutMilliseconds = 30000;

        #endregion

        #region Members

        /// <summary>
        /// The message object stream to use for sending requests and responses.
        /// </summary>
        private readonly XmlObjectStream _messageOjectStream;

        /// <summary>
        /// The message dispatcher to use for message interception.
        /// </summary>
        private readonly MessageDispatcher _messageDispatcher;

        /// <summary>
        /// The delivery number of the infeed input process.
        /// </summary>
        private readonly string _deliveryNumber;

        /// <summary>
        /// The current state of the infeed input process.
        /// </summary>
        private InfeedInputRequestState _currentState = InfeedInputRequestState.Created;

        /// <summary>
        /// The reason for a possible rejection after the infeed input process has finished.
        /// </summary>
        private InfeedInputRejectReason _rejectReason = InfeedInputRejectReason.None;

        /// <summary>
        /// The optional additional description for a possible rejection after the infeed input process has finished.
        /// </summary>
        private string _rejectionDescription = string.Empty;

        /// <summary>
        /// Holds the list of currently processed article information.
        /// </summary>
        private readonly List<Article> _inputArticles = new List<Article>();

        /// <summary>
        /// Thread synchronization object.
        /// </summary>
        private readonly object _syncLock = new object();

        #endregion

        #region WWKS 2.0 Properties

        [XmlAttribute]
        public string IsNewDelivery { get; set; }

        [XmlAttribute]
        public string SetPickingIndicator { get; set; }

        [XmlElement]
        public Details Details { get; set; }

        [XmlElement]
        public Article Article { get; set; }

        #endregion

        #region IInfeedInputRequest Specific Properties

        /// <summary>
        /// Gets the current state of the infeed input process.
        /// </summary>
        [XmlIgnore]
        public InfeedInputRequestState State 
        {  
            get 
            { 
                lock (_syncLock)
                {
                    return _currentState;
                }
            } 
        }

        /// <summary>
        /// The reason for the rejection of an infeed input.
        /// This property is set after the input process finished.
        /// </summary>
        [XmlIgnore]
        public InfeedInputRejectReason RejectionReason
        {
            get
            {
                lock (_syncLock)
                {
                    return _rejectReason;
                }
            }
        }

        /// <summary>
        /// Optional additional description for the rejection of an infeed input.
        /// This property is set after the input process finished.
        /// </summary>
        [XmlIgnore]
        public string RejectionDescription => _rejectionDescription;    // HINT: no need for a lock here (see Interlocked.Exchange)

        /// <summary>
        /// If the infeed pack input is part of a new stock delivery, this property
        /// specifies the delivery number which will be used during pack input.
        /// If this property is null, this pack input is NOT part of a new delivery.
        /// </summary>
        [XmlIgnore]
        public string DeliveryNumber => _deliveryNumber;

        /// <summary>
        /// Flag whether the storage system will set a so-called picking 
        /// indicator during the infeed input to enforce a redefinition 
        /// of the requested pack articles as storage system capable articles.
        /// This flag is usually used when the new storage system 
        /// gets filled for the very first time to realize a kind
        /// of first-time synchronization between the IT system and 
        /// the storage system according to which articles are "storage
        /// system articles".
        /// </summary>
        [XmlIgnore]
        public bool PickingIndicator => (this.SetPickingIndicator != null) && bool.Parse(this.SetPickingIndicator);

        /// <summary>
        /// Gets the number which defines the infeed source that should be used 
        /// to transfer the pack into the storage system. 
        /// </summary>
        [XmlIgnore]
        public int InfeedNumber => (this.Details?.InfeedNumber != null) ? int.Parse(this.Details.InfeedNumber) : 0;

        /// <summary>
        /// Gets the list of packs that are ready to be input.
        /// </summary>
        [XmlIgnore]
        public IPack[] InputPacks => this.Article?.Pack?.ToArray();

        /// <summary>
        /// Gets the information of the articles and the packs that are processed during
        /// the input. This property is set after the input process finished.
        /// </summary>
        [XmlIgnore]
        public IArticle[] InputArticles
        {
            get
            {
                lock (_syncLock)
                {
                    return _inputArticles.ToArray();
                }
            }
        }

        #endregion

        #region Logging Properties

        /// <summary>
        /// Returns a detailed description of this infeed input.
        /// Used for detailed logging.
        /// </summary>
        [XmlIgnore]
        private string Info => $"Input '{Id}' for infeed '{InfeedNumber}' at storage system '{Destination}'";

        #endregion

        #region IInfeedInputRequest Events

        /// <summary>
        /// Event which is raised when the infeed input process requested the placement of the packs to input.
        /// </summary>
        /// <param name="sender">The infeed input process instance which raised the event.</param>
        /// <param name="e">Not used.</param>
        public event EventHandler PlacePacks;

        /// <summary>
        /// Event which is raised when the infeed input process finished execution.
        /// </summary>
        /// <param name="sender">The infeed input process instance which raised the event.</param>
        /// <param name="e">Not used.</param>
        public event EventHandler Finished;

        #endregion

        #region IInfeedInputRequest Methods

        /// <summary>
        /// Adds a new pack that is ready for input.
        /// </summary>
        /// <param name="scanCode">Raw scan code of the pack.</param>
        /// <param name="batchNumber">Optional batch number of the pack.</param>
        /// <param name="externalId">Optional external identifier of the pack.</param>
        /// <param name="expiryDate">Optional expiry date of the pack.</param>
        /// <param name="expiryDateSource">Optional source for the pack's expiry date.</param>
        /// <param name="subItemQuantity">Optional number of elements (e.g. pills or ampoules) which are currently in this pack.</param>
        /// <param name="depth">Optional depth of the pack in mm.</param>
        /// <param name="width">Optional width of the pack in mm.</param>
        /// <param name="height">Optional height of the pack in mm.</param>
        /// <param name="shape">Optional shape of the pack.</param>
        /// <param name="stockLocationId">Optional stock location to use for the pack.</param>
        /// <param name="machineLocation">Optional machine location to use for the pack.</param>
        /// <param name="serialNumber">Optional serial number of the pack.</param>
        void IInfeedInputRequest.AddInputPack(string scanCode, 
            string batchNumber = null, 
            string externalId = null, 
            DateTime? expiryDate = null,
            ExpiryDateSource expiryDateSource = ExpiryDateSource.AutoCalculated,
            int subItemQuantity = 0, 
            int depth = 0, 
            int width = 0, 
            int height = 0, 
            PackShape shape = PackShape.Cuboid, 
            string stockLocationId = null, 
            string machineLocation = null,
            string serialNumber = null)
        {

            if (this.Article == null)
            {
                this.Article = new Article()
                {
                    Id = null,
                    Name = null,
                    DosageForm = null,
                    PackagingUnit = null,
                    Pack = new List<Pack>()
                };
            }

            this.Article.Pack.Add(new Pack
            {
                ScanCode = TextConverter.EscapeInvalidXmlChars(scanCode),
                BatchNumber = TextConverter.EscapeInvalidXmlChars(batchNumber),
                ExternalId = TextConverter.EscapeInvalidXmlChars(externalId),
                DeliveryNumber = TextConverter.EscapeInvalidXmlChars(_deliveryNumber),
                ExpiryDate = expiryDate.HasValue ? TypeConverter.ConvertDate(expiryDate.Value) : null,
                ExpiryDateSource = expiryDateSource.ToString(),
                SubItemQuantity = subItemQuantity.ToString(),
                Depth = depth.ToString(),
                Width = width.ToString(),
                Height = height.ToString(),
                Shape = shape.ToString(),
                StockLocationId = TextConverter.EscapeInvalidXmlChars(stockLocationId),
                MachineLocation = TextConverter.EscapeInvalidXmlChars(machineLocation),
                SerialNumber = TextConverter.EscapeInvalidXmlChars(serialNumber),
            });
        }

        /// <summary>
        /// Initiates the input process by sending the according request to the storage system.
        /// </summary>
        void IInfeedInputRequest.Start()
        {
            lock (_syncLock)
            {
                if (_currentState != InfeedInputRequestState.Created)
                    return;
            }

            var request = new InfeedInputRequestEnvelope() { InfeedInputRequest = this };
            var placePackRequestInterceptor = new MessageInterceptor(this.Id, typeof(InfeedInputPlacePackRequest));
            var infeedInputMessageInterceptor = new MessageInterceptor(this.Id, typeof(InfeedInputMessage));

            _messageDispatcher.AddInterceptor(placePackRequestInterceptor);
            _messageDispatcher.AddInterceptor(infeedInputMessageInterceptor);

            try
            {
                var response = (InfeedInputResponse)_messageDispatcher.SendAndWaitForResponse(request,
                                                                                              this.Id,
                                                                                              typeof(InfeedInputResponse),
                                                                                              WaitForResponseTimeoutMilliseconds);

                lock (_syncLock)
                {
                    if (response == null)
                    {
                        _currentState = InfeedInputRequestState.Aborted;
                        throw new ApplicationException($"{Info} failed while waiting for the message 'InfeedInputResponse'.");
                    }

                    if ((response.Details == null) || (!Enum.TryParse(response.Details.Status, out _currentState)))
                    {
                        _currentState = InfeedInputRequestState.Unknown;
                    }

                    if (!string.IsNullOrEmpty(response.Details.Reason))
                    {
                        if (!Enum.TryParse(response.Details.Reason, out _rejectReason))
                            _rejectReason = InfeedInputRejectReason.None;

                        _rejectionDescription = response.Details.Description;
                    }

                    if (_currentState != InfeedInputRequestState.Accepted)
                    {
                        this.Trace($"{Info} raises event 'Finished'.");
                        this.Finished?.Invoke(this, new EventArgs());
                    }
                    else
                    {
                        if (ThreadPool.QueueUserWorkItem(new WaitCallback(WaitForInfeedInputMessage),
                                                         new[] { infeedInputMessageInterceptor, placePackRequestInterceptor }))
                        {
                            infeedInputMessageInterceptor = null;
                        }

                        if (ThreadPool.QueueUserWorkItem(new WaitCallback(WaitForPlacePackRequest),
                                                         placePackRequestInterceptor))
                        {
                            placePackRequestInterceptor = null;
                        }

                        if ((placePackRequestInterceptor != null) || (infeedInputMessageInterceptor != null))
                            throw new ApplicationException($"{Info} failed to start observation threads.");

                        this.Trace($"{Info} successfully started.");
                    }
                }
            }
            finally
            {
                if (placePackRequestInterceptor != null)
                {
                    _messageDispatcher.RemoveInterceptor(placePackRequestInterceptor);
                    placePackRequestInterceptor.Dispose();
                }

                if (infeedInputMessageInterceptor != null)
                {
                    _messageDispatcher.RemoveInterceptor(infeedInputMessageInterceptor);
                    infeedInputMessageInterceptor.Dispose();
                }
            }
        }

        /// <summary>
        /// Notifies the storage system that the input packs have been placed.
        /// </summary>
        void IInfeedInputRequest.PacksPlaced()
        {
            lock (_syncLock)
            {
                if (_currentState != InfeedInputRequestState.WaitingForPack)
                    throw new InvalidOperationException($"{Info} cannot send PacksPlaced notification in status '{_currentState}'.");

                var response = new InfeedInputPlacePackResponseEnvelope()
                {
                    InfeedInputPlacePackResponse = new InfeedInputPlacePackResponse()
                    {
                        Id = this.Id,
                        Source = this.Source,
                        Destination = this.Destination,
                        Details = new Details()
                        {
                            InfeedNumber = this.InfeedNumber.ToString(),
                            Status = "Placed"
                        }
                    }
                };

                if (_messageOjectStream.Write(response))
                {
                    _currentState = InfeedInputRequestState.InProgress;
                    this.Info($"{Info} sent 'InfeedInputPlacePackResponse' with status 'Placed'.");
                    return;
                }

                this.Error($"{Info} failed sending 'InfeedInputPlacePackResponse' with status 'Placed'.");
            }
        }

        /// <summary>
        /// Notifies the storage system that the infeed input has to be aborted.
        /// </summary>
        void IInfeedInputRequest.Abort()
        {
            lock (_syncLock)
            {
                if (_currentState != InfeedInputRequestState.WaitingForPack)
                    throw new InvalidOperationException($"{Info} cannot send abort notification in status '{_currentState}'.");

                var response = new InfeedInputPlacePackResponseEnvelope()
                {
                    InfeedInputPlacePackResponse = new InfeedInputPlacePackResponse()
                    {
                        Id = this.Id,
                        Source = this.Source,
                        Destination = this.Destination,
                        Details = new Details()
                        {
                            InfeedNumber = this.InfeedNumber.ToString(),
                            Status = "Aborted"
                        }
                    }
                };

                if (_messageOjectStream.Write(response))
                {
                    _currentState = InfeedInputRequestState.InProgress;
                    this.Info($"{Info} sent 'InfeedInputPlacePackResponse' with status 'Aborted'.");
                    return;
                }

                this.Error($"{Info} failed sending 'InfeedInputPlacePackResponse' with status 'Aborted'.");
            }
        }

        #endregion

        #region Additional Methods

        /// <summary>
        /// Initializes a new instance of the <see cref="InfeedInputRequest"/> class.
        /// </summary>
        public InfeedInputRequest()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="InfeedInputRequest" /> class.
        /// </summary>
        /// <param name="messageOjectStream">The message object stream to use for sending the requests.</param>
        /// <param name="messageDispatcher">The message dispatcher to use for message interception.</param>
        /// <param name="deliveryNumber">The delivery number to use for the infeed input.</param>
        public InfeedInputRequest(XmlObjectStream messageOjectStream, MessageDispatcher messageDispatcher, string deliveryNumber)
        {
            if (messageOjectStream == null)
                throw new ArgumentNullException(nameof(messageOjectStream));

            if (messageDispatcher == null)
                throw new ArgumentNullException(nameof(messageDispatcher));

            _messageOjectStream = messageOjectStream;
            _messageDispatcher = messageDispatcher;
            _deliveryNumber = deliveryNumber;
            this.IsNewDelivery = (deliveryNumber != null).ToString();
            this.Details = new Details();
        }

        /// <summary>
        /// Asynchronous thread which waits for the completion of the specified infeed input place pack request interceptor.
        /// </summary>
        /// <param name="messageInterceptor">The message interceptor to wait for.</param>
        private void WaitForPlacePackRequest(object messageInterceptor)
        {
            try
            {
                using (var interceptor = messageInterceptor as MessageInterceptor)
                {
                    interceptor.Wait();
                    _messageDispatcher.RemoveInterceptor(interceptor);

                    var placePackRequest = (InfeedInputPlacePackRequest)interceptor.Message;

                    if (placePackRequest != null)
                    {
                        lock (_syncLock)
                        {
                            _currentState = InfeedInputRequestState.WaitingForPack;
                        }

                        this.Info($"{Info} received 'InfeedInputPlacePackRequest'.");
                        this.PlacePacks?.Invoke(this, new EventArgs());
                    }
                    else
                    {
                        this.Error($"{Info} failed while waiting for the message 'InfeedInputPlacePackRequest'.");
                    }
                }
            }
            catch (Exception ex)
            {
                this.Error($"{Info} failed to receive and process 'InfeedInputPlacePackRequest'!", ex);
            }
        }

        /// <summary>
        /// Asynchronous thread which waits for the completion of the specified infeed input message interceptor.
        /// </summary>
        /// <param name="messageInterceptors">The message interceptors to wait for.</param>
        private void WaitForInfeedInputMessage(object messageInterceptors)
        {
            try
            {
                var interceptors = messageInterceptors as MessageInterceptor[];

                using (var interceptor = interceptors[0])
                {
                    interceptor.Wait();
                    _messageDispatcher.RemoveInterceptor(interceptor);

                    var inputMessage = (InfeedInputMessage)interceptor.Message;

                    if (inputMessage != null)
                    {
                        lock (_syncLock)
                        {
                            if (inputMessage.Article != null)
                                _inputArticles.AddRange(inputMessage.Article);

                            if (!Enum.TryParse(inputMessage.Details.Status, out _currentState))
                                _currentState = InfeedInputRequestState.Unknown;
                            
                            if (!string.IsNullOrEmpty(inputMessage.Details.Reason))
                            {
                                if (!Enum.TryParse(inputMessage.Details.Reason, out _rejectReason))
                                    _rejectReason = InfeedInputRejectReason.None;

                                _rejectionDescription = inputMessage.Details.Description;
                            }

                            this.Info($"{Info} finished with result '{_currentState}'.");
                        }
                    }
                    else
                    {
                        this.Error($"{Info} failed to wait for the message 'InfeedInputMessage'.");

                        lock (_syncLock)
                        {
                            _currentState = InfeedInputRequestState.Unknown;
                        }
                    }

                    interceptors[1].Cancel();

                    this.Trace($"{Info} raises event 'Finished'.");
                    this.Finished?.Invoke(this, new EventArgs());
                }
            }
            catch (Exception ex)
            {
                this.Error($"{Info} failed while receiving and processing 'InfeedInputMessage' message!", ex);
            }
        }

        #endregion
    }
}
