using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using com.itac.mes.imsapi.protocol;

namespace com.itac.mes.imsapi.transport
{
    public sealed class IhapClient : IDisposable
    {
        private readonly IhapClientOptions _options;
        private readonly HttpClient _http;
        private readonly IhapLocator _locator;
        private long _requestNumber;
        private string _serviceUrl;
        private bool _disposed;

        public IhapClient(IhapClientOptions options)
        {
            _options=options??throw new ArgumentNullException(nameof(options));
            var h=new HttpClientHandler { AutomaticDecompression=DecompressionMethods.GZip|DecompressionMethods.Deflate };
            if(options.Proxy!=null){h.Proxy=options.Proxy;h.UseProxy=true;}
            _http=new HttpClient(h,true){Timeout=options.Timeout}; _locator=new IhapLocator(_http,options);
        }
        public T Invoke<T>(string wireMethodName, params object[] args) where T:class => InvokeAsync<T>(wireMethodName,args,CancellationToken.None).GetAwaiter().GetResult();
        public async Task<T> InvokeAsync<T>(string wireMethodName, object[] args, CancellationToken ct) where T:class
        {
            object o=await InvokeAsync(wireMethodName,args,ct).ConfigureAwait(false); return o as T ?? throw new IhapCommunicationException("Unexpected IMSAPI result type. Expected "+typeof(T).FullName+", got "+(o==null?"null":o.GetType().FullName)+".");
        }
        public async Task<object> InvokeAsync(string wireMethodName, object[] args, CancellationToken ct)
        {
            if(_disposed)throw new ObjectDisposedException(nameof(IhapClient));
            byte[] request=IhapProtocol.SerializeCall(wireMethodName,_options.ProtocolMode,args??Array.Empty<object>());
            Exception last=null; string failedBase=null;
            int attempts=_options.EnableFailover?Math.Max(1,_options.ClusterNodes.Count):1;
            for(int attempt=0;attempt<attempts;attempt++)
            {
                try
                {
                    if(string.IsNullOrEmpty(_serviceUrl))_serviceUrl=await _locator.ResolveAsync(ct,failedBase).ConfigureAwait(false);
                    return await PostAsync(_serviceUrl,request,ct).ConfigureAwait(false);
                }
                catch(Exception ex) when (!(ex is IhapRemoteException))
                {
                    last=ex; failedBase=BaseFromService(_serviceUrl); _serviceUrl=null;
                    if(!_options.EnableFailover || attempt+1>=attempts)break;
                }
            }
            throw new IhapCommunicationException("IMSAPI remote server call failed.",last);
        }
        private async Task<object> PostAsync(string url,byte[] payload,CancellationToken ct)
        {
            long nr=Interlocked.Increment(ref _requestNumber);
            using(var req=new HttpRequestMessage(HttpMethod.Post,url))
            {
                req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/octet-stream"));
                req.Headers.TryAddWithoutValidation("reqnr",nr.ToString(System.Globalization.CultureInfo.InvariantCulture));
                if(!string.IsNullOrWhiteSpace(_options.AuthorizationHeader))req.Headers.TryAddWithoutValidation("Authorization",_options.AuthorizationHeader);
                byte[] body=payload;
                if(_options.CompressRequests)
                {
                    using(var ms=new MemoryStream()){using(var gz=new GZipStream(ms,CompressionMode.Compress,true))gz.Write(payload,0,payload.Length);body=ms.ToArray();}
                }
                req.Content=new ByteArrayContent(body); req.Content.Headers.ContentType=new MediaTypeHeaderValue("application/octet-stream");
                if(_options.CompressRequests)req.Content.Headers.ContentEncoding.Add("gzip");
                using(var rsp=await _http.SendAsync(req,HttpCompletionOption.ResponseHeadersRead,ct).ConfigureAwait(false))
                {
                    if((int)rsp.StatusCode!=200)throw new IhapCommunicationException("IMSAPI HTTP status "+(int)rsp.StatusCode+" ("+rsp.ReasonPhrase+").");
                    if(rsp.Headers.TryGetValues("reqnr",out var vals))
                    {
                        string echo=vals.FirstOrDefault(); if(!string.IsNullOrEmpty(echo) && echo!=nr.ToString(System.Globalization.CultureInfo.InvariantCulture)) throw new IhapCommunicationException("IHAP request-number/cache collision: sent "+nr+", received "+echo+".");
                    }
                    using(var stream=await rsp.Content.ReadAsStreamAsync().ConfigureAwait(false)) return new IhapReader(stream).ReadReply();
                }
            }
        }
        private static string BaseFromService(string s){ if(string.IsNullOrEmpty(s))return null; const string tail="/imsapi/service"; return s.EndsWith(tail,StringComparison.OrdinalIgnoreCase)?s.Substring(0,s.Length-tail.Length):s; }
        public void Dispose(){if(_disposed)return;_disposed=true;_http.Dispose();}
    }
}
