
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Net;
using System.Text;
using GetUsernameAsyncFunc=System.Func<System.Collections.Generic.IDictionary<string,string>, System.Threading.Tasks.Task<string>>;
using System.IO;
using Newtonsoft.Json;

namespace Yavsc.Authentication
{
    public class OAuthenticator
    {
        public OAuthenticator()
        {
           
        }
        string clientId;
        string clientSecret;
        string scope;
        Uri authorizeUrl;
        Uri accessTokenUrl;
        Uri redirectUrl;
        GetUsernameAsyncFunc getUsernameAsync;

        string requestState;
        bool reportedForgery = false;

        /// <summary>
        /// Gets the client identifier.
        /// </summary>
        /// <value>The client identifier.</value>
        public string ClientId
        {
            get { return this.clientId; }
        }

        /// <summary>
        /// Gets the client secret.
        /// </summary>
        /// <value>The client secret.</value>
        public string ClientSecret
        {
            get { return this.clientSecret; }
        }

        /// <summary>
        /// Gets the authorization scope.
        /// </summary>
        /// <value>The authorization scope.</value>
        public string Scope
        {
            get { return this.scope; }
        }

        /// <summary>
        /// Gets the authorize URL.
        /// </summary>
        /// <value>The authorize URL.</value>
        public Uri AuthorizeUrl
        {
            get { return this.authorizeUrl; }
        }

        /// <summary>
        /// Gets the access token URL.
        /// </summary>
        /// <value>The URL used to request access tokens after an authorization code was received.</value>
        public Uri AccessTokenUrl
        {
            get { return this.accessTokenUrl; }
        }

        /// <summary>
        /// Redirect Url
        /// </summary>
        public Uri RedirectUrl
        {
            get { return this.redirectUrl; }
        }

        /// <summary>
        /// Initializes a new <see cref="Yavsc.Authentication.YaOAuth2WebAuthenticator"/>
        /// that authenticates using implicit granting (token).
        /// </summary>
        /// <param name='clientId'>
        /// Client identifier.
        /// </param>
        /// <param name='scope'>
        /// Authorization scope.
        /// </param>
        /// <param name='authorizeUrl'>
        /// Authorize URL.
        /// </param>
        /// <param name='redirectUrl'>
        /// Redirect URL.
        /// </param>
        /// <param name='getUsernameAsync'>
        /// Method used to fetch the username of an account
        /// after it has been successfully authenticated.
        /// </param>
        public OAuthenticator(string clientId, string scope, Uri authorizeUrl, Uri redirectUrl, GetUsernameAsyncFunc getUsernameAsync = null)
            : this(redirectUrl)
        {
            if (string.IsNullOrEmpty(clientId))
            {
                throw new ArgumentException("clientId must be provided", "clientId");
            }
            if (authorizeUrl==null)
                throw new ArgumentNullException("authorizeUrl");

            this.clientId = clientId;
            this.scope = scope ?? "";
            this.authorizeUrl = authorizeUrl ;
            this.getUsernameAsync = getUsernameAsync;
            this.accessTokenUrl = null;
        }

        /// <summary>
        /// Initializes a new instance <see cref="ZicMoove.Droid.OAuth.YaOAuth2WebAuthenticator"/>
        /// that authenticates using authorization codes (code).
        /// </summary>
        /// <param name='clientId'>
        /// Client identifier.
        /// </param>
        /// <param name='clientSecret'>
        /// Client secret.
        /// </param>
        /// <param name='scope'>
        /// Authorization scope.
        /// </param>
        /// <param name='authorizeUrl'>
        /// Authorize URL.
        /// </param>
        /// <param name='redirectUrl'>
        /// Redirect URL.
        /// </param>
        /// <param name='accessTokenUrl'>
        /// URL used to request access tokens after an authorization code was received.
        /// </param>
        /// <param name='getUsernameAsync'>
        /// Method used to fetch the username of an account
        /// after it has been successfully authenticated.
        /// </param>
        public OAuthenticator(string clientId, string clientSecret, string scope, Uri authorizeUrl, Uri redirectUrl, Uri accessTokenUrl, GetUsernameAsyncFunc getUsernameAsync = null)
            : this(redirectUrl, clientSecret, accessTokenUrl)
        {
            if (string.IsNullOrEmpty(clientId))
            {
                throw new ArgumentException("clientId must be provided", "clientId");
            }
            this.clientId = clientId;

            if (string.IsNullOrEmpty(clientSecret))
            {
                throw new ArgumentException("clientSecret must be provided", "clientSecret");
            }
            this.clientSecret = clientSecret;

            this.scope = scope ?? "";

            if (authorizeUrl == null)
            {
                throw new ArgumentNullException("authorizeUrl");
            }
            this.authorizeUrl = authorizeUrl;

            if (accessTokenUrl == null)
            {
                throw new ArgumentNullException("accessTokenUrl");
            }
            this.accessTokenUrl = accessTokenUrl;

            if (redirectUrl == null)
                throw new Exception("redirectUrl is null");

            this.redirectUrl = redirectUrl;

            this.getUsernameAsync = getUsernameAsync;
        }


        OAuthenticator(Uri redirectUrl, string clientSecret = null, Uri accessTokenUrl = null)
               
        {
            this.redirectUrl = redirectUrl;

            this.clientSecret = clientSecret;

            this.accessTokenUrl = accessTokenUrl;

            //
            // Generate a unique state string to check for forgeries
            //
            var chars = new char[16];
            var rand = new Random();
            for (var i = 0; i < chars.Length; i++)
            {
                chars[i] = (char)rand.Next((int)'a', (int)'z' + 1);
            }
            this.requestState = new string(chars);
        }

        bool IsImplicit { get { return accessTokenUrl == null; } }

        /// <summary>
        /// Method that returns the initial URL to be displayed in the web browser.
        /// </summary>
        /// <returns>
        /// A task that will return the initial URL.
        /// </returns>
        public  Task<Uri> GetInitialUrlAsync()
        {
            var url = new Uri(string.Format(
                "{0}?client_id={1}&redirect_uri={2}&response_type={3}&scope={4}&state={5}",
                authorizeUrl.AbsoluteUri,
                Uri.EscapeDataString(clientId),
                Uri.EscapeDataString(RedirectUrl.AbsoluteUri),
                IsImplicit ? "token" : "code",
                Uri.EscapeDataString(scope),
                Uri.EscapeDataString(requestState)));

            var tcs = new TaskCompletionSource<Uri>();
            tcs.SetResult(url);
            return tcs.Task;
        }

        /// <summary>
        /// Raised when a new page has been loaded.
        /// </summary>
        /// <param name='url'>
        /// URL of the page.
        /// </param>
        /// <param name='query'>
        /// The parsed query of the URL.
        /// </param>
        /// <param name='fragment'>
        /// The parsed fragment of the URL.
        /// </param>
        protected  void OnPageEncountered(Uri url, IDictionary<string, string> query, IDictionary<string, string> fragment)
        {

            if (url.AbsoluteUri.StartsWith(this.redirectUrl.AbsoluteUri))
            {
                // if (!this.redirectUrl.Equals(url))            {

                // this is not our redirect page,
                // but perhaps one one the third party identity providers
                // One don't check for a state here.
                // 
                /*  if (fragment.ContainsKey("continue")) { 
                  var cont = fragment["continue"];
                      // TODO continue browsing this address

                      var tcs = new TaskCompletionSource<Uri>();
                      tcs.SetResult(new Uri(cont));
                      tcs.Task.RunSynchronously();
                  }
                  return;*/
                //   }

                var all = new Dictionary<string, string>(query);
                foreach (var kv in fragment)
                    all[kv.Key] = kv.Value;

                //
                // Check for forgeries
                //
                if (all.ContainsKey("state"))
                {
                    if (all["state"] != requestState && !reportedForgery)
                    {
                        reportedForgery = true;
                        OnError("Invalid state from server. Possible forgery!");
                        return;
                    }
                }
            }
        }

        private void OnError(string v)
        {
            throw new NotImplementedException();
        }
        private void OnError(AggregateException ex)
        {
            throw new NotImplementedException();
        }
        /// <summary>
        /// Raised when a new page has been loaded.
        /// </summary>
        /// <param name='url'>
        /// URL of the page.
        /// </param>
        /// <param name='query'>
        /// The parsed query string of the URL.
        /// </param>
        /// <param name='fragment'>
        /// The parsed fragment of the URL.
        /// </param>
        protected  void OnRedirectPageLoaded(Uri url, IDictionary<string, string> query, IDictionary<string, string> fragment)
        {
            //
            // Look for the access_token
            //
            if (fragment.ContainsKey("access_token"))
            {
                //
                // We found an access_token
                //
                OnRetrievedAccountProperties(fragment);
            }
            else if (!IsImplicit)
            {
                //
                // Look for the code
                //
                if (query.ContainsKey("code"))
                {
                    var code = query["code"];
                    RequestAccessTokenAsync(code).ContinueWith(task =>
                    {
                        if (task.IsFaulted)
                        {
                            OnError(task.Exception);
                        }
                        else
                        {
                            OnRetrievedAccountProperties(task.Result);
                        }
                    }, TaskScheduler.FromCurrentSynchronizationContext());
                }
                else
                {
                    OnError("Expected code in response, but did not receive one.");
                    return;
                }
            }
            else
            {
                OnError("Expected access_token in response, but did not receive one.");
                return;
            }
        }

        /// <summary>
        /// Asynchronously requests an access token with an authorization <paramref name="code"/>.
        /// </summary>
        /// <returns>
        /// A dictionary of data returned from the authorization request.
        /// </returns>
        /// <param name='code'>The authorization code.</param>
        /// <remarks>Implements: http://tools.ietf.org/html/rfc6749#section-4.1</remarks>
        Task<IDictionary<string, string>> RequestAccessTokenAsync(string code)
        {
            var queryValues = new Dictionary<string, string> {
                { "grant_type", "authorization_code" },
                { "code", code },
                { "redirect_uri", RedirectUrl.AbsoluteUri },
                { "client_id", clientId }
            };
            if (!string.IsNullOrEmpty(clientSecret))
            {
                queryValues["client_secret"] = clientSecret;
            }

            return RequestAccessTokenAsync(queryValues);
        }

        /// <summary>
        /// Asynchronously makes a request to the access token URL with the given parameters.
        /// </summary>
        /// <param name="queryValues">The parameters to make the request with.</param>
        /// <returns>The data provided in the response to the access token request.</returns>
        public async Task<IDictionary<string, string>> RequestAccessTokenAsync(IDictionary<string, string> queryValues)
        {
            StringBuilder postData = new StringBuilder();
            if (!queryValues.ContainsKey("client_id"))
            {
                postData.Append("client_id="+Uri.EscapeDataString($"{this.clientId}")+"&");
            }
            if (!queryValues.ContainsKey("client_secret"))
            {
                postData.Append("client_secret="+Uri.EscapeDataString($"{this.clientSecret}")+"&");
            }
            if (!queryValues.ContainsKey("scope"))
            {
                postData.Append("scope="+Uri.EscapeDataString($"{this.scope}")+"&");
            }
            foreach (string key in queryValues.Keys)
            {
                postData.Append($"{key}="+Uri.EscapeDataString($"{queryValues[key]}")+"&");
            }
            var req = WebRequest.Create(accessTokenUrl);
            (req as HttpWebRequest).Accept = "application/json";
            req.Method = "POST";
            var body = Encoding.UTF8.GetBytes(postData.ToString());
            req.ContentLength = body.Length;
            req.ContentType = "application/x-www-form-urlencoded";
            var s = req.GetRequestStream();
            
            s.Write(body, 0, body.Length);

            var auth = await req.GetResponseAsync();
            
            var repstream = auth.GetResponseStream();

            var respReader = new StreamReader(repstream);
           
            var text = await respReader.ReadToEndAsync();
                   
            req.Abort();
            // Parse the response
            var data = text.Contains("{") ? JsonDecode(text) : FormDecode(text);

            if (data.ContainsKey("error"))
            {
                OnError("Error authenticating: " + data["error"]);
            }
            else if (data.ContainsKey("access_token"))
            {
                return data;
            }
            else
            {
                OnError("Expected access_token in access token response, but did not receive one.");
            }
            return data;
        }

        private IDictionary<string,string> FormDecode(string text)
        {
            
            throw new NotImplementedException();
        }

        private IDictionary<string,string> JsonDecode(string text)
        {
            return JsonConvert.DeserializeObject<Dictionary<string,string>>(text);
        }

        /// <summary>
        /// Event handler that is fired when an access token has been retreived.
        /// </summary>
        /// <param name='accountProperties'>
        /// The retrieved account properties
        /// </param>
        protected virtual void OnRetrievedAccountProperties(IDictionary<string, string> accountProperties)
        {
            //
            // Now we just need a username for the account
            //
            if (getUsernameAsync != null)
            {
                getUsernameAsync(accountProperties).ContinueWith(task =>
                {
                    if (task.IsFaulted)
                    {
                        OnError(task.Exception);
                    }
                    else
                    {
                        OnSucceeded(task.Result, accountProperties);
                    }
                }, TaskScheduler.FromCurrentSynchronizationContext());
            }
            else
            {
                OnSucceeded("", accountProperties);
            }
        }

        private void OnSucceeded(string v, IDictionary<string, string> accountProperties)
        {
            throw new NotImplementedException();
        }
    }

}


