"""
Sentry Service - Sentry API 封裝
================================
Phase 10: Sentry + OpenClaw + UI 整合

遵循 leWOOOgo 積木化原則:
- Service 層負責外部 API 呼叫
- Router 層只做 HTTP 轉發
- 單一職責: 只處理 Sentry API 互動

版本: v1.0
建立: 2026-03-26 21:15 (台北時區)
建立者: Claude Code (P2 架構改善)
"""

from typing import Any

import httpx

from src.core.config import settings
from src.core.logging import get_logger

logger = get_logger("awoooi.sentry")


class SentryService:
    """
    Sentry API Service

    職責:
    1. 封裝 Sentry API 呼叫
    2. 處理認證與錯誤
    3. 提供類型化的資料存取
    """

    def __init__(
        self,
        base_url: str | None = None,
        org: str | None = None,
        project: str | None = None,
        auth_token: str | None = None,
        timeout: float = 10.0,
    ) -> None:
        """
        初始化 Sentry Service

        Args:
            base_url: Sentry API URL (預設從 settings)
            org: Sentry organization slug
            project: Sentry project slug
            auth_token: API auth token
            timeout: 請求超時秒數
        """
        self.base_url = base_url or settings.SENTRY_SELF_HOSTED_URL
        self.org = org or settings.SENTRY_ORG
        self.project = project or settings.SENTRY_PROJECT
        self.auth_token = auth_token or settings.SENTRY_AUTH_TOKEN
        self.timeout = timeout

    async def _request(
        self,
        endpoint: str,
        params: dict[str, Any] | None = None,
        method: str = "GET",
        json_data: dict[str, Any] | None = None,
    ) -> dict | list | None:
        """
        發送 Sentry API 請求

        Args:
            endpoint: API 端點 (不含 /api/0/ 前綴)
            params: 查詢參數
            method: HTTP 方法 (GET, POST, PUT, DELETE)
            json_data: POST/PUT 請求的 JSON body

        Returns:
            JSON 回應，失敗返回 None

        變更: 2026-03-29 v1.1 - 支援 POST 方法 (Wave A.1/A.4 Sentry Comment)
        """
        headers = {}
        if self.auth_token:
            headers["Authorization"] = f"Bearer {self.auth_token}"

        url = f"{self.base_url}/api/0/{endpoint}"

        try:
            async with httpx.AsyncClient(timeout=self.timeout) as client:
                if method == "GET":
                    response = await client.get(url, headers=headers, params=params)
                elif method == "POST":
                    response = await client.post(
                        url, headers=headers, params=params, json=json_data
                    )
                else:
                    logger.error("sentry_api_unsupported_method", method=method)
                    return None

                if response.status_code in (200, 201):
                    return response.json()
                elif response.status_code == 401:
                    logger.warning("sentry_api_unauthorized", endpoint=endpoint)
                    return None
                else:
                    logger.warning(
                        "sentry_api_error",
                        status_code=response.status_code,
                        endpoint=endpoint,
                        response_text=response.text[:200],
                    )
                    return None

        except httpx.TimeoutException:
            logger.error("sentry_api_timeout", endpoint=endpoint)
            return None
        except Exception as e:
            logger.error("sentry_api_failed", endpoint=endpoint, error=str(e))
            return None

    # =========================================================================
    # Organization APIs
    # =========================================================================

    async def list_projects(self) -> list[dict] | None:
        """取得組織內所有專案"""
        return await self._request(f"organizations/{self.org}/projects/")

    # =========================================================================
    # Project APIs
    # =========================================================================

    async def list_issues(
        self,
        project: str | None = None,
        query: str = "is:unresolved",
        limit: int = 25,
        cursor: str | None = None,
    ) -> list[dict] | None:
        """
        列出專案 Issues

        Args:
            project: 專案 slug (預設使用設定值)
            query: Sentry 搜尋語法
            limit: 每頁數量
            cursor: 分頁游標
        """
        project_slug = project or self.project
        params: dict[str, Any] = {"query": query, "limit": limit}
        if cursor:
            params["cursor"] = cursor

        return await self._request(
            f"projects/{self.org}/{project_slug}/issues/",
            params=params,
        )

    async def get_project_stats(
        self,
        project: str | None = None,
        stat: str = "received",
        resolution: str = "1h",
    ) -> list | None:
        """
        取得專案統計數據

        Args:
            project: 專案 slug
            stat: 統計類型 (received, rejected, blacklisted)
            resolution: 時間解析度 (1h, 1d, etc.)
        """
        project_slug = project or self.project
        return await self._request(
            f"projects/{self.org}/{project_slug}/stats/",
            params={"stat": stat, "resolution": resolution},
        )

    # =========================================================================
    # Issue APIs
    # =========================================================================

    async def get_issue(self, issue_id: str) -> dict | None:
        """取得 Issue 詳情"""
        return await self._request(f"issues/{issue_id}/")

    async def get_issue_events(
        self,
        issue_id: str,
        limit: int = 1,
        full: bool = False,
    ) -> list[dict] | None:
        """
        取得 Issue 事件

        Args:
            issue_id: Issue ID
            limit: 事件數量
            full: 是否包含完整堆疊
        """
        params: dict[str, Any] = {"limit": limit}
        if full:
            params["full"] = "true"

        return await self._request(f"issues/{issue_id}/events/", params=params)

    async def post_issue_comment(
        self,
        issue_id: str,
        text: str,
    ) -> dict | None:
        """
        發送 Issue Comment (AI 分析回寫)

        Args:
            issue_id: Sentry Issue ID
            text: Markdown 格式評論內容

        Returns:
            成功返回 comment dict，失敗返回 None

        變更: 2026-03-29 v1.1 - Wave A.4 Sentry Comment 回寫 (ADR-037)
        """
        if not self.auth_token:
            logger.warning(
                "sentry_comment_skipped",
                issue_id=issue_id,
                reason="SENTRY_AUTH_TOKEN not configured",
            )
            return None

        result = await self._request(
            f"issues/{issue_id}/comments/",
            method="POST",
            json_data={"text": text},
        )

        if result:
            logger.info(
                "sentry_comment_posted",
                issue_id=issue_id,
                comment_id=result.get("id"),
            )
        else:
            logger.error("sentry_comment_failed", issue_id=issue_id)

        return result

    # =========================================================================
    # Session Replay APIs (2026-03-29 Phase 19 UX 監控)
    # =========================================================================

    async def list_replays(
        self,
        project: str | None = None,
        query: str = "",
        limit: int = 25,
        sort: str = "-started_at",
    ) -> list[dict] | None:
        """
        列出 Session Replay 錄製

        Args:
            project: 專案 slug
            query: Sentry Discover 語法 (如 user.email:xxx)
            limit: 每頁數量
            sort: 排序欄位

        Returns:
            Replay 列表，包含 id, urls, duration, error_count 等
        """
        project_slug = project or self.project
        params: dict[str, Any] = {"limit": limit, "sort": sort}
        if query:
            params["query"] = query

        return await self._request(
            f"projects/{self.org}/{project_slug}/replays/",
            params=params,
        )

    async def get_replay(
        self,
        replay_id: str,
        project: str | None = None,
    ) -> dict | None:
        """取得單一 Replay 詳情"""
        project_slug = project or self.project
        return await self._request(
            f"projects/{self.org}/{project_slug}/replays/{replay_id}/"
        )

    async def list_ui_errors(
        self,
        project: str | None = None,
        limit: int = 50,
    ) -> list[dict] | None:
        """
        查詢 UI 相關錯誤 (click, interaction, render)

        專門抓取可能影響 UX 的前端錯誤:
        - TypeError: Cannot read property
        - Unhandled rejection
        - ResizeObserver
        - Click/Touch handler errors
        """
        ui_query = (
            "is:unresolved "
            "level:error "
            "(message:*click* OR message:*touch* OR message:*render* "
            "OR message:*TypeError* OR message:*Cannot read*)"
        )
        return await self.list_issues(
            project=project,
            query=ui_query,
            limit=limit,
        )

    async def get_ux_audit_summary(
        self,
        project: str | None = None,
    ) -> dict:
        """
        取得 UX 審計摘要

        Returns:
            {
                "replays_with_errors": int,   # 有錯誤的 replay 數
                "rage_clicks": int,           # 憤怒點擊數
                "ui_errors": int,             # UI 錯誤數
                "dead_clicks": int,           # 無效點擊數
                "details": [...]              # 細節列表
            }
        """
        project_slug = project or self.project

        # 並行查詢多種 UX 問題
        results = {
            "replays_with_errors": 0,
            "rage_clicks": 0,
            "ui_errors": 0,
            "dead_clicks": 0,
            "details": [],
        }

        # 1. 查詢有錯誤的 Replay
        error_replays = await self.list_replays(
            project=project_slug,
            query="count_errors:>0",
            limit=10,
        )
        if error_replays:
            results["replays_with_errors"] = len(error_replays)
            for replay in error_replays:
                results["details"].append({
                    "type": "replay_with_errors",
                    "replay_id": replay.get("id"),
                    "url": self.get_replay_url(replay.get("id", "")),
                    "error_count": replay.get("count_errors", 0),
                    "urls": replay.get("urls", [])[:3],  # 前 3 個訪問頁面
                })

        # 2. 查詢 UI 錯誤
        ui_errors = await self.list_ui_errors(project=project_slug, limit=10)
        if ui_errors:
            results["ui_errors"] = len(ui_errors)
            for issue in ui_errors:
                results["details"].append({
                    "type": "ui_error",
                    "issue_id": issue.get("id"),
                    "url": self.get_issue_url(issue.get("id", "")),
                    "title": issue.get("title", "")[:100],
                    "count": issue.get("count", 0),
                })

        # 3. 查詢憤怒點擊 (Rage Clicks)
        rage_replays = await self.list_replays(
            project=project_slug,
            query="count_rage_clicks:>0",
            limit=10,
        )
        if rage_replays:
            results["rage_clicks"] = sum(
                r.get("count_rage_clicks", 0) for r in rage_replays
            )

        # 4. 查詢死亡點擊 (Dead Clicks)
        dead_replays = await self.list_replays(
            project=project_slug,
            query="count_dead_clicks:>0",
            limit=10,
        )
        if dead_replays:
            results["dead_clicks"] = sum(
                r.get("count_dead_clicks", 0) for r in dead_replays
            )

        return results

    # =========================================================================
    # Helper Methods
    # =========================================================================

    def get_issue_url(self, issue_id: str) -> str:
        """取得 Issue 在 Sentry UI 的連結"""
        return f"{self.base_url}/organizations/{self.org}/issues/{issue_id}/"

    def get_replay_url(self, replay_id: str) -> str:
        """取得 Replay 在 Sentry UI 的連結"""
        return f"{self.base_url}/organizations/{self.org}/replays/{replay_id}/"

    # =========================================================================
    # Dedup (Phase 10.2.1 - 2026-03-27)
    # =========================================================================

    async def check_dedup(self, issue_id: str, ttl: int = 600) -> bool:
        """
        檢查 Sentry Issue 是否已在去重窗口內

        Args:
            issue_id: Sentry Issue ID
            ttl: 去重窗口秒數 (預設 10 分鐘)

        Returns:
            bool: True = 應處理, False = 已去重跳過
        """
        from src.core.redis_client import get_redis

        if not issue_id:
            return True

        redis = get_redis()
        key = f"sentry_dedup:{issue_id}"

        # 檢查是否已存在 (redis-py async)
        exists = await redis.exists(key)
        if exists:
            logger.info("sentry_dedup_hit", issue_id=issue_id)
            return False

        # 設置去重標記
        await redis.setex(key, ttl, "1")
        logger.debug("sentry_dedup_set", issue_id=issue_id, ttl=ttl)
        return True


# =============================================================================
# Singleton
# =============================================================================

_sentry_service: SentryService | None = None


def get_sentry_service() -> SentryService:
    """取得 Sentry Service 實例 (Singleton)"""
    global _sentry_service
    if _sentry_service is None:
        _sentry_service = SentryService()
    return _sentry_service


def set_sentry_service(service: SentryService) -> None:
    """設定 Sentry Service 實例 (for testing)"""
    global _sentry_service
    _sentry_service = service
