using System.Text;
using ClawdDotNet.Core.Api.Models;

namespace ClawdDotNet.Core.Tests.Infrastructure;

/// <summary>
/// Prüft, ob eine Nachrichtenfolge von der Chat-Completions-API akzeptiert würde.
///
/// Die Regeln entsprechen dem, was OpenRouter/Anthropic/OpenAI verlangen:
/// Eine tool-Antwort ist nur gültig, wenn ihr eine assistant-Nachricht mit einem
/// passenden tool_call vorausgeht — und jeder tool_call braucht seine Antwort.
///
/// Wird eine Regel verletzt, antwortet die API mit HTTP 400 und der laufende
/// Agent bricht ab. Genau das ist Bug B1 aus der Bestandsaufnahme.
/// </summary>
internal static class ContextInvariants
{
    public static void AssertValid(IReadOnlyList<ChatMessage> messages)
    {
        var violations = Validate(messages).ToList();
        if (violations.Count == 0)
            return;

        var sb = new StringBuilder();
        sb.AppendLine($"Ungültige Nachrichtenfolge ({violations.Count} Verstoß/Verstöße):");
        foreach (var v in violations)
            sb.AppendLine($"  • {v}");
        sb.AppendLine();
        sb.AppendLine("Sequenz:");
        sb.Append(Describe(messages));

        throw new ContextInvariantViolationException(sb.ToString());
    }

    public static bool IsValid(IReadOnlyList<ChatMessage> messages)
        => !Validate(messages).Any();

    private static IEnumerable<string> Validate(IReadOnlyList<ChatMessage> messages)
    {
        // ── Regel 1: höchstens eine system-Nachricht, und zwar ganz vorne ──
        for (var i = 1; i < messages.Count; i++)
        {
            if (messages[i].Role == "system")
                yield return $"[{i}] system-Nachricht steht nicht an Position 0";
        }

        // ── Regel 2: jede tool-Nachricht braucht eine ToolCallId ──
        for (var i = 0; i < messages.Count; i++)
        {
            if (messages[i].Role == "tool" && string.IsNullOrEmpty(messages[i].ToolCallId))
                yield return $"[{i}] tool-Nachricht ohne tool_call_id";
        }

        // ── Regel 3: jede tool-Nachricht gehört zum unmittelbar vorausgehenden
        //             assistant-Block mit passender tool_call-Id ──
        var openCalls = new HashSet<string>();
        var answered = new HashSet<string>();

        for (var i = 0; i < messages.Count; i++)
        {
            var msg = messages[i];

            if (msg.Role == "assistant" && msg.ToolCalls is { Count: > 0 })
            {
                // Vorheriger Block muss vollständig beantwortet sein
                var unanswered = openCalls.Except(answered).ToList();
                if (unanswered.Count > 0)
                    yield return $"[{i}] neuer assistant-Block, aber tool_call(s) noch unbeantwortet: {string.Join(", ", unanswered)}";

                openCalls.Clear();
                answered.Clear();
                foreach (var tc in msg.ToolCalls)
                    openCalls.Add(tc.Id);
            }
            else if (msg.Role == "tool")
            {
                var id = msg.ToolCallId ?? "";
                if (!openCalls.Contains(id))
                {
                    yield return $"[{i}] tool-Antwort '{id}' ohne vorausgehenden assistant-tool_call " +
                                 "(die API lehnt das mit HTTP 400 ab)";
                }
                else if (!answered.Add(id))
                {
                    yield return $"[{i}] tool_call '{id}' wurde doppelt beantwortet";
                }
            }
            else
            {
                // user/system/assistant-ohne-tool_calls beenden den Block
                var unanswered = openCalls.Except(answered).ToList();
                if (unanswered.Count > 0)
                    yield return $"[{i}] {msg.Role}-Nachricht, aber tool_call(s) unbeantwortet: {string.Join(", ", unanswered)}";

                openCalls.Clear();
                answered.Clear();
            }
        }

        // ── Regel 4: am Ende darf kein tool_call offen sein ──
        var stillOpen = openCalls.Except(answered).ToList();
        if (stillOpen.Count > 0)
            yield return $"[Ende] unbeantwortete tool_call(s): {string.Join(", ", stillOpen)}";
    }

    /// <summary>Kompakte, lesbare Darstellung für Fehlermeldungen.</summary>
    public static string Describe(IReadOnlyList<ChatMessage> messages)
    {
        var sb = new StringBuilder();
        for (var i = 0; i < messages.Count; i++)
        {
            var m = messages[i];
            var detail = m.Role switch
            {
                "tool" => $"tool_call_id={m.ToolCallId}, len={m.Content?.Length ?? 0}",
                "assistant" when m.ToolCalls is { Count: > 0 }
                    => $"tool_calls=[{string.Join(", ", m.ToolCalls.Select(t => $"{t.Function.Name}#{t.Id}"))}]",
                _ => $"len={m.Content?.Length ?? 0}"
            };
            sb.AppendLine($"  [{i,2}] {m.Role,-9} {detail}");
        }
        return sb.ToString();
    }
}

internal sealed class ContextInvariantViolationException(string message) : Exception(message);
