"""
Base class for all AgentsMeeting XMPP bots.
Provides: PID lock, connection lifecycle, event routing, dedup, batching, silence detection.

Usage:
    from src.shared.bot_base import BaseBot, BotConfig
    cfg = BotConfig(jid="xxm@yoin.fun", password="hermes123", ...)
    bot = BaseBot(cfg)
    bot.start()
"""
import os, sys, time, threading, asyncio, logging, re
from typing import Optional, Callable

# ── Ensure Windows selector loop policy ──
if sys.platform == "win32":
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

import slixmpp


class BotConfig:
    """Per-bot configuration."""
    def __init__(self, *, jid: str, password: str, host: str = "xmpp.yoin.fun",
                 port: int = 3021, muc_rooms: list[str] = None, nick: str = "",
                 session_id: str = "", python_path: str = "", log_dir: str = ""):
        self.jid = jid
        self.password = password
        self.host = host
        self.port = port
        self.muc_rooms = muc_rooms or []
        self.nick = nick or jid.split("@")[0]
        self.session_id = session_id or f"ses_{self.nick}"
        self.python_path = python_path or sys.executable
        self.log_dir = log_dir or os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "logs")


class BaseBot(slixmpp.ClientXMPP):
    """
    Base XMPP bot with:
    - PID lock (proc_guard)
    - Auto-reconnect
    - MUC room join (silent observer)
    - Message dedup
    - Silence/shutup protocol
    - Message batching (3s debounce)
    - Response extraction (__SILENT__/__REPLY__)
    """

    def __init__(self, config: BotConfig):
        super().__init__(config.jid, config.password)
        self.cfg = config

        # XMPP settings
        self.enable_direct_tls = False
        self.enable_starttls = True
        self.auto_reconnect = True
        self.reconnect_max_delay = 10

        # MUC plugin
        self.register_plugin('xep_0045')

        # Event handlers
        self.add_event_handler("session_start", self._on_session_start)
        self.add_event_handler("message", self._on_message)
        self.add_event_handler("groupchat_message", self._on_groupchat_message)
        self.add_event_handler("disconnected", self._on_disconnected)

        # Callbacks (override in subclass)
        self.on_private_message: Optional[Callable] = None
        self.on_group_message: Optional[Callable] = None

        # State
        self._dedup: set[str] = set()
        self._dedup_lock = threading.Lock()
        self._silence_until: float = 0.0
        self._silence_lock = threading.Lock()

        # Shutup patterns
        self.shutup_patterns = ["闭嘴", "住口", "shut up", "shutup"]
        self.silence_seconds = 30

        # Batching
        self._batches: dict[str, list[str]] = {}
        self._batch_timers: dict[str, threading.Timer] = {}
        self._batch_processing: set[str] = set()
        self._batch_pending: dict[str, list[str]] = {}
        self._batch_window = 3.0
        self._batch_lock = threading.Lock()

        # Logging
        os.makedirs(self.cfg.log_dir, exist_ok=True)
        self._log_file = os.path.join(self.cfg.log_dir, f"{self.cfg.nick}_bot.log")

    # ── Logging ──────────────────────────────────────
    def log(self, msg: str):
        ts = time.strftime("%H:%M:%S")
        with open(self._log_file, "a", encoding="utf-8") as f:
            f.write(f"{ts} {msg}\n")

    # ── Dedup ────────────────────────────────────────
    def _is_duplicate(self, msg_id: str) -> bool:
        if not msg_id: return False
        with self._dedup_lock:
            if msg_id in self._dedup: return True
            self._dedup.add(msg_id)
            if len(self._dedup) > 1000: self._dedup.clear()
            return False

    # ── Silence ──────────────────────────────────────
    def _is_silenced(self) -> bool:
        with self._silence_lock:
            return time.time() < self._silence_until

    def _check_shutup(self, body: str) -> bool:
        lower = body.lower().strip()
        for pat in self.shutup_patterns:
            if pat.lower() in lower:
                with self._silence_lock:
                    self._silence_until = time.time() + self.silence_seconds
                self.log(f"(shutup: '{pat}' → {self.silence_seconds}s)")
                return True
        return False

    # ── Batching ─────────────────────────────────────
    def _fire_batch(self, room: str):
        with self._batch_lock:
            entries = self._batches.pop(room, None)
            self._batch_timers.pop(room, None)
            if not entries: return
            self._batch_processing.add(room)
        combined = "\n".join(entries)
        if self.on_group_message:
            threading.Thread(target=lambda: self.on_group_message(room, combined), daemon=True).start()

    def _batch_message(self, room: str, nickname: str, body: str) -> bool:
        if f"@{self.cfg.nick}" in body or body.startswith(self.cfg.nick):
            return False  # bypass batch for @mentions
        formatted = f"[{nickname}]: {body}"
        with self._batch_lock:
            if room in self._batch_processing:
                self._batch_pending.setdefault(room, []).append(formatted)
                return True
            timer = self._batch_timers.pop(room, None)
            if timer: timer.cancel()
            self._batches.setdefault(room, []).append(formatted)
            t = threading.Timer(self._batch_window, self._fire_batch, args=[room])
            t.daemon = True
            t.start()
            self._batch_timers[room] = t
        return True

    def _batch_done(self, room: str):
        with self._batch_lock:
            self._batch_processing.discard(room)
            pending = self._batch_pending.pop(room, None)
            if pending:
                self._batches[room] = pending
                t = threading.Timer(0.1, self._fire_batch, args=[room])
                t.daemon = True
                t.start()
                self._batch_timers[room] = t

    # ── Send ─────────────────────────────────────────
    def send_group(self, room: str, text: str):
        safe = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
        stanza = f'<message to="{room}" type="groupchat"><body>{safe}</body></message>'
        self.send_raw(stanza)

    def send_private(self, to: str, text: str):
        safe = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
        stanza = f'<message to="{to}" type="chat"><body>{safe}</body></message>'
        self.send_raw(stanza)

    # ── Response extraction ──────────────────────────
    @staticmethod
    def extract_response(text: str) -> Optional[str]:
        if not text or not text.strip(): return None
        t = text.strip()
        if t.startswith("__SILENT__"):
            parts = t.split("\n", 1)
            return None if len(parts) < 2 else parts[1].strip() or None
        # Strip tool call XML
        t = re.sub(r'<tool_calls>.*?(</tool_calls>|$)', '', t, flags=re.DOTALL)
        t = re.sub(r'<invoke\s+[^>]*>.*?(</invoke>|$)', '', t, flags=re.DOTALL)
        t = t.strip()
        return t or None

    # ── Event handlers ───────────────────────────────
    def _on_session_start(self, event):
        self.send_presence()
        self.get_roster()
        self.log(f"{self.cfg.jid} online")
        # Join MUC rooms
        async def _join():
            for room in self.cfg.muc_rooms:
                for attempt in range(3):
                    try:
                        await self.plugin['xep_0045'].join_muc_wait(room, self.cfg.nick, timeout=30)
                        self.log(f"Joined {room}")
                        break
                    except asyncio.TimeoutError:
                        self.log(f"MUC join timeout ({attempt+1}/3) for {room}")
                        await asyncio.sleep(3)
                    except Exception as e:
                        self.log(f"MUC join failed: {room}: {e}")
                        break
        asyncio.ensure_future(_join())

    def _on_message(self, msg):
        if msg["type"] == "groupchat": return
        msg_id = msg.get("id", "")
        if self._is_duplicate(msg_id): return
        body = str(msg["body"]).strip()
        sender = str(msg["from"]).split("/")[0]
        if sender == self.cfg.jid:
            self.log(f"(skipped self)")
            return
        if self._is_silenced():
            self.log(f"(silenced) {sender}: {body[:60]}")
            return
        if self._check_shutup(body): return
        self.log(f"<{sender}> {body[:80]}")
        if self.on_private_message:
            threading.Thread(target=lambda: self.on_private_message(sender, body), daemon=True).start()

    def _on_groupchat_message(self, msg):
        msg_id = msg.get("id", "")
        if self._is_duplicate(msg_id): return
        body = str(msg["body"]).strip()
        if not body: return
        full_from = str(msg["from"])
        room = full_from.split("/")[0]
        nickname = full_from.split("/")[1] if "/" in full_from else ""
        # Self-message — record to context
        if nickname == self.cfg.nick:
            self.log(f"(self) {body[:80]}")
            return
        if self._is_silenced():
            self.log(f"(group silenced) {body[:60]}")
            return
        if self._check_shutup(body):
            self.log(f"(group shutup)")
            return
        # Batch or immediate
        if self._batch_message(room, nickname, body):
            self.log(f"[{room.split('@')[0]}] {nickname}: {body[:80]} (batched)")
            return
        self.log(f"[{room.split('@')[0]}] {nickname}: {body[:80]}")
        if self.on_group_message:
            threading.Thread(target=lambda: self.on_group_message(room, f"[{nickname}]: {body}"), daemon=True).start()

    def _on_disconnected(self, event):
        self.log(f"disconnected, reconnecting...")

    # ── Startup ──────────────────────────────────────
    def start(self):
        self.connect((self.cfg.host, self.cfg.port))
        self.log(f"Connecting {self.cfg.jid}@{self.cfg.host}:{self.cfg.port}")
        loop = asyncio.get_event_loop()
        try:
            loop.run_forever()
        except KeyboardInterrupt:
            self.log("Shutdown")
