"""
ReAct-Orchestrator für den AI Agent.

Implementiert einen synchronen ReAct-Loop (Reason + Act) mit:
  - max. 5 Iterationen
  - Tool-Calling
  - Streaming via Generator
  - Audit-Logging
"""
from __future__ import annotations

import json
import logging
from typing import Generator

from .providers import get_provider, LLMError
from .tools import execute_tool, TOOL_SCHEMAS

logger = logging.getLogger(__name__)

MAX_ITERATIONS = 5


def run_agent_stream(
    session,
    user_message: str,
    page_context: str = "",
    user=None,
) -> Generator[str, None, None]:
    """
    Führt den ReAct-Loop aus und streamt SSE-kompatible Daten-Strings.

    Yield-Format (Server-Sent Events):
      "data: {json}\n\n"

    JSON-Typen:
      {"type": "text", "content": "..."}        – Textfragment
      {"type": "tool_start", "name": "..."}     – Tool wird aufgerufen
      {"type": "tool_result", "name": "...", "result": "..."}
      {"type": "done"}
      {"type": "error", "message": "..."}
    """
    from .models import AgentConfig, ChatMessage

    config = AgentConfig.get_config()

    # Systemkontext aufbauen
    system_content = config.system_prompt
    if page_context:
        system_content += f"\n\nAktueller Seitenkontext:\n{page_context}"

    # Nachrichtenhistorie laden (letzte 20 Nachrichten)
    history = list(
        session.messages.exclude(role="tool")
        .order_by("-created_at")[:20]
    )
    history.reverse()

    messages = [{"role": "system", "content": system_content}]
    for msg in history:
        if msg.role in ("user", "assistant"):
            messages.append({"role": msg.role, "content": msg.content})

    # Neue User-Nachricht
    messages.append({"role": "user", "content": user_message})

    # Neue User-Message in DB speichern
    ChatMessage.objects.create(
        session=session,
        role="user",
        content=user_message,
    )

    # Sesstionttitel setzen falls leer
    if not session.title and user_message:
        session.title = user_message[:100]
        session.save(update_fields=["title", "updated_at"])

    tools = TOOL_SCHEMAS if not getattr(config, "allow_write", False) else TOOL_SCHEMAS

    try:
        provider = get_provider(config)
    except LLMError as e:
        yield _sse({"type": "error", "message": str(e)})
        return

    full_assistant_text = ""
    iteration = 0
    tools_disabled = False

    while iteration < MAX_ITERATIONS:
        iteration += 1
        text_buffer = ""
        pending_tool_calls = []
        current_tools = None if tools_disabled else tools

        try:
            for chunk in provider.chat_stream(messages=messages, tools=current_tools):
                chunk_type = chunk.get("type")

                if chunk_type == "text":
                    text = chunk["content"]
                    text_buffer += text
                    full_assistant_text += text
                    yield _sse({"type": "text", "content": text})

                elif chunk_type == "tool_call":
                    pending_tool_calls.append(chunk)

                elif chunk_type == "done":
                    break

                elif chunk_type == "error":
                    yield _sse({"type": "error", "message": chunk.get("message", "Unbekannter Fehler")})
                    return

        except LLMError as e:
            if not tools_disabled and iteration == 1:
                # Tool-Calling hat den Provider zum Absturz gebracht (z.B. OOM).
                # Fallback: ohne Tools erneut versuchen.
                # Warte kurz, damit Ollama nach OOM-Crash neu starten kann.
                import time
                logger.warning("LLM-Fehler mit Tools, Fallback auf Chat-only: %s", e)
                tools_disabled = True
                full_assistant_text = ""
                time.sleep(15)
                continue
            yield _sse({"type": "error", "message": str(e)})
            return

        if not pending_tool_calls:
            # Kein Tool-Call → Antwort fertig
            break

        # Tool-Calls verarbeiten
        # Assistent-Nachricht mit tool_calls in History
        tool_calls_for_msg = []
        for tc in pending_tool_calls:
            tool_calls_for_msg.append({
                "id": tc["id"],
                "type": "function",
                "function": {
                    "name": tc["name"],
                    "arguments": json.dumps(tc["arguments"], ensure_ascii=False),
                },
            })

        assistant_msg: dict = {"role": "assistant", "content": text_buffer or ""}
        if tool_calls_for_msg:
            assistant_msg["tool_calls"] = tool_calls_for_msg
        messages.append(assistant_msg)

        # Jeden Tool-Call ausführen
        for tc in pending_tool_calls:
            tool_name = tc["name"]
            tool_args = tc["arguments"]
            tool_call_id = tc["id"]

            yield _sse({"type": "tool_start", "name": tool_name})

            result = execute_tool(tool_name, tool_args, user)

            # Tool-Ergebnis in DB
            ChatMessage.objects.create(
                session=session,
                role="tool",
                content=result,
                tool_name=tool_name,
                tool_call_id=tool_call_id,
            )

            yield _sse({"type": "tool_result", "name": tool_name, "result": result[:500]})

            # Tool-Ergebnis in Messages für nächste LLM-Iteration
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call_id,
                "content": result,
            })

        full_assistant_text = ""  # Reset für nächste Iteration

    # Abschließende Assistent-Nachricht in DB speichern
    if full_assistant_text:
        ChatMessage.objects.create(
            session=session,
            role="assistant",
            content=full_assistant_text,
        )

    # Session updated_at aktualisieren
    from django.utils import timezone
    session.updated_at = timezone.now()
    session.save(update_fields=["updated_at"])

    yield _sse({"type": "done"})


def _sse(data: dict) -> str:
    """Formatiert ein Dict als SSE data-Zeile."""
    return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
