using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.NodeServices.HostingModels;

namespace Microsoft.AspNetCore.NodeServices
{
    /// <summary>
    /// Default implementation of INodeServices. This is the primary API surface through which developers
    /// make use of this package. It provides simple "InvokeAsync" methods that dispatch calls to the
    /// correct Node instance, creating and destroying those instances as needed.
    ///
    /// If a Node instance dies (or none was yet created), this class takes care of creating a new one.
    /// If a Node instance signals that it needs to be restarted (e.g., because a file changed), then this
    /// class will create a new instance and dispatch future calls to it, while keeping the old instance
    /// alive for a defined period so that any in-flight RPC calls can complete. This latter feature is
    /// analogous to the "connection draining" feature implemented by HTTP load balancers.
    ///
    /// TODO: Implement everything in the preceding paragraph.
    /// </summary>
    /// <seealso cref="Microsoft.AspNetCore.NodeServices.INodeServices" />
    internal class NodeServicesImpl : INodeServices
    {
        private NodeServicesOptions _options;
        private Func<INodeInstance> _nodeInstanceFactory;
        private INodeInstance _currentNodeInstance;
        private object _currentNodeInstanceAccessLock = new object();

        internal NodeServicesImpl(NodeServicesOptions options, Func<INodeInstance> nodeInstanceFactory)
        {
            _options = options;
            _nodeInstanceFactory = nodeInstanceFactory;
        }

        public Task<T> InvokeAsync<T>(string moduleName, params object[] args)
        {
            return InvokeExportAsync<T>(moduleName, null, args);
        }

        public Task<T> InvokeExportAsync<T>(string moduleName, string exportedFunctionName, params object[] args)
        {
            return InvokeExportWithPossibleRetryAsync<T>(moduleName, exportedFunctionName, args, allowRetry: true);
        }

        public async Task<T> InvokeExportWithPossibleRetryAsync<T>(string moduleName, string exportedFunctionName, object[] args, bool allowRetry)
        {
            var nodeInstance = GetOrCreateCurrentNodeInstance();

            try
            {
                return await nodeInstance.InvokeExportAsync<T>(moduleName, exportedFunctionName, args);
            }
            catch (NodeInvocationException ex)
            {
                // If the Node instance can't complete the invocation because it needs to restart (e.g., because the underlying
                // Node process has exited, or a file it depends on has changed), then we make one attempt to restart transparently.
                if (allowRetry && ex.NodeInstanceUnavailable)
                {
                    // Perform the retry after clearing away the old instance
                    lock (_currentNodeInstanceAccessLock)
                    {
                        if (_currentNodeInstance == nodeInstance)
                        {
                            DisposeNodeInstance(_currentNodeInstance);
                            _currentNodeInstance = null;
                        }
                    }

                    // One the next call, don't allow retries, because we could get into an infinite retry loop, or a long retry
                    // loop that masks an underlying problem. A newly-created Node instance should be able to accept invocations,
                    // or something more serious must be wrong.
                    return await InvokeExportWithPossibleRetryAsync<T>(moduleName, exportedFunctionName, args, allowRetry: false);
                }
                else
                {
                    throw;
                }
            }
        }

        public void Dispose()
        {
            lock (_currentNodeInstanceAccessLock)
            {
                if (_currentNodeInstance != null)
                {
                    DisposeNodeInstance(_currentNodeInstance);
                    _currentNodeInstance = null;
                }
            }
        }

        private static void DisposeNodeInstance(INodeInstance nodeInstance)
        {
            // TODO: Implement delayed disposal for connection draining
            // Or consider having the delayedness of it being a responsibility of the INodeInstance
            nodeInstance.Dispose();
        }

        private INodeInstance GetOrCreateCurrentNodeInstance()
        {
            var instance = _currentNodeInstance;
            if (instance == null)
            {
                lock (_currentNodeInstanceAccessLock)
                {
                    instance = _currentNodeInstance;
                    if (instance == null)
                    {
                        instance = _currentNodeInstance = CreateNewNodeInstance();
                    }
                }
            }

            return instance;
        }

        private INodeInstance CreateNewNodeInstance()
        {
            return _nodeInstanceFactory();
        }

        // Obsolete method - will be removed soon
        public Task<T> Invoke<T>(string moduleName, params object[] args)
        {
            return InvokeAsync<T>(moduleName, args);
        }

        // Obsolete method - will be removed soon
        public Task<T> InvokeExport<T>(string moduleName, string exportedFunctionName, params object[] args)
        {
            return InvokeExportAsync<T>(moduleName, exportedFunctionName, args);
        }
    }
}