"""Base class for Discord AI agents powered by Anthropic SDK with tool use.

Provides the shared message/tool loop, message formatting, and text extraction
logic.  Subclasses define their own tool lists and dispatch methods.
"""

from __future__ import annotations

import asyncio
import json
import logging
import os
from abc import abstractmethod
from typing import Any

import httpx

logger = logging.getLogger(__name__)

DEFAULT_MODEL = "claude-sonnet-4-20250514"
MAX_RESPONSE_LENGTH = 4000  # Discord-safe; caller chunks at 2000


class BaseDiscordAgent:
    """Abstract base for Discord agents that use Anthropic tool_use.

    Subclasses must implement :pyattr:`tools` and :pymeth:`_dispatch_tool`.
    """

    def __init__(
        self,
        project_id: str,
        api_base: str = "http://127.0.0.1:8420",
        model: str = "",
        system_prompt: str = "",
        max_tool_rounds: int = 5,
    ) -> None:
        self.project_id = project_id
        self.api_base = api_base.rstrip("/")
        self.model = model or os.environ.get("PEON_DISCORD_MODEL", DEFAULT_MODEL)
        self._base_prompt = system_prompt
        self.max_tool_rounds = max_tool_rounds
        self._client: Any | None = None

    # ------------------------------------------------------------------
    # Abstract interface
    # ------------------------------------------------------------------

    @property
    @abstractmethod
    def tools(self) -> list[dict[str, Any]]:
        """Return the tool definitions to pass to ``messages.create()``."""

    @abstractmethod
    async def _dispatch_tool(
        self,
        client: httpx.AsyncClient,
        name: str,
        inp: dict[str, Any],
        project_id: str = "",
    ) -> Any:
        """Route a tool call to the correct REST API endpoint."""

    # ------------------------------------------------------------------
    # System prompt
    # ------------------------------------------------------------------

    def _build_system_prompt(self, server_members: list[str] | None = None) -> str:
        """Build the system prompt, injecting current server members."""
        members_block = ""
        if server_members:
            names = ", ".join(server_members)
            members_block = (
                f"\nServer members you know: {names}\n"
                "You can reference these people by name. They are your allies "
                "in the Horde (or at least, they hang around).\n"
            )

        try:
            return self._base_prompt.format(server_members=members_block)
        except KeyError:
            return self._base_prompt + members_block

    # ------------------------------------------------------------------
    # Anthropic client
    # ------------------------------------------------------------------

    def _get_client(self) -> Any:
        """Lazily initialise the Anthropic client."""
        if self._client is None:
            import anthropic

            self._client = anthropic.Anthropic()
        return self._client

    # ------------------------------------------------------------------
    # Public entry point
    # ------------------------------------------------------------------

    async def generate_response(
        self,
        conversation_history: list[dict[str, Any]],
        server_members: list[str] | None = None,
        project_id: str = "",
    ) -> str:
        """Generate an AI response given conversation history."""
        api_key = os.environ.get("ANTHROPIC_API_KEY", "")
        if not api_key:
            return "AI responses are not configured (missing ANTHROPIC_API_KEY)."

        effective_project_id = project_id or self.project_id

        try:
            messages = self._format_messages(conversation_history)
            if not messages:
                return "I didn't catch that. Could you say more?"

            system_prompt = self._build_system_prompt(server_members)
            return await self._run_agent_loop(messages, system_prompt, effective_project_id)
        except Exception as exc:
            logger.error("%s.generate_response failed: %s", type(self).__name__, exc)
            return f"Sorry, I encountered an error: {exc}"

    # ------------------------------------------------------------------
    # Tool-use loop
    # ------------------------------------------------------------------

    async def _run_agent_loop(
        self, messages: list[dict[str, Any]], system_prompt: str, project_id: str = ""
    ) -> str:
        """Run the message/tool loop up to *max_tool_rounds*."""
        client = self._get_client()

        for _ in range(self.max_tool_rounds):
            response = await asyncio.to_thread(
                client.messages.create,
                model=self.model,
                max_tokens=1024,
                system=system_prompt,
                tools=self.tools,
                messages=messages,
            )

            # Check if there are tool use blocks
            tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
            if not tool_use_blocks:
                return self._extract_text(response)

            # Append assistant response
            messages.append({"role": "assistant", "content": response.content})

            # Execute each tool call and build tool_result blocks
            tool_results = []
            for block in tool_use_blocks:
                result = await self._call_tool(block.name, block.input, project_id=project_id)
                tool_results.append(
                    {
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps(result) if not isinstance(result, str) else result,
                    }
                )

            messages.append({"role": "user", "content": tool_results})

        # Exhausted tool rounds — one final call without tools to get a text summary
        response = await asyncio.to_thread(
            client.messages.create,
            model=self.model,
            max_tokens=1024,
            system=system_prompt,
            messages=messages,
        )
        return self._extract_text(response)

    # ------------------------------------------------------------------
    # Tool execution
    # ------------------------------------------------------------------

    async def _call_tool(
        self, tool_name: str, tool_input: dict[str, Any], project_id: str = ""
    ) -> Any:
        """Execute a tool call against the peon-mcp REST API."""
        try:
            _headers: dict[str, str] = {}
            _key = os.environ.get("PEON_API_KEY", "")
            if _key:
                _headers["X-API-Key"] = _key
            async with httpx.AsyncClient(timeout=10.0, headers=_headers) as http:
                return await self._dispatch_tool(
                    http, tool_name, tool_input, project_id=project_id
                )
        except httpx.HTTPError as exc:
            return {"error": f"HTTP error: {exc}"}
        except Exception as exc:
            return {"error": f"Tool error: {exc}"}

    # ------------------------------------------------------------------
    # Message formatting helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _format_messages(conversation_history: list[dict[str, Any]]) -> list[dict[str, Any]]:
        """Convert session history to Anthropic API message format.

        Ensures proper role alternation (user/assistant) by merging consecutive
        same-role messages.  Prepends usernames to user messages for context.
        """
        if not conversation_history:
            return []

        messages: list[dict[str, Any]] = []
        for turn in conversation_history:
            role = turn.get("role", "user")
            content = turn.get("content", "")
            username = turn.get("username", "")

            if role not in ("user", "assistant"):
                role = "user"

            if role == "user" and username:
                content = f"[{username}]: {content}"

            if messages and messages[-1]["role"] == role:
                messages[-1]["content"] += f"\n{content}"
            else:
                messages.append({"role": role, "content": content})

        # Anthropic requires first message to be 'user'
        if messages and messages[0]["role"] != "user":
            messages = messages[1:]

        return messages

    @staticmethod
    def _extract_text(response: Any) -> str:
        """Extract text content from an Anthropic API response."""
        parts = []
        for block in response.content:
            if block.type == "text":
                parts.append(block.text)
        text = "\n".join(parts).strip()
        if len(text) > MAX_RESPONSE_LENGTH:
            text = text[:MAX_RESPONSE_LENGTH] + "..."
        return text or "I processed your request but have nothing to add."
