using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FluentValidation;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;

namespace NzbDrone.Core.Indexers.Definitions
{
    public class AnimeBytes : TorrentIndexerBase<AnimeBytesSettings>
    {
        public override string Name => "AnimeBytes";
        public override string[] IndexerUrls => new[] { "https://animebytes.tv/" };
        public override string Description => "AnimeBytes (AB) is the largest private torrent tracker that specialises in anime and anime-related content.";
        public override string Language => "en-US";
        public override Encoding Encoding => Encoding.UTF8;
        public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
        public override IndexerCapabilities Capabilities => SetCapabilities();
        public override TimeSpan RateLimit => TimeSpan.FromSeconds(4);

        private readonly ICached<IndexerQueryResult> _queryResultCache;

        public AnimeBytes(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, ICacheManager cacheManager)
            : base(httpClient, eventAggregator, indexerStatusService, configService, logger)
        {
            _queryResultCache = cacheManager.GetCache<IndexerQueryResult>(GetType(), "QueryResults");
        }

        public override IIndexerRequestGenerator GetRequestGenerator()
        {
            return new AnimeBytesRequestGenerator(Settings, Capabilities);
        }

        public override IParseIndexerResponse GetParser()
        {
            return new AnimeBytesParser(Settings);
        }

        protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
        {
            return false;
        }

        protected string BuildQueryResultCacheKey(IndexerRequest request)
        {
            return $"{request.HttpRequest.Url.FullUri}.{HashUtil.ComputeSha256Hash(Settings.ToJson())}";
        }

        protected override async Task<IndexerQueryResult> FetchPage(IndexerRequest request, IParseIndexerResponse parser)
        {
            var cacheKey = BuildQueryResultCacheKey(request);
            var queryResult = _queryResultCache.Find(cacheKey);

            if (queryResult != null)
            {
                queryResult.Cached = true;

                return queryResult;
            }

            _queryResultCache.ClearExpired();

            queryResult = await base.FetchPage(request, parser);
            _queryResultCache.Set(cacheKey, queryResult, TimeSpan.FromMinutes(3));

            return queryResult;
        }

        protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
        {
            var cleanReleases = base.CleanupReleases(releases, searchCriteria);

            if (searchCriteria.IsRssSearch)
            {
                cleanReleases = cleanReleases.Where((r, index) => r.PublishDate > DateTime.UtcNow.AddDays(-1) || index < 20).ToList();
            }

            return cleanReleases.Select(r => (ReleaseInfo)r.Clone()).ToList();
        }

        private IndexerCapabilities SetCapabilities()
        {
            var caps = new IndexerCapabilities
            {
                TvSearchParams = new List<TvSearchParam>
                {
                    TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
                },
                MovieSearchParams = new List<MovieSearchParam>
                {
                    MovieSearchParam.Q
                },
                MusicSearchParams = new List<MusicSearchParam>
                {
                    MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Year
                },
                BookSearchParams = new List<BookSearchParam>
                {
                    BookSearchParam.Q
                }
            };

            caps.Categories.AddCategoryMapping("anime[tv_series]", NewznabStandardCategory.TVAnime, "TV Series");
            caps.Categories.AddCategoryMapping("anime[tv_special]", NewznabStandardCategory.TVAnime, "TV Special");
            caps.Categories.AddCategoryMapping("anime[ova]", NewznabStandardCategory.TVAnime, "OVA");
            caps.Categories.AddCategoryMapping("anime[ona]", NewznabStandardCategory.TVAnime, "ONA");
            caps.Categories.AddCategoryMapping("anime[dvd_special]", NewznabStandardCategory.TVAnime, "DVD Special");
            caps.Categories.AddCategoryMapping("anime[bd_special]", NewznabStandardCategory.TVAnime, "BD Special");
            caps.Categories.AddCategoryMapping("anime[movie]", NewznabStandardCategory.Movies, "Movie");
            caps.Categories.AddCategoryMapping("audio", NewznabStandardCategory.Audio, "Music");
            caps.Categories.AddCategoryMapping("gamec[game]", NewznabStandardCategory.Console, "Game");
            caps.Categories.AddCategoryMapping("gamec[game]", NewznabStandardCategory.PCGames, "Game");
            caps.Categories.AddCategoryMapping("gamec[visual_novel]", NewznabStandardCategory.Console, "Game Visual Novel");
            caps.Categories.AddCategoryMapping("gamec[visual_novel]", NewznabStandardCategory.PCGames, "Game Visual Novel");
            caps.Categories.AddCategoryMapping("printedtype[manga]", NewznabStandardCategory.BooksComics, "Manga");
            caps.Categories.AddCategoryMapping("printedtype[oneshot]", NewznabStandardCategory.BooksComics, "Oneshot");
            caps.Categories.AddCategoryMapping("printedtype[anthology]", NewznabStandardCategory.BooksComics, "Anthology");
            caps.Categories.AddCategoryMapping("printedtype[manhwa]", NewznabStandardCategory.BooksComics, "Manhwa");
            caps.Categories.AddCategoryMapping("printedtype[light_novel]", NewznabStandardCategory.BooksComics, "Light Novel");
            caps.Categories.AddCategoryMapping("printedtype[artbook]", NewznabStandardCategory.BooksComics, "Artbook");

            return caps;
        }
    }

    public class AnimeBytesRequestGenerator : IIndexerRequestGenerator
    {
        private readonly AnimeBytesSettings _settings;
        private readonly IndexerCapabilities _capabilities;

        private static Regex YearRegex => new(@"\b((?:19|20)\d{2})$", RegexOptions.Compiled);

        public AnimeBytesRequestGenerator(AnimeBytesSettings settings, IndexerCapabilities capabilities)
        {
            _settings = settings;
            _capabilities = capabilities;
        }

        public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
            => GetRequestWithSearchType(searchCriteria, "anime");

        public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
            => GetRequestWithSearchType(searchCriteria, "music");

        public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
            => GetRequestWithSearchType(searchCriteria, "anime");

        public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
            => GetRequestWithSearchType(searchCriteria, "anime");

        public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
            => GetRequestWithSearchType(searchCriteria, "anime");

        private IndexerPageableRequestChain GetRequestWithSearchType(SearchCriteriaBase searchCriteria, string searchType)
        {
            var pageableRequests = new IndexerPageableRequestChain();

            pageableRequests.Add(GetRequest(searchCriteria, searchType));

            return pageableRequests;
        }

        private IEnumerable<IndexerRequest> GetRequest(SearchCriteriaBase searchCriteria, string searchType)
        {
            var searchUrl = $"{_settings.BaseUrl.TrimEnd('/')}/scrape.php";

            var term = searchCriteria.SanitizedSearchTerm.Trim();
            var searchTerm = CleanSearchTerm(term);

            var parameters = new NameValueCollection
            {
                { "username", _settings.Username },
                { "torrent_pass", _settings.Passkey },
                { "sort", "grouptime" },
                { "way", "desc" },
                { "type", searchType },
                { "searchstr", searchTerm },
                { "limit", searchTerm.IsNotNullOrWhiteSpace() ? "50" : "20" }
            };

            if (_settings.SearchByYear && searchType == "anime")
            {
                var searchYear = ParseYearFromSearchTerm(term);

                if (searchYear is > 0)
                {
                    parameters.Set("year", searchYear.ToString());
                }
            }

            if (searchType == "music" && searchCriteria is MusicSearchCriteria musicSearchCriteria)
            {
                if (musicSearchCriteria.Artist.IsNotNullOrWhiteSpace() && musicSearchCriteria.Artist != "VA")
                {
                    parameters.Set("artistnames", musicSearchCriteria.Artist);
                }

                if (musicSearchCriteria.Album.IsNotNullOrWhiteSpace())
                {
                    parameters.Set("groupname", musicSearchCriteria.Album);
                }

                if (musicSearchCriteria.Year is > 0)
                {
                    parameters.Set("year", musicSearchCriteria.Year.ToString());
                }
            }

            var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList();

            if (queryCats.Any() && searchCriteria is TvSearchCriteria { Season: > 0 })
            {
                // Avoid searching for specials if it's a non-zero season search
                queryCats.RemoveAll(cat => cat is "anime[tv_special]" or "anime[ova]" or "anime[dvd_special]" or "anime[bd_special]");
            }

            if (queryCats.Any())
            {
                queryCats.ForEach(cat => parameters.Set(cat, "1"));
            }

            if (_settings.FreeleechOnly)
            {
                parameters.Set("freeleech", "1");
            }

            if (_settings.ExcludeHentai && searchType == "anime")
            {
                parameters.Set("hentai", "0");
            }

            searchUrl += "?" + parameters.GetQueryString();

            yield return new IndexerRequest(searchUrl, HttpAccept.Json);
        }

        private static string CleanSearchTerm(string term)
        {
            // Tracer does not support searching with episode number so strip it if we have one
            term = Regex.Replace(term, @"\W(\dx)?\d?\d$", string.Empty, RegexOptions.Compiled);
            term = Regex.Replace(term, @"\W(S\d\d?E)?\d?\d$", string.Empty, RegexOptions.Compiled);
            term = Regex.Replace(term, @"\W\d+$", string.Empty, RegexOptions.Compiled);

            term = Regex.Replace(term.Trim(), @"\bThe Movie$", string.Empty, RegexOptions.Compiled | RegexOptions.IgnoreCase);

            return term.Trim();
        }

        private static int? ParseYearFromSearchTerm(string term)
        {
            if (term.IsNullOrWhiteSpace())
            {
                return null;
            }

            var yearMatch = YearRegex.Match(term);

            if (!yearMatch.Success)
            {
                return null;
            }

            return ParseUtil.CoerceInt(yearMatch.Groups[1].Value);
        }

        public Func<IDictionary<string, string>> GetCookies { get; set; }
        public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
    }

    public class AnimeBytesParser : IParseIndexerResponse
    {
        private static readonly HashSet<string> ExcludedProperties = new(StringComparer.OrdinalIgnoreCase) { "Freeleech" };
        private static readonly HashSet<string> RemuxResolutions = new(StringComparer.OrdinalIgnoreCase) { "1080i", "1080p", "2160p", "4K" };
        private static readonly HashSet<string> CommonReleaseGroupsProperties = new(StringComparer.OrdinalIgnoreCase)
        {
            "Softsubs",
            "Hardsubs",
            "RAW",
            "Translated"
        };
        private static readonly HashSet<string> ExcludedFileExtensions = new(StringComparer.OrdinalIgnoreCase) { ".mka", ".mds", ".md5", ".nfo", ".sfv", ".ass", ".mks", ".srt", ".ssa", ".sup", ".jpeg", ".jpg", ".png", ".otf", ".ttf" };

        private static readonly string[] PropertiesSeparator = { " | ", " / " };

        private readonly AnimeBytesSettings _settings;

        public AnimeBytesParser(AnimeBytesSettings settings)
        {
            _settings = settings;
        }

        public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
        {
            var releaseInfos = new List<ReleaseInfo>();

            if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
            {
                throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
            }

            if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
            {
                throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}");
            }

            var response = STJson.Deserialize<AnimeBytesResponse>(indexerResponse.Content);

            if (response.Error.IsNotNullOrWhiteSpace())
            {
                throw new IndexerException(indexerResponse, "Unexpected response from indexer request: {0}", response.Error);
            }

            if (response.Matches == 0)
            {
                return releaseInfos.ToArray();
            }

            foreach (var group in response.Groups)
            {
                var categoryName = group.CategoryName;
                var description = group.Description;
                var year = group.Year;
                var groupName = group.GroupName;
                var seriesName = group.SeriesName;
                var mainTitle = WebUtility.HtmlDecode(group.FullName);

                if (seriesName.IsNotNullOrWhiteSpace())
                {
                    mainTitle = seriesName;
                }

                var synonyms = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
                {
                    mainTitle
                };

                if (group.Synonymns != null && group.Synonymns.Any())
                {
                    if (_settings.AddJapaneseTitle && group.Synonymns.TryGetValue("Japanese", out var japaneseTitle) && japaneseTitle.IsNotNullOrWhiteSpace())
                    {
                        synonyms.Add(japaneseTitle.Trim());
                    }

                    if (_settings.AddRomajiTitle && group.Synonymns.TryGetValue("Romaji", out var romajiTitle) && romajiTitle.IsNotNullOrWhiteSpace())
                    {
                        synonyms.Add(romajiTitle.Trim());
                    }

                    if (_settings.AddAlternativeTitle && group.Synonymns.TryGetValue("Alternative", out var alternativeTitle) && alternativeTitle.IsNotNullOrWhiteSpace())
                    {
                        synonyms.UnionWith(alternativeTitle.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
                    }
                }

                List<IndexerCategory> categories = null;

                foreach (var torrent in group.Torrents)
                {
                    // Skip non-freeleech results when freeleech only is set
                    if (_settings.FreeleechOnly && torrent.RawDownMultiplier != 0)
                    {
                        continue;
                    }

                    var torrentId = torrent.Id;
                    var link = torrent.Link;
                    var publishDate = DateTime.ParseExact(torrent.UploadTime, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
                    var details = new Uri(_settings.BaseUrl + "torrent/" + torrentId + "/group");
                    var size = torrent.Size;
                    var snatched = torrent.Snatched;
                    var seeders = torrent.Seeders;
                    var leechers = torrent.Leechers;
                    var fileCount = torrent.FileCount;
                    var peers = seeders + leechers;
                    var rawDownMultiplier = torrent.RawDownMultiplier;
                    var rawUpMultiplier = torrent.RawUpMultiplier;

                    // MST with additional 5 hours per GB
                    var minimumSeedTime = 259200 + (int)(size / (int)Math.Pow(1024, 3) * 18000);

                    var propertyList = WebUtility.HtmlDecode(torrent.Property)
                        .Split(PropertiesSeparator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
                        .ToList();

                    propertyList.RemoveAll(p => ExcludedProperties.Any(p.ContainsIgnoreCase));
                    var properties = propertyList.ToHashSet();

                    if (properties.Any(p => p.StartsWith("M2TS", StringComparison.Ordinal)))
                    {
                        properties.Add("BR-DISK");
                    }

                    var isBluRayDisk = properties.Any(p => p.Equals("RAW", StringComparison.Ordinal) || p.StartsWith("M2TS", StringComparison.Ordinal) || p.StartsWith("ISO", StringComparison.Ordinal));

                    if (_settings.ExcludeRaw && isBluRayDisk)
                    {
                        continue;
                    }

                    properties = properties
                        .Select(property =>
                        {
                            if (isBluRayDisk)
                            {
                                property = Regex.Replace(property, @"\b(H\.?265)\b", "HEVC", RegexOptions.Compiled | RegexOptions.IgnoreCase);
                                property = Regex.Replace(property, @"\b(H\.?264)\b", "AVC", RegexOptions.Compiled | RegexOptions.IgnoreCase);
                            }

                            if (torrent.Files.Any(f => f.FileName.ContainsIgnoreCase("Remux"))
                                && RemuxResolutions.ContainsIgnoreCase(property))
                            {
                                property += " Remux";
                            }

                            return property;
                        })
                        .ToHashSet();

                    int? season = null;
                    int? episode = null;

                    var releaseInfo = _settings.EnableSonarrCompatibility && categoryName == "Anime" ? "S01" : "";
                    var editionTitle = torrent.EditionData?.EditionTitle;

                    if (editionTitle.IsNotNullOrWhiteSpace())
                    {
                        releaseInfo = WebUtility.HtmlDecode(editionTitle);

                        if (_settings.EnableSonarrCompatibility)
                        {
                            var simpleSeasonRegex = new Regex(@"\bSeason (\d+)\b", RegexOptions.Compiled);
                            var simpleSeasonRegexMatch = simpleSeasonRegex.Match(releaseInfo);
                            if (simpleSeasonRegexMatch.Success)
                            {
                                season = ParseUtil.CoerceInt(simpleSeasonRegexMatch.Groups[1].Value);
                            }
                        }

                        var episodeRegex = new Regex(@"\bEpisode (\d+)\b", RegexOptions.Compiled);
                        var episodeRegexMatch = episodeRegex.Match(releaseInfo);
                        if (episodeRegexMatch.Success)
                        {
                            episode = ParseUtil.CoerceInt(episodeRegexMatch.Groups[1].Value);
                        }
                    }

                    if (_settings.EnableSonarrCompatibility && categoryName == "Anime")
                    {
                        season ??= ParseSeasonFromTitles(synonyms);
                    }

                    if (episode is > 0 && season == null)
                    {
                        releaseInfo = $" - {episode:00}";
                    }
                    else if (_settings.EnableSonarrCompatibility && season is > 0)
                    {
                        releaseInfo = $"S{season:00}";

                        if (episode is > 0)
                        {
                            releaseInfo += $"E{episode:00} - {episode:00}";
                        }
                    }

                    releaseInfo = releaseInfo.Trim();

                    // Ignore these categories as they'll cause hell with the matcher
                    // TV Special, DVD Special, BD Special
                    if (groupName is "TV Special" or "DVD Special" or "BD Special")
                    {
                        continue;
                    }

                    if (groupName is "TV Series" or "OVA" or "ONA")
                    {
                        categories = new List<IndexerCategory> { NewznabStandardCategory.TVAnime };
                    }

                    if (groupName is "Movie" or "Live Action Movie")
                    {
                        categories = new List<IndexerCategory> { NewznabStandardCategory.Movies };
                    }

                    if (categoryName is "Manga" or "Oneshot" or "Anthology" or "Manhwa" or "Manhua" or "Light Novel")
                    {
                        categories = new List<IndexerCategory> { NewznabStandardCategory.BooksComics };
                    }

                    if (categoryName is "Novel" or "Artbook")
                    {
                        categories = new List<IndexerCategory> { NewznabStandardCategory.BooksComics };
                    }

                    if (categoryName is "Game" or "Visual Novel")
                    {
                        if (properties.Contains("PSP"))
                        {
                            categories = new List<IndexerCategory> { NewznabStandardCategory.Console, NewznabStandardCategory.ConsolePSP };
                        }

                        if (properties.Contains("PS3"))
                        {
                            categories = new List<IndexerCategory> { NewznabStandardCategory.Console, NewznabStandardCategory.ConsolePS3 };
                        }

                        if (properties.Contains("PS Vita"))
                        {
                            categories = new List<IndexerCategory> { NewznabStandardCategory.Console, NewznabStandardCategory.ConsolePSVita };
                        }

                        if (properties.Contains("3DS"))
                        {
                            categories = new List<IndexerCategory> { NewznabStandardCategory.Console, NewznabStandardCategory.Console3DS };
                        }

                        if (properties.Contains("NDS"))
                        {
                            categories = new List<IndexerCategory> { NewznabStandardCategory.Console, NewznabStandardCategory.ConsoleNDS };
                        }

                        if (properties.Contains("PSX") || properties.Contains("PS2") || properties.Contains("SNES") ||
                            properties.Contains("NES") || properties.Contains("GBA") || properties.Contains("Switch") ||
                            properties.Contains("N64"))
                        {
                            categories = new List<IndexerCategory> { NewznabStandardCategory.Console, NewznabStandardCategory.ConsoleOther };
                        }

                        if (properties.Contains("PC"))
                        {
                            categories = new List<IndexerCategory> { NewznabStandardCategory.PCGames };
                        }
                    }

                    if (categoryName is "Single" or "EP" or "Album" or "Compilation" or "Soundtrack" or "Remix CD" or "PV" or "Live Album" or "Image CD" or "Drama CD" or "Vocal CD")
                    {
                        if (properties.Any(p => p.Contains("Lossless")))
                        {
                            categories = new List<IndexerCategory> { NewznabStandardCategory.Audio, NewznabStandardCategory.AudioLossless };
                        }
                        else if (properties.Any(p => p.Contains("MP3")))
                        {
                            categories = new List<IndexerCategory> { NewznabStandardCategory.Audio, NewznabStandardCategory.AudioMP3 };
                        }
                        else
                        {
                            categories = new List<IndexerCategory> { NewznabStandardCategory.Audio, NewznabStandardCategory.AudioOther };
                        }
                    }

                    // We don't actually have a release name >.> so try to create one
                    var releaseGroup = properties.LastOrDefault(p => CommonReleaseGroupsProperties.Any(p.StartsWithIgnoreCase) && p.Contains('(') && p.Contains(')'));

                    if (releaseGroup.IsNotNullOrWhiteSpace())
                    {
                        var start = releaseGroup.IndexOf("(", StringComparison.Ordinal);
                        releaseGroup = "[" + releaseGroup.Substring(start + 1, releaseGroup.IndexOf(")", StringComparison.Ordinal) - 1 - start) + "] ";
                    }
                    else
                    {
                        releaseGroup = string.Empty;
                    }

                    var infoString = properties.Select(p => "[" + p + "]").Join(string.Empty);

                    if (_settings.UseFilenameForSingleEpisodes)
                    {
                        var files = torrent.Files.ToList();

                        if (files.Count > 1)
                        {
                            files = files.Where(f => !ExcludedFileExtensions.Contains(Path.GetExtension(f.FileName))).ToList();
                        }

                        if (files.Count == 1)
                        {
                            var fileName = files.First().FileName;

                            var guid = new Uri(details + "?nh=" + HashUtil.CalculateMd5(fileName));

                            var release = new TorrentInfo
                            {
                                MinimumRatio = 1,
                                MinimumSeedTime = minimumSeedTime,
                                Title = fileName,
                                Year = year.GetValueOrDefault(),
                                InfoUrl = details.AbsoluteUri,
                                Guid = guid.AbsoluteUri,
                                DownloadUrl = link.AbsoluteUri,
                                PublishDate = publishDate,
                                Categories = categories,
                                Description = description,
                                Size = size,
                                Seeders = seeders,
                                Peers = peers,
                                Grabs = snatched,
                                Files = fileCount,
                                DownloadVolumeFactor = rawDownMultiplier,
                                UploadVolumeFactor = rawUpMultiplier,
                            };

                            releaseInfos.Add(release);
                        }
                    }

                    var useYearInTitle = year is > 0 && torrent.Files.Any(f => f.FileName.Contains(year.Value.ToString()));

                    foreach (var title in synonyms)
                    {
                        var releaseTitle = groupName is "Movie" or "Live Action Movie" ?
                            $"{releaseGroup}{title} {year} {infoString}" :
                            $"{releaseGroup}{title}{(useYearInTitle ? $" {year}" : string.Empty)} {releaseInfo} {infoString}";

                        var guid = new Uri(details + "?nh=" + HashUtil.CalculateMd5(title));

                        var release = new TorrentInfo
                        {
                            MinimumRatio = 1,
                            MinimumSeedTime = minimumSeedTime,
                            Title = releaseTitle.Trim(),
                            Year = year.GetValueOrDefault(),
                            InfoUrl = details.AbsoluteUri,
                            Guid = guid.AbsoluteUri,
                            DownloadUrl = link.AbsoluteUri,
                            PublishDate = publishDate,
                            Categories = categories,
                            Description = description,
                            Size = size,
                            Seeders = seeders,
                            Peers = peers,
                            Grabs = snatched,
                            Files = fileCount,
                            DownloadVolumeFactor = rawDownMultiplier,
                            UploadVolumeFactor = rawUpMultiplier,
                        };

                        releaseInfos.Add(release);
                    }
                }
            }

            return releaseInfos
                .OrderByDescending(o => o.PublishDate)
                .ToArray();
        }

        private static int? ParseSeasonFromTitles(IReadOnlyCollection<string> titles)
        {
            var advancedSeasonRegex = new Regex(@"\b(?:(?<season>\d+)(?:st|nd|rd|th) Season|Season (?<season>\d+))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
            var seasonCharactersRegex = new Regex(@"(I{2,})$", RegexOptions.Compiled);
            var seasonNumberRegex = new Regex(@"\b(?<!Part[- ._])(?<!\d[/])(?:S)?(?<season>[2-9])$", RegexOptions.Compiled);

            foreach (var title in titles)
            {
                var advancedSeasonRegexMatch = advancedSeasonRegex.Match(title);
                if (advancedSeasonRegexMatch.Success)
                {
                    return ParseUtil.CoerceInt(advancedSeasonRegexMatch.Groups["season"].Value);
                }

                var seasonCharactersRegexMatch = seasonCharactersRegex.Match(title);
                if (seasonCharactersRegexMatch.Success)
                {
                    return seasonCharactersRegexMatch.Groups[1].Value.Length;
                }

                var seasonNumberRegexMatch = seasonNumberRegex.Match(title);
                if (seasonNumberRegexMatch.Success)
                {
                    return ParseUtil.CoerceInt(seasonNumberRegexMatch.Groups["season"].Value);
                }
            }

            return null;
        }

        public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
    }

    public class AnimeBytesSettingsValidator : NoAuthSettingsValidator<AnimeBytesSettings>
    {
        public AnimeBytesSettingsValidator()
        {
            RuleFor(c => c.Username).NotEmpty();

            RuleFor(c => c.Passkey).NotEmpty()
                .Must(x => x.Length is 32 or 48)
                .WithMessage("Passkey length must be 32 or 48");
        }
    }

    public class AnimeBytesSettings : NoAuthTorrentBaseSettings
    {
        private static readonly AnimeBytesSettingsValidator Validator = new();

        public AnimeBytesSettings()
        {
            Username = "";
            Passkey = "";
            FreeleechOnly = false;
            ExcludeRaw = false;
            ExcludeHentai = false;
            SearchByYear = false;
            EnableSonarrCompatibility = true;
            UseFilenameForSingleEpisodes = true;
            AddJapaneseTitle = true;
            AddRomajiTitle = true;
            AddAlternativeTitle = true;
        }

        [FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
        public string Username { get; set; }

        [FieldDefinition(3, Label = "Passkey", HelpText = "Site Passkey", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
        public string Passkey { get; set; }

        [FieldDefinition(4, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech torrents only")]
        public bool FreeleechOnly { get; set; }

        [FieldDefinition(5, Label = "Exclude RAW", Type = FieldType.Checkbox, HelpText = "Exclude RAW torrents from results")]
        public bool ExcludeRaw { get; set; }

        [FieldDefinition(6, Label = "Exclude Hentai", Type = FieldType.Checkbox, HelpText = "Exclude Hentai torrents from results")]
        public bool ExcludeHentai { get; set; }

        [FieldDefinition(7, Label = "Search By Year", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr to search by year as a different argument in the request.")]
        public bool SearchByYear { get; set; }

        [FieldDefinition(8, Label = "Enable Sonarr Compatibility", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr try to add Season information into Release names, without this Sonarr can't match any Seasons, but it has a lot of false positives as well")]
        public bool EnableSonarrCompatibility { get; set; }

        [FieldDefinition(9, Label = "Use Filenames for Single Episodes", Type = FieldType.Checkbox, HelpText = "Add a release using the actual filename, this currently only works for single episode releases")]
        public bool UseFilenameForSingleEpisodes { get; set; }

        [FieldDefinition(10, Label = "Add Japanese title as a synonym", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr add Japanese titles as synonyms, i.e kanji/hiragana/katakana.")]
        public bool AddJapaneseTitle { get; set; }

        [FieldDefinition(11, Label = "Add Romaji title as a synonym", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr add Romaji title as a synonym, i.e \"Shingeki no Kyojin\" with Attack on Titan")]
        public bool AddRomajiTitle { get; set; }

        [FieldDefinition(12, Label = "Add alternative title as a synonym", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr add alternative title as a synonym, i.e \"AoT\" with Attack on Titan, but also \"Attack on Titan Season 4\" Instead of \"Attack on Titan: The Final Season\"")]
        public bool AddAlternativeTitle { get; set; }

        public override NzbDroneValidationResult Validate()
        {
            return new NzbDroneValidationResult(Validator.Validate(this));
        }
    }

    public class AnimeBytesResponse
    {
        [JsonPropertyName("Matches")]
        public int Matches { get; set; }

        [JsonPropertyName("Groups")]
        public IReadOnlyCollection<AnimeBytesGroup> Groups { get; set; }

        public string Error { get; set; }
    }

    public class AnimeBytesGroup
    {
        [JsonPropertyName("ID")]
        public long Id { get; set; }

        [JsonPropertyName("CategoryName")]
        public string CategoryName { get; set; }

        [JsonPropertyName("FullName")]
        public string FullName { get; set; }

        [JsonPropertyName("GroupName")]
        public string GroupName { get; set; }

        [JsonPropertyName("SeriesName")]
        public string SeriesName { get; set; }

        [JsonPropertyName("Year")]
        [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
        public int? Year { get; set; }

        [JsonPropertyName("Image")]
        public string Image { get; set; }

        [JsonPropertyName("SynonymnsV2")]
        public IReadOnlyDictionary<string, string> Synonymns { get; set; }

        [JsonPropertyName("Description")]
        public string Description { get; set; }

        [JsonPropertyName("Tags")]
        public IReadOnlyCollection<string> Tags { get; set; }

        [JsonPropertyName("Torrents")]
        public IReadOnlyCollection<AnimeBytesTorrent> Torrents { get; set; }
    }

    public class AnimeBytesTorrent
    {
        [JsonPropertyName("ID")]
        public long Id { get; set; }

        [JsonPropertyName("EditionData")]
        public AnimeBytesEditionData EditionData { get; set; }

        [JsonPropertyName("RawDownMultiplier")]
        public double RawDownMultiplier { get; set; }

        [JsonPropertyName("RawUpMultiplier")]
        public double RawUpMultiplier { get; set; }

        [JsonPropertyName("Link")]
        public Uri Link { get; set; }

        [JsonPropertyName("Property")]
        public string Property { get; set; }

        [JsonPropertyName("Snatched")]
        public int Snatched { get; set; }

        [JsonPropertyName("Seeders")]
        public int Seeders { get; set; }

        [JsonPropertyName("Leechers")]
        public int Leechers { get; set; }

        [JsonPropertyName("Size")]
        public long Size { get; set; }

        [JsonPropertyName("FileCount")]
        public int FileCount { get; set; }

        [JsonPropertyName("FileList")]
        public IReadOnlyCollection<AnimeBytesFile> Files  { get; set; }

        [JsonPropertyName("UploadTime")]
        public string UploadTime { get; set; }
    }

    public class AnimeBytesFile
    {
        [JsonPropertyName("filename")]
        public string FileName { get; set; }

        [JsonPropertyName("size")]
        public long FileSize { get; set; }
    }

    public class AnimeBytesEditionData
    {
        [JsonPropertyName("EditionTitle")]
        public string EditionTitle { get; set; }
    }
}
