using Edge.Core.Database;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Linq;
using System.Reflection;
using RestSharp;
using Newtonsoft.Json;
using System.Threading.Tasks;
using System.Xml;
using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Xml.Serialization;

namespace Edge.Core.Configuration
{
    public class Configurator
    {
        public event Action<string> OnReadConfigInfo;
        private static Configurator instance = new Configurator();
        private Timer timelyCheckRemoteConfig;
        //private List<Action<object>> registeredChangeCallbacks = new List<Action<object>>();

        /// <summary>
        /// fired once the configuration file(settings.xml) changed.
        /// </summary>
        public event EventHandler OnConfigFileChanged;
        private IConfigurationRoot configurationRoot = null;

        public ILogger logger = NullLogger.Instance;

        private IConfigurationBuilder builder;
        public async Task<bool> LoadAsync()
        {
            try
            {
                //var ass = Assembly.GetExecutingAssembly();
                this.builder = new ConfigurationBuilder()
                   .SetBasePath(Directory.GetCurrentDirectory())
                   .AddJsonFile("noExistsNow.json", optional: true, reloadOnChange: true)
                   .AddXmlFile("settings.xml", optional: false, reloadOnChange: true);
                this.configurationRoot = this.builder.Build();
            }
            catch (FileNotFoundException fnf)
            {
                Console.WriteLine(fnf);
                logger.LogError("Could not find local settings.xml");
                this.OnReadConfigInfo?.Invoke("Could not find local settings.xml");
                return false;
            }

            this.MetaConfiguration = this.RebindMetaFromFile();
            string enableAndRetrieveConfigFromConfigServiceUrl =
                this.MetaConfiguration.Parameter?.FirstOrDefault(p => p.Name
                    == "enableAndRetrieveConfigFromConfigServiceUrl")?.Value;
            if (string.IsNullOrEmpty(enableAndRetrieveConfigFromConfigServiceUrl))
            {
                /* read config from local file */
                //logger.LogInformation("Use Local Config.");
                //this.OnReadConfigInfo?.Invoke("Use Local Config...");
                return this.LoadFromLocalFile();
            }
            else
            {
                /* read config from remote */
                logger.LogInformation("Use Remote Config against ConfigService url: " + enableAndRetrieveConfigFromConfigServiceUrl ?? "");
                this.OnReadConfigInfo?.Invoke("Use Remote Config: " + enableAndRetrieveConfigFromConfigServiceUrl ?? "");
                var loadResult = await this.LoadFromRemote(enableAndRetrieveConfigFromConfigServiceUrl);
                if (!loadResult) return false;

                // enable auto polling.
                if (this.timelyCheckRemoteConfig != null) this.timelyCheckRemoteConfig.Dispose();
                this.timelyCheckRemoteConfig = new Timer(async (o) =>
                {
                    await this.LoadFromRemote(enableAndRetrieveConfigFromConfigServiceUrl);
                },
                null, 20000, 20000);
                return true;
            }
        }

        private bool LoadFromLocalFile()
        {
            this.configurationRoot.GetReloadToken().RegisterChangeCallback((o) =>
            {
                this.MetaConfiguration = this.RebindMetaFromFile();
                this.NozzleExtraInfoConfiguration = this.RebindNozzleProductFromFile();
                this.DeviceProcessorConfiguration = this.RebindDeviceProcessorFromFile();
                this.OnConfigFileChanged?.Invoke(this, null);
                this.configurationRoot = builder.Build();
                this.LoadFromLocalFile();
                //this.registeredChangeCallbacks.ForEach(c =>
                //    // need improve, here just send a notification of the change
                //    // but no specific processor.
                //    c.Invoke(null));
            }, null);
            this.MetaConfiguration = this.RebindMetaFromFile();
            this.NozzleExtraInfoConfiguration = this.RebindNozzleProductFromFile();
            this.DeviceProcessorConfiguration = this.RebindDeviceProcessorFromFile();
            return true;
        }


        /// <summary>
        /// Read NozzleProduct and Processors configs from remote.
        /// </summary>
        /// <param name="enableAndRetrieveConfigFromConfigServiceUrl"></param>
        /// <returns></returns>
        private async Task<bool> LoadFromRemote(string enableAndRetrieveConfigFromConfigServiceUrl)
        {
            try
            {
                var newReadNozzleProductConfig = await this.ReadRemoteNozzleProductConfig(
                        enableAndRetrieveConfigFromConfigServiceUrl,
                        "LiteFccCoreSettings_NozzleProduct");
                if (newReadNozzleProductConfig != null)
                    if (!newReadNozzleProductConfig.Equals(this.NozzleExtraInfoConfiguration))
                    {
                        logger.LogInformation("Remote Nozzle product config changed to:" + Environment.NewLine
                            + this.ConvertObjectToString(newReadNozzleProductConfig) ?? ""
                            + " will fire ChangeCallbacks...");
                        this.OnReadConfigInfo?.Invoke("Remote Nozzle Product Config changed(read mapping count: " + newReadNozzleProductConfig.Mapping.Count + ")...");
                        this.NozzleExtraInfoConfiguration = newReadNozzleProductConfig;
                        this.OnConfigFileChanged?.Invoke(this, null);
                        //this.registeredChangeCallbacks.ForEach(c => c.Invoke(this.NozzleProductConfiguration));
                    }

                var newReadProcessorConfig = await this.ReadRemoteProcessorsConfig(
                     enableAndRetrieveConfigFromConfigServiceUrl,
                     "LiteFccCoreSettings_Processor");
                if (newReadProcessorConfig != null)
                    if (!newReadProcessorConfig.Equals(this.DeviceProcessorConfiguration))
                    {
                        logger.LogInformation("Remote processors config changed to:" + Environment.NewLine
                            + this.ConvertObjectToString(newReadProcessorConfig) ?? ""
                            + " will fire ChangeCallbacks...");
                        this.OnReadConfigInfo?.Invoke("Remote Processor Config changed(read processor count: " + newReadProcessorConfig.Processor.Count + ")...");
                        this.DeviceProcessorConfiguration = newReadProcessorConfig;
                        this.OnConfigFileChanged?.Invoke(this, null);
                        //this.registeredChangeCallbacks.ForEach(c => c.Invoke(this.DeviceProcessorConfiguration));
                    }

                return true;
            }
            catch (Exception exx)
            {
                logger.LogError("Configurator LoadFromRemote exceptioned: " + exx);
                this.OnReadConfigInfo?.Invoke("Read Remote Config error...");
                return false;
            }
        }

        private string ConvertObjectToString<T>(T config) where T : class
        {
            var loggingSerializer = new XmlSerializer(typeof(T));
            MemoryStream ms = new MemoryStream();
            loggingSerializer.Serialize(ms, config);
            ms.Position = 0;
            return new StreamReader(ms).ReadToEnd();
        }

        /// <summary>
        /// </summary>
        private DeviceProcessorConfiguration RebindDeviceProcessorFromFile()
        {
            var deviceProcessorConfiguration = new DeviceProcessorConfiguration();
            this.configurationRoot.GetSection("deviceProcessor").Bind(deviceProcessorConfiguration);
            return deviceProcessorConfiguration;
        }

        /// <summary>
        /// </summary>
        private NozzleExtraInfoConfiguration RebindNozzleProductFromFile()
        {
            var nozzleProductConfigurationFull = new NozzleExtraInfoConfiguration();
            this.configurationRoot.GetSection("nozzleProduct").Bind(nozzleProductConfigurationFull);
            return nozzleProductConfigurationFull;
        }

        /// <summary>
        /// </summary>
        private MetaConfiguration RebindMetaFromFile()
        {
            var metaConfigurationFull = new MetaConfiguration();
            configurationRoot.GetSection("meta").Bind(metaConfigurationFull);
            return metaConfigurationFull;
        }

        /// <summary>
        /// It's the necessary config, must be loaded from remote, otherwise exception.
        /// </summary>
        /// <param name="configServiceBaseUrl"></param>
        /// <param name="settingsGroupName"></param>
        /// <returns></returns>
        private async Task<DeviceProcessorConfiguration> ReadRemoteProcessorsConfig(
            string configServiceBaseUrl,
            string settingsGroupName)
        {
            RestClient restClient = new RestClient(configServiceBaseUrl);
            RestRequest configDescRequest = new RestRequest("api/Config/description", Method.GET);
            configDescRequest.AddHeader("Content-Type", "application/json");
            configDescRequest.AddQueryParameter("configGroupName", settingsGroupName);
            configDescRequest.AddQueryParameter("configOwnerIds", this.MetaConfiguration.Parameter.First(p => p.Name == "serialNumber").Value);
            configDescRequest.RequestFormat = DataFormat.Json;

            var configDescResponse = await restClient.ExecuteTaskAsync(configDescRequest);
            if (configDescResponse.StatusCode != System.Net.HttpStatusCode.OK)
            {
                logger.LogInformation("Remote config service(api/Config/description) returned with Status code: " + configDescResponse.StatusCode);
                throw new ArgumentException("Remote config service(api/Config/description) returned with Status code: " + configDescResponse.StatusCode);
            }

            var configDescriptions = JsonConvert.DeserializeObject<ConfigDescription[]>(configDescResponse.Content);
            var deviceProcessorConfiguration = new DeviceProcessorConfiguration() { Processor = new List<Processor>() };
            foreach (var configDesc in configDescriptions.Where(d => d.Enabled))
            {
                var configRequest = new RestRequest("api/Config/", Method.GET);
                configRequest.AddHeader("Content-Type", "application/json");

                configRequest.AddHeader("configName", configDesc.Name);
                configRequest.AddHeader("configOwnerId", this.MetaConfiguration.Parameter.First(p => p.Name == "clientId").Value);
                configRequest.RequestFormat = DataFormat.Json;

                var configResponse = await restClient.ExecuteTaskAsync(configRequest);
                if (configResponse.StatusCode == System.Net.HttpStatusCode.OK)
                {
                    var config =
                        JsonConvert.DeserializeObject<Edge.Core.Configuration.Configuration[]>(
                            configResponse.Content).First();
                    XmlDocument xmlDocument = new XmlDocument();
                    xmlDocument.LoadXml(config.Value);
                    var processorNode = xmlDocument.GetElementsByTagName("processor")[0];
                    deviceProcessorConfiguration.Processor.Add(new Edge.Core.Configuration.Processor()
                    {
                        Name = processorNode.Attributes["name"].Value,
                        //SerialNumber = processorNode.Attributes["serialNumber"]?.Value,
                        DeviceProcessorType = processorNode.Attributes["deviceProcessorType"].Value,
                        Complex = bool.Parse(processorNode.Attributes["complex"]?.Value ?? "false"),
                        ConstructorArg = processorNode.Attributes["constructorArg"]?.Value,
                        Parameter = processorNode.SelectNodes("parameter").Cast<XmlNode>()
                            .Select(n => new ProcessorParameter()
                            {
                                Complex = bool.Parse(n.Attributes["complex"]?.Value ?? "false"),
                                Name = n.Attributes["name"].Value,
                                Value = n.Attributes["value"]?.Value,
                                ConstructorArg = n.Attributes["constructorArg"]?.Value,
                                Description = n.Attributes["description"]?.Value
                            }).ToList()
                    });
                }
                else
                {
                    logger.LogInformation("Remote config service(api/Config/) returned with Status code: " + configResponse.StatusCode
                        + " for Config with Name: " + configDesc.Name ?? "");
                }
            }

            return deviceProcessorConfiguration;
        }

        /// <summary>
        /// It's the unnecessary config, HttpNotFound is acceptable, other status code will raise exception.
        /// </summary>
        /// <param name="configServiceBaseUrl"></param>
        /// <param name="configName"></param>
        /// <returns></returns>
        private async Task<NozzleExtraInfoConfiguration> ReadRemoteNozzleProductConfig(
            string configServiceBaseUrl,
            string configName)
        {
            var nozzleProductConfiguration = new NozzleExtraInfoConfiguration() { Mapping = new List<NozzleExtraInfo>() };
            RestClient restClient = new RestClient(configServiceBaseUrl);
            var configRequest = new RestRequest("api/Config/", Method.GET);
            configRequest.AddHeader("Content-Type", "application/json");

            configRequest.AddHeader("configName", configName);
            configRequest.AddHeader("configOwnerId", this.MetaConfiguration.Parameter.First(p => p.Name == "clientId").Value);
            configRequest.RequestFormat = DataFormat.Json;

            var configResponse = await restClient.ExecuteTaskAsync(configRequest);
            if (configResponse.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                logger.LogInformation("Remote config service(api/Config/) retunred with Status Code: NotFound for config with name: " + configName);
                return null;
            }
            else if (configResponse.StatusCode != System.Net.HttpStatusCode.OK)
            {
                logger.LogInformation("Remote config service(api/Config/) returned with Status code: " + configResponse.StatusCode + " for config with name: " + configName);
                throw new ArgumentException("Remote config service(api/Config/) returned with Status code: " + configResponse.StatusCode);
            }

            var config =
                JsonConvert.DeserializeObject<Edge.Core.Configuration.Configuration[]>(
                    configResponse.Content).First();
            if (!config.Enabled) return nozzleProductConfiguration;
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.LoadXml(config.Value);
            var mappingNodes = xmlDocument.GetElementsByTagName("mapping");
            foreach (var node in mappingNodes.Cast<XmlNode>())
            {
                nozzleProductConfiguration.Mapping.Add(new NozzleExtraInfo()
                {
                    Description = node.Attributes["description"]?.Value,
                    PumpId = int.Parse(node.Attributes["pumpId"]?.Value ?? "-1"),
                    NozzleLogicalId = int.Parse(node.Attributes["nozzleLogicalId"]?.Value ?? "-1"),
                    ProductBarcode = int.Parse(node.Attributes["productBarcode"]?.Value ?? "-1"),
                    SiteLevelNozzleId = int.Parse(node.Attributes["siteLevelNozzleId"]?.Value ?? "-1"),
                });
            }

            return nozzleProductConfiguration;
        }

        public static Configurator Default => instance;

        ///// <summary>
        ///// attach an Action for get notification once config file changed.
        ///// </summary>
        ///// <param name="callback">will be called once config file changed</param>
        //public void RegisterChangeCallback(Action<object> callback)
        //{
        //    this.registeredChangeCallbacks.Add(callback);
        //}

        //public FdcServerConfiguration FdcServerConfiguration { get; set; }
        public DeviceProcessorConfiguration DeviceProcessorConfiguration { get; set; }
        public NozzleExtraInfoConfiguration NozzleExtraInfoConfiguration { get; set; }

        public MetaConfiguration MetaConfiguration { get; set; }
    }
}