using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using C4IT.Logging;


namespace C4IT.MsGraph
{
    public class cMsGraphBase
    {
        public enum eHttpMethod { get, post, put, delete};

        public const string constAzureLoginUrl = "https://login.microsoftonline.com/{Tenant}/oauth2/v2.0/token";
        public const string constAzureAuthorizeUrl = "https://login.microsoftonline.com/{Tenant}/oauth2/v2.0/authorize";
        public const string constMsGraphUrl = "https://graph.microsoft.com/{Version}/{Request}";
        public const string constMsGraphApplicationGet = "applications?$filter=appId eq '{AppID}'";
        public const string constMsGraphProfileGet = "me";

        public bool IsOnline { get; private set; } = false;

        public string AccessToken { get; private set; } = null;
        public DateTime TokenExpiresIn { get; private set; } = DateTime.MinValue;

        private cMsGraphLogonInfo privLogonInfo = null;

        public Exception LastException { get; private set; } = null;
        public string LastErrorMessage { get; private set; } = null;

        public cMsGraphResultApplication Me { get; private set; } = null;

        public HttpStatusCode? LastHttpErrorCode { get; private set; } = null;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void ResetError()
        {
            LastException = null;
            LastErrorMessage = null;
            LastHttpErrorCode = null;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void SetErrorException(string Action, Exception E, LogLevels lev = LogLevels.Error)
        {
            LastException = E;
            LastErrorMessage = Action + ": " + E.Message;
            cLogManager.LogEntry(Action, lev);
        }

        private async Task<bool> privLogonAsync(cMsGraphLogonInfo LogonInfo)
        {
            try
            {
                ResetError();
                IsOnline = false;

                // prepare the http login request
                var postData = new List<KeyValuePair<string, string>>
                {
                    new KeyValuePair<string, string>("client_id", LogonInfo.ClientID),
                    new KeyValuePair<string, string>("scope", LogonInfo.Scope),
                    new KeyValuePair<string, string>("redirect_uri", $"https://login.microsoftonline.com/{LogonInfo.Tenant}/oauth2/nativeclient"),
                    new KeyValuePair<string, string>("grant_type", "client_credentials"),
                    new KeyValuePair<string, string>("client_secret", LogonInfo.ClientSecret)
                };

                var formData = new FormUrlEncodedContent(postData);
                var strUrl = constAzureLoginUrl.Replace("{Tenant}", LogonInfo.Tenant);
                var request = new HttpRequestMessage(System.Net.Http.HttpMethod.Post, strUrl)
                {
                    Content = formData
                };


                // do the http login request
                using (var httpClient = new HttpClient())
                {
                    var response = await httpClient.SendAsync(request);
                    LastHttpErrorCode = response.StatusCode;
                    if (!response.IsSuccessStatusCode)
                    {
                        LastErrorMessage = $"HTTP request error {response.StatusCode} while Azure tenant login: {response.ReasonPhrase}";
                        return false;
                    }

                    // get the response content & the access token
                    var content = await response.Content.ReadAsStringAsync();
                    dynamic iToken = JsonConvert.DeserializeObject(content);
                    AccessToken = iToken.access_token;
                    int ExpiresIn = iToken.expires_in;
                    TokenExpiresIn = DateTime.UtcNow + TimeSpan.FromSeconds(ExpiresIn * 0.7);
                    var TokenType = iToken.token_type;
                    if (TokenType != "Bearer")
                        return false;
                }

                if (AccessToken != null)
                {
                    IsOnline = true;
                    Me = await RequestApplicationAsync(LogonInfo.ClientID);
                    return true;
                }

            }
            catch (Exception E)
            {
                SetErrorException("HTTP exception error while Azure tenant login", E, LogLevels.Debug);
                cLogManager.LogException(E, LogLevels.Debug);
            }

            return false;
        }

        private async Task<bool> privRelogon()
        {
            if (DateTime.UtcNow < TokenExpiresIn)
                return true;
            if (privLogonInfo == null)
                return false;
            var RetVal = await privLogonAsync(privLogonInfo);
            return RetVal;
        }

        public async Task<bool> LogonAsync(cMsGraphLogonInfo LogonInfo)
        {
            var RetVal = await privLogonAsync(LogonInfo);
            if (RetVal == true)
                privLogonInfo = LogonInfo;
            return RetVal;
        }

        private async Task<dynamic> privRequestAsync(string Url, eHttpMethod httpMethod = eHttpMethod.get, object JsonData = null, bool retryForbidden = false)
        {
            ResetError();
            try
            {
                await RequestCriticalSection.Semaphore.WaitAsync();

                if (!IsOnline)
                    return null;

                if (!await privRelogon())
                    return null;

                var retryCount = 0;
                while (true)
                {
                    // create the request
                    using (var handler = new HttpClientHandler())
                    {
                        using (var client = new HttpClient(handler))
                        {
                            client.DefaultRequestHeaders.Accept.Clear();
                            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                            client.DefaultRequestHeaders.Authorization =
                                        new AuthenticationHeaderValue("Bearer", AccessToken);
                            HttpResponseMessage response;
                            StringContent CO = null;
                            if (JsonData != null)
                            {
                                var strJson = JsonConvert.SerializeObject(JsonData);
                                CO = new StringContent(strJson, Encoding.UTF8, "application/json");
                            }
                            switch (httpMethod)
                            {
                                case eHttpMethod.get:
                                    response = await client.GetAsync(Url);
                                    break;
                                case eHttpMethod.post:
                                    response = await client.PostAsync(Url, CO);
                                    break;
                                case eHttpMethod.delete:
                                    response = await client.DeleteAsync(Url);
                                    break;
                                case eHttpMethod.put:
                                    response = await client.PutAsync(Url, CO);
                                    break;
                                default:
                                    return null;

                            }

                            LastHttpErrorCode = response.StatusCode;
                            if (!response.IsSuccessStatusCode)
                            {
                                if (retryForbidden && (LastHttpErrorCode == HttpStatusCode.Forbidden) && (retryCount < 2))
                                {
                                    retryCount++;
                                    await (Task.Delay(100));
                                }
                                else
                                {
                                    LastErrorMessage = $"HTTP request error {(int)response.StatusCode} while MS Graph request '{Url}': {response.ReasonPhrase}";
                                    if (cLogManager.DefaultLogger.IsDebug)
                                        cLogManager.DefaultLogger.LogEntry(LogLevels.Warning, LastErrorMessage);
                                    return null;
                                }
                            }
                            else
                            {
                                if (response.StatusCode == HttpStatusCode.NoContent)
                                    return true;

                                var content = await response.Content.ReadAsStringAsync();
                                dynamic jsonData = JsonConvert.DeserializeObject(content); ;

                                return jsonData;
                            }

                        }
                    }
                }
            }
            catch (Exception E)
            {
                SetErrorException("HTTP exception error while while MS Graph request '{Request}'", E);
                cLogManager.LogException(E);
            }
            finally
            {
                RequestCriticalSection.Semaphore.Release();

            }

            return null;
        }

        public async Task<cMsGraphResultBase> RequestAsync(string Request, bool UseBeta = false, eHttpMethod httpMethod = eHttpMethod.get, object JsonData = null, bool retryForbidden = false)
        {
            try
            {
                // get the request url
                var strVersion = "v1.0";
                if (UseBeta)
                    strVersion = "beta";
                var strUrl = constMsGraphUrl.Replace("{Version}", strVersion);
                strUrl = strUrl.Replace("{Request}", Request);

                var data = await privRequestAsync(strUrl, httpMethod, JsonData, retryForbidden);
                if (data == null)
                    return null;

                if (data is bool isValid)
                {
                    if (isValid)
                        return new cMsGraphResultBase(null);
                    else
                        return null;
                }

                var RetVal = new cMsGraphResultBase(data);
                return RetVal;
            }
            catch (Exception E)
            {
                cLogManager.LogException(E);
            }
            return null;
        }

        public async Task<cMsGraphResultList> RequestListAsync(string Request, bool UseBeta = false, bool loadPaged = false,  bool retryForbidden = false)
        {
            try
            {
                // get the request url
                var strVersion = "v1.0";
                if (UseBeta)
                    strVersion = "beta";
                var strUrl = constMsGraphUrl.Replace("{Version}", strVersion);
                strUrl = strUrl.Replace("{Request}", Request);

                var res = await privRequestAsync(strUrl, retryForbidden: retryForbidden);
                if (res != null)
                {
                    var RetVal = new cMsGraphResultList(retryForbidden);
                    RetVal.AddResult(res);
                    if (!RetVal.hasRemaining || loadPaged)
                        return RetVal;
                    while (RetVal.hasRemaining)
                    {
                        if (!await RequestNextAsync(RetVal))
                            return RetVal;
                    }
                    return RetVal;
                }
            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogException(E);
            }
            return null;
        }

        public async Task<bool> RequestNextAsync(cMsGraphResultList List)
        {
            try
            {
                var res = await privRequestAsync(List.NextResultUrl, retryForbidden: List.retryForbidden);
                if (res != null)
                {
                    List.AddResult(res);
                    return true;
                }
            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogException(E);
            }

            return false;
        }

        public async Task<cMsGraphResultUser> RequestProfileAsync()
        {
            ResetError();
            try
            {
                var Result = await RequestAsync(constMsGraphProfileGet);
                if (Result != null)
                    return new cMsGraphResultUser(Result);
            }
            catch (Exception E)
            {
                cLogManager.LogException(E);
            }

            return null;
        }

        public async Task<cMsGraphResultApplication> RequestApplicationAsync(string AppID)
        {
            ResetError();
            try
            {
                var strRequest = constMsGraphApplicationGet.Replace("{AppID}", AppID);
                var Result = await RequestAsync(strRequest, retryForbidden: true);
                if (Result != null)
                {
                    dynamic lst = Result.Result.value;
                    if (Result.Result.value.Count >= 1)
                    {
                        dynamic app = Result.Result.value[0];
                        var RetVal = new cMsGraphResultBase(app);
                        return new cMsGraphResultApplication(RetVal);
                    }

                    return new cMsGraphResultApplication(Result);
                }
            }
            catch (Exception E)
            {
                cLogManager.LogException(E);
            }

            return null;
        }

    }

    public class cMsGraphLogonInfo
    {
        public string Tenant;
        public string ClientID;
        public string ClientSecret;
        public string Scope = "https://graph.microsoft.com/.default";
    }

    public static class RequestCriticalSection
    {
        public static SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);
    }

    public class cMsGraphResultBase
    {
        public string ID { get; private set; } = null;
        public string ODataId { get; private set; } = null;
        public string DisplayName { get; private set; } = null;

        public string Context { get; private set; } = null;

        public dynamic Result { get; private set; } = null;

        public cMsGraphResultBase(dynamic Result)
        {
            this.Result = Result;

            try { ID = Result.id; } catch { };
            try {
                if (Result.TryGetValue("displayName", out JToken JT1))
                    DisplayName = Result.displayName;
                else if (Result.TryGetValue("name", out JToken JT2))
                    DisplayName = Result.name;
            }
            catch { }
            try { Context = Result["@odata.context"]; } catch { }
            ODataId = GetStringFromDynamic(Result, "@odata.id");
            if (string.IsNullOrEmpty(ODataId))
                ODataId = @"https://graph.microsoft.com/v1.0/directoryObjects/" + ID;
        }

        public cMsGraphResultBase(cMsGraphResultBase Result)
        {
            if (Result == null)
                return;

            this.Result = Result.Result;
            ID = Result.ID;
            ODataId = Result.ODataId;
            DisplayName = Result.DisplayName;
            Context = Result.Context;

        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static string GetStringFromDynamic(dynamic O, string ProperyName)
        {
            try
            {
                return (string)O[ProperyName];
            }
            catch { }
            return null;
        }
    }

    public class cMsGraphResultList : List<cMsGraphResultBase>
    {
        public string NextResultUrl { get; private set; } = null;
        public bool retryForbidden { get; private set; } = false;

        public cMsGraphResultList(bool retryForbidden)
        {
            this.retryForbidden = retryForbidden;
        }

        public bool hasRemaining
        {
            get
            {
                return !string.IsNullOrEmpty(NextResultUrl);
            }
        }

        public void AddResult(dynamic Result)
        {
            try
            {
                NextResultUrl = null;

                if (Result.TryGetValue("@odata.nextLink", out JToken JT))
                {
                    var JO = (JValue)JT;

                    NextResultUrl = Result["@odata.nextLink"];
                }

                if (Result.TryGetValue("value", out JToken JT2))
                {
                    foreach (dynamic Entry in Result.value)
                        try
                        {
                            var val = new cMsGraphResultBase(Entry);
                            this.Add(val);
                        }
                        catch (Exception E)
                        {
                            cLogManager.DefaultLogger.LogException(E);
                        }
                }
            }
            catch (Exception E)
            {
                cLogManager.DefaultLogger.LogException(E);
            }
        }

    }

    public class cMsGraphResultUser : cMsGraphResultBase
    {
        public string UserPrincipalName { get; private set; } = null;
        public string EMail { get; private set; } = null;
        public string GivenName { get; private set; } = null;
        public string SurName { get; private set; } = null;
        public string JobTitle { get; private set; } = null;
        public string OfficeLocation { get; private set; } = null;
        public string PreferredLanguage { get; private set; } = null;

        public cMsGraphResultUser(cMsGraphResultBase Result) : base(Result)
        {
            UserPrincipalName = GetStringFromDynamic(Result.Result, "userPrincipalName");
            EMail = GetStringFromDynamic(Result.Result, "mail");
            GivenName = GetStringFromDynamic(Result.Result, "givenName");
            SurName = GetStringFromDynamic(Result.Result, "surname");
            JobTitle = GetStringFromDynamic(Result.Result, "jobTitle");
            OfficeLocation = GetStringFromDynamic(Result.Result, "officeLocation");
            PreferredLanguage = GetStringFromDynamic(Result.Result, "preferredLanguage");
        }

    }

    public class cMsGraphCollectionUsers : Dictionary<string, cMsGraphResultUser>
    {
        public cMsGraphCollectionUsers() { }
        public cMsGraphCollectionUsers(int n) : base(n) { }

        public void Add(cMsGraphResultUser User)
        {
            if (!this.ContainsKey(User.ID))
                this.Add(User.ID, User);
        }
    }

    public class cMsGraphResultApplication : cMsGraphResultBase
    {
        public string AppID { get; private set; } = null;
        public string PublisherDomain { get; private set; } = null;

        public cMsGraphResultApplication(cMsGraphResultBase Result) : base(Result)
        {
            try { AppID = Result.Result.appId; } catch { };
            try { PublisherDomain = Result.Result.publisherDomain; } catch { };
        }

    }
}
