using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;

namespace YMhut.Box.Core.Media;

public enum RemoteMediaKind
{
    Unknown,
    Image,
    Audio,
    Video
}

public enum RemoteMediaCatalogLoadSource
{
    Remote,
    Cache
}

public sealed record RemoteMediaCatalog(
    IReadOnlyList<RemoteMediaCategory> Categories,
    RemoteMediaUiConfig UiConfig,
    string LastUpdated,
    string LayoutVersion,
    DateTimeOffset LoadedAt)
{
    public IEnumerable<RemoteMediaCategory> EnabledCategories =>
        Categories.Where(category => category.Enabled);

    public IEnumerable<RemoteMediaEntry> Entries(bool enabledOnly = true)
    {
        foreach (var category in enabledOnly ? EnabledCategories : Categories)
        {
            foreach (var source in category.Sources)
            {
                yield return new RemoteMediaEntry(category, source);
            }
        }
    }
}

public sealed record RemoteMediaEntry(RemoteMediaCategory Category, RemoteMediaSource Source);

public sealed record RemoteMediaCategory(
    string Id,
    string Name,
    string Icon,
    bool Enabled,
    RemoteMediaKind Kind,
    RemoteMediaLayout Layout,
    IReadOnlyList<RemoteMediaSource> Sources)
{
    public string DisplayName => RemoteMediaCatalogNames.CategoryName(Id, Name, Kind);

    public IReadOnlyList<RemoteMediaSource> Subcategories => Sources;
}

public sealed record RemoteMediaSource(
    string Id,
    string Name,
    string Description,
    string ApiUrl,
    string ThumbnailUrl,
    bool Downloadable,
    int RefreshIntervalSeconds,
    IReadOnlyList<string> SupportedFormats,
    RemoteMediaKind Kind)
{
    public bool IsAvailable => Uri.TryCreate(ApiUrl, UriKind.Absolute, out _);

    public string DisplayName => RemoteMediaCatalogNames.SourceName(Id, Name);

    public string DisplayDescription => RemoteMediaCatalogNames.Description(Description);

    public int RefreshInterval => RefreshIntervalSeconds;
}

public sealed record RemoteMediaLayout(
    int Columns,
    string AspectRatio,
    bool ShowPreview,
    bool AutoPlay,
    string TransitionEffect);

public sealed record RemoteMediaUiConfig(
    bool DarkMode,
    bool ShowThumbnails,
    string DefaultView,
    RemoteMediaAnimationConfig Animations);

public sealed record RemoteMediaAnimationConfig(
    string TransitionEffect,
    int DurationMilliseconds);

public sealed record RemoteMediaCatalogLoadResult(
    RemoteMediaCatalog Catalog,
    RemoteMediaCatalogLoadSource Source,
    string? Warning = null);

public static class RemoteMediaCatalogParser
{
    private static readonly JsonDocumentOptions DocumentOptions = new()
    {
        AllowTrailingCommas = true,
        CommentHandling = JsonCommentHandling.Skip
    };

    public static RemoteMediaCatalog Parse(string content, DateTimeOffset? loadedAt = null)
    {
        if (string.IsNullOrWhiteSpace(content))
        {
            throw new InvalidDataException("Remote media configuration is empty.");
        }

        using var document = ParseDocument(content);
        var root = document.RootElement;
        if (root.ValueKind != JsonValueKind.Object ||
            !TryGet(root, out var categoriesElement, "categories") ||
            categoriesElement.ValueKind != JsonValueKind.Array)
        {
            throw new InvalidDataException("Remote media configuration does not contain categories.");
        }

        var uiConfig = ParseUiConfig(root);
        var categories = new List<RemoteMediaCategory>();
        foreach (var categoryElement in categoriesElement.EnumerateArray())
        {
            if (categoryElement.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            var id = JsonString(categoryElement, "id");
            var categoryKind = InferKind(id, []);
            var sources = ParseSources(categoryElement, categoryKind);
            if (categoryKind == RemoteMediaKind.Unknown)
            {
                categoryKind = InferKind(id, sources.SelectMany(source => source.SupportedFormats));
            }

            var normalizedSources = sources
                .Select(source => source.Kind == RemoteMediaKind.Unknown ? source with { Kind = categoryKind } : source)
                .ToArray();

            categories.Add(new RemoteMediaCategory(
                Id: string.IsNullOrWhiteSpace(id) ? $"category-{categories.Count + 1}" : id,
                Name: JsonString(categoryElement, "name"),
                Icon: JsonString(categoryElement, "icon"),
                Enabled: JsonBool(categoryElement, true, "enabled"),
                Kind: categoryKind,
                Layout: ParseLayout(categoryElement, categoryKind, uiConfig),
                Sources: normalizedSources));
        }

        if (categories.Count == 0)
        {
            throw new InvalidDataException("Remote media configuration contains no usable categories.");
        }

        return new RemoteMediaCatalog(
            Categories: categories,
            UiConfig: uiConfig,
            LastUpdated: JsonString(root, "last_updated", "lastUpdated"),
            LayoutVersion: JsonString(root, "layout_version", "layoutVersion"),
            LoadedAt: loadedAt ?? DateTimeOffset.Now);
    }

    public static bool TryParse(string content, out RemoteMediaCatalog catalog)
    {
        try
        {
            catalog = Parse(content);
            return true;
        }
        catch
        {
            catalog = new RemoteMediaCatalog([], DefaultUiConfig(), string.Empty, string.Empty, DateTimeOffset.Now);
            return false;
        }
    }

    private static JsonDocument ParseDocument(string content)
    {
        try
        {
            return JsonDocument.Parse(content, DocumentOptions);
        }
        catch (JsonException)
        {
            var repaired = RepairBrokenQuotedLines(content);
            return JsonDocument.Parse(repaired, DocumentOptions);
        }
    }

    private static string RepairBrokenQuotedLines(string content)
    {
        var normalized = content.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n');
        var lines = normalized.Split('\n');
        for (var index = 0; index < lines.Length; index++)
        {
            lines[index] = RepairBrokenQuotedLine(lines[index]);
        }

        return string.Join('\n', lines);
    }

    private static string RepairBrokenQuotedLine(string line)
    {
        var colon = line.IndexOf(':');
        if (colon < 0)
        {
            return line;
        }

        var valueStart = line.IndexOf('"', colon + 1);
        if (valueStart < 0)
        {
            return line;
        }

        var between = line[(colon + 1)..valueStart];
        if (!string.IsNullOrWhiteSpace(between))
        {
            return line;
        }

        var tail = line[(valueStart + 1)..];
        if (ContainsUnescapedQuote(tail))
        {
            return line;
        }

        var trimmed = line.TrimEnd();
        var endPadding = line.Length - trimmed.Length;
        var insertAt = trimmed.EndsWith(",", StringComparison.Ordinal)
            ? line.LastIndexOf(',', line.Length - endPadding - 1)
            : line.Length - endPadding;
        return insertAt > valueStart
            ? line.Insert(insertAt, "\"")
            : line;
    }

    private static bool ContainsUnescapedQuote(string value)
    {
        var escaped = false;
        foreach (var ch in value)
        {
            if (escaped)
            {
                escaped = false;
                continue;
            }

            if (ch == '\\')
            {
                escaped = true;
                continue;
            }

            if (ch == '"')
            {
                return true;
            }
        }

        return false;
    }

    private static RemoteMediaUiConfig ParseUiConfig(JsonElement root)
    {
        if (!TryGet(root, out var ui, "ui_config", "uiConfig") || ui.ValueKind != JsonValueKind.Object)
        {
            return DefaultUiConfig();
        }

        var animation = DefaultUiConfig().Animations;
        if (TryGet(ui, out var animations, "animations") && animations.ValueKind == JsonValueKind.Object)
        {
            animation = new RemoteMediaAnimationConfig(
                TransitionEffect: NonEmpty(JsonString(animations, "transition_effect", "transitionEffect"), animation.TransitionEffect),
                DurationMilliseconds: Math.Clamp(JsonInt(animations, animation.DurationMilliseconds, "duration", "duration_ms", "durationMilliseconds"), 50, 3000));
        }

        return new RemoteMediaUiConfig(
            DarkMode: JsonBool(ui, false, "dark_mode", "darkMode"),
            ShowThumbnails: JsonBool(ui, true, "show_thumbnails", "showThumbnails"),
            DefaultView: NonEmpty(JsonString(ui, "default_view", "defaultView"), "grid"),
            Animations: animation);
    }

    private static RemoteMediaUiConfig DefaultUiConfig()
    {
        return new RemoteMediaUiConfig(
            DarkMode: false,
            ShowThumbnails: true,
            DefaultView: "grid",
            Animations: new RemoteMediaAnimationConfig("fade", 300));
    }

    private static IReadOnlyList<RemoteMediaSource> ParseSources(JsonElement category, RemoteMediaKind categoryKind)
    {
        if (!TryGet(category, out var subcategories, "subcategories", "sources") ||
            subcategories.ValueKind != JsonValueKind.Array)
        {
            return [];
        }

        var sources = new List<RemoteMediaSource>();
        foreach (var sourceElement in subcategories.EnumerateArray())
        {
            if (sourceElement.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            var id = JsonString(sourceElement, "id");
            var formats = JsonStringArray(sourceElement, "supported_formats", "supportedFormats");
            var kind = InferKind(id, formats);
            if (kind == RemoteMediaKind.Unknown)
            {
                kind = categoryKind;
            }

            if (formats.Count == 0)
            {
                formats = DefaultFormats(kind);
            }

            var apiUrl = JsonString(sourceElement, "api_url", "apiUrl", "url");
            var thumbnailUrl = JsonString(sourceElement, "thumbnail_url", "thumbnailUrl", "thumbnail", "cover");
            sources.Add(new RemoteMediaSource(
                Id: string.IsNullOrWhiteSpace(id) ? $"source-{sources.Count + 1}" : id,
                Name: JsonString(sourceElement, "name"),
                Description: JsonString(sourceElement, "description"),
                ApiUrl: apiUrl,
                ThumbnailUrl: string.IsNullOrWhiteSpace(thumbnailUrl) ? apiUrl : thumbnailUrl,
                Downloadable: JsonBool(sourceElement, true, "downloadable"),
                RefreshIntervalSeconds: NormalizedRefreshInterval(sourceElement, kind),
                SupportedFormats: formats,
                Kind: kind));
        }

        return sources;
    }

    private static RemoteMediaLayout ParseLayout(JsonElement category, RemoteMediaKind kind, RemoteMediaUiConfig uiConfig)
    {
        var columns = 1;
        var aspectRatio = "16:9";
        var showPreview = uiConfig.ShowThumbnails;
        var autoPlay = false;
        var transition = uiConfig.Animations.TransitionEffect;

        if (TryGet(category, out var layout, "layout") && layout.ValueKind == JsonValueKind.Object)
        {
            columns = JsonInt(layout, columns, "columns");
            aspectRatio = NonEmpty(JsonString(layout, "aspect_ratio", "aspectRatio"), aspectRatio);
            showPreview = JsonBool(layout, showPreview, "show_preview", "showPreview");
            autoPlay = JsonBool(layout, false, "auto_play", "autoPlay");
            transition = NonEmpty(JsonString(layout, "transition_effect", "transitionEffect"), transition);
        }

        if (kind == RemoteMediaKind.Audio)
        {
            showPreview = false;
        }

        return new RemoteMediaLayout(
            Columns: Math.Clamp(columns, 1, 4),
            AspectRatio: aspectRatio,
            ShowPreview: showPreview,
            AutoPlay: autoPlay,
            TransitionEffect: transition);
    }

    private static int NormalizedRefreshInterval(JsonElement source, RemoteMediaKind kind)
    {
        var fallback = kind == RemoteMediaKind.Video ? 60 : 30;
        var value = JsonInt(source, fallback, "refresh_interval", "refreshInterval");
        return Math.Clamp(value <= 0 ? fallback : value, 5, 3600);
    }

    private static List<string> DefaultFormats(RemoteMediaKind kind)
    {
        return kind switch
        {
            RemoteMediaKind.Video => ["mp4", "webm"],
            RemoteMediaKind.Audio => ["mp3", "wav", "m4a", "ogg"],
            _ => ["jpg", "jpeg", "png", "webp"]
        };
    }

    public static RemoteMediaKind InferKind(string id, IEnumerable<string> formats)
    {
        var normalizedId = id.Trim().ToLowerInvariant();
        if (normalizedId.Contains("video", StringComparison.Ordinal) ||
            normalizedId.Contains("sp", StringComparison.Ordinal) ||
            normalizedId.Contains("mv", StringComparison.Ordinal))
        {
            return RemoteMediaKind.Video;
        }

        if (normalizedId.Contains("audio", StringComparison.Ordinal) ||
            normalizedId.Contains("music", StringComparison.Ordinal))
        {
            return RemoteMediaKind.Audio;
        }

        if (normalizedId.Contains("image", StringComparison.Ordinal) ||
            normalizedId.Contains("img", StringComparison.Ordinal) ||
            normalizedId.Contains("pic", StringComparison.Ordinal))
        {
            return RemoteMediaKind.Image;
        }

        var kind = RemoteMediaKind.Unknown;
        foreach (var format in formats.Select(format => format.Trim().TrimStart('.').ToLowerInvariant()))
        {
            if (format is "mp4" or "mkv" or "webm" or "avi" or "mov" or "wmv" or "m4v")
            {
                return RemoteMediaKind.Video;
            }

            if (format is "mp3" or "wav" or "flac" or "aac" or "m4a" or "ogg" or "wma")
            {
                kind = RemoteMediaKind.Audio;
            }

            if (kind == RemoteMediaKind.Unknown &&
                format is "png" or "jpg" or "jpeg" or "bmp" or "gif" or "webp" or "tif" or "tiff")
            {
                kind = RemoteMediaKind.Image;
            }
        }

        return kind;
    }

    private static string JsonString(JsonElement root, params string[] names)
    {
        if (!TryGet(root, out var value, names))
        {
            return string.Empty;
        }

        return value.ValueKind switch
        {
            JsonValueKind.String => value.GetString() ?? string.Empty,
            JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False => value.ToString(),
            _ => string.Empty
        };
    }

    private static bool JsonBool(JsonElement root, bool fallback, params string[] names)
    {
        if (!TryGet(root, out var value, names))
        {
            return fallback;
        }

        return value.ValueKind switch
        {
            JsonValueKind.True => true,
            JsonValueKind.False => false,
            JsonValueKind.String when bool.TryParse(value.GetString(), out var parsed) => parsed,
            _ => fallback
        };
    }

    private static int JsonInt(JsonElement root, int fallback, params string[] names)
    {
        if (!TryGet(root, out var value, names))
        {
            return fallback;
        }

        return value.ValueKind switch
        {
            JsonValueKind.Number when value.TryGetInt32(out var parsed) => parsed,
            JsonValueKind.String when int.TryParse(value.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) => parsed,
            _ => fallback
        };
    }

    private static List<string> JsonStringArray(JsonElement root, params string[] names)
    {
        if (!TryGet(root, out var value, names))
        {
            return [];
        }

        if (value.ValueKind == JsonValueKind.Array)
        {
            return value
                .EnumerateArray()
                .Select(item => item.ValueKind == JsonValueKind.String ? item.GetString() ?? string.Empty : item.ToString())
                .Select(item => item.Trim().TrimStart('.').ToLowerInvariant())
                .Where(item => item.Length > 0)
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();
        }

        if (value.ValueKind == JsonValueKind.String)
        {
            return Regex.Split(value.GetString() ?? string.Empty, @"[\s,;/|]+")
                .Select(item => item.Trim().TrimStart('.').ToLowerInvariant())
                .Where(item => item.Length > 0)
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();
        }

        return [];
    }

    private static bool TryGet(JsonElement root, out JsonElement value, params string[] names)
    {
        value = default;
        if (root.ValueKind != JsonValueKind.Object)
        {
            return false;
        }

        foreach (var name in names)
        {
            if (root.TryGetProperty(name, out value))
            {
                return true;
            }
        }

        foreach (var property in root.EnumerateObject())
        {
            if (names.Any(name => string.Equals(name, property.Name, StringComparison.OrdinalIgnoreCase)))
            {
                value = property.Value;
                return true;
            }
        }

        return false;
    }

    private static string NonEmpty(string value, string fallback)
        => string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
}

public static class RemoteMediaCatalogFormatter
{
    public static string FormatRandomSource(RemoteMediaCatalog catalog, string input, string language = "zh-CN")
    {
        var query = (input ?? string.Empty).Trim();
        var entries = catalog
            .Entries()
            .Where(entry => entry.Source.IsAvailable)
            .ToList();

        if (!string.IsNullOrWhiteSpace(query))
        {
            entries = entries
                .Where(entry =>
                    Contains(entry.Category.Id, query) ||
                    Contains(entry.Category.DisplayName, query) ||
                    Contains(entry.Source.Id, query) ||
                    Contains(entry.Source.DisplayName, query) ||
                    Contains(entry.Source.DisplayDescription, query))
                .ToList();
        }

        if (entries.Count == 0)
        {
            throw new InvalidOperationException(T(language, "没有可用的远程媒体源。", "No available remote media sources."));
        }

        var selected = entries[Random.Shared.Next(entries.Count)];
        var source = selected.Source;
        var category = selected.Category;
        var lines = new[]
        {
            T(language, "随机放映室远程源", "Random Cinema remote source"),
            T(language, $"配置：远程媒体目录 {VersionText(catalog)}", $"Configuration: remote media catalog {VersionText(catalog)}"),
            T(language, $"分类：{category.DisplayName} ({category.Id})", $"Category: {category.DisplayName} ({category.Id})"),
            T(language, $"媒体：{source.DisplayName} ({source.Id})", $"Media: {source.DisplayName} ({source.Id})"),
            T(language, $"说明：{source.DisplayDescription}", $"Description: {source.DisplayDescription}"),
            T(language, "媒体源：已隐藏远程地址", "Media source: remote address hidden"),
            T(language, "缩略图：已隐藏远程地址", "Thumbnail: remote address hidden"),
            T(language, $"格式：{string.Join(" / ", source.SupportedFormats)}", $"Formats: {string.Join(" / ", source.SupportedFormats)}"),
            T(language, $"建议刷新间隔：{source.RefreshIntervalSeconds} 秒", $"Refresh interval: {source.RefreshIntervalSeconds} seconds"),
            T(language, source.Downloadable ? "下载：允许" : "下载：禁用", source.Downloadable ? "Download: allowed" : "Download: disabled"),
            T(language,
                $"布局：{category.Layout.Columns} 列 / {category.Layout.AspectRatio} / {(category.Layout.ShowPreview ? "显示预览" : "隐藏预览")} / {(category.Layout.AutoPlay ? "自动播放" : "手动播放")}",
                $"Layout: {category.Layout.Columns} columns / {category.Layout.AspectRatio} / {(category.Layout.ShowPreview ? "preview on" : "preview off")} / {(category.Layout.AutoPlay ? "autoplay" : "manual play")}")
        };
        return string.Join(Environment.NewLine, lines.Where(line => !string.IsNullOrWhiteSpace(line)));
    }

    private static bool Contains(string value, string query)
        => value.Contains(query, StringComparison.OrdinalIgnoreCase);

    private static string VersionText(RemoteMediaCatalog catalog)
        => string.IsNullOrWhiteSpace(catalog.LayoutVersion) ? string.Empty : $"v{catalog.LayoutVersion}";

    private static string T(string language, string zh, string en)
        => string.Equals(language, "en-US", StringComparison.OrdinalIgnoreCase) ? en : zh;
}

internal static class RemoteMediaCatalogNames
{
    private static readonly IReadOnlyDictionary<string, string> SourceFallbacks = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        ["xjj"] = "Portrait",
        ["baisi"] = "Light style",
        ["heisi"] = "Dark style",
        ["acg"] = "ACG 4K",
        ["miku"] = "Miku",
        ["wappller"] = "HD wallpaper",
        ["radom_xjj_leixing"] = "Style videos",
        ["radom_xjj_short"] = "Short videos",
        ["radom_xjj_mv"] = "JK video",
        ["radom_xjj_menv"] = "Random video"
    };

    public static string CategoryName(string id, string name, RemoteMediaKind kind)
    {
        if (!LooksBroken(name))
        {
            return name;
        }

        return kind switch
        {
            RemoteMediaKind.Video => "Random Videos",
            RemoteMediaKind.Audio => "Random Audio",
            RemoteMediaKind.Image => "Random Images",
            _ => string.IsNullOrWhiteSpace(id) ? "Remote Media" : id
        };
    }

    public static string SourceName(string id, string name)
    {
        if (!LooksBroken(name))
        {
            return name;
        }

        return SourceFallbacks.TryGetValue(id, out var fallback)
            ? fallback
            : string.IsNullOrWhiteSpace(id) ? "Remote source" : id;
    }

    public static string Description(string description)
    {
        return LooksBroken(description)
            ? "Remote random media source with reload, preview, and save support."
            : description;
    }

    private static bool LooksBroken(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return true;
        }

        return value.Contains('\uFFFD') ||
            value.Contains("锟", StringComparison.Ordinal) ||
            value.Contains("闅", StringComparison.Ordinal) ||
            value.Contains("濮", StringComparison.Ordinal) ||
            value.Contains("绮", StringComparison.Ordinal) ||
            value.Contains("瑙", StringComparison.Ordinal) ||
            value.Contains("鐭", StringComparison.Ordinal);
    }
}
