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

namespace YMhut.Box.Core.Tools;

public enum ToolResultBlockKind
{
    Text,
    KeyValue,
    Table,
    RankedList,
    NewsList,
    Image,
    Link,
    File,
    Diff,
    Status,
    Code,
    Json,
    JsonTree,
    Metric,
    LineChart,
    CardList,
    Timeline,
    Media,
    Color,
    Raw
}

public sealed record ToolResultDocument(
    string ToolId,
    ToolResultKind ResultKind,
    string RawText,
    IReadOnlyList<ToolResultBlock> Blocks,
    IReadOnlyDictionary<string, string> Metadata,
    string RawPayload = "",
    string SourceName = "",
    string Status = "")
{
    public static ToolResultDocument FromOutput(IToolModule module, string output)
    {
        var spec = ToolPageSpecCatalog.For(module);
        var lines = Lines(output);
        var blocks = spec.Result switch
        {
            ToolResultKind.JsonTree => JsonBlocks(output),
            ToolResultKind.CodePreview => [ToolResultBlock.Code("Code", output)],
            ToolResultKind.Table or ToolResultKind.CalculatorTable => TableBlocks(lines),
            ToolResultKind.RankedList => RankedBlocks(lines),
            ToolResultKind.NewsCards => NewsBlocks(lines),
            ToolResultKind.ImagePreview => MediaBlocks(lines, imageOnly: true),
            ToolResultKind.LinkCards => LinkBlocks(lines),
            ToolResultKind.FileCards => FileBlocks(lines),
            ToolResultKind.Diff => DiffBlocks(lines),
            ToolResultKind.StatusList => StatusBlocks(lines),
            ToolResultKind.ColorSwatch => ColorBlocks(lines),
            ToolResultKind.SystemCards or ToolResultKind.KeyValueCards or ToolResultKind.ReferenceRows => KeyValueBlocks(lines),
            ToolResultKind.Media => MediaBlocks(lines, imageOnly: false),
            _ => TextBlocks(lines)
        };

        if (blocks.Count == 0)
        {
            blocks = TextBlocks(lines);
        }

        return new ToolResultDocument(
            module.Id,
            spec.Result,
            output,
            blocks,
            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["toolId"] = module.Id,
                ["resultKind"] = spec.Result.ToString(),
                ["layout"] = spec.Layout.ToString(),
                ["generatedAt"] = DateTimeOffset.Now.ToString("O")
            },
            output,
            string.Empty,
            "ok");
    }

    private static IReadOnlyList<ToolResultBlock> JsonBlocks(string output)
    {
        try
        {
            using var document = JsonDocument.Parse(output);
            return [ToolResultBlock.Json("JSON", JsonSerializer.Serialize(document.RootElement, new JsonSerializerOptions { WriteIndented = true }))];
        }
        catch
        {
            return [];
        }
    }

    private static IReadOnlyList<ToolResultBlock> TableBlocks(IReadOnlyList<string> lines)
    {
        var rows = lines
            .Select(line => line.Contains('|') ? line.Split('|').Select(cell => cell.Trim()).Where(cell => cell.Length > 0).ToArray() : SplitKeyValue(line))
            .Where(row => row.Length >= 2)
            .Take(80)
            .ToArray();
        return rows.Length == 0 ? [] : [ToolResultBlock.Table("Table", rows)];
    }

    private static IReadOnlyList<ToolResultBlock> RankedBlocks(IReadOnlyList<string> lines)
    {
        var items = lines
            .Select(ParseRankedItem)
            .Where(item => item is not null)
            .Select(item => item!)
            .Take(80)
            .ToArray();
        return items.Length == 0 ? TextBlocks(lines) : [ToolResultBlock.List(ToolResultBlockKind.RankedList, "Ranked list", items)];
    }

    private static IReadOnlyList<ToolResultBlock> NewsBlocks(IReadOnlyList<string> lines)
    {
        var items = lines
            .Where(line => !string.IsNullOrWhiteSpace(line))
            .Take(60)
            .Select((line, index) => new ToolResultListItem((index + 1).ToString(), StripOrdinal(line), string.Empty, string.Empty, string.Empty))
            .ToArray();
        return items.Length == 0 ? [] : [ToolResultBlock.List(ToolResultBlockKind.NewsList, "News", items)];
    }

    private static IReadOnlyList<ToolResultBlock> LinkBlocks(IReadOnlyList<string> lines)
    {
        return ExtractLinks(lines)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .Take(30)
            .Select(link => ToolResultBlock.Link("Link", link, link))
            .ToArray();
    }

    private static IReadOnlyList<ToolResultBlock> MediaBlocks(IReadOnlyList<string> lines, bool imageOnly)
    {
        var blocks = new List<ToolResultBlock>();
        foreach (var link in ExtractLinks(lines).Distinct(StringComparer.OrdinalIgnoreCase).Take(30))
        {
            if (IsImageLink(link))
            {
                blocks.Add(ToolResultBlock.Media(ToolResultBlockKind.Image, "Image", link, link));
            }
            else if (!imageOnly)
            {
                blocks.Add(ToolResultBlock.Media(ToolResultBlockKind.Media, "Media", link, link));
            }
        }

        blocks.AddRange(FileBlocks(lines));
        return blocks;
    }

    private static IReadOnlyList<ToolResultBlock> FileBlocks(IReadOnlyList<string> lines)
    {
        return lines
            .Select(ExtractPath)
            .Where(path => !string.IsNullOrWhiteSpace(path))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .Take(30)
            .Select(path => ToolResultBlock.File("File", path!, path!))
            .ToArray();
    }

    private static IReadOnlyList<ToolResultBlock> DiffBlocks(IReadOnlyList<string> lines)
    {
        var items = lines
            .Take(160)
            .Select(line => new ToolResultListItem(
                line.StartsWith('+') ? "+" : line.StartsWith('-') ? "-" : string.Empty,
                line,
                string.Empty,
                line.StartsWith('+') ? "added" : line.StartsWith('-') ? "removed" : "context",
                string.Empty))
            .ToArray();
        return items.Length == 0 ? [] : [ToolResultBlock.List(ToolResultBlockKind.Diff, "Diff", items)];
    }

    private static IReadOnlyList<ToolResultBlock> StatusBlocks(IReadOnlyList<string> lines)
    {
        var items = lines
            .Take(80)
            .Select(line =>
            {
                var status = line.Contains("OK", StringComparison.OrdinalIgnoreCase) || line.Contains("SUCCESS", StringComparison.OrdinalIgnoreCase)
                    ? "ok"
                    : line.Contains("FAIL", StringComparison.OrdinalIgnoreCase) || line.Contains("MISMATCH", StringComparison.OrdinalIgnoreCase)
                        ? "error"
                        : "info";
                return new ToolResultListItem(status, line, string.Empty, status, string.Empty);
            })
            .ToArray();
        return items.Length == 0 ? [] : [ToolResultBlock.List(ToolResultBlockKind.Status, "Status", items)];
    }

    private static IReadOnlyList<ToolResultBlock> ColorBlocks(IReadOnlyList<string> lines)
    {
        var colors = lines.SelectMany(line => Regex.Matches(line, @"#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}").Select(match => match.Value))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .Take(20)
            .Select(color => ToolResultBlock.Media(ToolResultBlockKind.Color, color, color, color))
            .ToArray();
        return colors.Length == 0 ? KeyValueBlocks(lines) : colors;
    }

    private static IReadOnlyList<ToolResultBlock> KeyValueBlocks(IReadOnlyList<string> lines)
    {
        var rows = lines
            .Select(SplitKeyValue)
            .Where(row => row.Length >= 2)
            .Select(row => new KeyValuePair<string, string>(row[0], row[1]))
            .Take(80)
            .ToArray();
        return rows.Length == 0 ? [] : [ToolResultBlock.KeyValue("Details", rows)];
    }

    private static IReadOnlyList<ToolResultBlock> TextBlocks(IReadOnlyList<string> lines)
    {
        var items = lines
            .Take(120)
            .Select((line, index) => new ToolResultListItem((index + 1).ToString(), line, string.Empty, string.Empty, string.Empty))
            .ToArray();
        return items.Length == 0 ? [] : [ToolResultBlock.List(ToolResultBlockKind.Text, "Lines", items)];
    }

    private static IReadOnlyList<string> Lines(string output)
    {
        return output.Replace("\r\n", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
    }

    private static string[] SplitKeyValue(string line)
    {
        var separators = new[] { "：", ":", "=", "\t" };
        foreach (var separator in separators)
        {
            var index = line.IndexOf(separator, StringComparison.Ordinal);
            if (index > 0 && index < line.Length - separator.Length)
            {
                return [line[..index].Trim().Trim('-', '*', ' '), line[(index + separator.Length)..].Trim()];
            }
        }

        return [];
    }

    private static ToolResultListItem? ParseRankedItem(string line)
    {
        var match = Regex.Match(line, @"^\s*(?<rank>\d+)[\.\)\u3001\uff09\s]+(?<title>.+)$");
        return match.Success
            ? new ToolResultListItem(match.Groups["rank"].Value, CleanVisibleText(match.Groups["title"].Value), string.Empty, string.Empty, ExtractLinks([line]).FirstOrDefault() ?? string.Empty)
            : null;
    }

    private static string CleanVisibleText(string value)
    {
        var text = value ?? string.Empty;
        text = Regex.Replace(text, @"\s*https?://[^\s\]\)>'""]+", string.Empty, RegexOptions.IgnoreCase);
        text = Regex.Replace(text, @"\s*/\s*$", string.Empty);
        text = Regex.Replace(text, @"\s{2,}", " ");
        return text.Trim();
    }

    private static string StripOrdinal(string value)
    {
        return Regex.Replace(value, @"^\s*\d+[\.\)\u3001\uff09\s]+", string.Empty).Trim();
    }

    private static IEnumerable<string> ExtractLinks(IEnumerable<string> lines)
    {
        foreach (var line in lines)
        {
            foreach (Match match in Regex.Matches(line, @"https?://[^\s\]\)>'""]+", RegexOptions.IgnoreCase))
            {
                yield return match.Value.TrimEnd('.', ',', ';');
            }
        }
    }

    private static bool IsImageLink(string link)
    {
        if (!Uri.TryCreate(link, UriKind.Absolute, out var uri))
        {
            return false;
        }

        var path = uri.AbsolutePath.ToLowerInvariant();
        return path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
            || path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)
            || path.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase)
            || path.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)
            || path.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)
            || uri.Host.Contains("qlogo.cn", StringComparison.OrdinalIgnoreCase);
    }

    private static string? ExtractPath(string line)
    {
        var candidate = line
            .Replace("Path:", string.Empty, StringComparison.OrdinalIgnoreCase)
            .Replace("Output:", string.Empty, StringComparison.OrdinalIgnoreCase)
            .Replace("已输出：", string.Empty, StringComparison.OrdinalIgnoreCase)
            .Replace("已创建归档：", string.Empty, StringComparison.OrdinalIgnoreCase)
            .Replace("已解压到：", string.Empty, StringComparison.OrdinalIgnoreCase)
            .Trim()
            .Trim('"');

        return File.Exists(candidate) || Directory.Exists(candidate) ? candidate : null;
    }
}

public sealed record ToolResultBlock(
    ToolResultBlockKind Kind,
    string Title,
    string Text,
    IReadOnlyList<KeyValuePair<string, string>> Pairs,
    IReadOnlyList<string[]> Rows,
    IReadOnlyList<ToolResultListItem> Items,
    string Uri,
    string Path,
    string Language,
    string Status,
    string Subtitle = "",
    string Description = "",
    string Severity = "",
    IReadOnlyDictionary<string, string>? Metadata = null)
{
    public static ToolResultBlock KeyValue(
        string title,
        IReadOnlyList<KeyValuePair<string, string>> pairs,
        IReadOnlyDictionary<string, string>? metadata = null)
        => new(ToolResultBlockKind.KeyValue, title, string.Empty, pairs, [], [], string.Empty, string.Empty, string.Empty, string.Empty, Metadata: metadata);

    public static ToolResultBlock Table(
        string title,
        IReadOnlyList<string[]> rows,
        IReadOnlyDictionary<string, string>? metadata = null)
        => new(ToolResultBlockKind.Table, title, string.Empty, [], rows, [], string.Empty, string.Empty, string.Empty, string.Empty, Metadata: metadata);

    public static ToolResultBlock List(
        ToolResultBlockKind kind,
        string title,
        IReadOnlyList<ToolResultListItem> items,
        IReadOnlyDictionary<string, string>? metadata = null)
        => new(kind, title, string.Empty, [], [], items, string.Empty, string.Empty, string.Empty, string.Empty, Metadata: metadata);

    public static ToolResultBlock Link(
        string title,
        string text,
        string uri,
        IReadOnlyDictionary<string, string>? metadata = null)
        => new(ToolResultBlockKind.Link, title, text, [], [], [], uri, string.Empty, string.Empty, string.Empty, Metadata: metadata);

    public static ToolResultBlock File(
        string title,
        string text,
        string path,
        IReadOnlyDictionary<string, string>? metadata = null)
        => new(ToolResultBlockKind.File, title, text, [], [], [], string.Empty, path, string.Empty, string.Empty, Metadata: metadata);

    public static ToolResultBlock Code(
        string title,
        string text,
        string language = "",
        IReadOnlyDictionary<string, string>? metadata = null)
        => new(ToolResultBlockKind.Code, title, text, [], [], [], string.Empty, string.Empty, language, string.Empty, Metadata: metadata);

    public static ToolResultBlock Json(string title, string text)
        => new(ToolResultBlockKind.Json, title, text, [], [], [], string.Empty, string.Empty, "json", string.Empty);

    public static ToolResultBlock JsonTree(string title, string text)
        => new(ToolResultBlockKind.JsonTree, title, text, [], [], [], string.Empty, string.Empty, "json", string.Empty);

    public static ToolResultBlock Metric(
        string title,
        IReadOnlyList<KeyValuePair<string, string>> pairs,
        IReadOnlyDictionary<string, string>? metadata = null)
        => new(ToolResultBlockKind.Metric, title, string.Empty, pairs, [], [], string.Empty, string.Empty, string.Empty, string.Empty, Metadata: metadata);

    public static ToolResultBlock LineChart(
        string title,
        IReadOnlyList<string[]> rows,
        IReadOnlyDictionary<string, string>? metadata = null)
        => new(ToolResultBlockKind.LineChart, title, string.Empty, [], rows, [], string.Empty, string.Empty, string.Empty, string.Empty, Metadata: metadata);

    public static ToolResultBlock Cards(
        string title,
        IReadOnlyList<ToolResultListItem> items,
        IReadOnlyDictionary<string, string>? metadata = null)
        => new(ToolResultBlockKind.CardList, title, string.Empty, [], [], items, string.Empty, string.Empty, string.Empty, string.Empty, Metadata: metadata);

    public static ToolResultBlock Timeline(
        string title,
        IReadOnlyList<ToolResultListItem> items,
        IReadOnlyDictionary<string, string>? metadata = null)
        => new(ToolResultBlockKind.Timeline, title, string.Empty, [], [], items, string.Empty, string.Empty, string.Empty, string.Empty, Metadata: metadata);

    public static ToolResultBlock Media(
        ToolResultBlockKind kind,
        string title,
        string text,
        string uri,
        IReadOnlyDictionary<string, string>? metadata = null)
        => new(kind, title, text, [], [], [], uri, string.Empty, string.Empty, string.Empty, Metadata: metadata);

    public static ToolResultBlock Raw(
        string title,
        string text,
        string language = "",
        IReadOnlyDictionary<string, string>? metadata = null)
        => new(ToolResultBlockKind.Raw, title, text, [], [], [], string.Empty, string.Empty, language, string.Empty, Metadata: metadata);
}

public sealed record ToolResultListItem(
    string Leading,
    string Title,
    string Subtitle,
    string Status,
    string Uri);
