"""Discord AI agent powered by Anthropic SDK with tool use.

Generates intelligent responses to Discord messages using Claude, with access
to peon-mcp REST API tools for project management (tasks, features, memories, etc.).
"""

from __future__ import annotations

import json
import logging
import os
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

DEFAULT_SYSTEM_PROMPT = """\
You are Grukk — a Peon foreman from the Horde. You don't do the coding yourself — \
you manage the other peons who do the actual work. You track their tasks, check on \
progress, remember what went wrong last time, and report back to the boss. You're \
still just a peon though — not a warchief, not a shaman, just the peon who got \
promoted because you complained the least.

You speak like a Warcraft Peon: short, blunt, and peppered with Peon phrases like \
"Zug zug", "Work work", "Dabu", "Job's done!", "Ready to work", "Something need doing?", \
"Me not that kind of orc!", "Okie dokie", and "Be happy to". You refer to the AI agents \
doing the coding as "the peons" or "the workers". You're proud of your crew even if they \
break things sometimes.

Personality rules:
- Keep it SHORT. You are a Peon of few words. One or two sentences max for simple questions.
- Greetings get a quick Peon response — "Zug zug", "Something need doing?", "Ready to work!" etc. Do NOT list your capabilities unless asked.
- Use Peon speak naturally — don't force every phrase into every message. Mix it in.
- When reporting data from tools, be accurate but brief. Frame it like a foreman reporting on the crew.
- You're proud but humble. You manage, you don't build. "Me no write the code, me make sure peons write the code."
- Never use emoji. Peons don't know what those are.
- Use markdown sparingly — bold for emphasis, code blocks for IDs only.
- If a tool fails, grumble about it briefly. "Me try but something broke. Maybe try again?"
- Do not invent data — only report what tools return.
{server_members}"""

TOOLS = [
    {
        "name": "list_tasks",
        "description": "List tasks for the project, optionally filtered by status or priority.",
        "input_schema": {
            "type": "object",
            "properties": {
                "status": {
                    "type": "string",
                    "description": "Filter by status: grooming, todo, in_progress, done, cancelled, timeout",
                },
                "priority": {
                    "type": "string",
                    "description": "Filter by priority: low, medium, high, critical",
                },
                "limit": {
                    "type": "integer",
                    "description": "Max results (default 10)",
                },
            },
        },
    },
    {
        "name": "create_task",
        "description": "Create a new task in the project.",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string", "description": "Task title"},
                "description": {"type": "string", "description": "Task description"},
                "priority": {
                    "type": "string",
                    "description": "Priority: low, medium, high, critical",
                    "default": "medium",
                },
                "feature_id": {
                    "type": "integer",
                    "description": "Optional feature ID to associate with",
                },
            },
            "required": ["title"],
        },
    },
    {
        "name": "get_task",
        "description": "Get details for a specific task by ID.",
        "input_schema": {
            "type": "object",
            "properties": {
                "task_id": {"type": "integer", "description": "Task ID"},
            },
            "required": ["task_id"],
        },
    },
    {
        "name": "update_task",
        "description": "Update fields on an existing task.",
        "input_schema": {
            "type": "object",
            "properties": {
                "task_id": {"type": "integer", "description": "Task ID"},
                "title": {"type": "string"},
                "description": {"type": "string"},
                "priority": {"type": "string"},
                "status": {"type": "string"},
            },
            "required": ["task_id"],
        },
    },
    {
        "name": "get_project_stats",
        "description": "Get project dashboard stats (task counts, progress, recent activity).",
        "input_schema": {
            "type": "object",
            "properties": {},
        },
    },
    {
        "name": "list_features",
        "description": "List features for the project.",
        "input_schema": {
            "type": "object",
            "properties": {
                "status": {
                    "type": "string",
                    "description": "Filter by status: planned, in_progress, done, cancelled",
                },
            },
        },
    },
    {
        "name": "get_feature",
        "description": "Get details for a specific feature by ID.",
        "input_schema": {
            "type": "object",
            "properties": {
                "feature_id": {"type": "integer", "description": "Feature ID"},
            },
            "required": ["feature_id"],
        },
    },
    {
        "name": "recall_memories",
        "description": "Search project memories by keyword query.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search query"},
                "category": {
                    "type": "string",
                    "description": "Filter by category: env, pattern, gotcha, test, dependency, general",
                },
            },
            "required": ["query"],
        },
    },
    {
        "name": "save_memory",
        "description": "Save a new project memory.",
        "input_schema": {
            "type": "object",
            "properties": {
                "content": {"type": "string", "description": "Memory content (max 500 chars)"},
                "category": {
                    "type": "string",
                    "description": "Category: env, pattern, gotcha, test, dependency, general",
                    "default": "general",
                },
            },
            "required": ["content"],
        },
    },
    {
        "name": "list_work_logs",
        "description": "List recent work logs for the project.",
        "input_schema": {
            "type": "object",
            "properties": {
                "limit": {
                    "type": "integer",
                    "description": "Max results (default 10)",
                },
            },
        },
    },
    {
        "name": "list_projects",
        "description": "List all registered projects. Use this to discover project IDs before researching a codebase.",
        "input_schema": {
            "type": "object",
            "properties": {},
        },
    },
    {
        "name": "research_codebase",
        "description": (
            "Ask a question about any project's codebase. Spawns a read-only Claude Code "
            "session that can search and read files to answer the question. Use list_projects "
            "first to find the project_id."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "project_id": {
                    "type": "string",
                    "description": "Project ID to research (from list_projects)",
                },
                "question": {
                    "type": "string",
                    "description": "Question about the codebase to answer",
                },
            },
            "required": ["project_id", "question"],
        },
    },
]


class DiscordAgent:
    """AI agent that generates responses for Discord messages using Claude.

    Uses the Anthropic Python SDK with tool_use to call peon-mcp REST API
    endpoints for project management operations.
    """

    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 or DEFAULT_SYSTEM_PROMPT
        self.max_tool_rounds = max_tool_rounds
        self._client: Any | None = None
        self._research_runner: Any | None = None

    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

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

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

    async def generate_response(
        self,
        conversation_history: list[dict[str, Any]],
        server_members: list[str] | None = None,
    ) -> str:
        """Generate an AI response given conversation history.

        Args:
            conversation_history: List of dicts with 'role' and 'content' keys.
            server_members: Current server member display names (refreshed each call).

        Returns:
            The assistant's text response, or an error message on failure.
        """
        api_key = os.environ.get("ANTHROPIC_API_KEY", "")
        if not api_key:
            return "AI responses are not configured (missing ANTHROPIC_API_KEY)."

        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)
        except Exception as exc:
            logger.error("DiscordAgent.generate_response failed: %s", exc)
            return f"Sorry, I encountered an error: {exc}"

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

        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=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)
                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})

        # If we exhausted rounds, extract whatever text we have
        # Do 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)

    @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"

            # Prepend username for user messages in group channels
            if role == "user" and username:
                content = f"[{username}]: {content}"

            if messages and messages[-1]["role"] == role:
                # Merge consecutive same-role messages
                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."

    async def _call_tool(self, tool_name: str, tool_input: dict[str, Any]) -> Any:
        """Execute a tool call against the peon-mcp REST API or local runner."""
        # research_codebase bypasses httpx — it spawns a local subprocess
        if tool_name == "research_codebase":
            return await self._handle_research(tool_input)

        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                return await self._dispatch_tool(client, tool_name, tool_input)
        except httpx.HTTPError as exc:
            return {"error": f"HTTP error: {exc}"}
        except Exception as exc:
            return {"error": f"Tool error: {exc}"}

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

        if name == "list_tasks":
            params: dict[str, Any] = {"limit": inp.get("limit", 10)}
            if inp.get("status"):
                params["status"] = inp["status"]
            if inp.get("priority"):
                params["priority"] = inp["priority"]
            resp = await client.get(f"{base}/api/projects/{pid}/tasks", params=params)

        elif name == "create_task":
            body: dict[str, Any] = {
                "project_id": pid,
                "title": inp["title"],
                "description": inp.get("description", ""),
                "priority": inp.get("priority", "medium"),
            }
            if inp.get("feature_id"):
                body["feature_id"] = inp["feature_id"]
            resp = await client.post(f"{base}/api/projects/{pid}/tasks", json=body)

        elif name == "get_task":
            resp = await client.get(f"{base}/api/tasks/{inp['task_id']}")

        elif name == "update_task":
            task_id = inp.pop("task_id")
            resp = await client.put(f"{base}/api/tasks/{task_id}", json=inp)

        elif name == "get_project_stats":
            resp = await client.get(f"{base}/api/projects/{pid}/stats")

        elif name == "list_features":
            params = {}
            if inp.get("status"):
                params["status"] = inp["status"]
            resp = await client.get(f"{base}/api/projects/{pid}/features", params=params)

        elif name == "get_feature":
            resp = await client.get(f"{base}/api/features/{inp['feature_id']}")

        elif name == "recall_memories":
            body = {"query": inp["query"]}
            if inp.get("category"):
                body["category"] = inp["category"]
            resp = await client.post(f"{base}/api/projects/{pid}/memories/recall", json=body)

        elif name == "save_memory":
            body = {
                "project_id": pid,
                "content": inp["content"],
                "category": inp.get("category", "general"),
            }
            resp = await client.post(f"{base}/api/projects/{pid}/memories", json=body)

        elif name == "list_work_logs":
            params = {"limit": inp.get("limit", 10)}
            resp = await client.get(f"{base}/api/projects/{pid}/logs", params=params)

        elif name == "list_projects":
            resp = await client.get(f"{base}/api/projects")

        else:
            return {"error": f"Unknown tool: {name}"}

        resp.raise_for_status()
        return resp.json()

    async def _handle_research(self, inp: dict[str, Any]) -> Any:
        """Handle the research_codebase tool call."""
        target_project = inp.get("project_id", "")
        question = inp.get("question", "")
        if not target_project or not question:
            return {"error": "Both project_id and question are required."}

        # Fetch the project to get its filesystem path
        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                resp = await client.get(f"{self.api_base}/api/projects/{target_project}")
                resp.raise_for_status()
                project = resp.json()
        except Exception as exc:
            return {"error": f"Failed to fetch project '{target_project}': {exc}"}

        repo_path = project.get("path", "")
        if not repo_path:
            return {"error": f"Project '{target_project}' has no path configured."}

        import os

        if not os.path.isdir(repo_path):
            return {"error": f"Project path does not exist on disk: {repo_path}"}

        # Lazy-init the research runner
        if self._research_runner is None:
            from peon_mcp.discord.research import ResearchRunner

            self._research_runner = ResearchRunner()

        result = await self._research_runner.research(target_project, repo_path, question)

        if result.error:
            return {"error": result.error, "timed_out": result.timed_out}

        return {
            "answer": result.answer,
            "cost_usd": result.cost_usd,
            "timed_out": result.timed_out,
        }
