using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Net;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Text;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Collections.Specialized;

using FastReport.Utils;

namespace FastReport.Auth
{

    /// <summary>
    /// Service for working with auth in the Fast Report.
    /// </summary>
    public class AuthService
    {
        #region Private Fields

        private string code;
        private string code_verifier;
        private string nonce;
        private string scopes;
        private string session;
        private string state;
        private string redirectUri;

        #endregion Private Fields

        #region Public Properties

        /// <summary>
        /// Instance of default Service.
        /// </summary>
        public static AuthService Instance { get; } = new AuthService();

        /// <summary>
        /// Gets or sets indicator to enable or disable personalisation service
        /// </summary>
        public bool IsEnable { get; set; } = true;

        /// <summary>
        /// Setting of the service.
        /// </summary>
        public AppSettings Settings { get; } = new AppSettings();

        /// <summary>
        /// User of the service.
        /// </summary>
        public AppUser User { get; } = new AppUser();

        #endregion Public Properties

        #region Public Methods

        /// <summary>
        /// The method creates an sign in link.
        /// </summary>
        /// <returns></returns>
        public string GenerateSignInUri(string redirectURI)
        {
            try
            {
                this.nonce = NewRandomString(10);
                this.state = NewRandomString(64);

                this.code_verifier = NewRandomString(128);

                string codeChallenge = Sha256Url(code_verifier);

                StringBuilder sb = new StringBuilder(1024);
                sb.Append(Settings.Host + Settings.AuthorizationEndpoint).Append('?')
                    .Append("response_type=").Append(Uri.EscapeDataString(Settings.ResponseType))
                    .Append('&')
                    .Append("client_id=").Append(Uri.EscapeDataString(Settings.ClientId))
                    .Append('&')
                    .Append("nonce=").Append(Uri.EscapeDataString(this.nonce))
                    .Append('&')
                    .Append("redirect_uri=").Append(Uri.EscapeDataString(redirectURI))
                    .Append('&')
                    .Append("scope=").Append(Uri.EscapeDataString(Settings.Scopes))
                    //.Append('&')
                    //.Append("response_mode=").Append(Uri.EscapeDataString(Settings.ResponseMode))
                    .Append('&')
                    .Append("code_challenge_method=").Append(Uri.EscapeDataString(Settings.CodeChallengeMethod))
                    .Append('&')
                    .Append("code_challenge=").Append(Uri.EscapeDataString(codeChallenge))
                    .Append('&')
                    .Append("state=").Append(Uri.EscapeDataString(this.state));
                ;

                return sb.ToString();
            }
            catch (Exception e)
            {
                throw new AuthenticationException("Error generating sign in uri, maybe one of the path parameters is null.", e);
            }
        }


        /// <summary>
        /// The method creates an sign out link.
        /// </summary>
        /// <returns></returns>
        public string GenerateSignOutUri(string redirectURI)
        {
            try
            {
                StringBuilder sb = new StringBuilder(1024);
                sb.Append(Settings.Host + Settings.EndSessionEndpoint).Append('?')
                    .Append("id_token_hint=")
                    .Append(Uri.EscapeDataString(User.IdToken))
                    .Append("&post_logout_redirect_uri=")
                    .Append(Uri.EscapeDataString(redirectURI));

                return sb.ToString();
            }
            catch (Exception e)
            {
                throw new AuthenticationException("Error generating sign out uri, maybe one of the path parameters is null.", e);
            }
        }

        /// <summary>
        /// Returns true, if user has offline_access scope and refresh_token is not null
        /// </summary>
        public bool CanRefresh
        {
            get
            {
                return !String.IsNullOrEmpty(User.RefreshToken) && (User.Scopes == null || User.Scopes != null && Contains(User.Scopes, "offline_access"));
            }
        }

        /// <summary>
        /// If possible, the method updates the user credentials.
        /// </summary>
        /// <returns>True if success</returns>
        public bool Refresh()
        {
            try
            {
                if (CanRefresh)
                {
                    var request = HttpWebRequest.Create(new Uri(Settings.Host + Settings.TokenEndpoint));
                    request.Method = "POST";
                    request.ContentType = "application/x-www-form-urlencoded";

                    using (Stream requestStream = request.GetRequestStream())
                    {
                        byte[] bytes = Encoding.UTF8.GetBytes(GenerateTokenRequestBodyByRefresh());
                        requestStream.Write(bytes, 0, bytes.Length);
                    }

                    using (var response = request.GetResponse())
                    {
                        using (Stream responseStream = response.GetResponseStream())
                        {
                            using (TextReader tr = new StreamReader(responseStream, Encoding.UTF8))
                            {
                                var result = tr.ReadToEnd();
                                SaveTokens(result);
                                ValidateTokens();
                                ParseTokens();
                            }
                        }
                    }
                }

                return true;
            }
            catch (Exception ex)
            {
                User.RefreshToken = null;
            }
            return false;
        }

        /// <summary>
        /// The method resets auth, without sign out process.
        /// </summary>
        public void Reset()
        {
            User.Reset();
        }

        /// <summary>
        /// The method shows sign in form and auth the user.
        /// </summary>
        public void SignIn()
        {
            NameValueCollection queryString;
            using(var authServer = new TCPServerListener())
            {
                redirectUri = authServer.RedirectURL;
                redirectUri = redirectUri.Remove(redirectUri.Length - 1);
                var uri = GenerateSignInUri(redirectUri + Settings.RedirectSignInUri);

                authServer.Open();

                ProcessHelper.StartProcess(uri);

                var context = authServer.WaitConnect();

                var response = context.Response;
                var request = context.Request;
                response.Redirect(Settings.Host + Settings.RedirectSignInUri);
                response.OutputStream.Close();

                queryString = request.QueryString;
            }
            SignInCalback(queryString);
        }


        /// <summary>
        /// The method shows sign out form and resets the user credentials.
        /// </summary>
        public void SignOut()
        {
            using (var authServer = new TCPServerListener())
            {
                redirectUri = authServer.RedirectURL;
                redirectUri = redirectUri.Remove(redirectUri.Length - 1);
                string uri = GenerateSignOutUri(redirectUri + Settings.RedirectSignOutUri);

                authServer.Open();
                
                ProcessHelper.StartProcess(uri);
                var context = authServer.WaitConnect();

                var response = context.Response;
                response.Redirect(Settings.Host + Settings.RedirectSignInUri);
                response.OutputStream.Close();
            }

            User.Reset();
        }

        #endregion Public Methods

        #region Internal Methods

        internal static string NewRandomString(int v)
        {
            const string chars = "abcdefghijklmnopqrstuvwxyz1234567890";
            Random r = new Random();
            StringBuilder sb = new StringBuilder(v);
            for (int i = 0; i < v; i++)
            {
                sb.Append(chars[r.Next(chars.Length)]);
            }
            return sb.ToString();
        }



        #endregion Internal Methods

        #region Private Methods

        private void SignInCalback(NameValueCollection queryString)
        {
            // Checks for errors.
            if (HasError(queryString))
                return;

            Process(queryString);
            SignInPart2SecondRequest();
        }

        private bool HasError(NameValueCollection query)
        {
            var error = query.Get("error");
            if (error != null)
            {
                if (error == "access_denied")
                    return true;
                else
                    throw new AuthenticationException(error);

                //output(String.Format("OAuth authorization error: {0}.", error));
            }

            return false;
        }
        private static string Base64UrlToBase64(string base64url)
        {
            string base64 = base64url.Replace('-', '+').Replace('_', '/');

            if (base64.Length % 4 != 0)
            {
                base64 += new string('=', 4 - base64.Length % 4);
            }

            return base64;
        }

        private static string ConvertToString(object v, string defaultValue)
        {
            if (v != null)
                return v.ToString();
            return defaultValue;
        }

        private static string Sha256Url(string input)
        {
            if (String.IsNullOrEmpty(input))
                return string.Empty;

            using (var sha = SHA256.Create())
            {
                var bytes = Encoding.UTF8.GetBytes(input);
                var hash = sha.ComputeHash(bytes);

                return Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_').Replace("=", "");
            }
        }

        private static bool Contains(IEnumerable<string> scopes, string value)
        {
            foreach (string scope in scopes)
            {
                if (scope == value)
                    return true;
            }
            return false;
        }

        private Stream Download(string url)
        {
            MemoryStream memoryStream = new MemoryStream();
#if MONO
            ServicePointManager.Expect100Continue = true;
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
#endif
            try
            {
                // send second request
                var request = HttpWebRequest.Create(new Uri(url));

                request.Method = "GET";

                using (var response = request.GetResponse())
                {
                    using (Stream responseStream = response.GetResponseStream())
                    {
                        responseStream.CopyTo(memoryStream);
                    }
                }
            }
            catch
            {
            }
            memoryStream.Position = 0;
            return memoryStream;
        }

        private string GenerateTokenRequestBodyByCode(string redirectUri)
        {
            try
            {
                StringBuilder sb = new StringBuilder(1024);
                sb
                    .Append("grant_type=authorization_code")
                    //.Append(Uri.EscapeDataString(Settings.GrandType))
                    .Append("&client_id=").Append(Uri.EscapeDataString(Settings.ClientId))
                    .Append("&scope=").Append(Uri.EscapeDataString(this.scopes))
                    .Append("&redirect_uri=").Append(Uri.EscapeDataString(redirectUri))
                    .Append("&code=").Append(Uri.EscapeDataString(this.code))
                    .Append("&code_verifier=").Append(Uri.EscapeDataString(this.code_verifier))
                    .Append("&client_secret=").Append(Uri.EscapeDataString(Settings.ClientSecret));

                return sb.ToString();
            }
            catch (Exception e)
            {
                throw new AuthenticationException("Error generating token request body, maybe one of the path parameters is null.", e);
            }
        }

        private string GenerateTokenRequestBodyByRefresh()
        {
            try
            {
                StringBuilder sb = new StringBuilder(1024);
                sb
                    .Append("grant_type=refresh_token")
                    //.Append(Uri.EscapeDataString(Settings.GrandType))
                    .Append("&client_id=").Append(Uri.EscapeDataString(Settings.ClientId))
                    .Append("&refresh_token=").Append(Uri.EscapeDataString(User.RefreshToken))
                    .Append("&client_secret=").Append(Uri.EscapeDataString(Settings.ClientSecret));
                ;

                if (User.Scopes != null && User.Scopes.Length > 0)
                {
                    sb.Append("&scope=").Append(Uri.EscapeDataString(String.Join(" ", User.Scopes)));
                }

                return sb.ToString();
            }
            catch (Exception e)
            {
                throw new AuthenticationException("Error generating token request body, maybe one of the path parameters is null.", e);
            }
        }

        private string Gravatar(string email)
        {
            MD5 md5Hasher = MD5.Create();
            byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(email));
            StringBuilder sBuilder = new StringBuilder("https://www.gravatar.com/avatar/");
            for (int i = 0; i < data.Length; i++)
            {
                sBuilder.Append(data[i].ToString("x2"));
            }
            sBuilder.Append("?s=150");
            return sBuilder.ToString();
        }

        /// <summary>
        /// Do not make this method public, use refresh token for save-load. <br/>
        /// You need only refresh token (<see cref="AppUser.RefreshToken"/>) to get a new token set.<br/>
        /// This method is used to save time for starting a designer.
        /// </summary>
        internal void ParseTokens(bool isProgramStart = false)
        {
            try
            {
                if (!String.IsNullOrEmpty(User.IdToken))
                {
                    string[] token = User.IdToken.Split('.');
                    string payload = token[1];

                    JsonBase json
                        = JsonBase.FromString(
                            Encoding.UTF8.GetString(
                                Convert.FromBase64String(
                                    Base64UrlToBase64(payload)
                                    )
                                )
                            );

                    User.Subject = ConvertToString(json["sub"], "");
                    User.Email = ConvertToString(json["email"], "");

                    User.Username = ConvertToString(json["preferred_username"], User.Email);
                    User.FullName = ConvertToString(json["name"], "");

                    if ((ConvertToString(json["nonce"], nonce) != nonce) && !isProgramStart)
                    {
                        throw new AuthenticationException("Nonce check error, token is not valid.");
                    }

                    try
                    {
                        var url = Gravatar(User.Email);
                        var ms = Download(url);
                        User.Avatar = Image.FromStream(ms);
                    }
                    catch (Exception e)
                    {
                        User.Avatar = null;
                    }
                }
            }
            catch (AuthenticationException e)
            {
                User.IdToken = "";
                throw e;
            }
            catch (Exception e)
            {
                User.IdToken = "";
                throw new AuthenticationException("Identity token parse error!", e);
            }

            try
            {
                if (!String.IsNullOrEmpty(User.Token))
                {
                    string[] token = User.Token.Split('.');
                    string payload = token[1];

                    JsonBase json
                        = JsonBase.FromString(
                            Encoding.UTF8.GetString(
                                Convert.FromBase64String(
                                    Base64UrlToBase64(payload)
                                    )
                                )
                            );

                    double nbf = Convert.ToDouble(json["nbf"]);
                    double exp = Convert.ToDouble(json["exp"]);

                    double time = exp - nbf;

                    User.ExpiresIn = new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds(exp).ToLocalTime();
                    User.ExpiresInternal = new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds(nbf + time * 0.95).ToLocalTime();

                    JsonBase scopes = json["scope"] as JsonBase;
                    if (scopes != null && scopes.IsArray)
                    {
                        List<string> allowedScopes = new List<string>();
                        for (int i = 0; i < scopes.Count; i++)
                        {
                            allowedScopes.Add(scopes[i].ToString());
                        }
                        User.Scopes = allowedScopes.ToArray();
                    }
                }
            }
            catch (AuthenticationException e)
            {
                User.Token = "";
                throw e;
            }
            catch (Exception e)
            {
                User.Token = "";
                throw new AuthenticationException("Access token parse error!", e);
            }
        }

        private void Process(NameValueCollection query)
        {
            foreach (var key in query.AllKeys)
            {
                var value = query[key];
                switch (key.ToLower())
                {
                    case "code":
                        this.code = value;
                        break;

                    case "scope":
                        scopes = value;
                        break;

                    case "state":
                        if (value != this.state)
                            throw new Exception("State is not valid");
                        break;

                    case "session_state":
                        this.session = value;
                        break;
                }
            }
        }


        private void SaveTokens(string result)
        {
            JsonBase json = JsonBase.FromString(result);

            if (json.ContainsKey("expires_in"))
            {
                var expiresIn = Convert.ToSingle(json["expires_in"]);
                User.ExpiresIn = DateTime.Now.AddSeconds(expiresIn);
                User.ExpiresInternal = DateTime.Now.AddSeconds(expiresIn * 0.95);
            }
            else
            {
                // if no expires_in value, then default token lifetime
                User.ExpiresIn = DateTime.Now.AddMinutes(5);
                User.ExpiresInternal = User.ExpiresIn;
            }

            if (!json.ContainsKey("id_token"))
            {
                throw new AuthenticationException("No id token provided in server response.");
            }

            if (!json.ContainsKey("access_token"))
            {
                throw new AuthenticationException("No access token provided in server response.");
            }

            if (!json.ContainsKey("token_type"))
            {
                throw new AuthenticationException("No token type provided in server response.");
            }

            User.IdToken = json.ReadString("id_token");
            User.Token = json.ReadString("access_token");
            User.TokenType = json.ReadString("token_type");

            User.RefreshToken = json.ReadString("refresh_token");
        }

        private void SignInPart2SecondRequest()
        {
            var request = HttpWebRequest.Create(new Uri(Settings.Host + Settings.TokenEndpoint));
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";

            using (Stream requestStream = request.GetRequestStream())
            {
                string requestBody = GenerateTokenRequestBodyByCode(redirectUri + Settings.RedirectSignInUri);
                byte[] bytes = Encoding.UTF8.GetBytes(requestBody);
                requestStream.Write(bytes, 0, bytes.Length);
            }

            using (var response = request.GetResponse())
            {
                using (Stream responseStream = response.GetResponseStream())
                {
                    using (TextReader tr = new StreamReader(responseStream, Encoding.UTF8))
                    {
                        var result = tr.ReadToEnd();
                        SaveTokens(result);
                        ValidateTokens();
                        ParseTokens();
                    }
                }
            }
        }

        private void ValidateTokens()
        {
            // External library for validation signature on tokens
            User.IsValid = false;
        }

        #endregion Private Methods

        #region Public Classes

        /// <summary>
        /// Class for store appsettings, by default appsettings is hardcoded.
        /// </summary>
        public class AppSettings
        {
            internal const string DefaultBackendHost = "https://fastreport.cloud";


            #region Public Properties

            /// <summary>
            /// Authorization Endpoint from the OAuth2 specification.
            /// </summary>
            public string AuthorizationEndpoint { get; set; } = "/connect/authorize";

            /// <summary>
            /// Host for callback requests.
            /// </summary>
            public string CallbackHost { get; set; } = "https://id.fast-report.com";

            /// <summary>
            /// Client identifier or client name from the OAuth2 specification.
            /// </summary>
            public string ClientId { get; set; } = "FastReport.Net.Designer";

            /// <summary>
            /// Client secret or client name from the OAuth2 specification.
            /// </summary>
            public string ClientSecret { get; set; } = "91d18a32-1630-66d5-7f43-05d6e2caf02f";

            /// <summary>
            /// Code challenge method from the OAuth2 specification.
            /// </summary>
            public string CodeChallengeMethod { get; set; } = "S256";

            /// <summary>
            /// EndSession Endpoint from the OAuth2 specification.
            /// </summary>
            public string EndSessionEndpoint { get; set; } = "/connect/endsession";

            /// <summary>
            /// Host for sign in requests
            /// </summary>
            public string Host { get; set; } = "https://id.fast-report.com";

            // TODO:
            internal string BackendHost { get; set; } = DefaultBackendHost;

            /// <summary>
            /// JSON Web Key Set Endpoint from the OAuth2 specification.
            /// </summary>
            public string JwksEndpoint { get; set; } = "/.well-known/openid-configuration/jwks";

            /// <summary>
            /// Error result
            /// </summary>
            public string RedirectError { get; set; } = "/home/error";

            /// <summary>
            /// Redirent sign in link for this application.
            /// </summary>
            public string RedirectSignInUri { get; set; } = "/native/sign-in";

            /// <summary>
            /// Redirent sign out link for this application.
            /// </summary>
            public string RedirectSignOutUri { get; set; } = "/native/sign-out";

            /// <summary>
            /// Success result
            /// </summary>
            public string RedirectSuccess { get; set; } = "/home/success";

            /// <summary>
            /// Type of the reponse from the OAuth2 specification.
            /// </summary>
            public string ResponseType { get; set; } = "code";

            /// <summary>
            /// Scopes for the request from the OAuth2 specification, splited by space.
            /// </summary>
            public string Scopes { get; set; } = "openid email profile offline_access";

            /// <summary>
            /// Token Endpoint from the OAuth2 specification.
            /// </summary>
            public string TokenEndpoint { get; set; } = "/connect/token";

            #endregion Public Properties
        }

        public class AppUser
        {
            #region Private Fields

            private Image avatar;
            private Image defaultAvatar;

            #endregion Private Fields

            #region Public Properties

            /// <summary>
            /// Avatar of the user, by default is 150x150 picture.
            /// </summary>
            public Image Avatar
            {
                get { return avatar; }
                set
                {
                    if (avatar != null)
                        avatar.Dispose();
                    avatar = value;
                }
            }

            /// <summary>
            /// Returns the display avatar of the user, cannot return null
            /// </summary>
            /// <returns></returns>
            public Image DisplayAvatar
            {
                get
                {
                    if (avatar != null)
                        return avatar;
                    if (defaultAvatar == null)
                        defaultAvatar = ResourceLoader.GetBitmap("defaultAvatar.jpg");
                    return defaultAvatar;
                }
            }

            /// <summary>
            /// Returns the display email of the user, cannot return null
            /// </summary>
            /// <returns></returns>
            public string DisplayEmail
            {
                get
                {
                    if (Email == null)
                        return "";
                    return Email;
                }
            }

            /// <summary>
            /// Returns the display name of the user, cannot return null
            /// </summary>
            /// <returns></returns>
            public string DisplayName
            {
                get
                {
                    if (String.IsNullOrEmpty(FullName))
                    {
                        if (String.IsNullOrEmpty(Username))
                        {
                            if (String.IsNullOrEmpty(Subject))
                            {
                                return "";
                            }
                            return Subject;
                        }
                        return Username;
                    }
                    return FullName;
                }
            }

            /// <summary>
            /// Email of the user.
            /// </summary>
            public string Email { get; set; }

            /// <summary>
            /// Local time when the token will go out.
            /// </summary>
            public DateTime ExpiresIn { get; set; }

            /// <summary>
            /// Full name of the user.
            /// </summary>
            public string FullName { get; set; }

            /// <summary>
            /// Returns true if user is authenticated.
            /// </summary>
            public bool IsAuthenticated
            {
                get
                {
                    return !String.IsNullOrEmpty(IdToken) && !String.IsNullOrEmpty(Token);
                }
            }

            /// <summary>
            /// Returns true if token is expired and is need to referesh
            /// </summary>
            public bool IsExpired
            {
                get
                {
                    return ExpiresInternal < DateTime.Now;
                }
            }

            /// <summary>
            /// Indicates that token is check by external method, see <see cref="TokenValidator.CustomValidator"/> for details.
            /// </summary>
            public bool IsValid { get; set; }

            /// <summary>
            /// List of allowed scopes.
            /// </summary>
            public string[] Scopes { get; set; }

            /// <summary>
            /// Identifier of the user.
            /// </summary>
            public string Subject { get; set; }

            /// <summary>
            /// Type of token for resource request header, e.g. Bearer.
            /// </summary>
            public string TokenType { get; set; }

            /// <summary>
            /// Preferred username of the user.
            /// </summary>
            public string Username { get; set; }

            /// <summary>
            /// User's api key.
            /// </summary>
            public string ApiKey { get; set; }

            #endregion Public Properties

            #region Internal Properties

            /// <summary>
            /// Local time when the token needs to be updated.
            /// </summary>
            internal DateTime ExpiresInternal { get; set; }

            internal string IdToken { get; set; }
            internal string RefreshToken { get; set; }
            internal string Token { get; set; }

            #endregion Internal Properties

            #region Public Methods

            /// <summary>
            /// Reset the values
            /// </summary>
            public void Reset()
            {
                Avatar = null;
                Email = null;
                ExpiresIn = DateTime.MinValue;
                FullName = null;
                IdToken = null;
                IsValid = false;
                RefreshToken = null;
                Scopes = null;
                Subject = null;
                Token = null;
                TokenType = null;
                Username = null;
            }

            #endregion Public Methods
        }

        #endregion Public Classes
    }
}