using System;
using System.Threading;

using Best.HTTP.Caching;
using Best.HTTP.Hosts.Connections.HTTP1;
using Best.HTTP.HostSetting;
using Best.HTTP.Request.Timings;
using Best.HTTP.Response;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.FileSystem;
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
using Best.HTTP.Shared.PlatformSupport.Network.Tcp.Streams;
using Best.HTTP.Shared.Streams;

namespace Best.HTTP.Hosts.Connections.File
{
    internal sealed class FileConnection : ConnectionBase, IContentConsumer, IDownloadContentBufferAvailable
    {
        public PeekableContentProviderStream ContentProvider { get; private set; }

        PeekableHTTP1Response _response;
        NonblockingUnderlyingStream _stream;

        UnityEngine.Hash128 _cacheHash;

        public FileConnection(HostKey hostKey)
            : base(hostKey)
        { }

        protected override void ThreadFunc()
        {
            this.CurrentRequest.Timing.StartNext(TimingEventNames.Waiting_TTFB);

            this.Context.Remove("Request");
            this.Context.Add("Request", this.CurrentRequest.Context);

            bool isFromLocalCache = this.CurrentRequest.CurrentUri.Host.Equals(HTTPCache.CacheHostName, StringComparison.OrdinalIgnoreCase);

            if (this._response == null)
                this.CurrentRequest.Response = this._response = new PeekableHTTP1Response(this.CurrentRequest, isFromLocalCache, this);
            this._response.Context.Add(nameof(FileConnection), this.Context);

            StreamList stream = new StreamList();
            try
            {
                var headers = new BufferPoolMemoryStream();
                stream.AppendStream(headers);
                
                headers.WriteLine("HTTP/1.1 200 Ok");

                System.IO.Stream contentStream = null;

                if (isFromLocalCache)
                {
                    var hashStr = this.CurrentRequest.CurrentUri.AbsolutePath.Substring(1);
                    var hash = UnityEngine.Hash128.Parse(hashStr);

                    // BeginReadContent tries to acquire a read lock on the content and returns null if couldn't.
                    contentStream = HTTPManager.LocalCache?.BeginReadContent(hash, this.Context);
                    if (contentStream == null)
                        throw new HTTPCacheAcquireLockException($"Coulnd't acquire read lock on cached entity.");

                    this._cacheHash = hash;

                    headers.WriteLine($"BestHTTP-Origin: cachefile({hashStr})");

                    stream.AppendStream(HTTPManager.IOService.CreateFileStream(HTTPManager.LocalCache.GetHeaderPathFromHash(hash), FileStreamModes.OpenRead));
                }
                else
                {
                    headers.WriteLine($"BestHTTP-Origin: file");
                    headers.WriteLine("Content-Type: application/octet-stream");

                    contentStream = HTTPManager.IOService.CreateFileStream(this.CurrentRequest.CurrentUri.LocalPath, FileStreamModes.OpenRead);
                }

                headers.WriteLine($"Content-Length: {contentStream.Length.ToString()}");
                if (!isFromLocalCache)
                    headers.WriteLine();

                headers.Seek(0, System.IO.SeekOrigin.Begin);

                stream.AppendStream(contentStream);

                this.CurrentRequest.TimeoutSettings.SetProcessing(DateTime.UtcNow);
                
                this._stream = new NonblockingUnderlyingStream(stream, 1024 * 1024, this.Context);
                this._stream.SetTwoWayBinding(this);
                this._stream.BeginReceive();

                this.CurrentRequest.OnCancellationRequested += OnCancellationRequested;
            }
            catch(Exception ex)
            {
                FinishedProcessing(ex);
                stream?.Dispose();
            }
        }

        void IDownloadContentBufferAvailable.BufferAvailable(DownloadContentStream stream)
        {
            //HTTPManager.Logger.Verbose(nameof(FileConnection), "IDownloadContentBufferAvailable.BufferAvailable", this.Context);

            // Here we should trigger somehow the read stream and that should call OnContent(IPeekableContentProvider provider, PeekableStream peekable)
            //  to go the regular route.

            if (this._response != null)
                OnContent();
        }

        public void SetBinding(PeekableContentProviderStream contentProvider)
        {
            this.ContentProvider = contentProvider;
        }

        public void UnsetBinding() => this.ContentProvider = null;

        public void OnContent()
        {
            try
            {
                if (this.CurrentRequest.TimeoutSettings.IsTimedOut(DateTime.UtcNow))
                    throw new TimeoutException();

                if (this.CurrentRequest.IsCancellationRequested)
                    throw new Exception("Cancellation requested!");

                this._response.ProcessPeekable(this.ContentProvider);
            }
            catch (Exception e)
            {
                if (this.ShutdownType == ShutdownTypes.Immediate)
                    return;

                FinishedProcessing(e);
            }

            // After an exception, this._response will be null!
            if (this._response != null && this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
                FinishedProcessing(null);
        }

        public void OnConnectionClosed()
        {
            HTTPManager.Logger.Information(nameof(FileConnection), $"OnConnectionClosed({this.ContentProvider?.Length}, {this._response?.ReadState})", this.Context);

            // If the consumer still have a request: error it and close the connection
            if (this.CurrentRequest != null && this._response != null)
            {
                FinishedProcessing(new Exception("Underlying TCP connection closed unexpectedly!"));
            }
            else // If no current request: close the connection
                ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
        }

        public void OnError(Exception e)
        {
            HTTPManager.Logger.Information(nameof(FileConnection), $"OnError({this.ContentProvider?.Length}, {this._response?.ReadState}, {this.ShutdownType})", this.Context);

            if (this.ShutdownType == ShutdownTypes.Immediate)
                return;

            FinishedProcessing(e);
        }

        private void OnCancellationRequested(HTTPRequest req)
        {
            HTTPManager.Logger.Information(nameof(FileConnection), "OnCancellationRequested()", this.Context);

            Interlocked.Exchange(ref this._response, null);
            req.OnCancellationRequested -= OnCancellationRequested;
            this._stream.Dispose();
        }

        void FinishedProcessing(Exception ex)
        {
            // Warning: FinishedProcessing might be called from different threads in parallel:
            //  - send thread triggered by a write failure
            //  - read thread oncontent/OnError/OnConnectionClosed

            var resp = Interlocked.Exchange(ref this._response, null);
            if (resp == null)
                return;

            HTTPManager.Logger.Verbose(nameof(FileConnection), $"{nameof(FinishedProcessing)}({resp}, {ex})", this.Context);

            HTTPManager.LocalCache?.EndReadContent(this._cacheHash, this.Context);
            this._cacheHash = new UnityEngine.Hash128();

            // Unset the consumer, we no longer expect another OnContent call until further notice.
            this._stream?.Unbind();
            this._stream?.Dispose();
            this._stream = null;

            var req = this.CurrentRequest;

            req.OnCancellationRequested -= OnCancellationRequested;

            bool resendRequest = false;
            HTTPRequestStates requestState = HTTPRequestStates.Finished;
            HTTPConnectionStates connectionState = HTTPConnectionStates.Recycle;
            Exception error = ex;

            if (error != null)
            {
                // Timeout is a non-retryable error
                if (ex is TimeoutException)
                {
                    error = null;
                    requestState = HTTPRequestStates.TimedOut;
                }
                else if (ex is HTTPCacheAcquireLockException)
                {
                    error = null;
                    resendRequest = true;
                }
                else
                {
                    if (req.RetrySettings.Retries < req.RetrySettings.MaxRetries && !HTTPManager.IsQuitting)
                    {
                        req.RetrySettings.Retries++;
                        error = null;
                        resendRequest = true;
                    }
                    else
                    {
                        requestState = HTTPRequestStates.Error;
                    }
                }

                // Any exception means that the connection is in an unknown state, we shouldn't try to reuse it.
                connectionState = HTTPConnectionStates.Closed;

                resp.Dispose();
            }
            else
            {
                // After HandleResponse connectionState can have the following values:
                //  - Processing: nothing interesting, caller side can decide what happens with the connection (recycle connection).
                //  - Closed: server sent an connection: close header.
                //  - ClosedResendRequest: in this case resendRequest is true, and the connection must not be reused.
                //      In this case we can send only one ConnectionEvent to handle both case and avoid concurrency issues.

                KeepAliveHeader keepAlive = null;
                error = ConnectionHelper.HandleResponse(req, out resendRequest, out connectionState, ref keepAlive, this.Context);
                connectionState = HTTPConnectionStates.Recycle;

                if (!resendRequest && resp.IsUpgraded)
                    requestState = HTTPRequestStates.Processing;
            }

            req.Timing.StartNext(TimingEventNames.Queued);

            HTTPManager.Logger.Verbose(nameof(FileConnection), $"{nameof(FinishedProcessing)} final decision. ResendRequest: {resendRequest}, RequestState: {requestState}, ConnectionState: {connectionState}", this.Context);

            // If HandleResponse returned with ClosedResendRequest or there were an error and we can retry the request
            if (connectionState == HTTPConnectionStates.ClosedResendRequest || (resendRequest && connectionState == HTTPConnectionStates.Closed))
            {
                ConnectionHelper.ResendRequestAndCloseConnection(this, req);
            }
            else if (resendRequest && requestState == HTTPRequestStates.Finished)
            {
                RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, RequestEvents.Resend));
                ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, connectionState));
            }
            else
            {
                // Otherwise set the request's then the connection's state
                ConnectionHelper.EnqueueEvents(this, connectionState, req, requestState, error);
            }
        }
    }
}
