using AutoMapper;
using Edge.Core.Configuration;
using Edge.Core.Database;
using Edge.Core.Database.Models;
using Edge.Core.Processor;
using Edge.Core.Processor.Dispatcher.Attributes;
using Edge.Core.UniversalApi.Auditing;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Edge.Core.UniversalApi
{
    public class UniversalApiHub
    {
        private IEnumerable<IProcessor> processors;
        private IEnumerable<IAuditingStore> auditingStores;
        private IServiceProvider services;
        private bool config_DisableApiAuditing = false;
        private bool config_EnableApiInputAndOutputAuditing = false;

        public event EventHandler<UniversalGenericAlarmEventFiredEventArg> OnUniversalGenericAlarmEventFired;
        public async Task InitAsync(IEnumerable<IProcessor> processors)
        {
            this.processors = processors;
            foreach (var p in this.CommunicationProviders)
            {
                await p.SetupAsync(this.processors);
            }
        }

        public IEnumerable<ICommunicationProvider> CommunicationProviders { get; }

        public UniversalApiHub(IEnumerable<ICommunicationProvider> communicationProviders, IServiceProvider services)
        {
            this.CommunicationProviders = communicationProviders;

            this.services = services;
            var configurator = 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");
            };
        }

        /// <summary>
        /// Fire an event to all underlying CommunicationProviders.
        ///     Make sure class Attribute of `Event UniversalApi` declared in App or DeviceHandler.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="eventName"></param>
        /// <param name="eventData"></param>
        /// <returns></returns>
        public async Task FireEvent(IProcessor source, string eventName, object eventData)
        {

            #region

            AuditLogInfo auditLogInfo = null;
            if (!this.config_DisableApiAuditing)
            {
                auditLogInfo = new AuditLogInfo()
                {
                    ClientIdentity = "",
                    DeviceHandlerOrAppName = source.SelectHandlerOrAppThenCast<object>().GetType().FullName,
                    ExecutionTime = DateTime.Now,
                };
                auditLogInfo.Actions.Add(new AuditLogActionInfo()
                {
                    ApiName = "event : " + eventName,
                    InputParameters =
                        this.config_EnableApiInputAndOutputAuditing ?
                            JsonSerializer.Serialize(eventData) : null,
                });
                auditLogInfo.Prepare();
            }

            #endregion

            try
            {
                if (this.CommunicationProviders == null) return;
                foreach (var provider in this.CommunicationProviders)
                {
                    try
                    {
                        var executeResult = await provider.RouteEventAsync(source,
                             new EventDescriptor() { Name = eventName, Data = eventData });
                    }
                    catch (Exception exxx)
                    {

                        #region 

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

                        #endregion

                    }
                }
            }
            finally
            {
                if (!this.config_DisableApiAuditing)
                {
                    auditLogInfo.CommitWithExeResult(true);
                    Task.WaitAll(this.auditingStores.Select(s => s.SaveAsync(auditLogInfo)).ToArray());
                }
            }
        }

        /// <summary>
        /// Fire generic event without persist it(like fire and forget).
        ///     the UI component would receive it at the moment and only for onetime, to showing a message to user.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="genericAlarm"></param>
        /// <returns></returns>
        public Task FireGenericAlarm(IProcessor source, GenericAlarm genericAlarm)
        {
            return this.FireGenericAlarm(source, genericAlarm, false, null);
        }

        /// <summary>
        /// Fire generic event and persist it.
        ///     the UI component would receive it at the moment and can retrieve it anytime later, to showing a message to user.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="genericAlarm"></param>
        /// <param name="hiddenDataSelector">any custom data to persist together with this alarm, used for searching.</param>
        /// <returns>the persist record id, use it for retrieve back the record.</returns>
        public Task<int?> FirePersistGenericAlarm(IProcessor source, GenericAlarm genericAlarm, Func<GenericAlarm, string> hiddenDataSelector)
        {
            return this.FireGenericAlarm(source, genericAlarm, true, hiddenDataSelector(genericAlarm));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="source"></param>
        /// <param name="genericAlarm"></param>
        /// <param name="persist">if true, the alarm will be persist</param>
        /// <param name="hiddenData"></param>
        /// <param name="muteFiring">not firing the event, used for scenario that refresh the persist alarm without re-firing it.</param>
        /// <returns></returns>
        private async Task<int?> FireGenericAlarm(IProcessor source, GenericAlarm genericAlarm, bool persist, string hiddenData, bool muteFiring = false)
        {
            int? persistAlarmDbRecordKey = null;
            if (!persist)
            {
            }
            else
            {
                var mapper = this.services.GetRequiredService<IMapper>();
                using (var scope = this.services.CreateScope())
                {

                }
            }

            if (!muteFiring)
                this.OnUniversalGenericAlarmEventFired?.Invoke(this, new UniversalGenericAlarmEventFiredEventArg(source, genericAlarm, persistAlarmDbRecordKey));
            //await this.FireEvent(source, GenericAlarm.UniversalApiEventName, genericAlarm);
            return persistAlarmDbRecordKey;
        }

        /// <summary>
        /// Mark several Opening state alarms to Closed state, and fill with close reason for each.
        /// </summary>
        /// <param name="alarmIdAndCloseReasons">list of opening state alarm id and close reason.</param>
        /// <returns></returns>
        public async Task ClosePersistGenericAlarms(IEnumerable<Tuple<int, string>> alarmIdAndCloseReasons)
        {
            using (var scope = this.services.CreateScope())
            {

                //await dbContext.SaveChangesAsync();
            }
        }

        public async Task AckPersistGenericAlarms(IEnumerable<Tuple<int, string>> alarmIdAndAckReasons, bool isForUnAck = false)
        {
            using (var scope = this.services.CreateScope())
            {
             
               // await dbContext.SaveChangesAsync();
            }
        }

        /// <summary>
        /// Mark all Opening state alarms to Closed state, and fill with the same close reason, for a specific processor.
        /// </summary>
        /// <param name="source">all alarms for this processor will be closed.</param>
        /// <param name="closeReason">all target alarms will set with this close reason</param>
        /// <returns></returns>
        public Task<int> ClosePersistGenericAlarms(IProcessor source, string closeReason)
        {
            return this.ClosePersistGenericAlarms(source, null, closeReason);
        }

        /// <summary>
        /// Mark all Opening state alarms to Closed state, and fill with the same close reason.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="hiddenDataHint">the searching criteria, the hidden data fields contains this value will be matched.</param>
        /// <param name="closeReason">all target alarms will set with this close reason</param>
        /// <returns></returns>
        public async Task<int> ClosePersistGenericAlarms(IProcessor source, string hiddenDataHint, string closeReason)
        {
            var originator = source.ProcessorDescriptor()?.DeviceHandlerOrApp?.GetType()
                     ?.GetCustomAttributes<MetaPartsDescriptor>()?.FirstOrDefault()?.DisplayName;
            if (string.IsNullOrEmpty(originator))
                originator = source.ProcessorDescriptor()?.DeviceHandlerOrApp?.GetType().FullName;
            using (var scope = this.services.CreateScope())
            {
              

                //var effectRowCount = await dbContext.SaveChangesAsync();
                return 1;
            }
        }

        /// <summary>
        /// Query the persist generic alarms by search conditions.
        /// </summary>
        /// <param name="source">only alarms from this processor are returned, leave null to ignore this constrait.</param>
        /// <param name="originators">null to ignore this constrait.</param>
        /// <param name="alarmCategories">null to ignore this constrait.</param>
        /// <param name="alarmSubCategories">null to ignore this constrait.</param>
        /// <param name="alarmHiddenData">null to ignore this constrait.</param>
        /// <param name="alarmOpeningDateTimeFrom">null to ignore this constrait.</param>
        /// <param name="alarmOpeningDateTimeTo">null to ignore this constrait.</param>
        /// <param name="severity">null to ignore this constrait.</param>
        /// <param name="includeAckedStateAlarm"></param>
        /// <param name="includeClosedStateAlarm"></param>
        /// <param name="pageIndex"></param>
        /// <param name="pageRowCount"></param>
        /// <returns></returns>
        public async Task<IEnumerable<GenericAlarmDbModel>> GetPersistGenericAlarms(
            IProcessor source,
            string[] originators,
            string[] alarmCategories,
            string[] alarmSubCategories,
            string alarmHiddenData,
            DateTime? alarmOpeningDateTimeFrom, DateTime? alarmOpeningDateTimeTo,
            GenericAlarmSeverity? severity,
            bool includeAckedStateAlarm,
            bool includeClosedStateAlarm,
            int pageIndex, int pageRowCount)
        {
            using (var scope = this.services.CreateScope())
            {
                return null;
                //return r.Select(d => new
                //{
                //    d.Id,
                //    d.Originator,
                //    d.Severity,
                //    d.Category,
                //    d.SubCategory,
                //    d.Title,
                //    d.Detail,
                //    d.ClosedTimestamp,
                //    d.ClosedReason,
                //    d.Action,
                //});
            }
        }

        /// <summary>
        /// Close the alarms by match its hidden data, and create a new persist generic alarm.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="closingAlarmsHiddenDataPredicate">predictor for hidden data match for test with all exist alarms, the matched ones will be all closed.</param>
        /// <param name="closeReason"></param>
        /// <param name="newAlarm"></param>
        /// <param name="newAlarmHiddenDataSelector">generator for hidden data that will be used for create a new alarm.</param>
        /// <param name="muteNewAlarmFiring">if true, then new alarm event will not be fired, used for scenario that create an alarm but not firing event to gain user attention.</param>
        /// <returns></returns>
        public async Task<int?> CloseAndFirePersistGenericAlarm(IProcessor source, GenericAlarm newAlarm,
            Func<GenericAlarm, string> closingAlarmsHiddenDataPredicate, string closeReason,
            Func<GenericAlarm, string> newAlarmHiddenDataSelector, bool muteNewAlarmFiring = false)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            bool lockTaken = false;
            try
            {
                this.spinLock_GuardTwoStageOperation.Enter(ref lockTaken);
                var alarmsForClosing = await GetPersistGenericAlarms(source, null, null, null, closingAlarmsHiddenDataPredicate(newAlarm), null, null, null, true, false, 0, 100);
                if (alarmsForClosing.Any())
                    await this.ClosePersistGenericAlarms(alarmsForClosing.Select(c => new Tuple<int, string>(c.Id, closeReason)));
                var r = await this.FireGenericAlarm(source, newAlarm, true, newAlarmHiddenDataSelector(newAlarm), muteNewAlarmFiring);
                return r;
            }
            finally
            {
                if (lockTaken) this.spinLock_GuardTwoStageOperation.Exit(false);
            }
        }

        private SpinLock spinLock_GuardTwoStageOperation = new SpinLock();
        /// <summary>
        /// Clear(remove from db) the alarms by match its hidden data, and create a new persist generic alarm.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="newAlarm"></param>
        /// <param name="clearingAlarmsHiddenDataPredicate">predictor for hidden data match for test with all exist alarms, the matched ones will be all cleared.</param>
        /// <param name="newAlarmHiddenDataSelector"></param>
        /// <param name="muteNewAlarmFiring">if true, then new alarm event will not be fired, used for scenario that create an alarm but not firing event to gain user attention.</param>
        /// <returns></returns>
        public async Task<int?> ClearAndFirePersistGenericAlarm(IProcessor source, GenericAlarm newAlarm,
            Func<GenericAlarm, string> clearingAlarmsHiddenDataPredicate,
            Func<GenericAlarm, string> newAlarmHiddenDataSelector, bool muteNewAlarmFiring = false)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            bool lockTaken = false;
            try
            {
                this.spinLock_GuardTwoStageOperation.Enter(ref lockTaken);
                using (var scope = this.services.CreateScope())
                {
              
                }

                var r = await this.FireGenericAlarm(source, newAlarm, true, newAlarmHiddenDataSelector(newAlarm), muteNewAlarmFiring);
                return r;
            }
            finally
            {
                if (lockTaken) this.spinLock_GuardTwoStageOperation.Exit(false);
            }
        }

        /// <summary>
        /// Try firing an alarm by only there's no matched opened alarm in db.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="newAlarm"></param>
        /// <param name="existsAlarmsHiddenDataPredicate">predictor for hidden data match for test with all exist alarms, any matched ones will be considered as EXISTS, and then will not firing anymore.</param>
        /// <param name="newAlarmHiddenDataSelector"></param>
        /// <returns></returns>
        public async Task<int?> FirePersistGenericAlarmIfNotExists(IProcessor source, GenericAlarm newAlarm,
            Func<GenericAlarm, string> existsAlarmsHiddenDataPredicate,
            Func<GenericAlarm, string> newAlarmHiddenDataSelector)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            bool lockTaken = false;
            try
            {
                this.spinLock_GuardTwoStageOperation.Enter(ref lockTaken);
                using (var scope = this.services.CreateScope())
                {
                 
                    return null;
                }
            }
            finally
            {
                if (lockTaken) this.spinLock_GuardTwoStageOperation.Exit(false);
            }
        }
    }

    public class UniversalGenericAlarmEventFiredEventArg : System.EventArgs
    {
        public IProcessor Originator { get; }
        public GenericAlarm GenericAlarm { get; }
        public int? PersistId { get; }
        public UniversalGenericAlarmEventFiredEventArg(IProcessor originator, GenericAlarm genericAlarm, int? persistId)
        {
            this.Originator = originator;
            this.GenericAlarm = genericAlarm;
            this.PersistId = persistId;
        }
    }
}