"""
AWOOOI API - GitHub Webhook Handler
====================================
Phase 13.1: GitHub PR/Push/CI → OpenClaw AI 整合

整合流程:
1. GitHub Webhook (PR/Push/Workflow) → AWOOOI API
2. HMAC-SHA256 簽章驗證 (X-Hub-Signature-256)
3. 解析 PR diff / Push commits / Workflow failure
4. 呼叫 OpenClaw 進行 AI 代碼審查 / CI 失敗診斷
5. 儲存審查結果到 Redis
6. 發送 Telegram 通知
7. (可選) 建立 Approval 等待人工確認

支援事件:
- pull_request: PR 代碼審查 (#74-75)
- push: 主分支推送審查 (#74-75)
- workflow_run: CI 失敗診斷 (#76)

安全要求 (feedback_openclaw_security.md):
- HMAC 簽章驗證 (X-Hub-Signature-256)
- Webhook Secret 存放於 K8s Secret
- Rate limiting 防止 DoS
- 倉庫白名單驗證

🔴 HARD RULE: 時間顯示使用 Asia/Taipei (UTC+8)

版本: v2.0
最後修改: 2026-03-26 16:30 (台北時區)
修改者: Claude Code
變更: Phase 13.1 #76 CI 失敗診斷
"""

import hashlib
import hmac
import json
import uuid
from typing import Literal

from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request, status
from pydantic import BaseModel, Field

from src.core.config import settings
from src.core.logging import get_logger
from src.models.approval import (
    ApprovalRequestCreate,
    BlastRadius,
    DataImpact,
    RiskLevel,
)
from src.services.approval_db import get_approval_service
from src.services.github_api_service import get_github_api_service
from src.services.github_webhook_service import get_github_webhook_service
from src.services.openclaw_http_service import get_openclaw_http_service
from src.services.telegram_gateway import get_telegram_gateway
from src.utils.timezone import now_taipei_iso

logger = get_logger("awoooi.github_webhook")

router = APIRouter(prefix="/webhooks/github", tags=["GitHub Webhook"])

# =============================================================================
# Constants
# =============================================================================

# OpenClaw 配置 (使用 settings 中的 OPENCLAW_URL)
OPENCLAW_URL = settings.OPENCLAW_URL

# GitHub Review 結果 Redis TTL: 7 天 (秒)
GITHUB_REVIEW_TTL_SECONDS = 7 * 24 * 60 * 60


# =============================================================================
# Pydantic Models
# =============================================================================

class GitHubUser(BaseModel):
    """GitHub 使用者"""
    login: str
    id: int
    avatar_url: str | None = None


class GitHubRepository(BaseModel):
    """GitHub 倉庫"""
    id: int
    name: str
    full_name: str
    private: bool = False
    html_url: str


class GitHubPullRequest(BaseModel):
    """GitHub PR 資訊"""
    id: int
    number: int
    title: str
    body: str | None = None
    state: str  # open, closed
    html_url: str
    diff_url: str
    user: GitHubUser
    head: dict  # head branch info
    base: dict  # base branch info
    additions: int = 0
    deletions: int = 0
    changed_files: int = 0


class GitHubCommit(BaseModel):
    """GitHub Commit 資訊"""
    id: str  # SHA
    message: str
    timestamp: str
    url: str
    author: dict
    added: list[str] = []
    removed: list[str] = []
    modified: list[str] = []


class GitHubWorkflowRun(BaseModel):
    """GitHub Workflow Run 資訊 (Phase 13.1 #76)"""
    id: int
    name: str
    status: str  # queued, in_progress, completed
    conclusion: str | None = None  # success, failure, cancelled, skipped, timed_out
    html_url: str
    run_number: int
    run_attempt: int = 1
    head_sha: str
    head_branch: str | None = None
    event: str  # push, pull_request, schedule, workflow_dispatch
    created_at: str
    updated_at: str
    logs_url: str | None = None  # API URL for logs (requires auth)


class GitHubWorkflowJob(BaseModel):
    """GitHub Workflow Job 資訊"""
    id: int
    name: str
    status: str
    conclusion: str | None = None
    started_at: str | None = None
    completed_at: str | None = None
    steps: list[dict] = []


class CIFailureDiagnosis(BaseModel):
    """CI 失敗診斷結果 (Phase 13.1 #76)"""
    summary: str = Field(..., description="失敗摘要")
    root_cause: str = Field(..., description="根本原因分析")
    failed_step: str | None = Field(None, description="失敗的步驟名稱")
    error_type: str = Field(..., description="錯誤類型 (build/test/lint/deploy/timeout)")
    suggestions: list[str] = Field(default=[], description="修復建議")
    auto_fixable: bool = Field(False, description="是否可自動修復")
    fix_command: str | None = Field(None, description="自動修復指令 (如可自動修復)")
    risk_level: str = Field("medium", description="風險等級 (low/medium/high/critical)")
    analyzed_by: str = Field(..., description="分析模型")
    confidence: float = Field(..., ge=0, le=1, description="信心度")


class GitHubWebhookPayload(BaseModel):
    """GitHub Webhook Payload (通用)"""
    action: str | None = None  # PR: opened, synchronize, etc.
    repository: GitHubRepository
    sender: GitHubUser
    # PR 事件
    pull_request: GitHubPullRequest | None = None
    # Push 事件
    ref: str | None = None  # refs/heads/main
    before: str | None = None  # previous commit SHA
    after: str | None = None  # current commit SHA
    commits: list[GitHubCommit] | None = None
    pusher: dict | None = None
    # Workflow Run 事件 (Phase 13.1 #76)
    workflow_run: GitHubWorkflowRun | None = None
    workflow_job: GitHubWorkflowJob | None = None


class CodeReviewResult(BaseModel):
    """AI 代碼審查結果"""
    summary: str = Field(..., description="審查摘要")
    issues: list[dict] = Field(default=[], description="發現的問題列表")
    suggestions: list[dict] = Field(default=[], description="改進建議")
    security_concerns: list[str] = Field(default=[], description="安全疑慮")
    quality_score: float = Field(..., ge=0, le=100, description="代碼品質分數 0-100")
    analyzed_by: str = Field(..., description="分析模型 (ollama/claude)")
    confidence: float = Field(..., ge=0, le=1, description="分析信心度 0-1")


class GitHubWebhookResponse(BaseModel):
    """Webhook 回應"""
    status: Literal["accepted", "ignored", "error"]
    message: str
    event_type: str | None = None
    review_id: str | None = None


# =============================================================================
# HMAC Signature Verification (CISO 安全要求)
# =============================================================================

class GitHubSignatureError(Exception):
    """GitHub 簽章驗證失敗"""
    pass


async def verify_github_signature(
    request: Request,
    x_hub_signature_256: str | None,
) -> bool:
    """
    驗證 GitHub Webhook 請求的 HMAC-SHA256 簽章

    CISO 安全要求:
    - 所有 GitHub Webhook 必須攜帶 X-Hub-Signature-256 Header
    - 簽章格式: sha256=<hex_digest>
    - 使用 GITHUB_WEBHOOK_SECRET 進行驗證

    安全鐵律 (Fail-Closed):
    - 生產環境: Secret 未設定 → 直接拒絕
    - 開發環境: 可跳過驗證 (僅供本地測試)

    Args:
        request: FastAPI Request 物件
        x_hub_signature_256: X-Hub-Signature-256 Header 值

    Returns:
        bool: 驗證是否通過

    Raises:
        GitHubSignatureError: 簽章驗證失敗
    """
    # ==========================================================================
    # Fail-Closed 安全策略 (CISO 要求)
    # ==========================================================================
    if not settings.GITHUB_WEBHOOK_SECRET:
        # 生產環境: 強制拒絕 (Fail-Closed)
        if settings.ENVIRONMENT == "prod":
            logger.critical(
                "github_webhook_secret_missing_in_production",
                environment=settings.ENVIRONMENT,
                message="CRITICAL: GITHUB_WEBHOOK_SECRET missing in production!",
            )
            raise GitHubSignatureError(
                "Critical: GITHUB_WEBHOOK_SECRET missing in production environment"
            )

        # 開發環境: 允許跳過 (僅供本地測試)
        logger.warning(
            "github_signature_verification_skipped_dev_only",
            environment=settings.ENVIRONMENT,
            reason="GITHUB_WEBHOOK_SECRET not configured (dev mode only)",
        )
        return True

    # 必須提供簽章
    if not x_hub_signature_256:
        logger.warning("github_signature_missing")
        raise GitHubSignatureError("Missing X-Hub-Signature-256 header")

    # 解析簽章格式
    if not x_hub_signature_256.startswith("sha256="):
        raise GitHubSignatureError("Invalid signature format (expected sha256=...)")

    provided_signature = x_hub_signature_256[7:]  # 移除 "sha256=" 前綴

    # 讀取 Request Body
    body = await request.body()

    # 計算預期簽章
    expected_signature = hmac.new(
        settings.GITHUB_WEBHOOK_SECRET.encode(),
        body,
        hashlib.sha256,
    ).hexdigest()

    # 常數時間比較 (防止計時攻擊)
    if not hmac.compare_digest(provided_signature, expected_signature):
        logger.warning(
            "github_signature_verification_failed",
            provided=provided_signature[:16] + "...",
            expected=expected_signature[:16] + "...",
        )
        raise GitHubSignatureError("Invalid signature")

    logger.info("github_signature_verification_success")
    return True


def verify_allowed_repo(full_name: str) -> bool:
    """
    驗證倉庫是否在白名單中

    Args:
        full_name: 完整倉庫名稱 (owner/repo)

    Returns:
        bool: 是否允許
    """
    allowed_repos = settings.get_github_allowed_repos()

    # 如果白名單為空，開發環境允許所有
    if not allowed_repos:
        if settings.ENVIRONMENT == "prod":
            logger.warning(
                "github_allowed_repos_empty_in_production",
                repo=full_name,
                message="No allowed repos configured in production",
            )
            return False
        # 開發環境: 白名單空 = 允許所有
        logger.debug(
            "github_repo_allowed_dev_mode",
            repo=full_name,
            reason="Empty whitelist in dev mode",
        )
        return True

    # 檢查是否在白名單中
    is_allowed = full_name in allowed_repos
    if not is_allowed:
        logger.warning(
            "github_repo_not_in_whitelist",
            repo=full_name,
            allowed_repos=allowed_repos,
        )
    return is_allowed


# =============================================================================
# Main Webhook Handler
# =============================================================================

@router.post(
    "",
    response_model=GitHubWebhookResponse,
    status_code=status.HTTP_202_ACCEPTED,
    summary="GitHub Webhook 接收端點",
    description="接收 GitHub PR/Push 事件並觸發 AI 代碼審查",
)
async def handle_github_webhook(
    request: Request,
    background_tasks: BackgroundTasks,
    x_github_event: str | None = Header(None, alias="X-GitHub-Event"),
    x_github_delivery: str | None = Header(None, alias="X-GitHub-Delivery"),
    x_hub_signature_256: str | None = Header(None, alias="X-Hub-Signature-256"),
):
    """
    GitHub Webhook Handler

    支援事件:
    - pull_request (opened, synchronize, reopened)
    - push (to default branch)

    處理流程:
    1. 驗證簽章
    2. 驗證倉庫白名單
    3. 解析事件類型
    4. 背景執行 AI 審查
    """
    try:
        # 1. 驗證 HMAC 簽章
        try:
            await verify_github_signature(request, x_hub_signature_256)
        except GitHubSignatureError as e:
            logger.warning("github_webhook_signature_failed", error=str(e))
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail=str(e),
            ) from e

        # 2. 解析 Payload
        body = await request.body()
        payload_dict = json.loads(body)
        payload = GitHubWebhookPayload(**payload_dict)

        # 3. 驗證倉庫白名單
        if not verify_allowed_repo(payload.repository.full_name):
            return GitHubWebhookResponse(
                status="ignored",
                message=f"Repository {payload.repository.full_name} not in whitelist",
                event_type=x_github_event,
            )

        # 4. 根據事件類型處理
        logger.info(
            "github_webhook_received",
            github_event=x_github_event,
            delivery_id=x_github_delivery,
            repo=payload.repository.full_name,
            sender=payload.sender.login,
        )

        # Pull Request 事件
        if x_github_event == "pull_request":
            return await handle_pull_request(
                payload=payload,
                background_tasks=background_tasks,
                delivery_id=x_github_delivery,
            )

        # Push 事件
        elif x_github_event == "push":
            return await handle_push(
                payload=payload,
                background_tasks=background_tasks,
                delivery_id=x_github_delivery,
            )

        # Workflow Run 事件 (Phase 13.1 #76 CI 失敗診斷)
        elif x_github_event == "workflow_run":
            return await handle_workflow_run(
                payload=payload,
                background_tasks=background_tasks,
                delivery_id=x_github_delivery,
            )

        # Ping 事件 (GitHub 測試連線)
        elif x_github_event == "ping":
            return GitHubWebhookResponse(
                status="accepted",
                message="Pong! Webhook configured successfully.",
                event_type="ping",
            )

        # 其他事件 (忽略)
        else:
            return GitHubWebhookResponse(
                status="ignored",
                message=f"Event type '{x_github_event}' not supported",
                event_type=x_github_event,
            )

    except HTTPException:
        raise
    except Exception as e:
        logger.exception("github_webhook_processing_failed", error=str(e))
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Internal error processing webhook",
        ) from e


# =============================================================================
# Event Handlers
# =============================================================================

async def handle_pull_request(
    payload: GitHubWebhookPayload,
    background_tasks: BackgroundTasks,
    delivery_id: str | None,
) -> GitHubWebhookResponse:
    """
    處理 Pull Request 事件

    支援 action:
    - opened: 新建 PR
    - synchronize: 推送新 commit 到 PR
    - reopened: 重新開啟 PR
    """
    pr = payload.pull_request
    if not pr:
        return GitHubWebhookResponse(
            status="error",
            message="Missing pull_request data",
            event_type="pull_request",
        )

    # 只處理需要審查的 action
    supported_actions = {"opened", "synchronize", "reopened"}
    if payload.action not in supported_actions:
        return GitHubWebhookResponse(
            status="ignored",
            message=f"PR action '{payload.action}' not supported",
            event_type="pull_request",
        )

    # 生成審查 ID
    review_id = f"gh-pr-{payload.repository.id}-{pr.number}-{uuid.uuid4().hex[:8]}"

    # 背景執行審查
    background_tasks.add_task(
        review_pull_request,
        repo=payload.repository,
        pr=pr,
        sender=payload.sender,
        review_id=review_id,
        action=payload.action,
    )

    logger.info(
        "github_pr_review_scheduled",
        review_id=review_id,
        repo=payload.repository.full_name,
        pr_number=pr.number,
        pr_title=pr.title[:50],
        action=payload.action,
    )

    return GitHubWebhookResponse(
        status="accepted",
        message=f"PR #{pr.number} review scheduled",
        event_type="pull_request",
        review_id=review_id,
    )


async def handle_push(
    payload: GitHubWebhookPayload,
    background_tasks: BackgroundTasks,
    delivery_id: str | None,
) -> GitHubWebhookResponse:
    """
    處理 Push 事件

    只處理 default branch (main/master) 的 push
    """
    # 檢查是否推送到主分支
    ref = payload.ref or ""
    # 通常是 refs/heads/main 或 refs/heads/master
    if not (ref.endswith("/main") or ref.endswith("/master")):
        return GitHubWebhookResponse(
            status="ignored",
            message=f"Push to non-default branch: {ref}",
            event_type="push",
        )

    commits = payload.commits or []
    if not commits:
        return GitHubWebhookResponse(
            status="ignored",
            message="No commits in push",
            event_type="push",
        )

    # 生成審查 ID
    review_id = f"gh-push-{payload.repository.id}-{payload.after[:8]}-{uuid.uuid4().hex[:8]}"

    # 背景執行審查
    background_tasks.add_task(
        review_push,
        repo=payload.repository,
        commits=commits,
        sender=payload.sender,
        review_id=review_id,
        ref=ref,
        before_sha=payload.before,
        after_sha=payload.after,
    )

    logger.info(
        "github_push_review_scheduled",
        review_id=review_id,
        repo=payload.repository.full_name,
        ref=ref,
        commit_count=len(commits),
        after_sha=payload.after[:8] if payload.after else None,
    )

    return GitHubWebhookResponse(
        status="accepted",
        message=f"Push with {len(commits)} commit(s) review scheduled",
        event_type="push",
        review_id=review_id,
    )


async def handle_workflow_run(
    payload: GitHubWebhookPayload,
    background_tasks: BackgroundTasks,
    delivery_id: str | None,
) -> GitHubWebhookResponse:
    """
    處理 Workflow Run 事件 (Phase 13.1 #76 CI 失敗診斷)

    只處理 completed + failure 的 workflow run
    """
    workflow_run = payload.workflow_run
    if not workflow_run:
        return GitHubWebhookResponse(
            status="ignored",
            message="No workflow_run in payload",
            event_type="workflow_run",
        )

    # 只處理 completed 狀態
    if workflow_run.status != "completed":
        return GitHubWebhookResponse(
            status="ignored",
            message=f"Workflow status '{workflow_run.status}' not completed",
            event_type="workflow_run",
        )

    # 只處理失敗的 workflow
    if workflow_run.conclusion not in ("failure", "timed_out"):
        return GitHubWebhookResponse(
            status="ignored",
            message=f"Workflow conclusion '{workflow_run.conclusion}' is not failure",
            event_type="workflow_run",
        )

    # 生成診斷 ID
    diagnosis_id = f"gh-ci-{payload.repository.id}-{workflow_run.id}-{uuid.uuid4().hex[:8]}"

    # 背景執行 CI 失敗診斷
    background_tasks.add_task(
        diagnose_ci_failure,
        repo=payload.repository,
        workflow_run=workflow_run,
        sender=payload.sender,
        diagnosis_id=diagnosis_id,
    )

    logger.info(
        "github_ci_failure_diagnosis_scheduled",
        diagnosis_id=diagnosis_id,
        repo=payload.repository.full_name,
        workflow_name=workflow_run.name,
        workflow_id=workflow_run.id,
        conclusion=workflow_run.conclusion,
        head_sha=workflow_run.head_sha[:8],
    )

    return GitHubWebhookResponse(
        status="accepted",
        message=f"CI failure diagnosis scheduled for '{workflow_run.name}'",
        event_type="workflow_run",
        review_id=diagnosis_id,
    )


# =============================================================================
# Background Tasks: AI Review
# =============================================================================

async def review_pull_request(
    repo: GitHubRepository,
    pr: GitHubPullRequest,
    sender: GitHubUser,
    review_id: str,
    action: str,
):
    """
    背景任務: PR 代碼審查

    1. 取得 PR diff
    2. 呼叫 OpenClaw 分析
    3. 儲存結果到 Redis
    4. 發送 Telegram 通知
    5. 建立 Approval (可選)
    """
    try:
        logger.info(
            "github_pr_review_started",
            review_id=review_id,
            repo=repo.full_name,
            pr_number=pr.number,
        )

        # 1. 取得 PR diff
        diff_content = await fetch_pr_diff(pr.diff_url)

        # 2. 呼叫 OpenClaw 進行代碼審查
        analysis = await call_openclaw_code_review(
            repo_name=repo.full_name,
            pr_title=pr.title,
            pr_body=pr.body or "",
            diff_content=diff_content,
            changed_files=pr.changed_files,
            additions=pr.additions,
            deletions=pr.deletions,
        )

        # 3. 儲存結果到 Redis
        await save_review_result(
            review_id=review_id,
            event_type="pull_request",
            repo=repo.full_name,
            target=f"PR #{pr.number}",
            analysis=analysis,
            metadata={
                "pr_number": pr.number,
                "pr_title": pr.title,
                "pr_url": pr.html_url,
                "author": pr.user.login,
                "action": action,
                "changed_files": pr.changed_files,
                "additions": pr.additions,
                "deletions": pr.deletions,
            },
        )

        # 4. 發送 Telegram 通知
        await send_github_telegram_alert(
            review_id=review_id,
            event_type="pull_request",
            repo=repo.full_name,
            target=f"PR #{pr.number}: {pr.title[:50]}",
            url=pr.html_url,
            author=pr.user.login,
            analysis=analysis,
        )

        # 5. 如果有安全疑慮，建立 Approval
        if analysis and analysis.security_concerns:
            await create_github_approval(
                review_id=review_id,
                repo=repo.full_name,
                target=f"PR #{pr.number}",
                url=pr.html_url,
                analysis=analysis,
            )

        logger.info(
            "github_pr_review_completed",
            review_id=review_id,
            quality_score=analysis.quality_score if analysis else None,
            has_security_concerns=bool(analysis and analysis.security_concerns),
        )

    except Exception as e:
        logger.exception(
            "github_pr_review_failed",
            review_id=review_id,
            error=str(e),
        )


async def review_push(
    repo: GitHubRepository,
    commits: list[GitHubCommit],
    sender: GitHubUser,
    review_id: str,
    ref: str,
    before_sha: str | None,
    after_sha: str | None,
):
    """
    背景任務: Push 代碼審查

    1. 整理 commit 資訊
    2. 呼叫 OpenClaw 分析
    3. 儲存結果到 Redis
    4. 發送 Telegram 通知
    """
    try:
        logger.info(
            "github_push_review_started",
            review_id=review_id,
            repo=repo.full_name,
            commit_count=len(commits),
        )

        # 1. 整理 commit 資訊
        commit_summary = []
        all_files = {"added": [], "modified": [], "removed": []}
        for commit in commits:
            commit_summary.append({
                "sha": commit.id[:8],
                "message": commit.message[:100],
                "author": commit.author.get("name", "unknown"),
            })
            all_files["added"].extend(commit.added)
            all_files["modified"].extend(commit.modified)
            all_files["removed"].extend(commit.removed)

        # 2. 呼叫 OpenClaw 進行代碼審查 (Push 版)
        analysis = await call_openclaw_push_review(
            repo_name=repo.full_name,
            ref=ref,
            commits=commit_summary,
            files_changed=all_files,
        )

        # 3. 儲存結果到 Redis
        await save_review_result(
            review_id=review_id,
            event_type="push",
            repo=repo.full_name,
            target=f"push to {ref.split('/')[-1]}",
            analysis=analysis,
            metadata={
                "ref": ref,
                "before_sha": before_sha,
                "after_sha": after_sha,
                "commit_count": len(commits),
                "pusher": sender.login,
                "files": all_files,
            },
        )

        # 4. 發送 Telegram 通知 (只有發現問題時才通知)
        if analysis and (analysis.issues or analysis.security_concerns or analysis.quality_score < 70):
            await send_github_telegram_alert(
                review_id=review_id,
                event_type="push",
                repo=repo.full_name,
                target=f"push to {ref.split('/')[-1]} ({len(commits)} commits)",
                url=repo.html_url,
                author=sender.login,
                analysis=analysis,
            )

        logger.info(
            "github_push_review_completed",
            review_id=review_id,
            quality_score=analysis.quality_score if analysis else None,
        )

    except Exception as e:
        logger.exception(
            "github_push_review_failed",
            review_id=review_id,
            error=str(e),
        )


async def diagnose_ci_failure(
    repo: GitHubRepository,
    workflow_run: GitHubWorkflowRun,
    sender: GitHubUser,
    diagnosis_id: str,
):
    """
    背景任務: CI 失敗診斷 (Phase 13.1 #76)

    1. 收集 workflow 失敗資訊
    2. 呼叫 OpenClaw 進行根因分析
    3. 評估風險等級與自動修復可行性
    4. 儲存結果到 Redis
    5. 發送 Telegram 通知
    6. (可選) 建立 Approval 等待人工確認
    """
    try:
        logger.info(
            "github_ci_failure_diagnosis_started",
            diagnosis_id=diagnosis_id,
            repo=repo.full_name,
            workflow_name=workflow_run.name,
            workflow_id=workflow_run.id,
        )

        # 1. 收集失敗資訊
        failure_context = {
            "workflow_name": workflow_run.name,
            "workflow_id": workflow_run.id,
            "run_number": workflow_run.run_number,
            "run_attempt": workflow_run.run_attempt,
            "conclusion": workflow_run.conclusion,
            "head_sha": workflow_run.head_sha,
            "head_branch": workflow_run.head_branch,
            "event_trigger": workflow_run.event,
            "html_url": workflow_run.html_url,
            "created_at": workflow_run.created_at,
            "updated_at": workflow_run.updated_at,
        }

        # 2. 呼叫 OpenClaw 進行 CI 失敗診斷
        diagnosis = await call_openclaw_ci_diagnosis(
            repo_name=repo.full_name,
            failure_context=failure_context,
        )

        # 3. 評估自動修復策略 (Phase 13.1 #78)
        repair_decision = None
        if diagnosis:
            from src.services.ci_auto_repair import get_ci_auto_repair_service
            repair_service = get_ci_auto_repair_service()
            repair_decision = await repair_service.evaluate_repair(
                error_type=diagnosis.error_type,
                workflow_name=workflow_run.name,
                repo=repo.full_name,
                failure_context=failure_context,
                diagnosis_summary=diagnosis.summary,
            )

        # 4. 儲存結果到 Redis (含修復決策)
        service = get_github_webhook_service()
        await service.save_review_result(
            review_id=diagnosis_id,
            result={
                "event_type": "workflow_run",
                "repo": repo.full_name,
                "target": f"CI: {workflow_run.name}",
                "diagnosis": diagnosis.model_dump() if diagnosis else None,
                "repair_decision": {
                    "should_repair": repair_decision.should_repair,
                    "execution_decision": repair_decision.execution_decision.value,
                    "risk_level": repair_decision.risk_level.value,
                    "reason": repair_decision.reason,
                    "recommendations": [
                        {"action": r.action.value, "command": r.command, "confidence": r.confidence}
                        for r in repair_decision.recommendations[:3]
                    ],
                } if repair_decision else None,
                "failure_context": failure_context,
                "reviewed_at": now_taipei_iso(),
            },
            ttl=GITHUB_REVIEW_TTL_SECONDS,
        )

        # 5. 發送 Telegram 通知 (含修復建議)
        await send_ci_failure_telegram_alert(
            diagnosis_id=diagnosis_id,
            repo=repo.full_name,
            workflow_name=workflow_run.name,
            workflow_url=workflow_run.html_url,
            sender=sender.login,
            diagnosis=diagnosis,
            repair_decision=repair_decision,
        )

        # 6. 根據修復決策建立 Approval 或自動執行
        if repair_decision:
            from src.services.ci_auto_repair import ExecutionDecision
            if repair_decision.execution_decision == ExecutionDecision.APPROVAL_REQUIRED:
                await create_ci_failure_approval(
                    diagnosis_id=diagnosis_id,
                    repo=repo.full_name,
                    workflow_run=workflow_run,
                    diagnosis=diagnosis,
                )
            elif repair_decision.execution_decision == ExecutionDecision.AUTO_EXECUTE:
                logger.info(
                    "ci_auto_repair_eligible",
                    diagnosis_id=diagnosis_id,
                    action=repair_decision.recommendations[0].action.value if repair_decision.recommendations else None,
                    # TODO: 實際執行修復指令 (Phase 13.1 後續迭代)
                )
        elif diagnosis and diagnosis.risk_level in ("high", "critical"):
            await create_ci_failure_approval(
                diagnosis_id=diagnosis_id,
                repo=repo.full_name,
                workflow_run=workflow_run,
                diagnosis=diagnosis,
            )

        logger.info(
            "github_ci_failure_diagnosis_completed",
            diagnosis_id=diagnosis_id,
            root_cause=diagnosis.root_cause if diagnosis else None,
            auto_fixable=diagnosis.auto_fixable if diagnosis else False,
            risk_level=diagnosis.risk_level if diagnosis else None,
            repair_decision=repair_decision.execution_decision.value if repair_decision else None,
        )

    except Exception as e:
        logger.exception(
            "github_ci_failure_diagnosis_failed",
            diagnosis_id=diagnosis_id,
            error=str(e),
        )


# =============================================================================
# Helper Functions
# =============================================================================

async def fetch_pr_diff(diff_url: str) -> str:
    """
    取得 PR diff 內容

    Args:
        diff_url: GitHub diff URL

    Returns:
        str: diff 內容

    Phase 22 P0 修復: 使用 GitHubApiService (2026-03-31)
    """
    service = get_github_api_service()
    return await service.fetch_pr_diff(diff_url)


async def call_openclaw_code_review(
    repo_name: str,
    pr_title: str,
    pr_body: str,
    diff_content: str,
    changed_files: int,
    additions: int,
    deletions: int,
) -> CodeReviewResult | None:
    """
    呼叫 OpenClaw 進行 PR 代碼審查

    優先使用 Ollama (本地，零成本)
    Fallback: Claude (大型 PR)

    Phase 22 P0 修復: 使用 OpenClawHttpService (2026-03-31)
    """
    try:
        service = get_openclaw_http_service()
        data = await service.code_review(
            repo_name=repo_name,
            pr_title=pr_title,
            pr_body=pr_body,
            diff_content=diff_content,
            changed_files=changed_files,
            additions=additions,
            deletions=deletions,
            prefer_local=True,
            timeout=120.0,
        )

        if data:
            return CodeReviewResult(**data)
        return None

    except Exception as e:
        logger.exception("openclaw_code_review_error", error=str(e))
        return None


async def call_openclaw_push_review(
    repo_name: str,
    ref: str,
    commits: list[dict],
    files_changed: dict,
) -> CodeReviewResult | None:
    """
    呼叫 OpenClaw 進行 Push 代碼審查

    Phase 22 P0 修復: 使用 OpenClawHttpService (2026-03-31)
    """
    try:
        service = get_openclaw_http_service()
        data = await service.push_review(
            repo_name=repo_name,
            ref=ref,
            commits=commits,
            files_changed=files_changed,
            prefer_local=True,
            timeout=120.0,
        )

        if data:
            return CodeReviewResult(**data)
        return None

    except Exception as e:
        logger.exception("openclaw_push_review_error", error=str(e))
        return None


async def call_openclaw_ci_diagnosis(
    repo_name: str,
    failure_context: dict,
) -> CIFailureDiagnosis | None:
    """
    呼叫 OpenClaw 進行 CI 失敗診斷 (Phase 13.1 #76)

    分析 CI/CD pipeline 失敗原因，提供根因分析和修復建議

    Phase 22 P0 修復: 使用 OpenClawHttpService (2026-03-31)
    """
    try:
        service = get_openclaw_http_service()
        data = await service.ci_diagnosis(
            repo_name=repo_name,
            failure_context=failure_context,
            prefer_local=True,
            timeout=120.0,
        )

        if data:
            return CIFailureDiagnosis(**data)
        else:
            # 返回基本診斷結果 (API 失敗時的 fallback)
            return CIFailureDiagnosis(
                summary=f"CI workflow '{failure_context.get('workflow_name')}' failed",
                root_cause="OpenClaw API unavailable, manual investigation required",
                error_type="unknown",
                suggestions=["Check workflow logs manually", "Verify runner status"],
                auto_fixable=False,
                risk_level="medium",
                analyzed_by="fallback",
                confidence=0.0,  # 🔴 Fallback 不是 AI 分析
            )

    except Exception as e:
        logger.exception("openclaw_ci_diagnosis_error", error=str(e))
        return CIFailureDiagnosis(
            summary="CI diagnosis error",
            root_cause=f"Exception: {str(e)}",
            error_type="error",
            suggestions=["Check OpenClaw service status"],
            auto_fixable=False,
            risk_level="low",
            analyzed_by="fallback",
            confidence=0.0,
        )


async def send_ci_failure_telegram_alert(
    diagnosis_id: str,
    repo: str,
    workflow_name: str,
    workflow_url: str,
    sender: str,
    diagnosis: CIFailureDiagnosis | None,
    repair_decision=None,  # Phase 13.1 #78: CIRepairDecision
):
    """
    發送 CI 失敗診斷 Telegram 通知 (Phase 13.1 #76-78)
    """
    try:
        telegram = get_telegram_gateway()

        # 構建訊息
        risk_emoji = {
            "low": "🟢",
            "medium": "🟡",
            "high": "🟠",
            "critical": "🔴",
        }
        emoji = risk_emoji.get(diagnosis.risk_level if diagnosis else "medium", "🟡")

        # 修復決策狀態
        decision_text = "❓ 待評估"
        if repair_decision:
            decision_map = {
                "auto_execute": "🤖 自動修復中",
                "telegram_confirm": "📱 等待確認",
                "approval_required": "📋 需人工審核",
                "blocked": "🚫 禁止自動修復",
            }
            decision_text = decision_map.get(repair_decision.execution_decision.value, "❓ 未知")

        message_lines = [
            f"{emoji} **CI 失敗診斷** | {repo}",
            "",
            f"📋 **Workflow**: {workflow_name}",
            f"👤 **觸發者**: {sender}",
            f"🔗 [查看 Workflow]({workflow_url})",
            "",
        ]

        if diagnosis:
            message_lines.extend([
                f"**📝 摘要**: {diagnosis.summary}",
                f"**🔍 根因**: {diagnosis.root_cause}",
                f"**⚠️ 錯誤類型**: {diagnosis.error_type}",
                f"**🎯 風險等級**: {diagnosis.risk_level.upper()}",
                f"**🔧 修復決策**: {decision_text}",
                "",
            ])

            if diagnosis.suggestions:
                message_lines.append("**💡 AI 建議**:")
                for i, suggestion in enumerate(diagnosis.suggestions[:3], 1):
                    message_lines.append(f"  {i}. {suggestion}")

        # 顯示修復建議 (Phase 13.1 #78)
        if repair_decision and repair_decision.recommendations:
            message_lines.extend(["", "**🔨 修復選項**:"])
            for i, rec in enumerate(repair_decision.recommendations[:2], 1):
                confidence_pct = int(rec.confidence * 100)
                message_lines.append(
                    f"  {i}. `{rec.action.value}` ({confidence_pct}% 信心)"
                )
                if rec.command:
                    message_lines.append(f"     `{rec.command[:50]}...`" if len(rec.command) > 50 else f"     `{rec.command}`")

        message_lines.extend([
            "",
            f"🆔 `{diagnosis_id}`",
        ])

        message = "\n".join(message_lines)

        await telegram.send_message(
            message=message,
            parse_mode="Markdown",
        )

        logger.info(
            "ci_failure_telegram_alert_sent",
            diagnosis_id=diagnosis_id,
            repo=repo,
            repair_decision=repair_decision.execution_decision.value if repair_decision else None,
        )

    except Exception as e:
        logger.exception(
            "ci_failure_telegram_alert_failed",
            diagnosis_id=diagnosis_id,
            error=str(e),
        )


async def create_ci_failure_approval(
    diagnosis_id: str,
    repo: str,
    workflow_run: GitHubWorkflowRun,
    diagnosis: CIFailureDiagnosis,
) -> str:
    """
    為需要人工審核的 CI 修復建立 Approval 記錄 (Phase 13.1 #76)

    Returns:
        str: Approval ID
    """
    try:
        approval_service = get_approval_service()

        # 映射風險等級
        risk_map = {
            "low": RiskLevel.LOW,
            "medium": RiskLevel.MEDIUM,
            "high": RiskLevel.HIGH,
            "critical": RiskLevel.CRITICAL,
        }
        risk_level = risk_map.get(diagnosis.risk_level, RiskLevel.MEDIUM)

        # P1-2 修正: 欄位對齊 ApprovalRequestBase (2026-03-29)
        suggestion = diagnosis.fix_command or "; ".join(diagnosis.suggestions[:2])
        approval_request = ApprovalRequestCreate(
            action=f"CI Failure Repair: {repo}",
            description=f"Root Cause: {diagnosis.root_cause}\nSuggestion: {suggestion}",
            risk_level=risk_level,
            blast_radius=BlastRadius(
                affected_pods=1 if diagnosis.auto_fixable else 2,
                estimated_downtime="~5min",
                related_services=[repo],
                data_impact=DataImpact.NONE,
            ),
            dry_run_checks=[],
            requested_by="github-webhook",
            metadata={
                "source": "github",
                "alert_type": "ci_failure_repair",
                "target_resource": repo,
                "namespace": "github-actions",
                "ci_diagnosis_id": diagnosis_id,
                "workflow_name": workflow_run.name,
                "workflow_id": workflow_run.id,
                "workflow_url": workflow_run.html_url,
                "head_sha": workflow_run.head_sha,
                "error_type": diagnosis.error_type,
                "auto_fixable": diagnosis.auto_fixable,
                "fix_command": diagnosis.fix_command,
                "llm_provider": diagnosis.analyzed_by,
                "llm_confidence": diagnosis.confidence,
            },
        )

        # 創建 Approval
        approval_id = str(uuid.uuid4())
        await approval_service.create_approval(
            approval_id=approval_id,
            request=approval_request,
        )

        logger.info(
            "ci_failure_approval_created",
            approval_id=approval_id,
            diagnosis_id=diagnosis_id,
            risk_level=risk_level.value,
        )

        return approval_id

    except Exception as e:
        logger.exception("ci_failure_approval_creation_failed", error=str(e))
        return f"temp-{uuid.uuid4().hex[:8]}"


async def save_review_result(
    review_id: str,
    event_type: str,
    repo: str,
    target: str,
    analysis: CodeReviewResult | None,
    metadata: dict,
):
    """
    儲存審查結果到 Redis (透過 Service)

    Key: github_review:{review_id}
    TTL: 7 天
    """
    result = {
        "review_id": review_id,
        "event_type": event_type,
        "repo": repo,
        "target": target,
        "created_at": now_taipei_iso(),
        "analysis": analysis.model_dump() if analysis else None,
        "metadata": metadata,
    }

    service = get_github_webhook_service()
    success = await service.save_review_result(review_id, result)

    if success:
        logger.info(
            "github_review_saved",
            review_id=review_id,
            ttl_days=7,
        )
    else:
        logger.error("github_review_save_failed", review_id=review_id)


async def send_github_telegram_alert(
    review_id: str,
    event_type: str,
    repo: str,
    target: str,
    url: str,
    author: str,
    analysis: CodeReviewResult | None,
):
    """
    發送 GitHub 審查告警到 Telegram

    格式:
    ═══════════════════════════
    🔍 GITHUB CODE REVIEW
    ═══════════════════════════
    📦 repo/name
    🔀 PR #123: Feature title
    👤 @author
    ───────────────────────────
    📊 品質分數: 85/100
    ⚠️ 發現 2 個問題
    🔐 1 個安全疑慮
    ───────────────────────────
    🧠 AI 摘要:
    「代碼品質良好，但建議...」
    ───────────────────────────
    [ 🔗 查看 PR ] [ 📋 詳情 ]
    """
    try:
        telegram = get_telegram_gateway()

        # 檢查是否有設定 Bot Token
        if not settings.OPENCLAW_TG_BOT_TOKEN:
            logger.debug(
                "github_telegram_skipped",
                reason="Bot token not configured",
            )
            return

        await telegram.initialize()

        # 構建訊息
        quality_emoji = "🟢" if analysis and analysis.quality_score >= 80 else "🟡" if analysis and analysis.quality_score >= 60 else "🔴"

        message_lines = [
            "═══════════════════════════",
            "🔍 GITHUB CODE REVIEW",
            "═══════════════════════════",
            f"📦 {repo}",
            f"🔀 {target}",
            f"👤 @{author}",
            "───────────────────────────",
        ]

        if analysis:
            message_lines.extend([
                f"{quality_emoji} 品質分數: {analysis.quality_score:.0f}/100",
            ])
            if analysis.issues:
                message_lines.append(f"⚠️ 發現 {len(analysis.issues)} 個問題")
            if analysis.security_concerns:
                message_lines.append(f"🔐 {len(analysis.security_concerns)} 個安全疑慮")
            message_lines.extend([
                "───────────────────────────",
                "🧠 AI 摘要:",
                f"「{analysis.summary[:150]}」",
            ])
        else:
            message_lines.append("❌ AI 分析失敗")

        message_lines.extend([
            "───────────────────────────",
            f"🔗 {url}",
            f"📋 Review ID: {review_id}",
        ])

        message = "\n".join(message_lines)

        # 發送訊息 (使用 send_notification 而非 send_message)
        await telegram.send_notification(message)

        logger.info(
            "github_telegram_sent",
            review_id=review_id,
            repo=repo,
        )

    except Exception as e:
        logger.exception("github_telegram_failed", error=str(e))


async def create_github_approval(
    review_id: str,
    repo: str,
    target: str,
    url: str,
    analysis: CodeReviewResult,
) -> str:
    """
    為有安全疑慮的 PR 建立 Approval 記錄

    Returns:
        str: Approval ID
    """
    try:
        approval_service = get_approval_service()

        # 決定風險等級
        if len(analysis.security_concerns) > 2 or analysis.quality_score < 50:
            risk_level = RiskLevel.CRITICAL
        elif analysis.security_concerns or analysis.quality_score < 70:
            risk_level = RiskLevel.HIGH
        else:
            risk_level = RiskLevel.MEDIUM

        # P1-2 修正: 欄位對齊 ApprovalRequestBase (2026-03-29)
        root_cause = f"Code review found security concerns in {target}"
        suggestion = f"Review {len(analysis.security_concerns)} security concern(s): {', '.join(analysis.security_concerns[:3])}"
        approval_request = ApprovalRequestCreate(
            action=f"Code Review Security: {repo}",
            description=f"Root Cause: {root_cause}\nSuggestion: {suggestion}",
            risk_level=risk_level,
            blast_radius=BlastRadius(
                affected_pods=1,
                estimated_downtime="0",
                related_services=[repo],
                data_impact=DataImpact.READ_ONLY,
            ),
            dry_run_checks=[],
            requested_by="github-webhook",
            metadata={
                "source": "github",
                "alert_type": "code_review_security",
                "target_resource": repo,
                "namespace": "github",
                "github_review_id": review_id,
                "target": target,
                "url": url,
                "quality_score": analysis.quality_score,
                "security_concerns": analysis.security_concerns,
                "issues_count": len(analysis.issues),
                "llm_provider": analysis.analyzed_by,
                "llm_confidence": analysis.confidence,
            },
        )

        # 創建 Approval
        approval_id = str(uuid.uuid4())
        await approval_service.create_approval(
            approval_id=approval_id,
            request=approval_request,
        )

        logger.info(
            "github_approval_created",
            approval_id=approval_id,
            review_id=review_id,
            risk_level=risk_level.value,
        )

        return approval_id

    except Exception as e:
        logger.exception("github_approval_creation_failed", error=str(e))
        return f"temp-{uuid.uuid4().hex[:8]}"


# =============================================================================
# Query Endpoints
# =============================================================================

@router.get(
    "/reviews/{review_id}",
    summary="取得審查結果",
    description="根據 review_id 取得 GitHub 代碼審查結果",
)
async def get_review_result(review_id: str):
    """
    取得 GitHub 審查結果 (透過 Service)

    Args:
        review_id: 審查 ID

    Returns:
        dict: 審查結果
    """
    service = get_github_webhook_service()
    result = await service.get_review_result(review_id)

    if not result:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Review {review_id} not found or expired",
        )

    return result
