"""
Sentry MCP Tool Provider — MCP Phase 3
========================================
提供三個 Sentry 查詢工具，供 AI 關聯告警與程式錯誤：

  sentry_list_issues      — 列出最近 N 筆 Sentry Issues
  sentry_get_issue        — 取得特定 Issue 詳細資訊（stacktrace / culprit）
  sentry_search_issues    — 按 query 搜尋 Issues（如 alertname、service）

設計原則:
- 唯讀（只呼叫 GET endpoints）
- 使用 SENTRY_AUTH_TOKEN + SENTRY_ORG + SENTRY_PROJECT
- 逾時 10 秒
- 失敗回傳 MCPToolResult(success=False)，不拋出

建立時間: 2026-04-11 (台北時區)
建立者: Claude Sonnet 4.6 — MCP Phase 3

@see docs/superpowers/specs/2026-04-10-infra-rebuild-sprint-abc-design.md §MCP-3
"""

import re
from typing import Any

import httpx
import structlog

from src.plugins.mcp.interfaces import MCPTool, MCPToolProvider, MCPToolResult

logger = structlog.get_logger(__name__)

_HTTP_TIMEOUT = 10.0
_RE_SAFE_ISSUE_ID = re.compile(r'^\d{1,20}$')
# P2 fix 2026-04-11: Sentry query 語意白名單 — 只允許已知安全的 Sentry 搜尋語法
# 允許: 純文字識別字、is:unresolved、level:error、project:xxx、assigned:me 等
_RE_SAFE_SENTRY_QUERY = re.compile(
    r'^[\w\s:.\-/]+$'  # alphanumeric, space, colon, dot, dash, slash — Sentry filter 所需
)


class SentryProvider(MCPToolProvider):
    """
    Sentry MCP Tool Provider

    三個工具:
      sentry_list_issues    — 列出最近 Issues
      sentry_get_issue      — 取得 Issue 詳情
      sentry_search_issues  — 搜尋 Issues
    """

    @property
    def name(self) -> str:
        return "sentry"

    @property
    def enabled(self) -> bool:
        from src.core.config import settings
        return (
            getattr(settings, "SENTRY_MCP_ENABLED", False)
            and bool(getattr(settings, "SENTRY_AUTH_TOKEN", ""))
        )

    def _base_url(self) -> str:
        from src.core.config import settings
        return settings.SENTRY_SELF_HOSTED_URL.rstrip("/")

    def _org(self) -> str:
        from src.core.config import settings
        return settings.SENTRY_ORG

    def _project(self) -> str:
        from src.core.config import settings
        return settings.SENTRY_PROJECT

    def _headers(self) -> dict:
        from src.core.config import settings
        return {"Authorization": f"Bearer {settings.SENTRY_AUTH_TOKEN}"}

    async def list_tools(self) -> list[MCPTool]:
        return [
            MCPTool(
                name="sentry_list_issues",
                description=(
                    "List recent Sentry issues for the configured project. "
                    "Returns issue ID, title, culprit, count, last seen, and status."
                ),
                input_schema={
                    "type": "object",
                    "properties": {
                        "limit": {
                            "type": "integer",
                            "description": "Number of issues to return (1-25, default 10)",
                            "default": 10,
                        },
                        "status": {
                            "type": "string",
                            "description": "Filter by status: 'unresolved' (default), 'resolved', 'ignored'",
                            "default": "unresolved",
                        },
                    },
                    "required": [],
                },
            ),
            MCPTool(
                name="sentry_get_issue",
                description=(
                    "Get detailed information about a specific Sentry issue, "
                    "including the latest event's stacktrace and metadata."
                ),
                input_schema={
                    "type": "object",
                    "properties": {
                        "issue_id": {
                            "type": "string",
                            "description": "Sentry issue ID (numeric string)",
                        }
                    },
                    "required": ["issue_id"],
                },
            ),
            MCPTool(
                name="sentry_search_issues",
                description=(
                    "Search Sentry issues by query string. "
                    "Supports Sentry search syntax, e.g. 'is:unresolved service:api'."
                ),
                input_schema={
                    "type": "object",
                    "properties": {
                        "query": {
                            "type": "string",
                            "description": "Sentry search query, e.g. 'is:unresolved TypeError'",
                        },
                        "limit": {
                            "type": "integer",
                            "description": "Number of results (1-25, default 10)",
                            "default": 10,
                        },
                    },
                    "required": ["query"],
                },
            ),
        ]

    async def execute(self, tool_name: str, parameters: dict[str, Any]) -> MCPToolResult:
        handlers = {
            "sentry_list_issues": self._list_issues,
            "sentry_get_issue": self._get_issue,
            "sentry_search_issues": self._search_issues,
        }
        handler = handlers.get(tool_name)
        if not handler:
            return MCPToolResult(success=False, error=f"Unknown tool: {tool_name}")
        try:
            return await handler(parameters)
        except Exception as e:
            logger.error("sentry_mcp_error", tool=tool_name, error=str(e))
            return MCPToolResult(success=False, error=str(e))

    def _format_issue(self, issue: dict) -> dict:
        return {
            "id": issue.get("id"),
            "title": issue.get("title", "")[:120],
            "culprit": issue.get("culprit", "")[:100],
            "status": issue.get("status"),
            "count": issue.get("count"),
            "user_count": issue.get("userCount"),
            "first_seen": issue.get("firstSeen"),
            "last_seen": issue.get("lastSeen"),
            "short_id": issue.get("shortId"),
        }

    async def _list_issues(self, parameters: dict) -> MCPToolResult:
        limit = max(1, min(int(parameters.get("limit", 10)), 25))
        status = parameters.get("status", "unresolved")
        # sanitize status
        if status not in ("unresolved", "resolved", "ignored"):
            status = "unresolved"

        url = f"{self._base_url()}/api/0/projects/{self._org()}/{self._project()}/issues/"
        params = {"query": f"is:{status}", "limit": limit}

        async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT) as client:
            resp = await client.get(url, headers=self._headers(), params=params)
            resp.raise_for_status()
            issues = resp.json()

        return MCPToolResult(
            success=True,
            data={"issues": [self._format_issue(i) for i in issues], "count": len(issues)},
        )

    async def _get_issue(self, parameters: dict) -> MCPToolResult:
        issue_id = parameters["issue_id"]
        if not _RE_SAFE_ISSUE_ID.match(str(issue_id)):
            return MCPToolResult(success=False, error=f"Invalid issue_id: {issue_id!r}")

        url = f"{self._base_url()}/api/0/issues/{issue_id}/"
        events_url = f"{self._base_url()}/api/0/issues/{issue_id}/events/?limit=1&full=true"

        async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT) as client:
            issue_resp = await client.get(url, headers=self._headers())
            issue_resp.raise_for_status()
            issue = issue_resp.json()

            stacktrace = None
            events_resp = await client.get(events_url, headers=self._headers())
            if events_resp.status_code == 200:
                events = events_resp.json()
                if events:
                    entries = events[0].get("entries", [])
                    for entry in entries:
                        if entry.get("type") == "exception":
                            exc = entry.get("data", {}).get("values", [{}])[0]
                            frames = exc.get("stacktrace", {}).get("frames", [])
                            # 只取最後 5 frame
                            stacktrace = [
                                f"{f.get('filename')}:{f.get('lineNo')} in {f.get('function')}"
                                for f in frames[-5:]
                            ]
                            break

        result = self._format_issue(issue)
        result["stacktrace"] = stacktrace
        result["tags"] = {t["key"]: t["value"] for t in issue.get("tags", [])[:10]}

        return MCPToolResult(success=True, data=result)

    async def _search_issues(self, parameters: dict) -> MCPToolResult:
        query = str(parameters["query"])[:200]
        # P2 fix 2026-04-11: 語意白名單驗證，拒絕含特殊字元的 query
        if not _RE_SAFE_SENTRY_QUERY.match(query):
            return MCPToolResult(success=False, error=f"Unsafe sentry query: {query!r}")
        limit = max(1, min(int(parameters.get("limit", 10)), 25))

        url = f"{self._base_url()}/api/0/projects/{self._org()}/{self._project()}/issues/"
        params = {"query": query, "limit": limit}

        async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT) as client:
            resp = await client.get(url, headers=self._headers(), params=params)
            resp.raise_for_status()
            issues = resp.json()

        return MCPToolResult(
            success=True,
            data={"issues": [self._format_issue(i) for i in issues], "count": len(issues)},
        )

    async def health_check(self) -> bool:
        try:
            url = f"{self._base_url()}/api/0/projects/{self._org()}/{self._project()}/"
            async with httpx.AsyncClient(timeout=5.0) as client:
                resp = await client.get(url, headers=self._headers())
                return resp.status_code < 500
        except Exception:
            return False
