using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace Tesses.Http
{
    public sealed class HTTPClientRequest
    {
        HttpParser p;
        internal HTTPClientRequest(HttpParser parser)
        {
            p=parser;
        }
        public Dictionary<string,List<string>> Headers {get {return p.SentHeaders;}}
        public void WriteRequestBody(string data)
        {
            WriteRequestBody(Encoding.UTF8.GetBytes(data));
        }
        public HTTPClientRequest WithHeader(string key,string value)
        {
            Headers.Add(key,value);
            return this;
        }
        public HTTPClientRequest WithRange(long? start,long? end)
        {
            string _s="";
            string _e="";
            if(start.HasValue)
            {
                _s=start.Value.ToString();
            }
            if(end.HasValue)
            {
                _e =end.Value.ToString();
            }
            string ohdr=$"{_s}-{_e}";
            WithHeader("Range",ohdr);
            return this;
        }
        public void WriteRequestBody(object o)
        {
            WithContentType("application/json").WriteRequestBody(JsonConvert.SerializeObject(o));
        }
        public void WriteRequestBody(byte[] data)
        {
            WriteRequestBody(new MemoryStream(data));
        }
        public void WriteRequestBody(Stream strm)
        {
            //we need to write request body
            Headers.Add("Content-Length",strm.Length.ToString());
            p.SendHeaders();
            p.WriteBody(strm);
            p.ReceiveHeaders();
        }
        public HTTPClientRequest WithContentType(string content_type)
        {
            Headers.Add("Content-Type",content_type);
            return this;
        }

        public void WriteUrlEncodedPost(Dictionary<string,List<string>> args)
        {
          string urlencoded=  string.Join("&",args.Select<KeyValuePair<string,List<string>>,string>(e=>{
                 StringBuilder b=new StringBuilder();
                 foreach(var item in e.Value)
                 {
                     b.Append($"{e.Key}={WebUtility.UrlEncode(item)}");
                 }
                 return b.ToString();
            }));

            WithContentType("application/x-www-form-urlencoded").WriteRequestBody(urlencoded);
        }
        public void WriteEmptyRequest()
        {
            p.SendHeaders();
            p.ReceiveHeaders();
        }
        public RequestLine RequestLine {get{return p.SentHeaders.FirstLine;} set{p.SentHeaders.FirstLine=value;}}
    }
    public sealed class HTTPClient
    {
        TcpClient client;
        Stream strm;
        HttpParser parser;
        private HTTPClient(string method,string url)
        {
            string host="";
            int port;
            string hostHdr;
            Uri uri=new Uri(url);
             hostHdr = uri.Host;
             host=hostHdr;
                     bool isSecure;
             
             isSecure = uri.Scheme != "http" && uri.Scheme !=  "ws";
            if(((uri.Scheme == "http") && uri.Port != 80 ) || ((uri.Scheme == "https") && uri.Port != 443))
            {
                hostHdr += $":{uri.Port}";
                port = uri.Port;
            }else{
                if(uri.Scheme=="http")
                {
                    port=80;
                }
                else{
                    port=443;
                }
            }
            client=new TcpClient();
            client.Connect(host,port);
           if(isSecure)
            {
                
               var m=new SslStream(client.GetStream());  
               m.AuthenticateAsClient(host);
               strm=m;
                
            }else{
                strm =  client.GetStream();
            }
            parser=new HttpParser(strm);
            parser.SentHeaders=new HeaderCollection();
            parser.SentHeaders.FirstLine = new RequestLine(method,uri.PathAndQuery,"HTTP/1.1");
            Request=new HTTPClientRequest(parser);
            Response=new HTTPClientResponse(parser);
            Request.Headers.Add("Host",hostHdr);
        }
        public static HTTPClient Open(string method,string url)
        {
            return new HTTPClient(method,url);
        }
        
        public HTTPClientRequest Request {get;private set;}

        public HTTPClientResponse Response {get;private set;}

        public void SendToServer(ServerContext ctx,bool replaceHost=false)
        {
             //Send Request to Server
             foreach(var hdr in ctx.Request.Headers)
             {
                if(hdr.Key != "Host")
                {
                    if(parser.SentHeaders.ContainsKey(hdr.Key))
                    {
                        parser.SentHeaders[hdr.Key].Clear();
                    }
                    foreach(var value in hdr.Value)
                    {
                        parser.SentHeaders.Add(hdr.Key,value);
                    }
                    
                }
             }
             if(replaceHost)
             {
                 if(parser.SentHeaders.ContainsKey("Host") && ctx.Request.Headers.ContainsKey("Host"))
                 {
                     parser.SentHeaders["Host"].Clear();
                     parser.SentHeaders.Add("Host",ctx.Request.Headers["Host"][0]);
                 }
                 
             }

             //we will send to the second server we are reverse proxing

             //we need to set dest Method
             RequestLine req=parser.SentHeaders.FirstLine;
             parser.SentHeaders.FirstLine = new RequestLine(ctx.Request.RequestLine.Method,req.Path,req.HttpVersion);
             parser.SendHeaders();
             if(ctx.Request.Headers.ContainsKey("Content-Length") && ctx.Request.RequestLine.Method != "GET")
             {
                 //not a get request and has content-length
                 //so we must copy it
                 parser.WriteBody(ctx.Request.GetRequestStream());
             }
             //we will write the response from HTTPClient to ServerContext
            parser.ReceiveHeaders();
            ctx.Response.Headers.Clear();
            ctx.Response.StatusLine = parser.ReceivedHeaders.FirstLine;
            
            foreach(var hdr in parser.ReceivedHeaders)
            {
                foreach(var value in hdr.Value)
                {
                    ctx.Response.Headers.Add(hdr.Key,value);
                }
            }
            ctx.p.SendHeaders();
            if(ctx.Response.Headers.ContainsKey("Content-Type"))
            {
                ctx.p.WriteBody(parser.ReadBody());
            }

        }

       
        public static HTTPClient Open(HTTPServer server,string method,string path)
        {
            string myUrl;
            if(server.LocalEndPoint.Address.ToString()=="0.0.0.0")
            {
                //we can access via 127.0.0.1
                myUrl = $"http://127.0.0.1:{server.LocalEndPoint.Port}/{path.TrimStart('/')}";
            }else{
                //we can access via X.X.X.X
                myUrl = $"http://{server.LocalEndPoint.Address.ToString()}:{server.LocalEndPoint.Port}/{path.TrimStart('/')}";
            }

            return HTTPClient.Open(method,myUrl);
        }
    }

    public class HTTPClientResponse
    {
        HttpParser p;
        public HTTPClientResponse(HttpParser parser)
        {
            p=parser;
        }

        public Dictionary<string,List<string>> Headers {get{return p.ReceivedHeaders;}}

        public StatusLine StatusLine {get{return p.ReceivedHeaders.FirstLine;} set{p.ReceivedHeaders.FirstLine=value;}}
        public void DownloadFile(string file)
        {
            using(var f = File.Create(file))
            {
                DownloadFile(f);
            }
        }
        public void DownloadFile(Stream strm)
        {
            var strm0=GetResponseStream();
            strm0.CopyTo(strm);
        }
        public Stream GetResponseStream()
        {
            return p.ReadBody();
        }
        public string GetResponseString()
        {
            using(var sr=new StreamReader(GetResponseStream()))
            {
                return sr.ReadToEnd();
            }
        }
        public T GetResponseJson<T>()
        {
            return JsonConvert.DeserializeObject<T>(GetResponseString());
        }
        
    }

    public sealed class HTTPPassthroughReverseProxy
    {
        ServerContext ctx;
        string host="";
        int port;
        string hostHdr;
        bool useSrcHost;
        bool isSecure;
        string path;
        public HTTPPassthroughReverseProxy(ServerContext ctx,string url,bool useSrcHost)
        {
            Uri uri=new Uri(url);
             hostHdr = uri.Host;
             host=hostHdr;
             this.useSrcHost = useSrcHost;
             isSecure = uri.Scheme != "http" && uri.Scheme !=  "ws";
            if(((uri.Scheme == "http" || uri.Scheme == "ws") && uri.Port != 80 ) || ((uri.Scheme == "https" || uri.Scheme == "wss") && uri.Port != 443))
            {
                hostHdr += $":{uri.Port}";
                port = uri.Port;
            }else{
                if(uri.Scheme=="http" || uri.Scheme == "ws")
                {
                    port=80;
                }
                else{
                    port=443;
                }
            }
            path = uri.PathAndQuery;
            ctx.Request.RequestLine=new RequestLine(ctx.Request.RequestLine.Method,path,ctx.Request.RequestLine.HttpVersion);
            this.ctx=ctx;
        }

        public void Exchange(CancellationToken token=default(CancellationToken))
        {
             using (TcpClient clt=new TcpClient())
             {
                 
            if(!useSrcHost)
            {
                ctx.Request.Headers.Remove("Host");
                ctx.Request.Headers.Add("Host",hostHdr);
            }
            clt.Connect(host,port);  
            Stream strm;

            if(isSecure)
            {
                
               var m=new SslStream(clt.GetStream());  
               m.AuthenticateAsClient(host);
               strm=m;
                
            }else{
                strm =  clt.GetStream();
            }

            using(StreamPiper piper=new StreamPiper(ctx.GetRawStreamWithHeaders(),strm)){

            
            
            piper.Pipe(token);
            }

             }
            
        }
    }
}