using System.Text;

namespace ClawdDotNet.Core.Tasks;

/// <summary>
/// Liest und schreibt eine Task-Datei: ein YAML-artiger Frontmatter-Block zwischen
/// <c>---</c>-Zeilen, gefolgt vom Rumpf.
///
/// Bewusst ein enger, eigener Parser statt einer YAML-Bibliothek: Die Frontmatter ist
/// ein kleiner, flacher Satz aus Skalaren, einem verschachtelten <c>when</c>, einer
/// Inline-Liste und einem Block-Skalar. Dafür eine Abhängigkeit hereinzuziehen passt
/// nicht zum Stil des Projekts — und ein voller YAML-Parser würde Formen zulassen, die
/// wir gar nicht deuten wollen. Unbekanntes wird übergangen, nicht erraten.
/// </summary>
public static class TaskFrontmatter
{
    /// <summary>
    /// Zerlegt eine Task-Datei in ihre Definition. Gibt <c>false</c> mit einer Meldung
    /// zurück, wenn kein Frontmatter-Block gefunden wird — der Ausführungszustand
    /// (Marker, Claim) wird hier nicht berührt, er kommt aus der DB.
    /// </summary>
    public static bool TryParse(string text, out TaskItem task, out string? error)
    {
        task = new TaskItem();
        error = null;

        var normalized = (text ?? "").Replace("\r\n", "\n").Replace("\r", "\n");

        // Öffnendes und schließendes "---" finden (BOM/Leerzeilen davor tolerieren).
        var lines = normalized.Split('\n');
        var open = -1;
        for (var i = 0; i < lines.Length; i++)
        {
            var t = lines[i].TrimStart('﻿').Trim();
            if (t.Length == 0) continue;
            if (t == "---") { open = i; break; }
            break; // erste nicht-leere Zeile ist kein Fence
        }

        if (open < 0)
        {
            error = "Kein Frontmatter-Block gefunden (erwartet '---' als erste Zeile).";
            return false;
        }

        var close = -1;
        for (var i = open + 1; i < lines.Length; i++)
        {
            if (lines[i].Trim() == "---") { close = i; break; }
        }

        if (close < 0)
        {
            error = "Frontmatter-Block nicht geschlossen (zweites '---' fehlt).";
            return false;
        }

        var block = lines[(open + 1)..close];
        var body = string.Join('\n', lines[(close + 1)..]).Trim('\n');

        var scalars = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        var when = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        var blockedBy = new List<string>();
        string? acceptance = null;

        var j = 0;
        while (j < block.Length)
        {
            var raw = block[j];
            var trimmed = raw.Trim();
            if (trimmed.Length == 0 || trimmed.StartsWith('#')) { j++; continue; }

            // Nur Zeilen ohne Einrückung sind Top-Level-Schlüssel; eingerückte Zeilen
            // gehören zum jeweils vorangehenden Schlüssel und werden dort mitgelesen.
            if (Indent(raw) > 0) { j++; continue; }

            var colon = trimmed.IndexOf(':');
            if (colon < 0) { j++; continue; }

            var key = trimmed[..colon].Trim();
            var rest = trimmed[(colon + 1)..].Trim();

            if (key.Equals("when", StringComparison.OrdinalIgnoreCase) && rest.Length == 0)
            {
                j = CollectChildren(block, j + 1, child =>
                {
                    var c = child.IndexOf(':');
                    if (c < 0) return;
                    when[child[..c].Trim()] = Unquote(child[(c + 1)..].Trim());
                });
                continue;
            }

            if (key.Equals("acceptance", StringComparison.OrdinalIgnoreCase) && IsBlockScalar(rest))
            {
                acceptance = ReadBlockScalar(block, ref j);
                continue;
            }

            if (key.Equals("blocked_by", StringComparison.OrdinalIgnoreCase))
            {
                if (rest.StartsWith('['))
                {
                    blockedBy.AddRange(ParseInlineList(rest));
                    j++;
                }
                else if (rest.Length == 0)
                {
                    // Block-Liste: nachfolgende "- item"-Zeilen.
                    j = CollectChildren(block, j + 1, child =>
                    {
                        var item = child.TrimStart();
                        if (item.StartsWith('-'))
                            blockedBy.Add(Unquote(item[1..].Trim()));
                    });
                }
                else
                {
                    blockedBy.AddRange(ParseInlineList(rest));
                    j++;
                }
                continue;
            }

            scalars[key] = Unquote(rest);
            j++;
        }

        TaskWhen? whenValue = null;
        if (TaskText.WhenKind(when.GetValueOrDefault("kind")) is { } kind)
        {
            whenValue = new TaskWhen
            {
                Kind = kind,
                Value = when.GetValueOrDefault("value", ""),
                TimeZone = when.GetValueOrDefault("tz", "")
            };
        }

        task = new TaskItem
        {
            Id = scalars.GetValueOrDefault("id", ""),
            Title = scalars.GetValueOrDefault("title", ""),
            Status = TaskText.Status(scalars.GetValueOrDefault("status")),
            Type = TaskText.Type(scalars.GetValueOrDefault("type")),
            Priority = ParsePriority(scalars.GetValueOrDefault("priority")),
            Assignee = EmptyToDefault(scalars.GetValueOrDefault("assignee"), TaskAssignee.Human),
            When = whenValue,
            RequireApproval = ParseBool(scalars.GetValueOrDefault("require_approval")),
            Acceptance = acceptance?.Trim() ?? "",
            BlockedBy = blockedBy,
            OnlyWhenMarketOpen = ParseBool(scalars.GetValueOrDefault("onlyWhenMarketOpen")),
            ToolName = scalars.GetValueOrDefault("tool_name", ""),
            JobTypeId = scalars.GetValueOrDefault("job_type", ""),
            Body = body
        };
        return true;
    }

    /// <summary>Schreibt eine Definition zurück in Dateiform. Der Ausführungszustand
    /// bleibt außen vor — er gehört in die DB, nicht in die Datei.</summary>
    public static string Serialize(TaskItem task)
    {
        var sb = new StringBuilder();
        sb.Append("---\n");
        sb.Append($"id: {Scalar(task.Id)}\n");
        sb.Append($"title: {Scalar(task.Title)}\n");
        sb.Append($"status: {TaskText.Of(task.Status)}\n");
        sb.Append($"type: {TaskText.Of(task.Type)}\n");
        sb.Append($"priority: {Math.Clamp(task.Priority, 1, 5)}\n");
        sb.Append($"assignee: {Scalar(task.Assignee)}\n");

        if (task.When is { } w)
        {
            sb.Append("when:\n");
            sb.Append($"  kind: {TaskText.Of(w.Kind)}\n");
            sb.Append($"  value: {Scalar(w.Value)}\n");
            sb.Append($"  tz: {Scalar(w.TimeZone)}\n");
        }

        // Nur für Poll-Tasks (tool_job): welches Tool mit welcher Job-Art getickt wird.
        if (task.Type == TaskItemType.ToolJob || !string.IsNullOrWhiteSpace(task.ToolName))
        {
            sb.Append($"tool_name: {Scalar(task.ToolName)}\n");
            sb.Append($"job_type: {Scalar(task.JobTypeId)}\n");
        }

        sb.Append($"require_approval: {(task.RequireApproval ? "true" : "false")}\n");

        if (!string.IsNullOrWhiteSpace(task.Acceptance))
        {
            sb.Append("acceptance: |\n");
            foreach (var line in task.Acceptance.Replace("\r\n", "\n").Split('\n'))
                sb.Append("  ").Append(line).Append('\n');
        }

        if (task.BlockedBy.Count > 0)
            sb.Append($"blocked_by: [{string.Join(", ", task.BlockedBy)}]\n");

        sb.Append($"onlyWhenMarketOpen: {(task.OnlyWhenMarketOpen ? "true" : "false")}\n");
        sb.Append("---\n");

        if (!string.IsNullOrWhiteSpace(task.Body))
            sb.Append('\n').Append(task.Body.Replace("\r\n", "\n").TrimEnd('\n')).Append('\n');

        return sb.ToString();
    }

    // ─── Hilfsfunktionen ───

    private static int Indent(string line)
    {
        var n = 0;
        while (n < line.Length && line[n] == ' ') n++;
        return n;
    }

    /// <summary>Ruft <paramref name="onChild"/> für jede eingerückte Folgezeile auf und
    /// gibt den Index der ersten nicht mehr zugehörigen Zeile zurück.</summary>
    private static int CollectChildren(string[] block, int start, Action<string> onChild)
    {
        var k = start;
        while (k < block.Length)
        {
            var line = block[k];
            if (line.Trim().Length == 0) { k++; continue; }
            if (Indent(line) == 0) break;
            onChild(line.Trim());
            k++;
        }
        return k;
    }

    private static bool IsBlockScalar(string rest) => rest is "|" or "|-" or "|+" or ">" or ">-";

    /// <summary>Liest einen eingerückten Block-Skalar und entfernt die gemeinsame
    /// Einrückung. Setzt <paramref name="j"/> hinter den Block.</summary>
    private static string ReadBlockScalar(string[] block, ref int j)
    {
        var collected = new List<string>();
        var k = j + 1;
        var commonIndent = int.MaxValue;

        while (k < block.Length)
        {
            var line = block[k];
            if (line.Trim().Length == 0) { collected.Add(""); k++; continue; }
            if (Indent(line) == 0) break;
            commonIndent = Math.Min(commonIndent, Indent(line));
            collected.Add(line);
            k++;
        }

        j = k;
        if (commonIndent == int.MaxValue) commonIndent = 0;

        var sb = new StringBuilder();
        foreach (var line in collected)
            sb.Append(line.Length >= commonIndent ? line[commonIndent..] : line).Append('\n');

        return sb.ToString().TrimEnd('\n');
    }

    private static IEnumerable<string> ParseInlineList(string rest)
    {
        var inner = rest.Trim();
        if (inner.StartsWith('[')) inner = inner[1..];
        if (inner.EndsWith(']')) inner = inner[..^1];

        return inner.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Select(Unquote)
            .Where(s => s.Length > 0);
    }

    private static string Unquote(string value)
    {
        var v = value.Trim();
        if (v.Length >= 2 && v[0] == '"' && v[^1] == '"')
            return v[1..^1].Replace("\\\"", "\"").Replace("\\\\", "\\");
        if (v.Length >= 2 && v[0] == '\'' && v[^1] == '\'')
            return v[1..^1].Replace("''", "'");
        return v;
    }

    /// <summary>Quotiert nur, wenn nötig — hält die Datei sonst gut lesbar. Ein führendes
    /// <c>@</c> (Assignee) etwa ist in YAML ein reserviertes Zeichen und muss quotiert
    /// werden.</summary>
    private static string Scalar(string? value)
    {
        var v = value ?? "";
        if (v.Length == 0) return "\"\"";

        var needsQuote =
            v != v.Trim()
            || "@-[]{}>|*&!%#`,\"'".Contains(v[0])
            || v.Contains(':')
            || v.Contains('#')
            || v.Contains('\n');

        if (!needsQuote) return v;

        return "\"" + v.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
    }

    private static bool ParseBool(string? value)
        => value?.Trim().ToLowerInvariant() is "true" or "yes" or "1" or "on";

    private static int ParsePriority(string? value)
        => int.TryParse(value?.Trim(), out var p) ? Math.Clamp(p, 1, 5) : 3;

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