using Newtonsoft.Json;
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

using VRH.Common;
using VRH.Log4Pro.MultiLanguageManager;
using Vrh.XmlProcessing;
using System.Timers;
using Vrh.Logger;
using System.Data.SqlClient;
using System.Xml.Linq;
using Hangfire;
using Hangfire.Storage;
using System.Web.Hosting;
using System.Configuration;

namespace Vrh.iScheduler
{
    public class WAHostedMonitorHangfire
    {
        /// <summary>
        /// A Hangfire alrendszer inicializálását+indítását, vagy a leállítását végzi
        /// </summary>
        /// <param name="start">true:inicalizálás/indítás; false:leállítás</param>
        public static void Start()
        {
            var le = new DCLogEntry(LogLevel.Information, $"{typeof(WAHostedMonitorHangfire).FullName}.{System.Reflection.MethodBase.GetCurrentMethod().Name}");
            try
            {
                _StartHangfire(le);

                foreach (string cf in Config_iSchedulerConfigList)
                {
                    if (!ParseiSchedulerConfigString(cf, out string jobid, out string xmlcs, out int checkintervalinseconds)) { le.AddDataField("Adding iScheduler job", $"iScheduler job definition error, job skipped! definition:{cf}"); continue; };
                    le.AddDataField("Adding iScheduler job", cf);

                    IntervalCronExpression = $"*/{(int)(checkintervalinseconds / 60)} * * * *";
                    RecurringJob.AddOrUpdate(jobid, () => Monitor.ExaminationRunOnce(Process.GetCurrentProcess().Id, xmlcs), IntervalCronExpression);
                    //Hangfire.SqlServer.SqlServerStorageOptions.
                    //IntervalCronExpression = Cron.MinuteInterval((int)(m_xmlp.CheckIntervalInSeconds / 60));//"*/1 * * * *"
                    //RecurringJob.RemoveIfExists("ischedulermonitorcycle");
                    //var HanfireJobId = BackgroundJob.Enqueue(() => new Monitor().Examination(null));
                    //RecurringJob.AddOrUpdate("ischedulermonitorcycle", () => new Monitor(m_xmlp).Examination(null), intervalcron); //minutes interval
                    //RecurringJob.AddOrUpdate("ischedulermonitorcycle", () => new Monitor(m_xmlp).Examination(null), $"{(int)(m_xmlp.CheckInterval / 60)} * * * *"); // cron expression
                }
            }
            catch (Exception ex) { le.AddExceptionResult(ex); le.SetLogLevel(LogLevel.Error); }
            finally { le.Write(); }
        }

        public static void Stop()
        {
            var le = new DCLogEntry(LogLevel.Information, $"{typeof(WAHostedMonitorHangfire).FullName}.{System.Reflection.MethodBase.GetCurrentMethod().Name}");
            try { _StopHangfire(le); }
            catch (Exception ex) { le.AddExceptionResult(ex); le.SetLogLevel(LogLevel.Error); }
            finally { le.Write(); }
        }

        #region private members - Cron expression
        /// <summary>
        /// Cron Expression
        ///   <minute> <hour> <day-of-month> <month> <day-of-week>
        ///       <minute>:0-59
        ///       <hour>:0-23
        ///       <day-of-month>:1-31
        ///       <month>:1-12
        ///       <day-of-week>:0-6 (Sunday to Saturday; 7 is Sunday on some systems)
        /// Special Characters in Cron Expression
        ///   * (all) all values. event should happen for every time unit.For example, “*” in the<minute> field means “for every minute.”
        ///   ? (any) any value.used for the<day-of-month> and <day-of-week> fields.For example “?” in the<day-of-week> field and 5 in the<day-of-month>
        ///          field means “5th of every month” irrespective of what day is this of the week.
        ///   – (range) value range.For example, “10-11” in the<hour> field means “10th and 11th hours.”
        ///   , (values) multiple values.For example, “MON, WED, FRI“ in <day-of-week> field means on the days “Monday, Wednesday and Friday.”
        ///   / (increments) specifies the incremental values.For example, a “5/15” in the<minute> field means at “5, 20, 35 and 50 minutes of an hour.”
        ///
        ///   L (last) has different meanings when used in various fields. For example, if it's applied in the <day-of-month> field, it means last day of the month,
        ///          i.e. “31st of January” and so on as per the calendar month. It can be used with an offset value, like “L-3”,
        ///          which denotes the “third to last day of the calendar month.” In<day-of-week>, it specifies the “last day of a week.” It can also be used with
        ///          another value in <day-of-week>, like “6L”, which denotes the “last Friday.”
        ///
        ///   W (weekday) determines the weekday (Monday to Friday) nearest to a given day of the month.For example, if we specify “10W” in the<day-of-month> field,
        ///          it means the “weekday near to 10th of that month.” So if “10th” is a Saturday, the job will be triggered on “9th,” and if “10th” is a Sunday,
        ///          it will trigger on “11th.” If we specify “1W” in <day-of-month> and if “1st” is Saturday, the job will be triggered on “3rd,” which is Monday,
        ///          and it will not jump back to the previous month.
        ///   # specifies the “N-th” occurrence of a weekday of the month, for example, “third Friday of the month” can be indicated as “6#3”.
        /// Cron Special Strings
        ///   run once at the startup: "@reboot"
        ///   run once a year: "@yearly" or "@annualy" (= "0 0 1 1 *")
        ///   run once a month: "@monthly" (="0 0 1 * *")
        ///   run once a week: "@weekly" (="0 0 * * 0")
        ///   run once a day: "@daily" or "@midnight" (="0 0 * * *")
        ///   run hourly: "@hourly" (="0 * * * *")
        ///  
        /// Cron samples
        ///   At 12:00pm(noon) every day: "0 12 * * ?"
        ///   At Every five minutes starting at 1pm and ending at 1:55pm and then starting at 6pm and ending at 6:55pm, every day: "0/5 13,18 * * ?"
        ///   At Every minute starting at 1pm and ending at 1:05pm, every day: "0-5 13 * * ?"
        ///   At 1:15pm and 1:45pm every Tuesday in the month of June: "15,45 13 ? 6 Tue"
        ///   At 9:30am every Monday, Tuesday, Wednesday, Thursday and Friday: "30 9 ? *MON - FRI"
        ///   At 9:30am on the 15th day of every month: "30 9 15 * ?"
        ///   At 6pm on the last day of every month: "0 18 L * ?"
        ///   At 6pm on the third to last day of every month: "0 18 L - 3 * ?"
        ///   At 10:30am on the last Thursday of every month: "30 10 ? *5L"
        ///   At 10am on the third Monday of every month: "0 10 ? *2#3"
        ///   At 12 midnight on every 5th day, starting from the 10th until the end of the month: "0 0 10 / 5 * ?"
        ///  
        ///   At every hour: "0 * * * *"
        ///   At every hour: "@hourly"
        ///   At once a day at midnight: "0 0 * * *"
        ///   At every Sunday at midnight: "0 0 * * 0"
        ///   At every hour on Mondays: "0 * * *1"
        ///   At twice a day at 6am and 6pm: "0 6,18 * * *"
        ///   At every 10 minutes: "*/10 * * * *"
        ///   At every minute on July 20: "* * 20 7 *"
        ///   At every weekday(Monday to Friday) at 10pm: "0 22 * * 1-5"
        ///   At at midnight every Tuesday: "0 0 * * 2 *"
        ///   At every minute during January, February, and May "* * * 1,2,5 *"
        ///   At every 10 minutes at 5am, starting from 5:10am: "10-59/10 5 * * *"
        ///   At quarterly on the first day of the month at 8am "0 8 1 */3 *"
        ///   At every time you turn on the system: "@reboot"
        ///   At the first Monday of each month, at 8am: "0 8 1-7 * 1"
        ///   At every Sunday at 4:05 am: "5 4 * * 0"
        ///   At at 9:15 pm on the 1st and 20th of every month: "15 9 1,20 * *"
        ///   At at midnight every Wednesday between the 1st and 15th of every month: "0 0 1,15 * 3"
        ///   At the first day of every month at 2:15pm: "15 14 1 * *"
        ///   At every day from Monday to Friday every hour from 8 to 5pm: "00 08-17 * * 1-5"
        ///   At every January 1st at 6:15am: "15 6 1 1 *"
        ///   At at midnight on the 15th of every month: "0 0 15 * *"
        ///   At every Monday and Friday: "* * * * 1,5"
        ///  
        ///   At Every Minute of Every Day: "* * * * *" or "0-59 0-23 0-31 0-12 0-7"
        ///   At Every 10 Minutes of Every Day: "*/10 * * * *" or "0-59/10 * * * *" or "0,10,20,30,40,50 * * * *"
        ///   At Every 5 Minutes of the 6am hour starting at 6:07 (tu run at 6:07, 6:012, 6:17, 6:22, 6:27, and so on until 6:57):"07-59/5 06 * * *"
        ///   At Every day at midnight: "0 0 * * *" or "0 0 * * 0-7"
        ///   At Thrice Daily: "0 */8 * * *" or "0 0-23/8 * * *" or "0 0,8,16 * * *"
        ///   At Every weekday at 6am: "0 06 * * 1-5"
        ///   At Weekends at 6am: "0 06 * * 6,7" or "0 06 * * 6-7"
        ///   At Once a month on the 20th at 6am: "0 06 20 * *"
        ///   At Every 4 days at 6am: "0 06 */4 * *"
        ///   At Every 4 Months at 6am on the 10th: "0 06 10 */4 *"
        /// </summary>
        private static string IntervalCronExpression = null;
        #endregion private members - Cron expression

        #region private members - _StartHangfire,_StopHangfire
        private static void _StartHangfire(DCLogEntry le)
        {
            lock (_HangfireStarterLocker)
            {
                le?.AddDataField("HangfireSqlDBConnectionString", Config_HangFireConnectionString);

                GlobalConfiguration.Configuration
                    .UseSqlServerStorage(Config_HangFireConnectionString)
                    // Specify other options here
                    ;
                _HangfireBackgroundJobServer = new BackgroundJobServer();
                _HangFireRunning = true;
            }
        }
        private static void _StopHangfire(DCLogEntry le)
        {
            lock (_HangfireStarterLocker)
            {
                if (!_HangFireRunning) return;
                le?.AddDataField("Function", nameof(_StopHangfire));
                if (_HangfireBackgroundJobServer != null) { _HangfireBackgroundJobServer.Dispose(); }
                _HangFireRunning = false;
            }
        }
        //private static WAHostedMonitorHangfire _MonitorHangfireInstance = new WAHostedMonitorHangfire();
        private static BackgroundJobServer _HangfireBackgroundJobServer { get; set; }
        private static readonly object _HangfireStarterLocker = new object();
        private static bool _HangFireRunning = false;
        #endregion private members - _StartHangfire,_StopHangfire

        #region appconfig elements
        const string WSHSHANGFIRE = "WebServerHostedService_iSchedulerHangfire:";
        const string HANGFIREDBCONNECTIONSTRING = WSHSHANGFIRE + "dbconnectionstring";
        const string HANGFIRECONFIGLIST = WSHSHANGFIRE + "configlist";
        private static List<string> Config_iSchedulerConfigList
        {
            get
            {
                if (config_ischedulerconfiglist == null)
                {
                    string configliststring = ConfigurationManager.AppSettings[HANGFIRECONFIGLIST];
                    if (string.IsNullOrWhiteSpace(configliststring)) configliststring = "";
                    config_ischedulerconfiglist = configliststring.Split(new char[] { ',', ';' },StringSplitOptions.RemoveEmptyEntries).ToList();
                }
                return config_ischedulerconfiglist;
            }
        }
        private static List<string> config_ischedulerconfiglist = null;
        private static bool ParseiSchedulerConfigString(string cf, out string jobid, out string xmlcs, out int checkintervalinseconds) 
        {
            jobid = "";xmlcs = ""; checkintervalinseconds = 60;
            if (string.IsNullOrWhiteSpace(cf)) return false;
            try
            {
                jobid = cf.Split(':')[0];
                xmlcs = cf.Split(':')[1];
                checkintervalinseconds = int.Parse(cf.Split(':')[2]);
            }
            catch { }
            if (checkintervalinseconds < 60) checkintervalinseconds = 60;
            return !string.IsNullOrWhiteSpace(jobid) && !string.IsNullOrWhiteSpace(xmlcs) && checkintervalinseconds >= 60;
        }
        private static string Config_HangFireConnectionString
        {
            get
            {
                if (config_hangfireconnectionstring == null)
                {
                    string HangfireSqlDBConnectionString = ConfigurationManager.AppSettings[HANGFIREDBCONNECTIONSTRING];
                    if (string.IsNullOrWhiteSpace(HangfireSqlDBConnectionString)) HangfireSqlDBConnectionString = "HANGFIRESQLDB";
                    config_hangfireconnectionstring = ConnectionStringStore.GetSQL(HangfireSqlDBConnectionString);
                }
                return config_hangfireconnectionstring;
            }
        }
        private static string config_hangfireconnectionstring = null;
        #endregion appconfig elements
    }
}
namespace Vrh.iScheduler
{    
    public class WAHostedMonitor : Monitor
    {
        public static void Start()
        {
            MonitorInstance = new Monitor(Config_scheduleMonitorXmlPath, Config_scheduleXmlPath);
            MonitorInstance.Start();
            new DCLogEntry(LogLevel.Information, "iSchedulerMonitor started.").Write();
        }
        public static void Stop()
        {
            MonitorInstance.Stop();
            MonitorInstance.Dispose();
            new DCLogEntry(LogLevel.Information, "iSchedulerMonitor stopped.").Write();
        }

        #region private members
        private static string Config_scheduleMonitorXmlPath
        {
            get
            {
                if (config_schedulemonitorxmlpath == null)
                {
                    string schedulemonitorxmlpath = ConfigurationManager.AppSettings[ISCHEDULERMONITORXMLPATH];
                    config_schedulemonitorxmlpath = ConnectionStringStore.GetSQL(schedulemonitorxmlpath);
                }
                return config_schedulemonitorxmlpath;
            }
        }
        private static string config_schedulemonitorxmlpath = null;
        private static string Config_scheduleXmlPath
        {
            get
            {
                if (config_schedulexmlpath == null)
                {
                    string schedulexmlpath = ConfigurationManager.AppSettings[ISCHEDULERXMLPATH];
                    config_schedulexmlpath = ConnectionStringStore.GetSQL(schedulexmlpath);
                }
                return config_schedulexmlpath;
            }
        }
        private static string config_schedulexmlpath = null;
        const string ISCHEDULER = "iScheduler:";
        const string ISCHEDULERMONITORXMLPATH = ISCHEDULER + "monitorxmlpath";
        const string ISCHEDULERXMLPATH = ISCHEDULER + "xmlpath";
        private static Monitor MonitorInstance;
        #endregion private members
    }
}

namespace Vrh.iScheduler
{    
    public class Monitor : IDisposable
    {
        #region Enums
        /// <summary>
        /// Ütemezések lehetséges állapotai.
        /// </summary>
        public enum ScheduleStates : byte
        {
            /// <summary>
            /// Az ütemezés végrehajtás előtt van, végrehajtásra várakozik.
            /// </summary>
            Active = 0,

            /// <summary>
            /// Az ütemezés a végrehajtás után, ha a végrehajtás sikeresen ért véget.
            /// </summary>
            Success = 1,

            /// <summary>
            /// Az ütemezés végrehajtás után, ha a végrehajtás hibával ért véget.
            /// </summary>
            Failed = 2,
        }
        #endregion Enums

        #region Private members

        private Timer m_timer;
        private iSchedulerXMLProcessor m_xmlp=null;
        #endregion Private members

        #region Constructor

        /// <summary>
        /// iSchedulerMonitor időzítés figyelő osztály.
        /// </summary>
        /// <param name="scheduleMonitorXmlPath">iScheduler.xml elérési helye a névvel együtt a plugin futtató környezetében.</param>
        /// <param name="scheduleXmlPath">ScheduleExecute akció által használt iScheduler.xml elérési helye a névvel együtt
        /// <param name="pluginReference">Az indító plugin példányra mutató referencia
        /// a távoli gépen. Ha egy gépen fut, akkor nem kötelező.</param>
        public Monitor(string scheduleMonitorXmlPath, string scheduleXmlPath) { Init(new iSchedulerXMLProcessor(scheduleMonitorXmlPath, scheduleXmlPath)); }
        public Monitor(iSchedulerXMLProcessor m_xmlp) { Init(m_xmlp); }
        public Monitor() { }
        private void Init(iSchedulerXMLProcessor m_xmlp,bool enabletimer=true)
        {
            this.m_xmlp = m_xmlp;
            //try
            //{
            if (!enabletimer)
            {
                m_timer = new Timer(m_xmlp.CheckIntervalInSeconds * 1000); // !!! Ez itt a jó sor !!! m_timer = new Timer(20000); meg a debug !!!
                m_timer.Elapsed += OnExamination;
            }

            var le = new DCLogEntry(LogLevel.Information, nameof(Monitor) + " constructor. Preparation ready.");
            le.AddDataField("iSchedulerMonitor xml path", m_xmlp.ScheduleMonitorXmlPath);
            le.AddDataField("iScheduler xml path", m_xmlp.ScheduleXmlPath);
            le.AddDataField("Scheduled object type", m_xmlp.ObjectType);
            le.AddDataField("Group Id", m_xmlp.GroupId);
            le.AddDataField("Check interval", m_xmlp.CheckIntervalInSeconds.ToString());
            le.Write();

            //}
            //catch (Exception ex)
            //{
            //    var message = String.Join(",", WebCommon.ErrorListBuilder(ex));
            //    MyLog("PREPARATION ERROR. " + message);
            //    throw new ApplicationException("PREPARATION ERROR. ", ex);
            //}
        }
        #endregion Constructor

        #region public methods - Start,Stop
        public void Start()
        {
            new DCLogEntry(LogLevel.Information, "iSchedulerMonitor started.").Write();
            m_timer.Start();
        }
        public void Stop()
        {
            m_timer.Stop();
            new DCLogEntry(LogLevel.Information, "iSchedulerMonitor stopped.").Write();
        }
        #endregion public methods - Start,Stop

        #region Private methods

        private void OnExamination(object sender, ElapsedEventArgs e)
        {
            m_timer.Stop();
            Examination(e.SignalTime,null);
            m_timer.Start();
        }

        #region Examination
        /// <summary>
        /// Időzített események megkeresése, és végrehajtása.
        /// </summary>
        /// <param name="signalTime"></param>
        [DisableConcurrentExecution(1000)]
        public void Examination(DateTime? signalTime,DCLogEntry le)
        {
            bool writele = false;
            if (le == null) { le = new DCLogEntry(LogLevel.Debug, nameof(Examination)); writele = true; }
            if (!signalTime.HasValue) signalTime = DateTime.Now;
            try
            {
                using (SqlConnection cnn = new SqlConnection(m_xmlp.DatabaseConnectionString))
                {
                    cnn.Open();
                    le.AddDataField($"{nameof(Examination)}: Database connection opened", $"Connection string: {m_xmlp.DatabaseConnectionString}");

                    string scmd = string.Concat(
                        " select *",
                        " from iScheduler.Schedules s",
                        "    inner join iScheduler.ScheduleObjects so on s.ScheduleObjectId = so.Id",
                        " where s.[State] = 0 and s.OperationTime < @signalTime",
                        "   and so.ObjectType = @objectType and so.ObjectGroupId = @groupId"
                        );
                    using (SqlCommand cmd = new SqlCommand(scmd, cnn))
                    {
                        cmd.Parameters.Add(new SqlParameter("signalTime", signalTime));
                        cmd.Parameters.Add(new SqlParameter("objectType", m_xmlp.ObjectType));
                        cmd.Parameters.Add(new SqlParameter("groupId", m_xmlp.GroupId));

                        SqlDataReader rdr = cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
                        if (rdr.HasRows)
                        {
                            int ixID = rdr.GetOrdinal("Id");
                            //string basefolder = AppDomain.CurrentDomain.BaseDirectory;
                            //basefolder = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
                            //basefolder = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase);
                            string basefolder = System.IO.Directory.GetCurrentDirectory();
                            ScheduleExecute se = new ScheduleExecute(m_xmlp.ScheduleXmlPath, basefolder);

                            int ScheduledJobCounter = 0;
                            while (rdr.Read())
                            {
                                ScheduledJobCounter++;
                                int id = rdr.GetInt32(ixID);
                                DateTime jobstartedat = DateTime.Now;
                                le.AddDataField($"{nameof(Examination)}: Scheduled job #{ScheduledJobCounter} started", $"id={id}");
                                se.Run(id);
                                le.AddDataField($"{nameof(Examination)}: Scheduled job #{ScheduledJobCounter} finished at", (DateTime.Now).Subtract(jobstartedat).ToString(@"hh\:mm\:ss"));
                                le.AddSuccessResult($"{nameof(Examination)}: Scheduled job execution SUCCESS");
                            }//while (rdr.Read())
                        }//if (rdr.HasRows)
                        else { le.AddSuccessResult($"{nameof(Examination)}: No scheduled job found!"); }
                    }
                }
            }
            catch (Exception ex)
            {
                le.SetLogLevel(LogLevel.Error);
                le.AddExceptionResult(ex);
            }
            finally { if (writele) le.Write(); }
        }
        public static void ExaminationRunOnce(int pid, string xmlcs) 
        {   
            var le = new DCLogEntry(LogLevel.Debug, nameof(ExaminationRunOnce));
            try { (new Monitor(xmlcs, "")).Examination(null, le); }
            catch (Exception ex) { le.AddExceptionResult(ex); }
            finally { le.Write(); }
        }
        #endregion Examination

        #endregion Private methods

        #region IDisposable Support
        private bool disposedValue = false; // To detect redundant calls

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    // TODO: dispose managed state (managed objects).
                }

                // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
                // TODO: set large fields to null.

                disposedValue = true;
            }
        }

        // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
        // ~iScheduler() {
        //   // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
        //   Dispose(false);
        // }

        // This code added to correctly implement the disposable pattern.
        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
            // TODO: uncomment the following line if the finalizer is overridden above.
            // GC.SuppressFinalize(this);
        }
        #endregion
    }
}
