using System.Text.RegularExpressions;

namespace TeleWave.Application.Media.MovieImport;

/// <summary>Что удалось вытащить из имени файла или папки релиза.</summary>
/// <param name="Title">Название для поиска в источнике; пустым не бывает.</param>
/// <param name="Year">Год, если он в имени был. Главный признак совпадения при подборе.</param>
public sealed record ParsedMovieName(string Title, int? Year);

/// <summary>
/// Разбирает имя релиза на название и год: «Voron.1994.TRIPLE.BDRip.XviD.AC3» → «Voron» + 1994.
///
/// Порядок разбора не случаен. Сначала отрезается хвост релиза по первому же техническому маркеру
/// (rip, кодек, разрешение, «by_...»), и только потом в остатке ищется год: иначе «Edward
/// Scissorhands by_IVAN@1990» дало бы название «Edward Scissorhands by IVAN@», а год в никнейме
/// релизера сошёл бы за год выпуска.
///
/// Разбор заведомо неточен — имена пишут люди и роботы десятка трекеров, — поэтому он и не обязан
/// быть точным: его результат попадает в таблицу, где строку правят руками. Задача парсера — чтобы
/// правил человек единицы строк, а не все.
/// </summary>
public static partial class MovieNameParser
{
    /// <summary>
    /// Технические маркеры релиза: с первого из них начинается хвост, к названию не относящийся.
    /// Сравниваются целыми словами, поэтому «Triple» из «TRIPLERUS» сюда не попадёт — и правильно,
    /// такие склейки к названию тоже не относятся, но резать по ним рискованно.
    /// </summary>
    private static readonly IReadOnlySet<string> Markers = new HashSet<string>(
        StringComparer.OrdinalIgnoreCase
    )
    {
        // Источник картинки. «web» и «rip» сами по себе не маркеры: «Charlotte's Web» — название,
        // и обрезать по такому слову дороже, чем оставить лишний хвост.
        "bdrip",
        "brrip",
        "bdremux",
        "remux",
        "bluray",
        "bdrip-avc",
        "dvdrip",
        "dvd5",
        "dvd9",
        "hdrip",
        "hdtv",
        "webrip",
        "web-dl",
        "webdl",
        "tvrip",
        "satrip",
        "camrip",
        "telesync",
        "scan",
        // Кодеки и звук
        "xvid",
        "divx",
        "x264",
        "x265",
        "h264",
        "h265",
        "avc",
        "hevc",
        "ac3",
        "aac",
        "dts",
        "mp3",
        "flac",
        "opus",
        // Разрешение и мастеринг
        "480p",
        "720p",
        "1080p",
        "1080i",
        "2160p",
        "4k",
        "uhd",
        "hdr",
        "sdr",
        "10bit",
        // Издание и озвучка
        "remastered",
        "extended",
        "theatrical",
        "unrated",
        "directors",
        "proper",
        "repack",
        "dual",
        "triple",
        "dub",
        "dubbed",
        "subbed",
        "hybrid",
        "licence",
        // Кто собрал
        "by",
    };

    /// <summary>Символы, которые остаются висеть на краях названия после отрезания хвоста.</summary>
    private static readonly char[] Edges =
    [
        '-',
        '–',
        '—',
        '_',
        '.',
        ',',
        ':',
        ';',
        '(',
        '[',
        '{',
        ')',
        ']',
        '}',
        '\'',
    ];

    [GeneratedRegex(@"(?<!\d)(19|20)\d{2}(?!\d)", RegexOptions.CultureInvariant)]
    private static partial Regex YearPattern();

    [GeneratedRegex(@"\s{2,}", RegexOptions.CultureInvariant)]
    private static partial Regex ExtraSpaces();

    /// <summary>
    /// Разбирает имя файла или папки. Расширение отрезается только знакомое: у папки
    /// «Voron.1994.TRIPLE.BDRip.XviD.AC3» иначе «расширением» стало бы «.AC3», а с ним ушёл бы
    /// кусок имени.
    /// </summary>
    public static ParsedMovieName Parse(string fileOrFolderName)
    {
        if (string.IsNullOrWhiteSpace(fileOrFolderName))
            return new ParsedMovieName(string.Empty, null);

        var text = MediaFormats.IsAllowed(fileOrFolderName)
            ? Path.GetFileNameWithoutExtension(fileOrFolderName)
            : fileOrFolderName;

        // Разделители релизов — точка и подчёркивание; дефис оставляем, он часто внутри названия
        // («Blade Runner 2049 - Final Cut»), а хвост всё равно отрежется по маркеру.
        var head = Head(text.Replace('.', ' ').Replace('_', ' '));

        var year = Year(head, out var beforeYear);
        var title = Clean(year is null ? head : beforeYear);

        // Название из одного года — это и есть название («2012», «1917»): в таком имени резать нечего.
        return new ParsedMovieName(title.Length > 0 ? title : Clean(head), year);
    }

    /// <summary>Часть имени до первого технического маркера.</summary>
    private static string Head(string text)
    {
        var words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        var taken = new List<string>(words.Length);

        foreach (var word in words)
        {
            var bare = word.Trim(Edges);
            // Первое слово маркером не считаем: названия «Extended Family» и «Dual» существуют,
            // а название из нуля слов — нет.
            var marker = taken.Count > 0 && Markers.Contains(bare);
            // Ник релизера — «by_IVAN@1990», «[Kubik@rutracker]»: собачка в слове бывает только там.
            if (marker || bare.Contains('@', StringComparison.Ordinal))
                break;
            taken.Add(word);
        }

        // Имя целиком из маркеров разобрать нечем — пусть человек видит его как есть.
        return taken.Count > 0 ? string.Join(' ', taken) : text;
    }

    /// <summary>
    /// Год выпуска и всё, что стоит до него. Год в самом начале не в счёт: «2012 2009 BDRip» —
    /// это фильм «2012» девятого года, а не наоборот.
    /// </summary>
    private static int? Year(string text, out string beforeYear)
    {
        beforeYear = text;

        foreach (Match match in YearPattern().Matches(text))
        {
            if (match.Index == 0)
                continue;

            beforeYear = text[..match.Index];
            return int.Parse(match.Value, System.Globalization.CultureInfo.InvariantCulture);
        }

        return null;
    }

    private static string Clean(string title) =>
        ExtraSpaces().Replace(title, " ").Trim().Trim(Edges).Trim();
}
