using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using IntroSkipper.Configuration;
using IntroSkipper.Data;
using IntroSkipper.Manager;
using IntroSkipper.ScheduledTasks;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace IntroSkipper.Services
{
    /// <summary>
    /// Server entrypoint.
    /// </summary>
    public sealed class Entrypoint : IHostedService, IDisposable
    {
        private readonly ITaskManager _taskManager;
        private readonly ILibraryManager _libraryManager;
        private readonly ILogger<Entrypoint> _logger;
        private readonly ILoggerFactory _loggerFactory;
        private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager;
        private readonly HashSet<Guid> _seasonsToAnalyze = [];
        private readonly Timer _queueTimer;
        private static readonly SemaphoreSlim _analysisSemaphore = new(1, 1);
        private PluginConfiguration _config;
        private bool _analyzeAgain;
        private static CancellationTokenSource? _cancellationTokenSource;

        /// <summary>
        /// Initializes a new instance of the <see cref="Entrypoint"/> class.
        /// </summary>
        /// <param name="libraryManager">Library manager.</param>
        /// <param name="taskManager">Task manager.</param>
        /// <param name="logger">Logger.</param>
        /// <param name="loggerFactory">Logger factory.</param>
        /// <param name="mediaSegmentUpdateManager">MediaSegment Update Manager.</param>
        public Entrypoint(
            ILibraryManager libraryManager,
            ITaskManager taskManager,
            ILogger<Entrypoint> logger,
            ILoggerFactory loggerFactory,
            MediaSegmentUpdateManager mediaSegmentUpdateManager)
        {
            _libraryManager = libraryManager;
            _taskManager = taskManager;
            _logger = logger;
            _loggerFactory = loggerFactory;
            _mediaSegmentUpdateManager = mediaSegmentUpdateManager;

            _config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
            _queueTimer = new Timer(
                    OnTimerCallback,
                    null,
                    Timeout.InfiniteTimeSpan,
                    Timeout.InfiniteTimeSpan);
        }

        /// <summary>
        /// Gets State of the automatic task.
        /// </summary>
        public static TaskState AutomaticTaskState => _cancellationTokenSource switch
        {
            null => TaskState.Idle,
            { IsCancellationRequested: true } => TaskState.Cancelling,
            _ => TaskState.Running
        };

        /// <inheritdoc />
        public Task StartAsync(CancellationToken cancellationToken)
        {
            _libraryManager.ItemAdded += OnItemChanged;
            _libraryManager.ItemUpdated += OnItemChanged;
            _taskManager.TaskCompleted += OnLibraryRefresh;
            Plugin.Instance!.ConfigurationChanged += OnSettingsChanged;

            FFmpegWrapper.Logger = _logger;

            // Enqueue all episodes at startup to ensure any FFmpeg errors appear as early as possible
            _logger.LogInformation("Running startup enqueue");
            new QueueManager(_loggerFactory.CreateLogger<QueueManager>(), _libraryManager).GetMediaItems();

            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public Task StopAsync(CancellationToken cancellationToken)
        {
            _libraryManager.ItemAdded -= OnItemChanged;
            _libraryManager.ItemUpdated -= OnItemChanged;
            _taskManager.TaskCompleted -= OnLibraryRefresh;
            Plugin.Instance!.ConfigurationChanged -= OnSettingsChanged;

            _queueTimer.Change(Timeout.Infinite, 0);
            return Task.CompletedTask;
        }

        /// <summary>
        /// Library item was added.
        /// </summary>
        /// <param name="sender">The sending entity.</param>
        /// <param name="itemChangeEventArgs">The <see cref="ItemChangeEventArgs"/>.</param>
        private void OnItemChanged(object? sender, ItemChangeEventArgs itemChangeEventArgs)
        {
            if ((_config.AutoDetectIntros || _config.AutoDetectCredits) &&
                itemChangeEventArgs.Item is { LocationType: not LocationType.Virtual } item)
            {
                Guid? id = item is Episode episode ? episode.SeasonId : (item is Movie movie ? movie.Id : null);

                if (id.HasValue)
                {
                    _seasonsToAnalyze.Add(id.Value);
                    StartTimer();
                }
            }
        }

        /// <summary>
        /// TaskManager task ended.
        /// </summary>
        /// <param name="sender">The sending entity.</param>
        /// <param name="eventArgs">The <see cref="TaskCompletionEventArgs"/>.</param>
        private void OnLibraryRefresh(object? sender, TaskCompletionEventArgs eventArgs)
        {
            if ((_config.AutoDetectIntros || _config.AutoDetectCredits) &&
                eventArgs.Result is { Key: "RefreshLibrary", Status: TaskCompletionStatus.Completed } &&
                AutomaticTaskState != TaskState.Running)
            {
                StartTimer();
            }
        }

        private void OnSettingsChanged(object? sender, BasePluginConfiguration e) => _config = (PluginConfiguration)e;

        /// <summary>
        /// Start timer to debounce analyzing.
        /// </summary>
        private void StartTimer()
        {
            if (AutomaticTaskState == TaskState.Running)
            {
                _analyzeAgain = true;
            }
            else if (AutomaticTaskState == TaskState.Idle)
            {
                _logger.LogDebug("Media Library changed, analyzis will start soon!");
                _queueTimer.Change(TimeSpan.FromSeconds(60), Timeout.InfiniteTimeSpan);
            }
        }

        private void OnTimerCallback(object? state) =>
            _ = RunAnalysisAsync();

        private async Task RunAnalysisAsync()
        {
            try
            {
                await PerformAnalysisAsync().ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                _logger.LogInformation("Automatic Analysis task cancelled");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in RunAnalysisAsync");
            }

            _cancellationTokenSource = null;
        }

        private async Task PerformAnalysisAsync()
        {
            await _analysisSemaphore.WaitAsync().ConfigureAwait(false);
            try
            {
                using (_cancellationTokenSource = new CancellationTokenSource())
                using (await ScheduledTaskSemaphore.AcquireAsync(_cancellationTokenSource.Token).ConfigureAwait(false))
                {
                    _logger.LogInformation("Initiating automatic analysis task");
                    var seasonIds = new HashSet<Guid>(_seasonsToAnalyze);
                    _seasonsToAnalyze.Clear();
                    _analyzeAgain = false;

                    var modes = new List<AnalysisMode>();

                    if (_config.AutoDetectIntros)
                    {
                        modes.Add(AnalysisMode.Introduction);
                    }

                    if (_config.AutoDetectCredits)
                    {
                        modes.Add(AnalysisMode.Credits);
                    }

                    var analyzer = new BaseItemAnalyzerTask(modes, _loggerFactory.CreateLogger<Entrypoint>(), _loggerFactory, _libraryManager, _mediaSegmentUpdateManager);
                    await analyzer.AnalyzeItems(new Progress<double>(), _cancellationTokenSource.Token, seasonIds).ConfigureAwait(false);

                    if (_analyzeAgain && !_cancellationTokenSource.IsCancellationRequested)
                    {
                        _logger.LogInformation("Analyzing ended, but we need to analyze again!");
                        _queueTimer.Change(TimeSpan.FromSeconds(60), Timeout.InfiniteTimeSpan);
                    }
                }
            }
            finally
            {
                _analysisSemaphore.Release();
            }
        }

        /// <summary>
        /// Method to cancel the automatic task.
        /// </summary>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
        public static async Task CancelAutomaticTaskAsync(CancellationToken cancellationToken)
        {
            if (_cancellationTokenSource is { IsCancellationRequested: false })
            {
                try
                {
                    await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
                }
                catch (ObjectDisposedException)
                {
                    _cancellationTokenSource = null;
                }
            }

            await _analysisSemaphore.WaitAsync(TimeSpan.FromSeconds(60), cancellationToken).ConfigureAwait(false);
        }

        /// <inheritdoc/>
        public void Dispose()
        {
            _queueTimer.Dispose();
            _cancellationTokenSource?.Dispose();
            _analysisSemaphore.Dispose();
        }
    }
}