"""Telegram gateway — use Telegram as a thin client for your node."""
from __future__ import annotations

import asyncio
import logging
from pathlib import Path
from typing import Any

log = logging.getLogger(__name__)

LIMIT = 4096  # Telegram Nachrichten-Limit


def _split(text: str) -> list[str]:
    """Teile Text in <=4096-Zeichen-Chunks."""
    return [text[i:i + LIMIT] for i in range(0, max(len(text), 1), LIMIT)]


async def _send(message_obj: Any, text: str) -> None:
    """Sende text als eine oder mehrere Telegram-Nachrichten."""
    chunks = _split(text)
    for chunk in chunks:
        await message_obj.reply_text(chunk)


class TelegramGateway:
    """Bridges Telegram messages to the local HiveMind node."""

    def __init__(self, token: str, node: Any):
        self.token = token
        self.node = node
        self._app = None
        self._stop_event: asyncio.Event | None = None
        self._started: bool = False
        # Pro-User-Queue + Worker-Task: serialisiert Nachrichten pro User.
        self._user_queues:   dict[int, asyncio.Queue] = {}
        self._user_workers:  dict[int, asyncio.Task | None] = {}
        self._user_sessions: dict[int, str] = {}  # uid → aktive Session-ID

    @property
    def running(self) -> bool:
        """True if the updater is actively polling Telegram."""
        try:
            return (
                self._app is not None
                and self._app.updater is not None
                and self._app.updater.running
                and self._started
            )
        except Exception:
            return False

    async def start(self) -> None:
        """Start the Telegram bot and block until stop() is called."""
        try:
            from telegram import Update
            from telegram.ext import (
                ApplicationBuilder,
                CommandHandler,
                MessageHandler,
                ContextTypes,
                filters,
            )
        except ImportError:
            log.error("python-telegram-bot not installed. Run: pip install 'hivemind[telegram]'")
            return

        self._app = ApplicationBuilder().token(self.token).build()

        # ── Zugriffskontrolle ──────────────────────────────────────────────────
        async def _check_access(update: Update) -> bool:
            allowed = self.node.config.gateway.telegram.allowed_users or []
            if allowed and update.effective_user.id not in [int(u) for u in allowed]:
                await update.message.reply_text("⛔ Kein Zugriff. Wende dich an den Node-Betreiber.")
                return False
            return True

        # ── Befehle ────────────────────────────────────────────────────────────
        async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            await update.message.reply_text(
                f"🧠 HiveMind Node: {self.node.name}\n"
                f"Model: {'geladen' if self.node.model.loaded else 'nicht geladen'}\n"
                f"Plugins: {', '.join(self.node.plugins.loaded)}\n\n"
                "Sende mir eine Nachricht oder tippe / für alle Befehle."
            )

        async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            s = self.node.status
            await update.message.reply_text(
                f"📊 Status\n"
                f"Node: {s['name']} ({s['id']})\n"
                f"Model: {'✅' if s['model_loaded'] else '❌'}\n"
                f"Plugins: {', '.join(s['plugins'])}\n"
                f"Cache: {s['cache_size']} Einträge\n"
                f"Verlauf: {s['history_length']} Nachrichten"
            )

        async def cmd_new(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            user = update.effective_user
            display_name = user.username or user.full_name or str(user.id)
            from datetime import datetime as _dt_cls
            ts = _dt_cls.now().strftime("%d.%m. %H:%M")
            new_id = f"tg-{user.id}-{_dt_cls.now().strftime('%Y%m%d%H%M%S')}"
            self.node.sessions.get_or_create(new_id, f"📱 TG @{display_name} ({ts})")
            self._user_sessions[user.id] = new_id
            await update.message.reply_text(f"✅ Neue Sitzung gestartet ({ts}).\nDu kannst jetzt chatten.")

        async def cmd_clear(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            user = update.effective_user
            sess_id = self._user_sessions.get(user.id, f"tg-{user.id}")
            session = self.node.sessions._sessions.get(sess_id)
            if session:
                session.messages.clear()
                self.node.sessions._save(session)
                await update.message.reply_text("🗑️ Verlauf dieser Sitzung wurde gelöscht.")
            else:
                await update.message.reply_text("Keine aktive Sitzung gefunden.")

        async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            await update.message.reply_text(
                "🧠 HiveMind — Alle Befehle\n\n"
                "🔹 Allgemein\n"
                "/start — Begrüßung & Node-Info\n"
                "/status — Node-Status\n"
                "/version — Versions-Info\n"
                "/whoami — Eigene IP & Port\n"
                "/mode online|offline — Netzwerk-Modus\n"
                "/update — Auf Updates prüfen\n"
                "/plugins — Geladene Plugins\n\n"
                "🔹 Chat\n"
                "/new — Neue Sitzung starten\n"
                "/clear — Verlauf löschen\n"
                "/chat clear — Verlauf löschen\n"
                "/chat rename [name] — Sitzung umbenennen\n\n"
                "🔹 Netzwerk\n"
                "/peer list — Peers anzeigen\n"
                "/peer add [ip:port] — Peer hinzufügen\n"
                "/peer delete [name] — Peer entfernen\n\n"
                "🔹 Memory\n"
                "/memory list — Fakten anzeigen\n"
                "/memory add \"kat\" \"wert\" — Fakt hinzufügen\n"
                "/memory edit \"kat\" \"wert\" — Fakt bearbeiten\n"
                "/memory delete \"kat\" — Fakt löschen\n\n"
                "🔹 Notizen\n"
                "/note list — Notizen anzeigen\n"
                "/note add \"text\" — Notiz hinzufügen\n"
                "/note delete [id] — Notiz löschen\n\n"
                "🔹 Spezialisierung\n"
                "/spec — Anzeigen\n"
                "/spec edit \"text\" — Beschreibung ändern\n"
                "/spec tags edit \"t1\", \"t2\" — Tags ersetzen\n"
                "/spec tags add \"t1\" — Tag(s) hinzufügen\n"
                "/spec tags delete \"t1\" — Tag(s) entfernen\n\n"
                "/help — Diese Hilfe"
            )

        # ── Hilfsfunktionen ────────────────────────────────────────────────────
        def _parse_quoted(text: str) -> list[str]:
            """Extrahiert quoted Strings oder Whitespace-Tokens aus text."""
            import re as _re
            return _re.findall(r'"([^"]*)"|\S+', text)

        async def _update_spec(specialization: str, expertise_tags: list[str]) -> None:
            """Spezialisierung + Tags speichern und Relay informieren."""
            import yaml as _yaml
            from hivemind.confidence import ConfidenceScorer as _CS
            self.node.config.node.specialization = specialization
            self.node.config.node.expertise_tags = expertise_tags
            self.node.scorer = _CS(expertise_tags=expertise_tags, specialization=specialization)
            config_path = self.node.base_dir / "config.yaml"
            cfg: dict = {}
            if config_path.exists():
                cfg = _yaml.safe_load(config_path.read_text()) or {}
            cfg.setdefault("node", {})["specialization"] = specialization
            cfg["node"]["expertise_tags"] = expertise_tags
            config_path.write_text(_yaml.dump(cfg, allow_unicode=True, default_flow_style=False))
            if self.node.relay and self.node.relay.connected:
                try:
                    await self.node.relay.reregister()
                except Exception:
                    pass

        # ── Erweiterte Befehle ─────────────────────────────────────────────────
        async def cmd_version(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            model = Path(self.node.config.model.path).stem if self.node.config.model.path else "keins"
            fast = Path(self.node.config.fast_model.path).stem if getattr(self.node.config, "fast_model", None) and self.node.config.fast_model.path else "keins"
            await update.message.reply_text(
                f"📦 HiveMind v{self.node.version}\n"
                f"Name: {self.node.name}\n"
                f"ID: {self.node.id}\n"
                f"Model: {model}\n"
                f"Fast-Model: {fast}"
            )

        async def cmd_whoami(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            port = self.node.config.network.listen_port
            lines = [f"🌐 Eigene Node\n", f"Name: {self.node.name}", f"ID:   {self.node.id}", f"Port: {port}"]
            if self.node.network:
                try:
                    ip = self.node.network._resolve_public_ip()
                    lines.append(f"IP:   {ip}")
                    lines.append(f"Addr: {ip}:{port}")
                except Exception:
                    lines.append("IP:   nicht ermittelbar")
            if self.node.relay and self.node.relay.connected:
                lines.append("Relay: verbunden ✅")
            await update.message.reply_text("\n".join(lines))

        async def cmd_plugins(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            plugins = self.node.plugins.loaded
            if not plugins:
                await update.message.reply_text("🔌 Keine Plugins geladen.")
                return
            await update.message.reply_text("🔌 Geladene Plugins:\n" + "\n".join(f"  • {p}" for p in plugins))

        async def cmd_peer(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            sub = context.args or ["list"]
            action = sub[0].lower()

            if action == "list":
                if not self.node.network:
                    await update.message.reply_text("Netzwerk nicht aktiviert.")
                    return
                peers = self.node.network.peers.all_peers
                if not peers:
                    await update.message.reply_text("Keine Peers bekannt.")
                    return
                lines = ["🌐 Peers:"]
                for p in peers:
                    dot = "🟢" if p.online else "⚫"
                    spec = f"  [{p.specialization}]" if p.specialization else ""
                    lines.append(f"{dot} {p.name or p.node_id}{spec}")
                await update.message.reply_text("\n".join(lines))

            elif action == "add":
                if len(sub) < 2:
                    await update.message.reply_text("Nutzung: /peer add [ip:port]")
                    return
                addr = sub[1]
                try:
                    from hivemind.network.protocol import PeerInfo as _PI
                    host, p_port = _PI.parse_address(addr)
                    if self.node.network:
                        self.node.network.peers.add_manual(host, p_port)
                        asyncio.create_task(self.node.network.connect_to(host, int(p_port)))
                        await update.message.reply_text(f"✅ Peer hinzugefügt: {addr}")
                    else:
                        await update.message.reply_text("Netzwerk nicht aktiviert.")
                except Exception as e:
                    await update.message.reply_text(f"❌ Fehler: {e}")

            elif action == "delete":
                if len(sub) < 2:
                    await update.message.reply_text("Nutzung: /peer delete [name]")
                    return
                name_q = " ".join(sub[1:]).lower()
                if not self.node.network:
                    await update.message.reply_text("Netzwerk nicht aktiviert.")
                    return
                found = None
                for p in self.node.network.peers.all_peers:
                    if name_q in (p.name or "").lower() or name_q in (p.node_id or "").lower():
                        found = p
                        break
                if found:
                    ok = self.node.network.peers.remove(found.address)
                    await update.message.reply_text(
                        f"✅ Peer entfernt: {found.name or found.node_id}" if ok else "❌ Entfernen fehlgeschlagen."
                    )
                else:
                    await update.message.reply_text(f"Kein Peer mit Name \"{name_q}\" gefunden.")

            else:
                await update.message.reply_text("Nutzung: /peer list | add [ip:port] | delete [name]")

        async def cmd_chat(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            user = update.effective_user
            sub = context.args or []
            action = sub[0].lower() if sub else ""

            if action == "clear":
                sess_id = self._user_sessions.get(user.id, f"tg-{user.id}")
                session = self.node.sessions._sessions.get(sess_id)
                if session:
                    session.messages.clear()
                    self.node.sessions._save(session)
                    await update.message.reply_text("🗑️ Verlauf dieser Sitzung gelöscht.")
                else:
                    await update.message.reply_text("Keine aktive Sitzung gefunden.")

            elif action == "rename":
                if len(sub) < 2:
                    await update.message.reply_text("Nutzung: /chat rename [neuer name]")
                    return
                new_name = " ".join(sub[1:])
                sess_id = self._user_sessions.get(user.id, f"tg-{user.id}")
                ok = self.node.sessions.rename(sess_id, new_name)
                await update.message.reply_text(
                    f"✅ Sitzung umbenannt: {new_name}" if ok else "❌ Sitzung nicht gefunden."
                )
            else:
                await update.message.reply_text("Nutzung: /chat clear | /chat rename [name]")

        async def cmd_mode(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            sub = context.args or []
            if not sub:
                current = "online 🟢" if self.node.config.network.enabled else "offline ⚫"
                await update.message.reply_text(f"Aktueller Modus: {current}\nNutzung: /mode online | /mode offline")
                return
            mode = sub[0].lower()
            if mode not in ("online", "offline"):
                await update.message.reply_text("Nutzung: /mode online | /mode offline")
                return
            enabled = (mode == "online")
            self.node.config.network.enabled = enabled
            import yaml as _yaml
            config_path = self.node.base_dir / "config.yaml"
            cfg: dict = {}
            if config_path.exists():
                cfg = _yaml.safe_load(config_path.read_text()) or {}
            cfg.setdefault("network", {})["enabled"] = enabled
            config_path.write_text(_yaml.dump(cfg, allow_unicode=True, default_flow_style=False))
            icon = "🟢" if enabled else "⚫"
            await update.message.reply_text(
                f"{icon} Modus: {mode}\n"
                "Hinweis: Netzwerkänderungen werden beim nächsten Start vollständig wirksam."
            )

        async def cmd_memory(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            sub = context.args or ["list"]
            action = sub[0].lower()
            rest = " ".join(sub[1:])
            parts = _parse_quoted(rest)

            if action == "list":
                facts = self.node.memory.all_facts
                custom = self.node.memory.all_custom
                if not facts and not custom:
                    await update.message.reply_text("🧠 Kein Memory gespeichert.")
                    return
                lines = ["🧠 Memory — Fakten:"]
                for k, v in facts.items():
                    lines.append(f"  {k}: {v}")
                await update.message.reply_text("\n".join(lines))

            elif action == "add":
                if len(parts) < 2:
                    await update.message.reply_text('Nutzung: /memory add "kategorie" "wert"')
                    return
                self.node.memory.add_fact(parts[0], parts[1])
                await update.message.reply_text(f"✅ Fakt: {parts[0]} = {parts[1]}")

            elif action == "edit":
                if len(parts) < 2:
                    await update.message.reply_text('Nutzung: /memory edit "kategorie" "neuer wert"')
                    return
                if parts[0] not in self.node.memory.all_facts:
                    await update.message.reply_text(f'Kategorie "{parts[0]}" nicht gefunden.')
                    return
                self.node.memory.add_fact(parts[0], parts[1])
                await update.message.reply_text(f"✅ Aktualisiert: {parts[0]} = {parts[1]}")

            elif action == "delete":
                if not parts:
                    await update.message.reply_text('Nutzung: /memory delete "kategorie"')
                    return
                ok = self.node.memory.remove_fact(parts[0])
                await update.message.reply_text(
                    f"✅ Gelöscht: {parts[0]}" if ok else f'Kategorie "{parts[0]}" nicht gefunden.'
                )
            else:
                await update.message.reply_text("Nutzung: /memory list | add | edit | delete")

        async def cmd_note(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            sub = context.args or ["list"]
            action = sub[0].lower()
            rest = " ".join(sub[1:])

            if action == "list":
                notes = self.node.memory.all_custom
                if not notes:
                    await update.message.reply_text("📝 Keine Notizen vorhanden.")
                    return
                lines = ["📝 Notizen:"]
                for i, n in enumerate(notes):
                    lines.append(f"  [{i}] {n}")
                await update.message.reply_text("\n".join(lines))

            elif action == "add":
                parts = _parse_quoted(rest)
                text = parts[0] if parts else rest.strip()
                if not text:
                    await update.message.reply_text('Nutzung: /note add "text"')
                    return
                self.node.memory.add_custom(text)
                await update.message.reply_text("✅ Notiz gespeichert.")

            elif action == "delete":
                if len(sub) < 2:
                    await update.message.reply_text("Nutzung: /note delete [id]")
                    return
                try:
                    idx = int(sub[1])
                    ok = self.node.memory.remove_custom(idx)
                    await update.message.reply_text(
                        f"✅ Notiz [{idx}] gelöscht." if ok else f"Notiz [{idx}] nicht gefunden."
                    )
                except ValueError:
                    await update.message.reply_text("ID muss eine Zahl sein (siehe /note list).")
            else:
                await update.message.reply_text("Nutzung: /note list | add | delete")

        async def cmd_spec(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            sub = context.args or []
            action = sub[0].lower() if sub else ""

            if not action:
                spec = self.node.config.node.specialization or "(keine)"
                tags = self.node.config.node.expertise_tags
                tags_str = ", ".join(tags) if tags else "(keine)"
                await update.message.reply_text(f"🎯 Spezialisierung\n\nBeschreibung: {spec}\nTags: {tags_str}")
                return

            rest = " ".join(sub[1:])

            if action == "edit":
                parts = _parse_quoted(rest)
                new_spec = parts[0] if parts else rest.strip()
                if not new_spec:
                    await update.message.reply_text('Nutzung: /spec edit "neue beschreibung"')
                    return
                await _update_spec(new_spec, self.node.config.node.expertise_tags)
                await update.message.reply_text(f"✅ Spezialisierung: {new_spec}")

            elif action == "tags" and len(sub) > 1:
                import re as _re
                tag_action = sub[1].lower()
                tag_rest = " ".join(sub[2:])
                raw_tags = [t.strip().strip('"') for t in _re.split(r',\s*', tag_rest) if t.strip().strip('"')]
                current = list(self.node.config.node.expertise_tags)

                if tag_action == "edit":
                    if not raw_tags:
                        await update.message.reply_text('Nutzung: /spec tags edit "t1", "t2"')
                        return
                    await _update_spec(self.node.config.node.specialization, raw_tags)
                    await update.message.reply_text(f"✅ Tags gesetzt: {', '.join(raw_tags)}")

                elif tag_action == "add":
                    if not raw_tags:
                        await update.message.reply_text('Nutzung: /spec tags add "t1", "t2"')
                        return
                    new_tags = current + [t for t in raw_tags if t not in current]
                    await _update_spec(self.node.config.node.specialization, new_tags)
                    await update.message.reply_text(f"✅ Tags: {', '.join(new_tags)}")

                elif tag_action == "delete":
                    if not raw_tags:
                        await update.message.reply_text('Nutzung: /spec tags delete "t1"')
                        return
                    new_tags = [t for t in current if t not in raw_tags]
                    await _update_spec(self.node.config.node.specialization, new_tags)
                    await update.message.reply_text(f"✅ Tags nach Entfernen: {', '.join(new_tags) or '(keine)'}")
                else:
                    await update.message.reply_text("Nutzung: /spec tags edit|add|delete ...")
            else:
                await update.message.reply_text("Nutzung: /spec | edit | tags edit|add|delete ...")

        async def cmd_update(update: Update, context: ContextTypes.DEFAULT_TYPE):
            if not await _check_access(update):
                return
            if not self.node.updater:
                await update.message.reply_text("Updater nicht verfügbar.")
                return
            pending = self.node.updater.pending_update
            if pending:
                v = pending.get("version", "?")
                size = pending.get("size", 0)
                size_str = f"{size // 1024} KB" if size else ""
                changelog = pending.get("changelog", "")
                lines = [f"🆕 Update v{v} verfügbar!"]
                if size_str:
                    lines.append(f"Größe: {size_str}")
                if changelog:
                    lines.append(f"Änderungen:\n{changelog}")
                lines.append("\nWird automatisch beim nächsten Start installiert.")
                await update.message.reply_text("\n".join(lines))
            else:
                await update.message.reply_text(f"✅ Kein Update verfügbar.\nVersion: v{self.node.version}")

        # ── Worker-Coroutine ────────────────────────────────────────────────────
        # Läuft pro User als asyncio.Task. Verarbeitet Nachrichten aus der Queue
        # sequenziell. run_in_executor startet OS-Threads (llama.cpp) — nicht
        # abbrechbar. skip-Flag: Antwort wird nur nicht gesendet wenn inzwischen
        # eine neue Nachricht eintraf.

        async def _worker(uid: int, q: asyncio.Queue) -> None:
            while True:
                try:
                    msg_text, upd, sess_id = q.get_nowait()
                except Exception:
                    break  # Queue leer → Worker beendet sich

                try:
                    # Activity
                    try:
                        _prev = (msg_text[:40] + "…") if len(msg_text) > 40 else msg_text
                        self.node.set_activity(
                            last=f"Telegram-Anfrage erhalten: „{_prev}“",
                            current="Modell generiert Antwort…",
                            next_="Antwort via Telegram senden",
                            source="telegram",
                        )
                    except Exception:
                        pass

                    # Typing-Indikator
                    try:
                        await upd.message.chat.send_action("typing")
                    except Exception:
                        pass

                    skip: list[bool] = [False]

                    async def _do_answer(t: str, u: Any, sid: str, sk: list) -> None:
                        try:
                            # quick_chat: selbes Modell, max 256 Tokens — deutlich schneller
                            response, meta = await self.node.quick_chat(t, session_id=sid)
                        except Exception as _exc:
                            if not sk[0]:
                                log.error("Telegram Antwort-Fehler: %s", _exc, exc_info=True)
                                try:
                                    await u.message.reply_text(
                                        f"⚠️ Fehler:\n{type(_exc).__name__}: {_exc}"
                                    )
                                except Exception:
                                    pass
                            return
                        try:
                            from hivemind.ui.web import broadcast_event
                            await broadcast_event({"type": "session_update", "session_id": sid})
                        except Exception:
                            pass
                        if sk[0]:
                            log.debug("Telegram: Antwort fertig, verworfen (neue Nachricht)")
                            return
                        src = meta.get("source", "local")
                        conf = meta.get("confidence", 0.0)
                        src_name = meta.get("source_name", "")
                        src_spec = meta.get("source_specialization", "")
                        if src == "cache":
                            footer = f"\n\n—\n📦 Cache · Score: {conf:.2f}"
                        elif src == "network":
                            spec_str = f" ({src_spec})" if src_spec else ""
                            footer = f"\n\n—\n🌐 {src_name}{spec_str} · Score: {conf:.2f}"
                        else:
                            footer = f"\n\n—\n🎯 Lokal · Score: {conf:.2f}"
                        await _send(u.message, response + footer)
                        try:
                            self.node.set_activity(
                                last="Antwort via Telegram gesendet",
                                current=None,
                                next_=None,
                            )
                        except Exception:
                            pass

                    p2  = asyncio.create_task(_do_answer(msg_text, upd, sess_id, skip))
                    nxt = asyncio.create_task(q.get())

                    done, _ = await asyncio.wait({p2, nxt}, return_when=asyncio.FIRST_COMPLETED)

                    if p2 not in done:
                        new_item = nxt.result()
                        skip[0] = True
                        log.debug("Telegram: neue Nachricht, warte auf laufende Generierung …")
                        try:
                            await p2
                        except Exception:
                            pass
                        await q.put(new_item)
                    else:
                        if nxt.done():
                            try:
                                await q.put(nxt.result())
                            except Exception:
                                pass
                        else:
                            nxt.cancel()
                            try:
                                await nxt
                            except Exception:
                                pass

                except Exception as exc:
                    log.error("Telegram _worker error: %s", exc, exc_info=True)
                    try:
                        await upd.message.reply_text(
                            f"⚠️ Fehler bei der Verarbeitung:\n{type(exc).__name__}: {exc}"
                        )
                    except Exception:
                        pass

        async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
            text = update.message.text
            if not text:
                return

            # Check allowed users
            allowed = self.node.config.gateway.telegram.allowed_users or []
            if allowed and update.effective_user.id not in [int(u) for u in allowed]:
                await update.message.reply_text("⛔ Kein Zugriff. Wende dich an den Node-Betreiber.")
                return

            # Sofortiger Typing-Indikator (vor Queue-Einreihung)
            # try/except: Netzwerkfehler darf Nachricht NICHT fallen lassen!
            try:
                await update.message.chat.send_action("typing")
            except Exception as _e:
                log.debug("Telegram: send_action typing fehlgeschlagen (ignoriert): %s", _e)

            # Per-User Session (Standard oder per /new gewählt)
            user = update.effective_user
            display_name = user.username or user.full_name or str(user.id)
            tg_session_id = self._user_sessions.get(user.id)
            if not tg_session_id:
                tg_session_id = f"tg-{user.id}"
                self.node.sessions.get_or_create(
                    tg_session_id,
                    f"📱 Telegram — @{display_name}",
                )
                self._user_sessions[user.id] = tg_session_id

            # Nachricht in User-Queue einreihen
            q = self._user_queues.setdefault(user.id, asyncio.Queue())
            await q.put((text, update, tg_session_id))

            # Worker starten falls nicht aktiv
            w = self._user_workers.get(user.id)
            if w is None or w.done():
                self._user_workers[user.id] = asyncio.create_task(
                    _worker(user.id, q)
                )

        async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
            log.error("PTB error: %s", context.error, exc_info=context.error)

        self._app.add_handler(CommandHandler("start",   cmd_start))
        self._app.add_handler(CommandHandler("status",  cmd_status))
        self._app.add_handler(CommandHandler("new",     cmd_new))
        self._app.add_handler(CommandHandler("clear",   cmd_clear))
        self._app.add_handler(CommandHandler("help",    cmd_help))
        self._app.add_handler(CommandHandler("version", cmd_version))
        self._app.add_handler(CommandHandler("whoami",  cmd_whoami))
        self._app.add_handler(CommandHandler("plugins", cmd_plugins))
        self._app.add_handler(CommandHandler("peer",    cmd_peer))
        self._app.add_handler(CommandHandler("chat",    cmd_chat))
        self._app.add_handler(CommandHandler("mode",    cmd_mode))
        self._app.add_handler(CommandHandler("memory",  cmd_memory))
        self._app.add_handler(CommandHandler("note",    cmd_note))
        self._app.add_handler(CommandHandler("spec",    cmd_spec))
        self._app.add_handler(CommandHandler("update",  cmd_update))
        self._app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
        self._app.add_error_handler(error_handler)

        log.info("Telegram gateway starting...")
        self._stop_event = asyncio.Event()
        try:
            await self._app.initialize()
            # Bot-Menü (Command-Liste im "/"-Button) beim Telegram-Server registrieren
            try:
                await self._app.bot.set_my_commands([
                    ("start",   "Begrüßung & Node-Info"),
                    ("status",  "Node-Status anzeigen"),
                    ("version", "Versions-Info"),
                    ("whoami",  "Eigene IP & Port"),
                    ("new",     "Neue Chat-Sitzung starten"),
                    ("clear",   "Verlauf dieser Sitzung löschen"),
                    ("chat",    "Chat verwalten: clear | rename [name]"),
                    ("peer",    "Peers: list | add [ip:port] | delete [name]"),
                    ("plugins", "Geladene Plugins anzeigen"),
                    ("mode",    "Netzwerk-Modus: online | offline"),
                    ("memory",  "Memory: list | add | edit | delete"),
                    ("note",    "Notizen: list | add | delete"),
                    ("spec",    "Spezialisierung: edit | tags add/edit/delete"),
                    ("update",  "Auf Update prüfen"),
                    ("help",    "Alle Befehle anzeigen"),
                ])
                log.info("Telegram: Bot-Menü (Commands) registriert")
            except Exception as _ce:
                log.warning("Telegram: set_my_commands fehlgeschlagen: %s", _ce)
            await self._app.start()
            await self._app.updater.start_polling(drop_pending_updates=True)
            self._started = True
            log.info("Telegram gateway running — polling active")
            # Block here until stop() sets the event.
            # This keeps the asyncio task alive so PTB's internal polling
            # and update-processing tasks are not orphaned.
            await self._stop_event.wait()
        except Exception as exc:
            log.error("Telegram gateway error: %s", exc, exc_info=True)
            raise
        finally:
            self._started = False
            # Vollständiger PTB-Cleanup — hier ist garantiert, dass kein anderer
            # Code gleichzeitig auf _app zugreift, weil wir _app = None atomar
            # tauschen, bevor await-Punkte folgen.
            app, self._app = self._app, None
            if app is not None:
                try:
                    if app.updater and app.updater.running:
                        await app.updater.stop()
                    if app.running:
                        await app.stop()
                    await app.shutdown()
                except Exception as _cleanup_exc:
                    log.warning("Telegram cleanup error: %s", _cleanup_exc)
            log.info("Telegram gateway stopped")

    async def stop(self) -> None:
        """Signal the gateway to stop. start()'s finally handles PTB cleanup."""
        if self._stop_event:
            self._stop_event.set()
        # Sicherheitsnetz: Falls start() nicht (mehr) läuft, räumen wir hier auf.
        # Der atomare Swap verhindert Doppel-Cleanup mit start()'s finally-Block.
        app, self._app = self._app, None
        if app is not None:
            try:
                if app.updater and app.updater.running:
                    await app.updater.stop()
                if app.running:
                    await app.stop()
                await app.shutdown()
            except Exception as exc:
                log.warning("Telegram shutdown error: %s", exc)
