using System.Text.Json;
using System.Text.Json.Serialization;

namespace ClawdDotNet.Core.Api.Models;

[JsonConverter(typeof(ChatMessageConverter))]
public sealed class ChatMessage
{
    public string Role { get; set; } = "";

    public string? Content { get; set; }

    public List<ToolCall>? ToolCalls { get; set; }

    public string? ToolCallId { get; set; }

    /// <summary>
    /// Setzt einen Prompt-Caching-Breakpoint auf diese Nachricht.
    ///
    /// Alles, was im Prompt VOR dem Breakpoint steht (Tool-Definitionen, System-Prompt,
    /// vorherige Nachrichten), wird beim nächsten Aufruf aus dem Cache gelesen und
    /// kostet nur einen Bruchteil. Der Inhalt wird dann als Block-Array statt als
    /// einfacher String serialisiert.
    ///
    /// Wichtig: Der Prompt-Abschnitt vor dem Breakpoint muss zwischen zwei Aufrufen
    /// zeichengenau identisch sein, sonst greift der Cache nicht.
    /// </summary>
    [JsonIgnore]
    public bool CacheBreakpoint { get; set; }

    public static ChatMessage System(string content) => new() { Role = "system", Content = content };
    public static ChatMessage User(string content) => new() { Role = "user", Content = content };
    public static ChatMessage Assistant(string content) => new() { Role = "assistant", Content = content };

    public static ChatMessage AssistantWithToolCalls(List<ToolCall> toolCalls) => new()
    {
        Role = "assistant",
        ToolCalls = toolCalls
    };

    public static ChatMessage ToolResponse(string toolCallId, string content) => new()
    {
        Role = "tool",
        ToolCallId = toolCallId,
        Content = content
    };
}

/// <summary>
/// Serialisiert <see cref="ChatMessage"/>. Der Inhalt geht normalerweise als einfacher
/// String raus; ist ein <see cref="ChatMessage.CacheBreakpoint"/> gesetzt, stattdessen
/// als Block-Array mit cache_control — das Format, das Anbieter für Prompt-Caching
/// erwarten.
///
/// Beim Lesen werden beide Formate akzeptiert: Antworten liefern den Inhalt als String,
/// gespeicherte Kontexte können ihn als Array enthalten.
/// </summary>
public sealed class ChatMessageConverter : JsonConverter<ChatMessage>
{
    public override ChatMessage Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException("ChatMessage: Objekt erwartet.");

        var message = new ChatMessage();

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                return message;

            if (reader.TokenType != JsonTokenType.PropertyName)
                continue;

            var propertyName = reader.GetString();
            reader.Read();

            switch (propertyName)
            {
                case "role":
                    message.Role = reader.GetString() ?? "";
                    break;

                case "content":
                    message.Content = ReadContent(ref reader);
                    break;

                case "tool_calls":
                    message.ToolCalls = reader.TokenType == JsonTokenType.Null
                        ? null
                        : JsonSerializer.Deserialize<List<ToolCall>>(ref reader, options);
                    break;

                case "tool_call_id":
                    message.ToolCallId = reader.GetString();
                    break;

                default:
                    reader.Skip();
                    break;
            }
        }

        throw new JsonException("ChatMessage: unerwartetes Ende.");
    }

    /// <summary>Nimmt den Inhalt als String oder als Block-Array entgegen.</summary>
    private static string? ReadContent(ref Utf8JsonReader reader)
    {
        if (reader.TokenType == JsonTokenType.Null)
            return null;

        if (reader.TokenType == JsonTokenType.String)
            return reader.GetString();

        if (reader.TokenType != JsonTokenType.StartArray)
        {
            reader.Skip();
            return null;
        }

        // Block-Array: die text-Anteile zusammenführen.
        var parts = new List<string>();
        while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                reader.Skip();
                continue;
            }

            using var block = JsonDocument.ParseValue(ref reader);
            if (block.RootElement.TryGetProperty("text", out var text) &&
                text.GetString() is { } value)
            {
                parts.Add(value);
            }
        }

        return parts.Count > 0 ? string.Join("", parts) : null;
    }

    public override void Write(Utf8JsonWriter writer, ChatMessage value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        writer.WriteString("role", value.Role);

        if (value.Content is not null)
        {
            if (value.CacheBreakpoint)
            {
                // [{ "type": "text", "text": "…", "cache_control": { "type": "ephemeral" } }]
                writer.WriteStartArray("content");
                writer.WriteStartObject();
                writer.WriteString("type", "text");
                writer.WriteString("text", value.Content);
                writer.WriteStartObject("cache_control");
                writer.WriteString("type", "ephemeral");
                writer.WriteEndObject();
                writer.WriteEndObject();
                writer.WriteEndArray();
            }
            else
            {
                writer.WriteString("content", value.Content);
            }
        }

        if (value.ToolCalls is { Count: > 0 })
        {
            writer.WritePropertyName("tool_calls");
            JsonSerializer.Serialize(writer, value.ToolCalls, options);
        }

        if (value.ToolCallId is not null)
            writer.WriteString("tool_call_id", value.ToolCallId);

        writer.WriteEndObject();
    }
}
