using Edge.Core.Configuration;
using Edge.Core.Processor;
using Edge.Core.UniversalApi.Auditing;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace Edge.Core.UniversalApi
{
    public class UniversalApiInvoker
    {
        private static JsonSerializerOptions jsonSerializerOptions;
        private IEnumerable<IAuditingStore> auditingStores;
        private IServiceProvider services;
        private bool config_DisableApiAuditing = false;
        private bool config_EnableApiInputAndOutputAuditing = false;
        static UniversalApiInvoker()
        {
            jsonSerializerOptions = new JsonSerializerOptions()
            {
                WriteIndented = true,
                PropertyNameCaseInsensitive = true,
            };
            jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
        }

        internal UniversalApiInvoker(IServiceProvider services)
        {
            this.services = services;
            var configurator = this.services.GetService<Configurator>();
            this.config_DisableApiAuditing = bool.Parse(configurator.MetaConfiguration.Parameter
                ?.FirstOrDefault(p => p.Name.Equals("disableUniversalApiAuditing", StringComparison.OrdinalIgnoreCase))?.Value ?? "false");
            this.config_EnableApiInputAndOutputAuditing = bool.Parse(configurator.MetaConfiguration.Parameter
                ?.FirstOrDefault(p => p.Name.Equals("enableUniversalApiInputAndOutputAuditing", StringComparison.OrdinalIgnoreCase))?.Value ?? "false");

            this.auditingStores = services.GetRequiredService<IEnumerable<IAuditingStore>>();

            configurator.OnConfigFileChanged += (_, __) =>
            {
                this.config_DisableApiAuditing = bool.Parse(configurator.MetaConfiguration.Parameter
                    ?.FirstOrDefault(p => p.Name.Equals("disableUniversalApiAuditing", StringComparison.OrdinalIgnoreCase))?.Value ?? "false");
                this.config_EnableApiInputAndOutputAuditing = bool.Parse(configurator.MetaConfiguration.Parameter
                    ?.FirstOrDefault(p => p.Name.Equals("enableUniversalApiInputAndOutputAuditing", StringComparison.OrdinalIgnoreCase))?.Value ?? "false");
            };
        }

        public async Task<object> InvokeUniversalApiServiceAsync(IProcessor processor,
              string apiServiceName, JsonElement[] jsonFormatParameters,
              RequestContext requestContext)
        {
            var pDescriptor = processor.ProcessorDescriptor();

            #region

            AuditLogInfo auditLogInfo = null;
            if (!this.config_DisableApiAuditing)
            {
                auditLogInfo = new AuditLogInfo()
                {
                    ClientIdentity = requestContext.ClientIdentity,
                    DeviceHandlerOrAppName = pDescriptor.DeviceHandlerOrApp.ToString(),
                    ExecutionTime = DateTime.Now,
                };
                auditLogInfo.Actions.Add(
                    new AuditLogActionInfo()
                    {
                        ApiName = "service : " + apiServiceName,
                        InputParameters =
                            this.config_EnableApiInputAndOutputAuditing ?
                                jsonFormatParameters?.Select(jp => jp.GetRawText())?.Aggregate("", (acc, n) => acc + ", " + n) ?? ""
                                : null,
                    });
                auditLogInfo.Prepare();
                //logging early for knowing the calling is started
                this.auditingStores.Select(s => s.SaveAsync(auditLogInfo)).ToArray();
            }

            #endregion

            try
            {
                var targetApiInfo = pDescriptor.UniversalApiInfos
                           .FirstOrDefault(api => api.ServiceApiInfo != null
                               && api.ServiceApiInfo.Name == apiServiceName
                               && api.ServiceApiInfo.GetParameters().Length == (jsonFormatParameters?.Length ?? 0));
                if (targetApiInfo == null) throw new ArgumentException("Could not find Universal Api in the processor has Service(Method) Parameter count == " + (jsonFormatParameters?.Length ?? 0));
                var apiDefinedParameters = targetApiInfo.ServiceApiInfo.GetParameters();
                List<object> ps = new List<object>();
                for (int i = 0; i < apiDefinedParameters.Length; i++)
                {
                    try
                    {
                        var para = JsonSerializer.Deserialize(jsonFormatParameters[i].GetRawText(), apiDefinedParameters[i].ParameterType, jsonSerializerOptions);
                        ps.Add(para);
                    }
                    catch (Exception exxx)
                    {
                        throw new ArgumentException("Could not Parse input parameter[" + i + "] to type: "
                            + apiDefinedParameters[i].ParameterType.Name + " for target Universal Api: " + apiServiceName
                            + Environment.NewLine + exxx);
                    }
                }

                var executeResult = await InvokeUniversalApiServiceAsync(processor, targetApiInfo.ServiceApiInfo, ps.ToArray(), requestContext);

                #region 

                if (!this.config_DisableApiAuditing)
                {
                    /* logging again as the service call has been done*/
                    if (this.config_EnableApiInputAndOutputAuditing)
                        auditLogInfo.CommitWithExeResult(executeResult);
                    else
                        auditLogInfo.CommitWithExeResult(null);
                }

                #endregion

                return executeResult;
            }
            catch (Exception exxx)
            {

                #region 

                if (!this.config_DisableApiAuditing)
                { auditLogInfo.CommitWithExceptions(new Exception[] { exxx }); }

                #endregion

                throw;
            }
            finally
            {

                #region  

                if (!this.config_DisableApiAuditing)
                    Task.WaitAll(this.auditingStores.Select(s => s.SaveAsync(auditLogInfo)).ToArray());

                #endregion

            }
        }

        public bool InvokeUniversalApiPropertySet(IProcessor processor,
            string apiPropertyName, JsonElement? propertySetValue,
            RequestContext requestContext)
        {
            var pDescriptor = processor.ProcessorDescriptor();

            #region

            AuditLogInfo auditLogInfo = null;
            if (!this.config_DisableApiAuditing)
            {
                auditLogInfo = new AuditLogInfo()
                {
                    ClientIdentity = requestContext.ClientIdentity,
                    DeviceHandlerOrAppName = pDescriptor.DeviceHandlerOrApp.ToString(),
                    ExecutionTime = DateTime.Now,
                };
                auditLogInfo.Actions.Add(
                    new AuditLogActionInfo()
                    {
                        ApiName = "property : " + apiPropertyName,
                        InputParameters =
                            this.config_EnableApiInputAndOutputAuditing ?
                                propertySetValue?.GetRawText() ?? ""
                                : null,
                    });
                //logging early for knowing the calling is started
                this.auditingStores.Select(s => s.SaveAsync(auditLogInfo)).ToArray();
            }

            #endregion

            try
            {
                var targetApiInfo = pDescriptor?.UniversalApiInfos
                            .FirstOrDefault(api => api.PropertyApiInfo != null
                                && api.PropertyApiInfo.Name == apiPropertyName);
                if (targetApiInfo == null) throw new ArgumentException("Could not find Universal Api in the processor has Property with name: " + apiPropertyName);
                object setValue = null;
                if (propertySetValue == null)
                {
                }
                else
                    setValue = JsonSerializer.Deserialize(propertySetValue.Value.GetRawText(),
                                       targetApiInfo.PropertyApiInfo.PropertyType, jsonSerializerOptions);
                var executeResult = InvokeUniversalApiPropertySet(processor, targetApiInfo.PropertyApiInfo, setValue, requestContext);

                #region 

                if (!this.config_DisableApiAuditing)
                {
                    /* logging again as the call has been done*/
                    if (this.config_EnableApiInputAndOutputAuditing)
                        auditLogInfo.CommitWithExeResult(executeResult);
                    else
                        auditLogInfo.CommitWithExeResult(null);
                }

                #endregion

                return executeResult;
            }
            catch (Exception exxx)
            {

                #region 

                if (!this.config_DisableApiAuditing)
                {
                    auditLogInfo.CommitWithExceptions(new Exception[] { exxx
                    });
                }

                #endregion

                throw;
            }
            finally
            {

                #region  

                if (!this.config_DisableApiAuditing)
                    Task.WaitAll(this.auditingStores.Select(s => s.SaveAsync(auditLogInfo)).ToArray());

                #endregion

            }
        }

        /// <summary>
        /// Invoke the Get method for the property.
        /// </summary>
        /// <param name="processor"></param>
        /// <param name="apiPropertyName"></param>
        /// <returns>the value of the Property</returns>
        public object InvokeUniversalApiPropertyGet(IProcessor processor,
            string apiPropertyName,
            RequestContext requestContext)
        {
            var pDescriptor = processor.ProcessorDescriptor();

            #region

            AuditLogInfo auditLogInfo = null;
            if (!this.config_DisableApiAuditing)
            {
                auditLogInfo = new AuditLogInfo()
                {
                    ClientIdentity = requestContext.ClientIdentity,
                    DeviceHandlerOrAppName = pDescriptor.DeviceHandlerOrApp.ToString(),
                    ExecutionTime = DateTime.Now,
                };
                auditLogInfo.Actions.Add(
                    new AuditLogActionInfo()
                    {
                        ApiName = "property : " + apiPropertyName,
                        InputParameters = null,
                    });
                auditLogInfo.Prepare();
                //logging early for knowing the calling is started
                this.auditingStores.Select(s => s.SaveAsync(auditLogInfo)).ToArray();
            }

            #endregion

            try
            {
                var targetPropertyInfo = pDescriptor.UniversalApiInfos.Where(apiInfo =>
                        (apiInfo.PropertyApiInfo?.Name ?? "") == apiPropertyName)
                        .FirstOrDefault()?.PropertyApiInfo;
                if (targetPropertyInfo == null)
                    throw new EntryPointNotFoundException("Could not find Universal Api Property with name: " + (targetPropertyInfo.Name ?? ""));
                if (!targetPropertyInfo.CanRead
                    || targetPropertyInfo.GetMethod == null
                    || (targetPropertyInfo.GetMethod?.IsPrivate ?? false))
                {
                    throw new EntryPointNotFoundException("Universal Api Property with name: " + (targetPropertyInfo.Name ?? "") + " does not allow Get");
                }

                try
                {
                    /* indicates 'get' operation*/
                    dynamic executeResult = null;
                    if (pDescriptor.ProcessorType == ProcessorType.Application)
                        executeResult = targetPropertyInfo.GetGetMethod().Invoke(processor, null);
                    else if (pDescriptor.ProcessorType == ProcessorType.DeviceProcessor)
                        executeResult = targetPropertyInfo.GetGetMethod().Invoke(pDescriptor.DeviceHandlerOrApp, null);

                    #region 

                    if (!this.config_DisableApiAuditing)
                    {
                        /* logging again as the call has been done*/
                        if (this.config_EnableApiInputAndOutputAuditing)
                            auditLogInfo.CommitWithExeResult(executeResult);
                        else
                            auditLogInfo.CommitWithExeResult(null);
                    }

                    #endregion

                    return executeResult;
                }
                catch (Exception exx)
                {
                    throw;// new InvalidOperationException("Invoke UniversalApi Property: " + apiPropertyName + " exceptioned, detail: " + exx.ToString());
                }
            }
            catch (Exception exxx)
            {

                #region 

                if (!this.config_DisableApiAuditing)
                {
                    auditLogInfo.CommitWithExceptions(new Exception[] { exxx });
                }

                #endregion

                throw;
            }
            finally
            {

                #region  

                if (!this.config_DisableApiAuditing)
                    Task.WaitAll(this.auditingStores.Select(s => s.SaveAsync(auditLogInfo)).ToArray());

                #endregion

            }
        }

        private async Task<object> InvokeUniversalApiServiceAsync(IProcessor processor,
              MethodInfo apiServiceInfo, object[] methodParameters,
              RequestContext requestContext)
        {
            var descriptor = processor.ProcessorDescriptor();
            var targetMethodInfo = descriptor.UniversalApiInfos.Where(apiInfo =>
                    (apiInfo.ServiceApiInfo?.Name ?? "") == apiServiceInfo.Name)
                    .FirstOrDefault()?.ServiceApiInfo;
            if (targetMethodInfo == null)
                throw new EntryPointNotFoundException("Could not find Universal Api Method with name: " + (apiServiceInfo.Name ?? ""));
            try
            {
                dynamic result = null;
                if (descriptor.ProcessorType == ProcessorType.Application)
                    result = await (dynamic)targetMethodInfo.Invoke(processor, methodParameters);
                else if (descriptor.ProcessorType == ProcessorType.DeviceProcessor)
                    result = await (dynamic)targetMethodInfo.Invoke(descriptor.DeviceHandlerOrApp, methodParameters);
                return result;
            }
            catch (Exception exx)
            {
                throw;// new InvalidOperationException("Invoke UniversalApi Service: " + apiServiceInfo.Name + " exceptioned, detail: " + exx.ToString());
            }
        }

        /// <summary>
        /// Invoke the Set method for the property, if set value successfully, will return true, otherwise false.
        /// </summary>
        /// <param name="processor"></param>
        /// <param name="apiPropertyInfo"></param>
        /// <param name="propertySetValue">the value will be set to Property</param>
        /// <returns>if set value successfully, will return true, otherwise false.</returns>
        private bool InvokeUniversalApiPropertySet(IProcessor processor,
            PropertyInfo apiPropertyInfo, object propertySetValue,
            RequestContext requestContext)
        {
            var descriptor = processor.ProcessorDescriptor();
            var targetPropertyInfo = descriptor.UniversalApiInfos.Where(apiInfo =>
                    (apiInfo.PropertyApiInfo?.Name ?? "") == apiPropertyInfo.Name)
                    .FirstOrDefault()?.PropertyApiInfo;
            if (targetPropertyInfo == null)
                throw new EntryPointNotFoundException("Could not find Universal Api Property with name: " + (targetPropertyInfo.Name ?? ""));
            if (!targetPropertyInfo.CanWrite
                || targetPropertyInfo.SetMethod == null
                || (targetPropertyInfo.SetMethod?.IsPrivate ?? false))
            {
                throw new EntryPointNotFoundException("Universal Api Property with name: " + (targetPropertyInfo.Name ?? "") + " does not allow Set");
            }

            if (propertySetValue == null
                && !targetPropertyInfo.PropertyType.IsClass
                && !(targetPropertyInfo.PropertyType.IsGenericType && targetPropertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)))
                throw new ArgumentException("Universal Api Property with name: " + (targetPropertyInfo.Name ?? "")
                    + " does not compatible has its Set type(" + targetPropertyInfo.PropertyType + ") to Null");
            if (propertySetValue != null && !targetPropertyInfo.PropertyType.IsAssignableFrom(propertySetValue?.GetType()))
                throw new ArgumentException("Universal Api Property with name: " + (targetPropertyInfo.Name ?? "")
                    + " does not compatible has its Set type(" + targetPropertyInfo.PropertyType + ") to " + (propertySetValue?.GetType().Name ?? "null"));
            try
            {
                /* indicates 'set' operation*/
                if (descriptor.ProcessorType == ProcessorType.Application)
                    targetPropertyInfo.SetValue(processor, propertySetValue);
                else if (descriptor.ProcessorType == ProcessorType.DeviceProcessor)
                    targetPropertyInfo.SetValue(descriptor.DeviceHandlerOrApp, propertySetValue);
                return true;
            }
            catch (Exception exx)
            {
                throw;// new InvalidOperationException("Invoke UniversalApi Property: " + apiPropertyInfo.Name + " exceptioned, detail: " + exx.ToString());
            }
        }


    }
}
