using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Web;

namespace Sign
{
    class Request
    {
        public string method = "";
        public string scheme = "";
        public string host = "";
        public string uri = "";
        public Dictionary<string, string> query = new Dictionary<string, string>();
        public Dictionary<string, string> headers = new Dictionary<string, string>();
        public Dictionary<string, string> __headers = new Dictionary<string, string>();
        public string body = "";
    }

    class Signer
    {
        private readonly string basicDateFormat = "yyyyMMddTHHmmssZ";
        private readonly string algorithm = "SDK-HMAC-SHA256";
        private readonly string headerXDate = "X-Sdk-Date";
        private readonly string headerHost = "host";
        private readonly string headerAuthorization = "Authorization";
        private readonly string headerContentSha256 = "x-sdk-content-sha256";

        public string appKey = "";
        public string appSecret = "";

        #region get hash
        public string GetSHA256hash(string input)
        {
            byte[] clearBytes = Encoding.UTF8.GetBytes(input);
            SHA256 sha256 = new SHA256Managed();
            sha256.ComputeHash(clearBytes);
            byte[] hashedBytes = sha256.Hash;
            sha256.Clear();
            string output = BitConverter.ToString(hashedBytes).Replace("-", "").ToLower();

            return output;
        }
        #endregion

        public string CanonicalRequest(Request r)
        {
            string CanonicalURI = this.CanonicalURI(r);
            string CanonicalQueryString = this.CanonicalQueryString(r);
            string canonicalHeaders = CanonicalHeaders(r);
            string signedHeadersString = SignedHeaders(r);
            string hash = r.__headers.ContainsKey(headerContentSha256) ? r.__headers[headerContentSha256] : GetSHA256hash(r.body);

            return string.Format("{0}\n{1}\n{2}\n{3}\n{4}\n{5}", r.method, CanonicalURI, CanonicalQueryString, canonicalHeaders, signedHeadersString, hash);
        }

        // Convert string to hex
        public string Replacement(string str)
        {
            string patten = HttpUtility.UrlEncode(str);
            string result = str;
            if (patten.Contains("+"))
            {
                result = str.Replace("+", "%20");
            }
            if (patten.Contains("%7E"))
            {
                result = str.Replace("%7E", "~");
            }

            return result;
        }

        //CanonicalURI
        public string CanonicalURI(Request r)
        {
            string[] patterns = r.uri.Split('/');
            List<string> uri = new List<string>();
            foreach (string s in patterns)
            {
                uri.Add(Replacement(s));
            }
            string urlpath = string.Join("/", uri);
            if (urlpath[urlpath.Length - 1] != '/')
            {
                urlpath += "/";
            }
            return urlpath;
        }

        //CanonicalQueryString
        public string CanonicalQueryString(Request r)
        {
            List<string> keys = new List<string>();
            foreach (string key in r.query.Keys)
            {
                string kv = "";
                if (r.query[key] == "")
                {
                    kv = HttpUtility.UrlEncode(key);
                }
                else
                {
                    kv = HttpUtility.UrlEncode(key) + "=" + HttpUtility.UrlEncode(r.query[key]);
                }
                keys.Add(kv);
            }
            keys.Sort();

            return string.Join("&", keys);
        }

        // CanonicalHeaders
        public string CanonicalHeaders(Request r)
        {
            List<string> a = new List<string>();
            Dictionary<string, string> __headers = new Dictionary<string, string>();
            foreach (string key in r.headers.Keys)
            {
                string value = r.headers[key];
                string keyEncoded = key.ToLower();
                string valueEncoded = value.Trim();
                a.Add(keyEncoded + ":" + valueEncoded);
                __headers.Add(keyEncoded, valueEncoded);
            }
            a.Sort();  //ascending sort
            r.__headers = __headers;
            return string.Join("\n", a) + "\n";
        }

        // SignedHeaders
        public string SignedHeaders(Request r)
        {
            List<string> keys = new List<string>();
            foreach (string key in r.headers.Keys)
            {
                keys.Add(key.ToLower());
            }
            keys.Sort();
            return string.Join(";", keys);
        }

        //Create a "String to Sign"
        public string StringToSign(string canonicalRequest, DateTime t)
        {
            string bytes = GetSHA256hash(canonicalRequest);
            string stringTime = t.ToString(basicDateFormat); // get the date string
            return string.Format("{0}\n{1}\n{2}", algorithm, stringTime, bytes);
        }

        //Create the HWS Signature
        public string SignStringToSign(string stringToSign, string signingKey)
        {
            string signRet = string.Empty;
            using (HMACSHA256 mac = new HMACSHA256(Encoding.UTF8.GetBytes(signingKey)))
            {
                byte[] hash = mac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign));
                //signRet = Convert.ToBase64String(hash);
                signRet = ToHexString(hash);
            }
            return signRet;
        }

        //byte[] to hex string
        public static string ToHexString(byte[] bytes)
        {
            string hexString = string.Empty;
            if (bytes != null)
            {
                StringBuilder strB = new StringBuilder();
                foreach (byte b in bytes)
                {
                    strB.AppendFormat("{0:x2}", b);
                }
                hexString = strB.ToString();
            }
            return hexString;
        }

        // Get the finalized value for the "Authorization" header. The signature parameter is the output from SignStringToSign
        public string AuthHeaderValue(string signature, string AppKey, string signedHeaders)
        {
            return string.Format("{0} Access={1}, SignedHeaders={2}, Signature={3}", algorithm, AppKey, signedHeaders, signature);
        }

        //SignRequest set Authorization header
        public void Sign(Request r)
        {
            r.headers.TryGetValue(headerXDate, out string headerTime);
            DateTime t;
            if (headerTime == null)
            {
                t = DateTime.UtcNow;
                r.headers[headerXDate] = t.ToString(basicDateFormat);
            }
            else
            {
                t = DateTime.ParseExact(headerTime, basicDateFormat, CultureInfo.InvariantCulture);
            }

            bool haveHost = false;
            foreach (string key in r.headers.Keys)
            {
                if (key.ToLower() == "host")
                {
                    haveHost = true;
                    break;
                }
            }
            if (!haveHost)
            {
                r.headers["host"] = r.host;
            }

            string canonicalRequest = CanonicalRequest(r);
            string stringToSign = StringToSign(canonicalRequest, t);
            string signature = SignStringToSign(stringToSign, appSecret);
            string signedHeaders = SignedHeaders(r);
            string authValue = AuthHeaderValue(signature, appKey, signedHeaders);
            r.headers[headerAuthorization] = authValue;
            r.headers["content-length"] = r.body.Length.ToString();
            string queryString = CanonicalQueryString(r);
            if (queryString != "")
            {
                r.uri = r.uri + "?" + queryString;
            }
        }
    }
}
