"""Bridge between peon-loop and the Discord bot integration.

Provides synchronous helpers that loop.py can call between task cycles to:
  - Start / stop Discord bots via a background asyncio thread
  - Poll the REST API for pending inbound messages
  - Acknowledge messages after processing
  - Send agent responses back to Discord channels
  - Decide whether a message warrants interrupting the current task

Usage in loop.py::

    from peon_mcp.discord.loop_bridge import (
        DiscordLoopThread,
        poll_messages,
        ack_message,
        send_agent_response,
        handle_interrupt,
    )

    bridge = DiscordLoopThread(db_path=db_path, project_id=project_id)
    bridge.start()
    # ... run agent cycles ...
    messages = poll_messages(project_id, api_url)
    for msg in messages:
        if handle_interrupt(msg):
            # Inject as agent context on next cycle
            pass
        ack_message(msg["id"], api_url)
    bridge.stop()
"""

from __future__ import annotations

import asyncio
import json
import logging
import sqlite3
import threading
import urllib.error
import urllib.request
from typing import Any

logger = logging.getLogger("peon-loop.discord")


# ---------------------------------------------------------------------------
# HTTP helper (reuses loop.py pattern — no external deps)
# ---------------------------------------------------------------------------


def _http_request(
    method: str,
    url: str,
    payload: dict | None = None,
    params: dict | None = None,
    api_key: str = "",
) -> Any:
    """Make a synchronous HTTP request, returning parsed JSON or None on failure."""
    if params:
        from urllib.parse import urlencode
        url = f"{url}?{urlencode(params)}"
    body = json.dumps(payload).encode("utf-8") if payload is not None else None
    headers: dict[str, str] = {"Content-Type": "application/json"}
    if api_key:
        headers["X-API-Key"] = api_key
    req = urllib.request.Request(url, data=body, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            return json.loads(resp.read())
    except (urllib.error.URLError, json.JSONDecodeError, OSError) as exc:
        logger.debug("Discord bridge HTTP call failed (%s %s): %s", method, url, exc)
        return None


# ---------------------------------------------------------------------------
# Message polling and acknowledgement
# ---------------------------------------------------------------------------


def poll_messages(project_id: str, api_url: str, api_key: str = "") -> list[dict]:
    """Fetch pending inbound Discord messages for a project from the REST API.

    Args:
        project_id: The project whose Discord configs to query.
        api_url: Base URL for the peon REST API (e.g. 'http://127.0.0.1:8420').
        api_key: Optional API key for authenticated instances.

    Returns:
        List of inbound message dicts, oldest first.  Empty list on error.
    """
    url = f"{api_url.rstrip('/')}/api/discord/messages/pending"
    result = _http_request("GET", url, params={"project_id": project_id}, api_key=api_key)
    if isinstance(result, list):
        return result
    return []


def ack_message(
    message_id: int,
    api_url: str,
    status: str = "processed",
    api_key: str = "",
) -> None:
    """Acknowledge (mark as processed) an inbound Discord message.

    Args:
        message_id: Inbound message ID.
        api_url: Base URL for the peon REST API.
        status: 'processed' or 'ignored' (default: 'processed').
        api_key: Optional API key.
    """
    url = f"{api_url.rstrip('/')}/api/discord/messages/{message_id}/ack"
    _http_request("POST", url, params={"status": status}, api_key=api_key)


# ---------------------------------------------------------------------------
# Sending responses
# ---------------------------------------------------------------------------


def send_agent_response(
    config_id: int,
    channel_id: str,
    content: str,
    api_url: str,
    embed_title: str = "",
    api_key: str = "",
) -> bool:
    """Send a response from the agent back to a Discord channel.

    Args:
        config_id: Discord config ID (determines which bot sends).
        channel_id: Target Discord channel snowflake ID.
        content: Message content to send.
        api_url: Base URL for the peon REST API.
        embed_title: Optional embed title for richer formatting.
        api_key: Optional API key.

    Returns:
        True if the API accepted the send request, False on error.
    """
    url = f"{api_url.rstrip('/')}/api/discord/configs/{config_id}/send"
    result = _http_request(
        "POST",
        url,
        payload={
            "channel_id": channel_id,
            "content": content,
            "embed_title": embed_title,
        },
        api_key=api_key,
    )
    if result is None:
        return False
    return bool(result.get("sent", False))


# ---------------------------------------------------------------------------
# Output formatting
# ---------------------------------------------------------------------------


def format_agent_output(tool_result: Any) -> str:
    """Convert an agent tool result to a Discord-friendly text string.

    Truncates to Discord's 2000-character message limit.

    Args:
        tool_result: Any value returned from an agent tool call.

    Returns:
        A formatted string safe to post to Discord.
    """
    if isinstance(tool_result, dict):
        text = json.dumps(tool_result, indent=2)
    elif isinstance(tool_result, (list, tuple)):
        text = "\n".join(str(item) for item in tool_result)
    else:
        text = str(tool_result)

    # Discord message limit is 2000 chars; leave room for code block markers
    max_len = 1990
    if len(text) > max_len:
        text = text[: max_len - 3] + "..."

    return text


# ---------------------------------------------------------------------------
# Interrupt detection
# ---------------------------------------------------------------------------

# Keywords that indicate a message is an urgent interrupt (case-insensitive).
_INTERRUPT_KEYWORDS = frozenset([
    "stop", "abort", "cancel", "pause", "halt",
    "urgent", "emergency", "critical", "asap",
])


def handle_interrupt(message: dict) -> bool:
    """Decide whether an inbound Discord message warrants interrupting the agent.

    Currently uses simple keyword matching on the message content.  Future
    versions may use NLP or configurable rules per channel mapping.

    Args:
        message: Inbound message dict from the REST API.

    Returns:
        True if the message should be treated as an interrupt signal.
    """
    content = message.get("content", "").lower()
    if not content:
        return False
    words = set(content.split())
    return bool(words & _INTERRUPT_KEYWORDS)


# ---------------------------------------------------------------------------
# Background asyncio thread for running Discord bots in the loop process
# ---------------------------------------------------------------------------


class DiscordLoopThread(threading.Thread):
    """Background thread that runs an asyncio event loop hosting Discord bots.

    Starts enabled Discord bots for the given project on thread start, and
    gracefully shuts them down on stop().

    This allows the synchronous peon-loop process to host live Discord
    connections without blocking the main thread.

    Example::

        bridge = DiscordLoopThread(db_path=db_path, project_id=project_id)
        bridge.start()
        # ... main loop runs ...
        bridge.stop()
    """

    def __init__(self, db_path: str, project_id: str) -> None:
        super().__init__(daemon=True, name="discord-loop-thread")
        self._db_path = db_path
        self._project_id = project_id
        self._loop: asyncio.AbstractEventLoop | None = None
        self._stop_event = threading.Event()
        self._ready_event = threading.Event()
        # Lazy import to avoid ImportError when discord.py is not installed
        self._bot_manager = None

    # ------------------------------------------------------------------
    # Thread lifecycle
    # ------------------------------------------------------------------

    def run(self) -> None:
        """Entry point: create event loop, start bots, wait for stop signal."""
        self._loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self._loop)
        try:
            self._loop.run_until_complete(self._run_async())
        except Exception as exc:
            logger.error("Discord loop thread encountered error: %s", exc)
        finally:
            try:
                self._loop.close()
            except Exception:
                pass

    async def _run_async(self) -> None:
        """Async body: import BotManager, start bots, idle until stopped."""
        try:
            from peon_mcp.discord.bot import BotManager
        except ImportError as exc:
            logger.warning("Discord package not available: %s", exc)
            self._ready_event.set()
            return

        self._bot_manager = BotManager()

        # Load enabled configs for this project directly from sqlite3.
        configs = self._load_enabled_configs()
        for cfg in configs:
            token = cfg.get("bot_token", "")
            if not token:
                continue
            try:
                await self._bot_manager.start_bot(
                    cfg["id"], token, cfg, db_path=self._db_path
                )
                logger.info(
                    "Discord loop: started bot for config_id=%s (project=%s)",
                    cfg["id"], self._project_id,
                )
            except Exception as exc:
                logger.error(
                    "Discord loop: failed to start bot config_id=%s: %s",
                    cfg["id"], exc,
                )

        self._ready_event.set()
        logger.info(
            "Discord loop thread ready (%d bot(s) started for project '%s')",
            len(configs), self._project_id,
        )

        # Idle until the stop signal is set
        while not self._stop_event.is_set():
            await asyncio.sleep(1)

        # Graceful shutdown
        logger.info("Discord loop thread shutting down bots...")
        try:
            await self._bot_manager.stop_all()
        except Exception as exc:
            logger.warning("Error stopping Discord bots on shutdown: %s", exc)

    def start(self) -> None:
        """Start the thread and wait until bots are initialised (up to 10s)."""
        super().start()
        self._ready_event.wait(timeout=10)

    def stop(self) -> None:
        """Signal the async loop to stop and wait for the thread to exit."""
        self._stop_event.set()
        self.join(timeout=15)

    # ------------------------------------------------------------------
    # Status
    # ------------------------------------------------------------------

    def get_discord_status(self) -> str:
        """Return a comma-separated list of connected bot config IDs.

        Returns an empty string when no bots are running.  This string is
        stored in the ``discord_status`` column of ``loop_heartbeats`` so the
        UI can display Discord connectivity alongside loop liveness.
        """
        if self._bot_manager is None:
            return ""
        connected = [
            str(cid)
            for cid, gw in self._bot_manager._gateways.items()
            if gw.is_connected
        ]
        return ",".join(connected)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _load_enabled_configs(self) -> list[dict]:
        """Load enabled Discord configs for this project directly from sqlite3."""
        try:
            conn = sqlite3.connect(self._db_path)
            conn.row_factory = sqlite3.Row
            try:
                cursor = conn.execute(
                    "SELECT * FROM discord_configs"
                    " WHERE project_id = ? AND enabled = 1",
                    (self._project_id,),
                )
                return [dict(row) for row in cursor.fetchall()]
            finally:
                conn.close()
        except sqlite3.Error as exc:
            logger.warning(
                "Discord loop: failed to load configs for '%s': %s",
                self._project_id, exc,
            )
            return []
