using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Validation;

namespace NzbDrone.Core.Notifications.Plex.Server
{
    public interface IPlexServerService
    {
        void UpdateLibrary(Movie movie, PlexServerSettings settings);
        void UpdateLibrary(IEnumerable<Movie> movie, PlexServerSettings settings);
        ValidationFailure Test(PlexServerSettings settings);
    }

    public class PlexServerService : IPlexServerService
    {
        private readonly ICached<Version> _versionCache;
        private readonly ICached<bool> _partialUpdateCache;
        private readonly ICached<bool> _pathScanCache;
        private readonly IPlexServerProxy _plexServerProxy;
        private readonly Logger _logger;

        public PlexServerService(ICacheManager cacheManager, IPlexServerProxy plexServerProxy, Logger logger)
        {
            _versionCache = cacheManager.GetCache<Version>(GetType(), "versionCache");
            _partialUpdateCache = cacheManager.GetCache<bool>(GetType(), "partialUpdateCache");
            _pathScanCache = cacheManager.GetCache<bool>(GetType(), "pathScanCache");
            _plexServerProxy = plexServerProxy;
            _logger = logger;
        }

        public void UpdateLibrary(Movie movie, PlexServerSettings settings)
        {
            UpdateLibrary(new[] { movie }, settings);
        }

        public void UpdateLibrary(IEnumerable<Movie> multipleMovies, PlexServerSettings settings)
        {
            try
            {
                _logger.Debug("Sending Update Request to Plex Server");
                var watch = Stopwatch.StartNew();

                var version = _versionCache.Get(settings.Host, () => GetVersion(settings), TimeSpan.FromHours(2));
                ValidateVersion(version);

                var sections = GetSections(settings);
                var partialUpdates = _partialUpdateCache.Get(settings.Host, () => PartialUpdatesAllowed(settings, version), TimeSpan.FromHours(2));
                var pathScanCache = _pathScanCache.Get(settings.Host, () => PathUpdatesAllowed(settings, version), TimeSpan.FromHours(2));

                var pathsUpdated = true;

                if (pathScanCache)
                {
                    foreach (var movie in multipleMovies)
                    {
                        pathsUpdated &= UpdatePath(movie, sections, settings);

                        if (!pathsUpdated)
                        {
                            break;
                        }
                    }
                }

                // If we couldn't path update then try partial and full update
                if (!pathScanCache || (pathScanCache && !pathsUpdated))
                {
                    if (partialUpdates)
                    {
                        var partiallyUpdated = true;

                        foreach (var movie in multipleMovies)
                        {
                            partiallyUpdated &= UpdatePartialSection(movie, sections, settings);

                            if (!partiallyUpdated)
                            {
                                break;
                            }
                        }

                        // Only update complete sections if all partial updates failed
                        if (!partiallyUpdated)
                        {
                            _logger.Debug("Unable to update partial section, updating all Movie sections");
                            sections.ForEach(s => UpdateSection(s.Id, settings));
                        }
                    }
                    else
                    {
                        sections.ForEach(s => UpdateSection(s.Id, settings));
                    }
                }

                _logger.Debug("Finished sending Update Request to Plex Server (took {0} ms)", watch.ElapsedMilliseconds);
            }
            catch (Exception ex)
            {
                _logger.Warn(ex, "Failed to Update Plex host: " + settings.Host);
                throw;
            }
        }

        private List<PlexSection> GetSections(PlexServerSettings settings)
        {
            _logger.Debug("Getting sections from Plex host: {0}", settings.Host);

            return _plexServerProxy.GetMovieSections(settings).ToList();
        }

        private bool PartialUpdatesAllowed(PlexServerSettings settings, Version version)
        {
            try
            {
                if (version >= new Version(0, 9, 12, 0))
                {
                    var preferences = GetPreferences(settings);
                    var partialScanPreference = preferences.SingleOrDefault(p => p.Id.Equals("FSEventLibraryPartialScanEnabled"));

                    if (partialScanPreference == null)
                    {
                        return false;
                    }

                    return Convert.ToBoolean(partialScanPreference.Value);
                }
            }
            catch (Exception ex)
            {
                _logger.Warn(ex, "Unable to check if partial updates are allowed");
            }

            return false;
        }

        private bool PathUpdatesAllowed(PlexServerSettings settings, Version version)
        {
            try
            {
                if (version >= new Version(1, 20, 0, 3125))
                {
                    var preferences = GetPreferences(settings);
                    var partialScanPreference = preferences.SingleOrDefault(p => p.Id.Equals("FSEventLibraryPartialScanEnabled"));

                    if (partialScanPreference == null)
                    {
                        return false;
                    }

                    return Convert.ToBoolean(partialScanPreference.Value);
                }
            }
            catch (Exception ex)
            {
                _logger.Warn(ex, "Unable to check if path updates are allowed");
            }

            return false;
        }

        private void ValidateVersion(Version version)
        {
            if (version >= new Version(1, 3, 0) && version < new Version(1, 3, 1))
            {
                throw new PlexVersionException("Found version {0}, upgrade to PMS 1.3.1 to fix library updating and then restart Radarr", version);
            }
        }

        private Version GetVersion(PlexServerSettings settings)
        {
            _logger.Debug("Getting version from Plex host: {0}", settings.Host);

            var rawVersion = _plexServerProxy.Version(settings);
            var version = new Version(Regex.Match(rawVersion, @"^(\d+[.-]){4}").Value.Trim('.', '-'));

            return version;
        }

        private List<PlexPreference> GetPreferences(PlexServerSettings settings)
        {
            _logger.Debug("Getting preferences from Plex host: {0}", settings.Host);

            return _plexServerProxy.Preferences(settings);
        }

        private void UpdateSection(int sectionId, PlexServerSettings settings)
        {
            _logger.Debug("Updating Plex host: {0}, Section: {1}", settings.Host, sectionId);

            _plexServerProxy.Update(sectionId, settings);
        }

        private bool UpdatePartialSection(Movie movie, List<PlexSection> sections, PlexServerSettings settings)
        {
            var partiallyUpdated = false;

            foreach (var section in sections)
            {
                var metadataId = GetMetadataId(section.Id, movie, section.Language, settings);

                if (metadataId.IsNotNullOrWhiteSpace())
                {
                    _logger.Debug("Updating Plex host: {0}, Section: {1}, Movie: {2}", settings.Host, section.Id, movie);
                    _plexServerProxy.UpdateMovie(metadataId, settings);

                    partiallyUpdated = true;
                }
            }

            return partiallyUpdated;
        }

        private bool UpdatePath(Movie movie, List<PlexSection> sections, PlexServerSettings settings)
        {
            var pathUpdated = false;

            var movieLocation = new OsPath(movie.Path);
            var mappedPath = movieLocation;

            if (settings.MapTo.IsNotNullOrWhiteSpace())
            {
                mappedPath = new OsPath(settings.MapTo) + (movieLocation - new OsPath(settings.MapFrom));

                _logger.Trace("Mapping Path from {0} to {1} for partial scan", movieLocation, mappedPath);
            }

            var matchingSection = sections.FirstOrDefault(section => section.Locations.Any(location => location.Path.IsParentPath(mappedPath.FullPath)));

            if (matchingSection != null)
            {
                _logger.Debug("Updating Path on Plex host: {0}, Section: {1}, Path: {2}", settings.Host, matchingSection.Id, mappedPath);

                _plexServerProxy.UpdatePath(mappedPath.FullPath, matchingSection.Id, settings);

                pathUpdated = true;
            }
            else
            {
                _logger.Debug("Unable to update path, mapped movie path {0} is not a child of any plex libraries: {1}", mappedPath, sections.SelectMany(s => s.Locations.Select(l => l.Path)).Join(", "));
            }

            return pathUpdated;
        }

        private string GetMetadataId(int sectionId, Movie movie, string language, PlexServerSettings settings)
        {
            _logger.Debug("Getting metadata from Plex host: {0} for movie: {1}", settings.Host, movie);

            return _plexServerProxy.GetMetadataId(sectionId, movie.ImdbId, language, settings);
        }

        public ValidationFailure Test(PlexServerSettings settings)
        {
            try
            {
                _versionCache.Remove(settings.Host);
                _partialUpdateCache.Remove(settings.Host);
                var sections = GetSections(settings);

                if (sections.Empty())
                {
                    return new ValidationFailure("Host", "At least one Movie library is required");
                }
            }
            catch (PlexAuthenticationException ex)
            {
                _logger.Error(ex, "Unable to connect to Plex Media Server");
                return new ValidationFailure("AuthToken", "Invalid authentication token");
            }
            catch (PlexException ex)
            {
                return new NzbDroneValidationFailure("Host", ex.Message);
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Unable to connect to Plex Media Server");

                return new NzbDroneValidationFailure("Host", "Unable to connect to Plex Media Server")
                       {
                           DetailedDescription = ex.Message
                       };
            }

            return null;
        }
    }
}
