using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Parser.RomanNumerals;

namespace NzbDrone.Core.Parser
{
    public interface IParsingService
    {
        Movie GetMovie(string title);
        MappingResult Map(ParsedMovieInfo parsedMovieInfo, string imdbId, SearchCriteriaBase searchCriteria = null);
        ParsedMovieInfo ParseMovieInfo(string title, List<object> helpers);
        ParsedMovieInfo ParseMinimalMovieInfo(string path, bool isDir = false);
        ParsedMovieInfo ParseMinimalPathMovieInfo(string path);
    }

    public class ParsingService : IParsingService
    {
        private static HashSet<ArabicRomanNumeral> _arabicRomanNumeralMappings;

        private readonly IMovieService _movieService;
        private readonly Logger _logger;

        public ParsingService(IMovieService movieService,
                              Logger logger)
        {
            _movieService = movieService;
            _logger = logger;

            if (_arabicRomanNumeralMappings == null)
            {
                _arabicRomanNumeralMappings = RomanNumeralParser.GetArabicRomanNumeralsMapping();
            }
        }

        public ParsedMovieInfo ParseMovieInfo(string title, List<object> helpers)
        {
            var result = Parser.ParseMovieTitle(title);

            if (result == null)
            {
                return null;
            }

            return result;
        }

        public ParsedMovieInfo ParseMinimalMovieInfo(string file, bool isDir = false)
        {
            return Parser.ParseMovieTitle(file, isDir);
        }

        public ParsedMovieInfo ParseMinimalPathMovieInfo(string path)
        {
            var fileInfo = new FileInfo(path);

            var result = ParseMinimalMovieInfo(fileInfo.Name, true);

            if (result == null)
            {
                _logger.Debug("Attempting to parse movie info using directory and file names. {0}", fileInfo.Directory.Name);
                result = ParseMinimalMovieInfo(fileInfo.Directory.Name + " " + fileInfo.Name);
            }

            if (result == null)
            {
                _logger.Debug("Attempting to parse movie info using directory name. {0}", fileInfo.Directory.Name);
                result = ParseMinimalMovieInfo(fileInfo.Directory.Name + fileInfo.Extension);
            }

            return result;
        }

        public Movie GetMovie(string title)
        {
            var parsedMovieInfo = Parser.ParseMovieTitle(title);

            if (parsedMovieInfo == null)
            {
                return _movieService.FindByTitle(title);
            }

            if (TryGetMovieByTitleAndOrYear(parsedMovieInfo, out var result) && result.MappingResultType == MappingResultType.Success)
            {
                return result.Movie;
            }

            return null;
        }

        public MappingResult Map(ParsedMovieInfo parsedMovieInfo, string imdbId, SearchCriteriaBase searchCriteria = null)
        {
            var result = GetMovie(parsedMovieInfo, imdbId, searchCriteria);

            if (result == null)
            {
                result = new MappingResult { MappingResultType = MappingResultType.Unknown };
                result.Movie = null;
            }

            result.RemoteMovie.ParsedMovieInfo = parsedMovieInfo;

            return result;
        }

        private MappingResult GetMovie(ParsedMovieInfo parsedMovieInfo, string imdbId, SearchCriteriaBase searchCriteria)
        {
            MappingResult result = null;

            if (!string.IsNullOrWhiteSpace(imdbId) && imdbId != "0")
            {
                if (TryGetMovieByImDbId(parsedMovieInfo, imdbId, out result))
                {
                    return result;
                }
            }

            if (searchCriteria != null)
            {
                if (TryGetMovieBySearchCriteria(parsedMovieInfo, searchCriteria, out result))
                {
                    return result;
                }
            }
            else
            {
                if (TryGetMovieByTitleAndOrYear(parsedMovieInfo, out result))
                {
                    return result;
                }
            }

            // nothing found up to here => logging that and returning null
            _logger.Debug($"No matching movie for titles {string.Join(", ", parsedMovieInfo.MovieTitles)} ({parsedMovieInfo.Year})");
            return result;
        }

        private bool TryGetMovieByImDbId(ParsedMovieInfo parsedMovieInfo, string imdbId, out MappingResult result)
        {
            var movie = _movieService.FindByImdbId(imdbId);

            // Should fix practically all problems, where indexer is shite at adding correct imdbids to movies.
            if (movie != null && parsedMovieInfo.Year > 1800 && (parsedMovieInfo.Year != movie.MovieMetadata.Value.Year && movie.MovieMetadata.Value.SecondaryYear != parsedMovieInfo.Year))
            {
                result = new MappingResult { Movie = movie, MappingResultType = MappingResultType.WrongYear };
                return false;
            }

            if (movie != null)
            {
                result = new MappingResult { Movie = movie };
            }
            else
            {
                result = new MappingResult { Movie = movie, MappingResultType = MappingResultType.TitleNotFound };
            }

            return movie != null;
        }

        private bool TryGetMovieByTitleAndOrYear(ParsedMovieInfo parsedMovieInfo, out MappingResult result)
        {
            var candidates = _movieService.FindByTitleCandidates(parsedMovieInfo.MovieTitles, out var otherTitles);

            Movie movieByTitleAndOrYear;
            if (parsedMovieInfo.Year > 1800)
            {
                movieByTitleAndOrYear = _movieService.FindByTitle(parsedMovieInfo.MovieTitles, parsedMovieInfo.Year, otherTitles, candidates);
                if (movieByTitleAndOrYear != null)
                {
                    result = new MappingResult { Movie = movieByTitleAndOrYear };
                    return true;
                }

                // Only default to not using year when one is parsed if only one movie candidate exists
                if (candidates != null && candidates.Count == 1)
                {
                    movieByTitleAndOrYear = _movieService.FindByTitle(parsedMovieInfo.MovieTitles, null, otherTitles, candidates);
                    if (movieByTitleAndOrYear != null)
                    {
                        result = new MappingResult { Movie = movieByTitleAndOrYear, MappingResultType = MappingResultType.WrongYear };
                        return false;
                    }
                }

                result = new MappingResult { Movie = movieByTitleAndOrYear, MappingResultType = MappingResultType.TitleNotFound };
                return false;
            }

            movieByTitleAndOrYear = _movieService.FindByTitle(parsedMovieInfo.MovieTitles, null, otherTitles, candidates);
            if (movieByTitleAndOrYear != null)
            {
                result = new MappingResult { Movie = movieByTitleAndOrYear };
                return true;
            }

            result = new MappingResult { Movie = movieByTitleAndOrYear, MappingResultType = MappingResultType.TitleNotFound };
            return false;
        }

        private bool TryGetMovieBySearchCriteria(ParsedMovieInfo parsedMovieInfo, SearchCriteriaBase searchCriteria, out MappingResult result)
        {
            Movie possibleMovie = null;

            var possibleTitles = new List<string>();

            possibleTitles.Add(searchCriteria.Movie.MovieMetadata.Value.CleanTitle);
            possibleTitles.AddRange(searchCriteria.Movie.MovieMetadata.Value.AlternativeTitles.Select(t => t.CleanTitle));
            possibleTitles.AddRange(searchCriteria.Movie.MovieMetadata.Value.Translations.Select(t => t.CleanTitle));

            var cleanTitle = parsedMovieInfo.PrimaryMovieTitle.CleanMovieTitle();

            foreach (var title in possibleTitles)
            {
                if (title == cleanTitle)
                {
                    possibleMovie = searchCriteria.Movie;
                }

                foreach (var numeralMapping in _arabicRomanNumeralMappings)
                {
                    var arabicNumeral = numeralMapping.ArabicNumeralAsString;
                    var romanNumeral = numeralMapping.RomanNumeralLowerCase;

                    // _logger.Debug(cleanTitle);
                    if (title.Replace(arabicNumeral, romanNumeral) == cleanTitle)
                    {
                        possibleMovie = searchCriteria.Movie;
                    }

                    if (title == cleanTitle.Replace(arabicNumeral, romanNumeral))
                    {
                        possibleMovie = searchCriteria.Movie;
                    }
                }
            }

            if (possibleMovie != null)
            {
                if (parsedMovieInfo.Year < 1800 || possibleMovie.MovieMetadata.Value.Year == parsedMovieInfo.Year || possibleMovie.MovieMetadata.Value.SecondaryYear == parsedMovieInfo.Year)
                {
                    result = new MappingResult { Movie = possibleMovie, MappingResultType = MappingResultType.Success };
                    return true;
                }

                result = new MappingResult { Movie = possibleMovie, MappingResultType = MappingResultType.WrongYear };
                return false;
            }

            result = new MappingResult { Movie = searchCriteria.Movie, MappingResultType = MappingResultType.WrongTitle };

            return false;
        }
    }

    public class MappingResult
    {
        public string Message
        {
            get
            {
                switch (MappingResultType)
                {
                    case MappingResultType.Success:
                        return $"Successfully mapped release name {ReleaseName} to movie {Movie}";
                    case MappingResultType.NotParsable:
                        return $"Failed to find movie title and/or year in release name {ReleaseName}";
                    case MappingResultType.TitleNotFound:
                        return $"Could not find {RemoteMovie.ParsedMovieInfo.PrimaryMovieTitle}";
                    case MappingResultType.WrongYear:
                        var movieYears = new HashSet<int> { RemoteMovie.Movie.MovieMetadata.Value.Year, RemoteMovie.Movie.MovieMetadata.Value.SecondaryYear.GetValueOrDefault() };

                        return $"Failed to map movie, expected year {string.Join(", ", movieYears.Where(x => x > 0))}, but found {RemoteMovie.ParsedMovieInfo.Year}";
                    case MappingResultType.WrongTitle:
                        var comma = RemoteMovie.Movie.MovieMetadata.Value.AlternativeTitles.Count > 0 ? ", " : "";
                        return
                            $"Failed to map movie, found title(s) {string.Join(", ", RemoteMovie.ParsedMovieInfo.MovieTitles)}, expected one of: {RemoteMovie.Movie.MovieMetadata.Value.Title}{comma}{string.Join(", ", RemoteMovie.Movie.MovieMetadata.Value.AlternativeTitles)}";
                    default:
                        return $"Failed to map movie for unknown reasons";
                }
            }
        }

        public RemoteMovie RemoteMovie;
        public MappingResultType MappingResultType { get; set; }
        public Movie Movie
        {
            get
            {
                return RemoteMovie.Movie;
            }
            set
            {
                ParsedMovieInfo parsedInfo = null;
                if (RemoteMovie != null)
                {
                    parsedInfo = RemoteMovie.ParsedMovieInfo;
                }

                RemoteMovie = new RemoteMovie
                {
                    Movie = value,
                    ParsedMovieInfo = parsedInfo
                };
            }
        }

        public string ReleaseName { get; set; }

        public override string ToString()
        {
            return string.Format(Message, RemoteMovie.Movie);
        }

        public Rejection ToRejection()
        {
            switch (MappingResultType)
            {
                case MappingResultType.Success:
                    return null;
                default:
                    return new Rejection(Message);
            }
        }
    }

    public enum MappingResultType
    {
        Unknown = -1,
        Success = 0,
        WrongYear = 2,
        WrongTitle = 3,
        TitleNotFound = 4,
        NotParsable = 5,
    }
}
