"""ModelQueue — serialisiert alle Modellanfragen mit Prioritätswarteschlange.

Jede Anfrage an das lokale Modell läuft durch diese Queue.
Prioritätsskala (höher = dringlicher / zuerst):
  10  User-Web-Chat         (Nutzer wartet im Browser)
   9  Telegram-Chat         (Nutzer wartet auf Antwort)
   7  Netzwerk/Relay-Query
   5  Assistent / Scheduler
   3  Moltbook-Bot Haupt (Kommentare, Posts)
   2  Moltbook-Bot Entscheidungen (ja/nein)
   1  Hintergrundaufgaben

Gleiche Priorität → FIFO (Reihenfolge der Einreichung).
"""
from __future__ import annotations

import asyncio
import datetime
import itertools
import logging
import uuid
from dataclasses import dataclass, field
from typing import Any

log = logging.getLogger(__name__)

PRIORITY_USER_CHAT = 10
PRIORITY_TELEGRAM  = 9
PRIORITY_NETWORK   = 7
PRIORITY_ASSISTANT = 5
PRIORITY_MOLTBOOK  = 3
PRIORITY_MOLTBOOK_DECIDE = 2
PRIORITY_BACKGROUND = 1


@dataclass
class QueueItem:
    """Ein einzelner Eintrag in der Model-Queue."""
    id: str
    source: str          # z.B. "chat", "telegram", "moltbook_bot", …
    priority: int        # Höher = dringlicher
    messages: list[dict] # Chat-Messages an das Modell
    max_tokens: int = 1024
    temperature: float = 0.7
    created_at: str = field(default_factory=lambda: datetime.datetime.now().strftime("%H:%M:%S"))
    # Internes asyncio.Future — wird von den Wartenden gepollt
    _future: asyncio.Future | None = field(default=None, compare=False, repr=False)
    # Sequenznummer für FIFO bei gleicher Priorität
    _seq: int = field(default=0, compare=False, repr=False)

    def to_dict(self, include_messages: bool = True) -> dict:
        """Serialisiert den Eintrag als JSON-kompatibles Dict."""
        system_prompt = ""
        user_prompt = ""
        for m in self.messages:
            if m.get("role") == "system" and not system_prompt:
                system_prompt = (m.get("content") or "")
            elif m.get("role") == "user":
                user_prompt = (m.get("content") or "")
        d: dict[str, Any] = {
            "id": self.id,
            "source": self.source,
            "priority": self.priority,
            "created_at": self.created_at,
            "max_tokens": self.max_tokens,
            "temperature": self.temperature,
        }
        if include_messages:
            d["system_prompt"] = system_prompt[:2000]
            d["user_prompt"] = user_prompt[:2000]
        return d


@dataclass
class CompletedItem:
    """Ein abgeschlossener Auftrag mit Antwort des Modells."""
    item: QueueItem
    response: str
    completed_at: str = field(default_factory=lambda: datetime.datetime.now().strftime("%H:%M:%S"))
    duration_ms: int = 0

    def to_dict(self) -> dict:
        d = self.item.to_dict()
        d["response"] = self.response[:3000]
        d["completed_at"] = self.completed_at
        d["duration_ms"] = self.duration_ms
        return d


class ModelQueue:
    """Prioritätswarteschlange für Modellanfragen.

    Alle Anfragen werden sequentiell abgearbeitet — so wie das
    Modell-Lock in model.py es sowieso erzwingt, aber jetzt mit
    sichtbarer Warteschlange und Priority-Steuerung.
    """

    def __init__(self, node: Any):
        self._node = node
        # (neg_priority, seq, item) — min-heap liefert höchste Priorität zuerst
        self._pqueue: asyncio.PriorityQueue = asyncio.PriorityQueue()
        self._seq_gen = itertools.count()
        # Aktuell in Bearbeitung
        self._current: QueueItem | None = None
        # Letzte N abgeschlossene Aufträge (neuester zuerst)
        self._completed: list[CompletedItem] = []
        self._completed_max = 20
        # Snapshot der Queue für das Dashboard (wird nach jedem get/put aktualisiert)
        self._queue_snapshot: list[QueueItem] = []
        self._worker_task: asyncio.Task | None = None
        self._running = False

    # ── öffentliche API ────────────────────────────────────────────────────

    async def enqueue(
        self,
        messages: list[dict],
        source: str = "unknown",
        priority: int = PRIORITY_BACKGROUND,
        max_tokens: int = 1024,
        temperature: float = 0.7,
    ) -> str:
        """Reiht eine Modellanfrage ein und wartet auf die Antwort.

        Gibt die generierte Antwort zurück (wie bisher model.generate()).
        Durch die priority-Logik werden wichtige Anfragen bevorzugt.
        """
        loop = asyncio.get_event_loop()
        future: asyncio.Future = loop.create_future()
        item = QueueItem(
            id=str(uuid.uuid4())[:8],
            source=source,
            priority=priority,
            messages=messages,
            max_tokens=max_tokens,
            temperature=temperature,
            _future=future,
            _seq=next(self._seq_gen),
        )
        await self._pqueue.put((-priority, item._seq, item))
        self._refresh_snapshot()
        log.debug(
            "ModelQueue: +%s [%s prio=%d] | queue=%d",
            item.id, source, priority, self._pqueue.qsize(),
        )
        # Auf Antwort warten (blockiert nur diesen Coroutine-Call, nicht den Event-Loop)
        return await future

    def start(self) -> None:
        """Startet den Worker-Task (muss im laufenden Event-Loop aufgerufen werden)."""
        if self._running:
            return
        self._running = True
        self._worker_task = asyncio.ensure_future(self._worker())
        log.info("ModelQueue worker gestartet.")

    def stop(self) -> None:
        if self._worker_task and not self._worker_task.done():
            self._worker_task.cancel()
        self._running = False

    # ── Dashboard-Daten ────────────────────────────────────────────────────

    def status(self) -> dict:
        """Aktueller Zustand der Queue — für das Dashboard."""
        queue_items = []
        for pos, item in enumerate(self._queue_snapshot, start=1):
            d = item.to_dict()
            d["position"] = pos
            queue_items.append(d)
        current = None
        if self._current:
            current = self._current.to_dict()
            current["position"] = 0
        completed = [c.to_dict() for c in self._completed]
        return {
            "queue": queue_items,
            "current": current,
            "completed": completed,
            "queue_length": len(queue_items),
        }

    # ── interner Worker ────────────────────────────────────────────────────

    def _refresh_snapshot(self) -> None:
        """Aktualisiert den snapshot der Queue-Items für das Dashboard.

        asyncio.PriorityQueue erlaubt keinen direkten Zugriff auf z.B. _queue.
        Wir greifen intern auf das interne Heap-Array zu — safe, weil alles
        im selben Event-Loop-Thread läuft.
        """
        try:
            heap = list(self._pqueue._queue)  # type: ignore[attr-defined]
            # Heap sortieren (ist bereits ein Min-Heap — nur kopieren & sortieren)
            heap.sort()
            self._queue_snapshot = [entry[2] for entry in heap]
        except Exception:
            self._queue_snapshot = []

    async def _worker(self) -> None:
        """Endloser Consumer: nimmt jeweils den dringlichsten Job und führt ihn aus."""
        log.debug("ModelQueue worker läuft.")
        while True:
            try:
                _neg_prio, _seq, item = await self._pqueue.get()
                self._current = item
                self._refresh_snapshot()

                t_start = asyncio.get_event_loop().time()
                log.info(
                    "ModelQueue: ▶ %s [%s prio=%d]",
                    item.id, item.source, item.priority,
                )
                try:
                    response = await self._run_item(item)
                except Exception as exc:
                    response = f"[Fehler: {exc}]"
                    log.error("ModelQueue: Fehler bei %s: %s", item.id, exc, exc_info=True)

                duration_ms = int((asyncio.get_event_loop().time() - t_start) * 1000)
                log.info(
                    "ModelQueue: ✓ %s [%s] → %d ms | %d Zeichen",
                    item.id, item.source, duration_ms, len(response),
                )

                # Completed-Liste aktualisieren
                completed = CompletedItem(
                    item=item, response=response, duration_ms=duration_ms
                )
                self._completed.insert(0, completed)
                if len(self._completed) > self._completed_max:
                    del self._completed[self._completed_max:]

                self._current = None
                self._refresh_snapshot()
                self._pqueue.task_done()

                # Future auflösen — weckt den wartenden await-Aufrufer
                if item._future and not item._future.done():
                    item._future.set_result(response)

            except asyncio.CancelledError:
                log.info("ModelQueue worker beendet.")
                break
            except Exception as exc:
                log.error("ModelQueue: unerwarteter Fehler im Worker: %s", exc, exc_info=True)
                await asyncio.sleep(0.1)

    async def _run_item(self, item: QueueItem) -> str:
        """Führt einen einzelnen Queue-Eintrag aus."""
        node = self._node
        chat_plugin = node.plugins.get("chat")
        if chat_plugin:
            return await chat_plugin.capabilities[0].handler(messages=item.messages)
        elif node.model.loaded:
            loop = asyncio.get_event_loop()
            return await loop.run_in_executor(
                None,
                lambda: node.model.generate(
                    item.messages,
                    max_tokens=item.max_tokens,
                    temperature=item.temperature,
                ),
            )
        else:
            return (
                "⚠️ Kein Modell geladen und kein Chat-Plugin verfügbar.\n"
                "Konfiguriere model.path in config.yaml oder installiere ein Plugin."
            )
