using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.ScheduledTasks
{
    /// 
    /// Class ScheduledTaskWorker
    /// 
    public class ScheduledTaskWorker : IScheduledTaskWorker
    {
        public event EventHandler> TaskProgress;
        /// 
        /// Gets or sets the scheduled task.
        /// 
        /// The scheduled task.
        public IScheduledTask ScheduledTask { get; private set; }
        /// 
        /// Gets or sets the json serializer.
        /// 
        /// The json serializer.
        private IJsonSerializer JsonSerializer { get; set; }
        /// 
        /// Gets or sets the application paths.
        /// 
        /// The application paths.
        private IApplicationPaths ApplicationPaths { get; set; }
        /// 
        /// Gets the logger.
        /// 
        /// The logger.
        private ILogger Logger { get; set; }
        /// 
        /// Gets the task manager.
        /// 
        /// The task manager.
        private ITaskManager TaskManager { get; set; }
        private readonly IFileSystem _fileSystem;
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The scheduled task.
        /// The application paths.
        /// The task manager.
        /// The json serializer.
        /// The logger.
        /// 
        /// scheduledTask
        /// or
        /// applicationPaths
        /// or
        /// taskManager
        /// or
        /// jsonSerializer
        /// or
        /// logger
        /// 
        public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem)
        {
            if (scheduledTask == null)
            {
                throw new ArgumentNullException(nameof(scheduledTask));
            }
            if (applicationPaths == null)
            {
                throw new ArgumentNullException(nameof(applicationPaths));
            }
            if (taskManager == null)
            {
                throw new ArgumentNullException(nameof(taskManager));
            }
            if (jsonSerializer == null)
            {
                throw new ArgumentNullException(nameof(jsonSerializer));
            }
            if (logger == null)
            {
                throw new ArgumentNullException(nameof(logger));
            }
            ScheduledTask = scheduledTask;
            ApplicationPaths = applicationPaths;
            TaskManager = taskManager;
            JsonSerializer = jsonSerializer;
            Logger = logger;
            _fileSystem = fileSystem;
            InitTriggerEvents();
        }
        private bool _readFromFile = false;
        /// 
        /// The _last execution result
        /// 
        private TaskResult _lastExecutionResult;
        /// 
        /// The _last execution result sync lock
        /// 
        private readonly object _lastExecutionResultSyncLock = new object();
        /// 
        /// Gets the last execution result.
        /// 
        /// The last execution result.
        public TaskResult LastExecutionResult
        {
            get
            {
                var path = GetHistoryFilePath();
                lock (_lastExecutionResultSyncLock)
                {
                    if (_lastExecutionResult == null && !_readFromFile)
                    {
                        if (File.Exists(path))
                        {
                            try
                            {
                                _lastExecutionResult = JsonSerializer.DeserializeFromFile(path);
                            }
                            catch (Exception ex)
                            {
                                Logger.LogError(ex, "Error deserializing {File}", path);
                            }
                        }
                        _readFromFile = true;
                    }
                }
                return _lastExecutionResult;
            }
            private set
            {
                _lastExecutionResult = value;
                var path = GetHistoryFilePath();
                Directory.CreateDirectory(Path.GetDirectoryName(path));
                lock (_lastExecutionResultSyncLock)
                {
                    JsonSerializer.SerializeToFile(value, path);
                }
            }
        }
        /// 
        /// Gets the name.
        /// 
        /// The name.
        public string Name => ScheduledTask.Name;
        /// 
        /// Gets the description.
        /// 
        /// The description.
        public string Description => ScheduledTask.Description;
        /// 
        /// Gets the category.
        /// 
        /// The category.
        public string Category => ScheduledTask.Category;
        /// 
        /// Gets the current cancellation token
        /// 
        /// The current cancellation token source.
        private CancellationTokenSource CurrentCancellationTokenSource { get; set; }
        /// 
        /// Gets or sets the current execution start time.
        /// 
        /// The current execution start time.
        private DateTime CurrentExecutionStartTime { get; set; }
        /// 
        /// Gets the state.
        /// 
        /// The state.
        public TaskState State
        {
            get
            {
                if (CurrentCancellationTokenSource != null)
                {
                    return CurrentCancellationTokenSource.IsCancellationRequested
                               ? TaskState.Cancelling
                               : TaskState.Running;
                }
                return TaskState.Idle;
            }
        }
        /// 
        /// Gets the current progress.
        /// 
        /// The current progress.
        public double? CurrentProgress { get; private set; }
        /// 
        /// The _triggers
        /// 
        private Tuple[] _triggers;
        /// 
        /// Gets the triggers that define when the task will run
        /// 
        /// The triggers.
        private Tuple[] InternalTriggers
        {
            get => _triggers;
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }
                // Cleanup current triggers
                if (_triggers != null)
                {
                    DisposeTriggers();
                }
                _triggers = value.ToArray();
                ReloadTriggerEvents(false);
            }
        }
        /// 
        /// Gets the triggers that define when the task will run
        /// 
        /// The triggers.
        /// value
        public TaskTriggerInfo[] Triggers
        {
            get
            {
                var triggers = InternalTriggers;
                return triggers.Select(i => i.Item1).ToArray();
            }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }
                // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly
                var triggerList = value.Where(i => i != null).ToArray();
                SaveTriggers(triggerList);
                InternalTriggers = triggerList.Select(i => new Tuple(i, GetTrigger(i))).ToArray();
            }
        }
        /// 
        /// The _id
        /// 
        private string _id;
        /// 
        /// Gets the unique id.
        /// 
        /// The unique id.
        public string Id
        {
            get
            {
                if (_id == null)
                {
                    _id = ScheduledTask.GetType().FullName.GetMD5().ToString("N");
                }
                return _id;
            }
        }
        private void InitTriggerEvents()
        {
            _triggers = LoadTriggers();
            ReloadTriggerEvents(true);
        }
        public void ReloadTriggerEvents()
        {
            ReloadTriggerEvents(false);
        }
        /// 
        /// Reloads the trigger events.
        /// 
        /// if set to true [is application startup].
        private void ReloadTriggerEvents(bool isApplicationStartup)
        {
            foreach (var triggerInfo in InternalTriggers)
            {
                var trigger = triggerInfo.Item2;
                trigger.Stop();
                trigger.Triggered -= trigger_Triggered;
                trigger.Triggered += trigger_Triggered;
                trigger.Start(LastExecutionResult, Logger, Name, isApplicationStartup);
            }
        }
        /// 
        /// Handles the Triggered event of the trigger control.
        /// 
        /// The source of the event.
        /// The  instance containing the event data.
        async void trigger_Triggered(object sender, EventArgs e)
        {
            var trigger = (ITaskTrigger)sender;
            var configurableTask = ScheduledTask as IConfigurableScheduledTask;
            if (configurableTask != null && !configurableTask.IsEnabled)
            {
                return;
            }
            Logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name);
            trigger.Stop();
            TaskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions);
            await Task.Delay(1000).ConfigureAwait(false);
            trigger.Start(LastExecutionResult, Logger, Name, false);
        }
        private Task _currentTask;
        /// 
        /// Executes the task
        /// 
        /// Task options.
        /// Task.
        /// Cannot execute a Task that is already running
        public async Task Execute(TaskOptions options)
        {
            var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false));
            _currentTask = task;
            try
            {
                await task.ConfigureAwait(false);
            }
            finally
            {
                _currentTask = null;
                GC.Collect();
            }
        }
        private async Task ExecuteInternal(TaskOptions options)
        {
            // Cancel the current execution, if any
            if (CurrentCancellationTokenSource != null)
            {
                throw new InvalidOperationException("Cannot execute a Task that is already running");
            }
            var progress = new SimpleProgress();
            CurrentCancellationTokenSource = new CancellationTokenSource();
            Logger.LogInformation("Executing {0}", Name);
            ((TaskManager)TaskManager).OnTaskExecuting(this);
            progress.ProgressChanged += progress_ProgressChanged;
            TaskCompletionStatus status;
            CurrentExecutionStartTime = DateTime.UtcNow;
            Exception failureException = null;
            try
            {
                if (options != null && options.MaxRuntimeTicks.HasValue)
                {
                    CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value));
                }
                await ScheduledTask.Execute(CurrentCancellationTokenSource.Token, progress).ConfigureAwait(false);
                status = TaskCompletionStatus.Completed;
            }
            catch (OperationCanceledException)
            {
                status = TaskCompletionStatus.Cancelled;
            }
            catch (Exception ex)
            {
                Logger.LogError(ex, "Error");
                failureException = ex;
                status = TaskCompletionStatus.Failed;
            }
            var startTime = CurrentExecutionStartTime;
            var endTime = DateTime.UtcNow;
            progress.ProgressChanged -= progress_ProgressChanged;
            CurrentCancellationTokenSource.Dispose();
            CurrentCancellationTokenSource = null;
            CurrentProgress = null;
            OnTaskCompleted(startTime, endTime, status, failureException);
        }
        /// 
        /// Progress_s the progress changed.
        /// 
        /// The sender.
        /// The e.
        void progress_ProgressChanged(object sender, double e)
        {
            e = Math.Min(e, 100);
            CurrentProgress = e;
            TaskProgress?.Invoke(this, new GenericEventArgs
            {
                Argument = e
            });
        }
        /// 
        /// Stops the task if it is currently executing
        /// 
        /// Cannot cancel a Task unless it is in the Running state.
        public void Cancel()
        {
            if (State != TaskState.Running)
            {
                throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state.");
            }
            CancelIfRunning();
        }
        /// 
        /// Cancels if running.
        /// 
        public void CancelIfRunning()
        {
            if (State == TaskState.Running)
            {
                Logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name);
                CurrentCancellationTokenSource.Cancel();
            }
        }
        /// 
        /// Gets the scheduled tasks configuration directory.
        /// 
        /// System.String.
        private string GetScheduledTasksConfigurationDirectory()
        {
            return Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
        }
        /// 
        /// Gets the scheduled tasks data directory.
        /// 
        /// System.String.
        private string GetScheduledTasksDataDirectory()
        {
            return Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks");
        }
        /// 
        /// Gets the history file path.
        /// 
        /// The history file path.
        private string GetHistoryFilePath()
        {
            return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js");
        }
        /// 
        /// Gets the configuration file path.
        /// 
        /// System.String.
        private string GetConfigurationFilePath()
        {
            return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js");
        }
        /// 
        /// Loads the triggers.
        /// 
        /// IEnumerable{BaseTaskTrigger}.
        private Tuple[] LoadTriggers()
        {
            // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly
            var settings = LoadTriggerSettings().Where(i => i != null).ToArray();
            return settings.Select(i => new Tuple(i, GetTrigger(i))).ToArray();
        }
        private TaskTriggerInfo[] LoadTriggerSettings()
        {
            string path = GetConfigurationFilePath();
            TaskTriggerInfo[] list = null;
            if (File.Exists(path))
            {
                list = JsonSerializer.DeserializeFromFile(path);
            }
            // Return defaults if file doesn't exist.
            return list ?? GetDefaultTriggers();
        }
        private TaskTriggerInfo[] GetDefaultTriggers()
        {
            try
            {
                return ScheduledTask.GetDefaultTriggers().ToArray();
            }
            catch
            {
                return new TaskTriggerInfo[]
                {
                    new TaskTriggerInfo
                    {
                        IntervalTicks = TimeSpan.FromDays(1).Ticks,
                        Type = TaskTriggerInfo.TriggerInterval
                    }
                };
            }
        }
        /// 
        /// Saves the triggers.
        /// 
        /// The triggers.
        private void SaveTriggers(TaskTriggerInfo[] triggers)
        {
            var path = GetConfigurationFilePath();
            Directory.CreateDirectory(Path.GetDirectoryName(path));
            JsonSerializer.SerializeToFile(triggers, path);
        }
        /// 
        /// Called when [task completed].
        /// 
        /// The start time.
        /// The end time.
        /// The status.
        private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex)
        {
            var elapsedTime = endTime - startTime;
            Logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds);
            var result = new TaskResult
            {
                StartTimeUtc = startTime,
                EndTimeUtc = endTime,
                Status = status,
                Name = Name,
                Id = Id
            };
            result.Key = ScheduledTask.Key;
            if (ex != null)
            {
                result.ErrorMessage = ex.Message;
                result.LongErrorMessage = ex.StackTrace;
            }
            LastExecutionResult = result;
            ((TaskManager)TaskManager).OnTaskCompleted(this, result);
        }
        /// 
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// 
        public void Dispose()
        {
            Dispose(true);
        }
        /// 
        /// Releases unmanaged and - optionally - managed resources.
        /// 
        /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
        protected virtual void Dispose(bool dispose)
        {
            if (dispose)
            {
                DisposeTriggers();
                var wassRunning = State == TaskState.Running;
                var startTime = CurrentExecutionStartTime;
                var token = CurrentCancellationTokenSource;
                if (token != null)
                {
                    try
                    {
                        Logger.LogInformation(Name + ": Cancelling");
                        token.Cancel();
                    }
                    catch (Exception ex)
                    {
                        Logger.LogError(ex, "Error calling CancellationToken.Cancel();");
                    }
                }
                var task = _currentTask;
                if (task != null)
                {
                    try
                    {
                        Logger.LogInformation(Name + ": Waiting on Task");
                        var exited = Task.WaitAll(new[] { task }, 2000);
                        if (exited)
                        {
                            Logger.LogInformation(Name + ": Task exited");
                        }
                        else
                        {
                            Logger.LogInformation(Name + ": Timed out waiting for task to stop");
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.LogError(ex, "Error calling Task.WaitAll();");
                    }
                }
                if (token != null)
                {
                    try
                    {
                        Logger.LogDebug(Name + ": Disposing CancellationToken");
                        token.Dispose();
                    }
                    catch (Exception ex)
                    {
                        Logger.LogError(ex, "Error calling CancellationToken.Dispose();");
                    }
                }
                if (wassRunning)
                {
                    OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null);
                }
            }
        }
        /// 
        /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger
        /// 
        /// The info.
        /// BaseTaskTrigger.
        /// 
        /// Invalid trigger type:  + info.Type
        private ITaskTrigger GetTrigger(TaskTriggerInfo info)
        {
            var options = new TaskOptions
            {
                MaxRuntimeTicks = info.MaxRuntimeTicks
            };
            if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase))
            {
                if (!info.TimeOfDayTicks.HasValue)
                {
                    throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));
                }
                return new DailyTrigger
                {
                    TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
                    TaskOptions = options
                };
            }
            if (info.Type.Equals(typeof(WeeklyTrigger).Name, StringComparison.OrdinalIgnoreCase))
            {
                if (!info.TimeOfDayTicks.HasValue)
                {
                    throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));
                }
                if (!info.DayOfWeek.HasValue)
                {
                    throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info));
                }
                return new WeeklyTrigger
                {
                    TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
                    DayOfWeek = info.DayOfWeek.Value,
                    TaskOptions = options
                };
            }
            if (info.Type.Equals(typeof(IntervalTrigger).Name, StringComparison.OrdinalIgnoreCase))
            {
                if (!info.IntervalTicks.HasValue)
                {
                    throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info));
                }
                return new IntervalTrigger
                {
                    Interval = TimeSpan.FromTicks(info.IntervalTicks.Value),
                    TaskOptions = options
                };
            }
            if (info.Type.Equals(typeof(StartupTrigger).Name, StringComparison.OrdinalIgnoreCase))
            {
                return new StartupTrigger();
            }
            throw new ArgumentException("Unrecognized trigger type: " + info.Type);
        }
        /// 
        /// Disposes each trigger
        /// 
        private void DisposeTriggers()
        {
            foreach (var triggerInfo in InternalTriggers)
            {
                var trigger = triggerInfo.Item2;
                trigger.Triggered -= trigger_Triggered;
                trigger.Stop();
            }
        }
    }
}