#!/usr/bin/env python3
"""
CI Code Review Script
- 从 Gitea 获取 PR diff
- 调用 LLM 进行代码审查
- 将审查结果写回 PR 评论
"""

import argparse
import json
import logging
import os
import re
import sys
from typing import Optional, Tuple

import requests

# ============== 日志配置 ==============
logging.basicConfig(
    level=logging.INFO,
    format="[%(asctime)s] [%(levelname)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("ci_code_review")


# ============== 常量配置 ==============
# diff 最大字符数（超过则截断）
MAX_DIFF_CHARS = int(os.getenv("MAX_DIFF_CHARS", "30000"))
# LLM 调用超时时间（秒）
LLM_TIMEOUT = int(os.getenv("LLM_TIMEOUT", "120"))
# Gitea API 超时时间（秒）
GITEA_TIMEOUT = int(os.getenv("GITEA_TIMEOUT", "30"))
# 最大重试次数
MAX_RETRIES = int(os.getenv("MAX_RETRIES", "2"))
# LLM 提供商: openai (OpenAI兼容) / coze (扣子原生Bot API)
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "coze").lower()


# ============== 工具函数 ==============
def truncate_diff(diff_text: str, max_chars: int) -> Tuple[str, bool]:
    """
    截断过大的 diff 内容，避免超出 LLM 上下文限制。
    优先保留文件头和前面的变更，末尾加提示。
    """
    if len(diff_text) <= max_chars:
        return diff_text, False

    # 找到一个合适的截断位置（尽量在文件边界）
    truncated = diff_text[:max_chars]
    # 尝试在最后一个 "diff --git" 处截断，避免截断到一半
    last_file_boundary = truncated.rfind("\ndiff --git ")
    if last_file_boundary > max_chars // 2:
        truncated = truncated[:last_file_boundary]

    truncated += (
        f"\n\n... [DIFF TRUNCATED] 原始 diff 共 {len(diff_text)} 字符，"
        f"已截断至 {len(truncated)} 字符，仅审查前半部分。\n"
    )
    return truncated, True


def get_env_or_fail(name: str) -> str:
    """从环境变量获取值，不存在则报错退出。"""
    value = os.getenv(name)
    if not value:
        logger.error(f"环境变量 {name} 未设置")
        sys.exit(1)
    return value


# ============== Gitea API 相关 ==============
class GiteaClient:
    """Gitea API 客户端"""

    def __init__(self, base_url: str, token: str, repo: str):
        # 确保 base_url 以 / 结尾
        self.base_url = base_url.rstrip("/") + "/"
        self.token = token
        self.repo = repo  # 格式: owner/repo
        self.session = requests.Session()
        self.session.headers.update(
            {
                "Authorization": f"token {token}",
                "Accept": "application/json",
                "Content-Type": "application/json",
            }
        )

    def _api_url(self, path: str) -> str:
        """拼接 API 路径"""
        return f"{self.base_url}api/v1/repos/{self.repo}/{path.lstrip('/')}"

    def get_pr_diff(self, pr_number: int) -> str:
        """
        获取 PR 的 diff 内容。
        Gitea API: GET /repos/{owner}/{repo}/pulls/{index}.diff
        """
        url = self._api_url(f"pulls/{pr_number}.diff")
        logger.info(f"获取 PR #{pr_number} diff: {url}")

        resp = self.session.get(
            url,
            timeout=GITEA_TIMEOUT,
            headers={
                "Accept": "text/plain",
            },
        )
        if resp.status_code != 200:
            logger.error(f"获取 diff 失败: HTTP {resp.status_code} - {resp.text[:200]}")
            raise RuntimeError(f"Failed to get PR diff: HTTP {resp.status_code}")

        diff_text = resp.text
        logger.info(f"获取到 diff，共 {len(diff_text)} 字符")
        return diff_text

    def get_pr_files(self, pr_number: int) -> list:
        """
        获取 PR 修改的文件列表。
        Gitea API: GET /repos/{owner}/{repo}/pulls/{index}/files
        """
        url = self._api_url(f"pulls/{pr_number}/files")
        logger.info(f"获取 PR #{pr_number} 文件列表")

        resp = self.session.get(url, timeout=GITEA_TIMEOUT)
        if resp.status_code != 200:
            logger.warning(f"获取文件列表失败: HTTP {resp.status_code}")
            return []

        files = resp.json()
        logger.info(f"PR 修改了 {len(files)} 个文件")
        return files

    def post_pr_comment(self, pr_number: int, body: str) -> bool:
        """
        在 PR 上发布评论。
        Gitea API: POST /repos/{owner}/{repo}/issues/{index}/comments
        （Gitea 中 PR 评论走 issues 接口）
        """
        url = self._api_url(f"issues/{pr_number}/comments")
        logger.info(f"发布 PR 评论: {url}")

        payload = {"body": body}
        resp = self.session.post(
            url,
            data=json.dumps(payload),
            timeout=GITEA_TIMEOUT,
        )
        if resp.status_code not in (200, 201):
            logger.error(f"发布评论失败: HTTP {resp.status_code} - {resp.text[:200]}")
            return False

        logger.info(f"评论发布成功，评论 ID: {resp.json().get('id', 'unknown')}")
        return True

    def get_existing_review_comments(self, pr_number: int, marker: str) -> list:
        """
        获取 PR 上已有的 AI 审查评论 ID 列表（带标识 marker）。
        """
        url = self._api_url(f"issues/{pr_number}/comments")
        resp = self.session.get(url, timeout=GITEA_TIMEOUT)
        if resp.status_code != 200:
            logger.warning(f"获取评论列表失败: HTTP {resp.status_code}")
            return []

        comments = resp.json()
        review_comment_ids = []
        for c in comments:
            body = c.get("body", "")
            if marker in body:
                review_comment_ids.append(c.get("id"))
        logger.info(f"找到 {len(review_comment_ids)} 条旧的 AI 审查评论")
        return review_comment_ids

    def delete_pr_comment(self, pr_number: int, comment_id: int) -> bool:
        """
        删除 PR 上的指定评论。
        """
        url = self._api_url(f"issues/comments/{comment_id}")
        resp = self.session.delete(url, timeout=GITEA_TIMEOUT)
        if resp.status_code not in (200, 204):
            logger.warning(f"删除评论 {comment_id} 失败: HTTP {resp.status_code}")
            return False
        return True

    def create_commit_status(
        self, sha: str, state: str, context: str, description: str = "", target_url: str = ""
    ) -> bool:
        """
        给指定 commit 打 status。
        state: pending / success / failure / error / warning
        Gitea API: POST /repos/{owner}/{repo}/statuses/{sha}
        """
        url = self._api_url(f"statuses/{sha}")
        logger.info(f"设置 commit status: sha={sha[:12]}..., state={state}, context={context}")

        payload = {
            "state": state,
            "context": context,
            "description": description[:200] if description else "",
        }
        if target_url:
            payload["target_url"] = target_url

        resp = self.session.post(
            url,
            data=json.dumps(payload),
            timeout=GITEA_TIMEOUT,
        )
        if resp.status_code not in (200, 201):
            logger.error(f"设置 status 失败: HTTP {resp.status_code} - {resp.text[:200]}")
            return False

        logger.info(f"Status 设置成功: {context} = {state}")
        return True


def call_llm_openai(
    prompt: str,
    llm_base_url: str,
    llm_api_key: str,
    llm_model: str,
) -> Optional[str]:
    """OpenAI 兼容模式调用"""
    base_url = llm_base_url.rstrip("/") + "/"
    api_url = f"{base_url}chat/completions"

    headers = {
        "Authorization": f"Bearer {llm_api_key}",
        "Content-Type": "application/json",
    }

    payload = {
        "model": llm_model,
        "messages": [
            {
                "role": "system",
                "content": "你是一位严谨的资深代码审查专家，擅长发现代码中的逻辑错误、安全隐患和性能问题。",
            },
            {
                "role": "user",
                "content": prompt,
            },
        ],
        "temperature": 0.3,
        "max_tokens": 2048,
    }

    logger.info(f"调用 LLM (OpenAI兼容): {api_url}, model={llm_model}")

    last_error = None
    for attempt in range(MAX_RETRIES + 1):
        try:
            resp = requests.post(
                api_url,
                headers=headers,
                json=payload,
                timeout=LLM_TIMEOUT,
            )
            if resp.status_code != 200:
                logger.warning(f"LLM 调用失败 (第 {attempt + 1} 次): " f"HTTP {resp.status_code} - {resp.text[:200]}")
                last_error = f"HTTP {resp.status_code}"
                continue

            data = resp.json()
            choices = data.get("choices", [])
            if not choices:
                logger.warning(f"LLM 返回空结果 (第 {attempt + 1} 次)")
                last_error = "empty choices"
                continue

            content = choices[0].get("message", {}).get("content", "")
            if not content.strip():
                logger.warning(f"LLM 返回空内容 (第 {attempt + 1} 次)")
                last_error = "empty content"
                continue

            logger.info(f"LLM 审查完成，结果长度: {len(content)} 字符")
            return content

        except requests.Timeout:
            logger.warning(f"LLM 调用超时 (第 {attempt + 1} 次)")
            last_error = "timeout"
        except requests.RequestException as e:
            logger.warning(f"LLM 调用异常 (第 {attempt + 1} 次): {e}")
            last_error = str(e)

    logger.error(f"LLM 调用最终失败: {last_error}")
    return None


def call_llm_coze(
    prompt: str,
    llm_base_url: str,
    llm_api_key: str,
    llm_model: str,
    coze_bot_id: str,
) -> Optional[str]:
    """扣子(Coze)原生 Bot API 调用（支持异步轮询）"""
    import time

    base_url = llm_base_url.rstrip("/") + "/"
    api_url = f"{base_url}v3/chat"

    headers = {
        "Authorization": f"Bearer {llm_api_key}",
        "Content-Type": "application/json",
    }

    payload = {
        "bot_id": coze_bot_id,
        "user_id": "ci-code-review-bot",
        "stream": False,
        "additional_messages": [
            {
                "role": "user",
                "content": prompt,
                "content_type": "text",
            }
        ],
    }

    logger.info(f"调用 LLM (Coze): {api_url}, bot_id={coze_bot_id}")

    last_error = None
    for attempt in range(MAX_RETRIES + 1):
        try:
            resp = requests.post(
                api_url,
                headers=headers,
                json=payload,
                timeout=LLM_TIMEOUT,
            )
            if resp.status_code != 200:
                logger.warning(f"Coze 调用失败 (第 {attempt + 1} 次): " f"HTTP {resp.status_code} - {resp.text[:300]}")
                last_error = f"HTTP {resp.status_code}"
                continue

            data = resp.json()
            chat_data = data.get("data", {})
            chat_id = chat_data.get("id", "")
            conversation_id = chat_data.get("conversation_id", "")
            status = chat_data.get("status", "")

            # Coze v3 API 异步：先返回 in_progress，需要轮询
            if status == "in_progress" and conversation_id and chat_id:
                logger.info(f"Coze 异步处理中，开始轮询... (chat_id={chat_id[:12]}...)")
                # 轮询 message 列表接口（GET + query参数），最多等 LLM_TIMEOUT 秒
                poll_url = f"{base_url}v3/chat/message/list"
                poll_start = time.time()
                poll_interval = 3  # 每3秒轮询一次

                while time.time() - poll_start < LLM_TIMEOUT:
                    time.sleep(poll_interval)
                    poll_params = {
                        "chat_id": chat_id,
                        "conversation_id": conversation_id,
                    }
                    poll_resp = requests.get(
                        poll_url,
                        headers=headers,
                        params=poll_params,
                        timeout=GITEA_TIMEOUT,
                    )
                    if poll_resp.status_code != 200:
                        logger.debug(f"轮询返回 HTTP {poll_resp.status_code}: {poll_resp.text[:100]}")
                        continue

                    poll_data = poll_resp.json()
                    if poll_data.get("code", 0) != 0:
                        logger.debug(f"轮询返回错误: {poll_data.get('msg', '')}")
                        continue

                    messages = poll_data.get("data", []) or []

                    # 找assistant的answer消息
                    content = None
                    for msg in messages:
                        if msg.get("role") == "assistant" and msg.get("type") == "answer":
                            content = msg.get("content", "")
                            break

                    if content and content.strip():
                        logger.info(f"Coze 审查完成，结果长度: {len(content)} 字符")
                        return content

                logger.warning(f"Coze 轮询超时 ({LLM_TIMEOUT}s)，未拿到结果")
                last_error = "poll timeout"
                continue

            # 同步返回的情况（兼容）
            content = None
            messages = chat_data.get("messages", []) or data.get("messages", [])
            for msg in messages:
                if msg.get("role") == "assistant" and msg.get("type") == "answer":
                    content = msg.get("content", "")
                    break

            if not content:
                content = chat_data.get("content") or data.get("content")

            if not content:
                choices = data.get("choices", [])
                if choices:
                    content = choices[0].get("message", {}).get("content", "")

            if not content or not content.strip():
                logger.warning(f"Coze 返回空内容 (第 {attempt + 1} 次): {str(data)[:200]}")
                last_error = "empty content"
                continue

            logger.info(f"Coze 审查完成，结果长度: {len(content)} 字符")
            return content

        except requests.Timeout:
            logger.warning(f"Coze 调用超时 (第 {attempt + 1} 次)")
            last_error = "timeout"
        except requests.RequestException as e:
            logger.warning(f"Coze 调用异常 (第 {attempt + 1} 次): {e}")
            last_error = str(e)

    logger.error(f"Coze 调用最终失败: {last_error}")
    return None


def build_review_prompt(diff_text: str, pr_number: int, file_list: list) -> str:
    """
    构建代码审查的 Prompt。
    包含：PR 基本信息、修改文件列表、diff 内容、审查要求。
    """
    # 提取文件名列表
    file_names = [f.get("filename", "") for f in file_list] if file_list else []
    file_list_str = "\n".join(f"  - {fn}" for fn in file_names) if file_names else "  （未获取到文件列表）"

    prompt = f"""请作为资深代码审查专家，对以下 Pull Request 的代码变更进行严格审查。

## PR 基本信息
- PR 编号: #{pr_number}
- 修改文件数: {len(file_list) if file_list else '未知'}

## 修改文件列表
{file_list_str}

## 代码变更（diff）
```diff
{diff_text}
```

## 审查要求
请从以下维度进行审查，重点关注**阻塞级问题**：

### 问题分级标准
- **🔴 阻塞级（BLOCKER）**：必须修复，否则不允许合并。包括：
  1. **明显逻辑bug**：条件判断错误、死循环、返回值错误、空指针/None引用未处理、边界条件遗漏导致功能异常
  2. **安全漏洞**：SQL注入、XSS、命令注入、敏感信息明文存储/泄露、权限绕过、认证缺失
  3. **语法错误**：代码存在语法层面的错误，无法运行
  4. **数据损坏风险**：可能导致数据丢失、数据不一致、脏数据写入的问题

- **💡 建议级（SUGGESTION）**：不阻塞合并，仅供参考改进。包括：
  1. 命名不规范、代码风格问题
  2. 最佳实践建议、设计模式优化
  3. 格式问题（缩进、空行、import顺序等）
  4. 代码可读性改进、注释补充
  5. 非关键路径的轻微性能优化建议
  6. 重复代码、过长函数等代码质量问题

1. **逻辑正确性**：是否有明显的逻辑错误、边界条件遗漏、空指针/None引用风险
2. **异常处理**：异常捕获是否合理，是否有裸except，错误处理是否完善
3. **参数校验**：函数入参、返回值是否有必要的校验
4. **代码质量**：是否有重复代码、命名不清晰、过于复杂的函数
5. **性能问题**：是否有明显的性能隐患（如循环内重复计算、不必要的数据库查询）
6. **安全问题**：是否有注入风险、敏感信息泄露、权限控制问题

## 输出格式
请使用以下格式输出，语言为中文。**必须严格按照格式输出，尤其是【阻塞级判定】部分**：

### 【阻塞级判定】
- 是否存在阻塞级问题：（是 / 否）
- 阻塞级问题数量：X 个

### 📊 审查概览
- 整体评价：（通过 / 有建议 / 需修改）
- 建议级问题数量：X 个

### 🔴 阻塞级问题（必须修复）
（如果没有阻塞级问题，写"无"）
1. **[文件: 行号] 问题标题**
   - 问题类型：（逻辑bug / 安全漏洞 / 语法错误 / 数据损坏风险）
   - 问题描述：...
   - 修改建议：...

### 💡 改进建议（不阻塞合并）
（如果没有建议，写"无"）
1. **[文件: 行号] 建议标题**
   - 具体内容：...

### ✅ 良好实践
（可选，列出值得肯定的地方）

请务必基于代码实际内容审查，不要编造不存在的问题。如果代码质量良好，直接给出通过结论即可。
**重要：【阻塞级判定】必须准确，只有确实存在严重问题时才写"是"。**
"""
    return prompt


def parse_blocker_result(review_text: str) -> Tuple[bool, int]:
    """
    从审查结果中解析是否存在阻塞级问题。
    返回 (has_blocker, blocker_count)
    """
    # 先找【阻塞级判定】部分的明确标记
    pattern = r"【阻塞级判定】[\s\S]*?是否存在阻塞级问题[：:]\s*(是|否)"
    match = re.search(pattern, review_text)
    if match:
        has_blocker = match.group(1) == "是"
    else:
        # fallback 1: 找"阻塞级问题数量"
        count_pattern = r"阻塞级问题数量[：:]\s*(\d+)"
        count_match = re.search(count_pattern, review_text)
        if count_match:
            has_blocker = int(count_match.group(1)) > 0
        else:
            # fallback 2: 检查是否有"阻塞级问题"section且内容不是"无"
            has_blocker = False
            blocker_section = re.search(r"### 🔴 阻塞级问题[\s\S]*?(?=### |\Z)", review_text)
            if blocker_section:
                section_text = blocker_section.group(0)
                # 如果有编号列表项，说明有问题
                if re.search(r"\d+\.\s*\*\*", section_text):
                    has_blocker = True

    # 提取数量
    count_pattern = r"阻塞级问题数量[：:]\s*(\d+)"
    count_match = re.search(count_pattern, review_text)
    blocker_count = int(count_match.group(1)) if count_match else (1 if has_blocker else 0)

    logger.info(f"阻塞级问题解析: 存在={has_blocker}, 数量={blocker_count}")
    return has_blocker, blocker_count


def call_llm_for_review(
    diff_text: str,
    pr_number: int,
    file_list: list,
    llm_base_url: str,
    llm_api_key: str,
    llm_model: str,
    coze_bot_id: str = "",
) -> Optional[str]:
    """
    调用 LLM 进行代码审查，返回审查结果文本。
    失败时返回 None。
    根据 LLM_PROVIDER 环境变量选择调用方式。
    """
    prompt = build_review_prompt(diff_text, pr_number, file_list)
    logger.info(f"Prompt 长度: {len(prompt)} 字符")

    provider = LLM_PROVIDER

    if provider == "coze":
        return call_llm_coze(prompt, llm_base_url, llm_api_key, llm_model, coze_bot_id)
    else:
        # 默认 OpenAI 兼容
        return call_llm_openai(prompt, llm_base_url, llm_api_key, llm_model)


# ============== 主流程 ==============
def main():
    parser = argparse.ArgumentParser(description="CI AI 代码审查脚本")
    parser.add_argument("--pr", type=int, help="PR 编号（也可通过 PR_NUMBER 环境变量）")
    parser.add_argument("--repo", type=str, help="仓库名 owner/repo（也可通过 REPO_NAME 环境变量）")
    parser.add_argument("--gitea-url", type=str, help="Gitea 地址（也可通过 GITEA_API_URL 环境变量）")
    parser.add_argument("--gitea-token", type=str, help="Gitea Token（也可通过 GITEA_TOKEN 环境变量）")
    parser.add_argument("--dry-run", action="store_true", help="只输出审查结果，不发表评论")
    args = parser.parse_args()

    # 读取配置
    gitea_url = args.gitea_url or os.getenv("GITEA_API_URL") or os.getenv("GITEA_SERVER_URL")
    gitea_token = args.gitea_token or os.getenv("GITEA_TOKEN")
    repo_name = args.repo or os.getenv("REPO_NAME") or os.getenv("GITEA_REPO")
    pr_number = args.pr or int(os.getenv("PR_NUMBER") or os.getenv("GITEA_PR_NUMBER") or 0)

    llm_base_url = os.getenv("LLM_BASE_URL")
    llm_api_key = os.getenv("LLM_API_KEY")
    llm_model = os.getenv("LLM_MODEL", "")
    coze_bot_id = os.getenv("COZE_BOT_ID", os.getenv("COZE_BOTID", ""))

    # 根据 provider 设置默认值
    provider = LLM_PROVIDER
    if provider == "coze":
        # 扣子模式：默认国内站，key 兼容多种环境变量名
        if not llm_base_url:
            llm_base_url = "https://api.coze.cn"
        if not llm_api_key:
            llm_api_key = os.getenv("COZE_API_KEY", "") or os.getenv("COZE_PAT", "")
    else:
        # OpenAI兼容模式：默认模型
        if not llm_model:
            llm_model = "gpt-4o-mini"

    # 必要参数校验
    missing = []
    if not gitea_url:
        missing.append("GITEA_API_URL")
    if not gitea_token:
        missing.append("GITEA_TOKEN")
    if not repo_name:
        missing.append("REPO_NAME")
    if not pr_number:
        missing.append("PR_NUMBER")
    if not llm_base_url:
        missing.append("LLM_BASE_URL")
    if not llm_api_key:
        missing.append("LLM_API_KEY")
    if provider == "coze" and not coze_bot_id:
        missing.append("COZE_BOT_ID (扣子模式需要)")

    if missing:
        logger.error(f"缺少必要配置: {', '.join(missing)}")
        sys.exit(1)

    logger.info(f"开始审查 PR #{pr_number}，仓库: {repo_name}")
    logger.info(f"Gitea: {gitea_url}")
    logger.info(f"LLM: {llm_base_url} (model={llm_model})")

    try:
        # 1. 初始化 Gitea 客户端
        gitea = GiteaClient(gitea_url, gitea_token, repo_name)

        # 2. 获取 PR diff 和文件列表
        try:
            diff_text = gitea.get_pr_diff(pr_number)
            file_list = gitea.get_pr_files(pr_number)
        except Exception as e:
            logger.error(f"获取 PR 信息失败: {e}")
            sys.exit(1)

        # 3. 过滤掉不需要审查的文件（如 lock 文件、生成的文件、二进制文件等）
        skip_extensions = (
            ".lock",
            ".sum",
            ".min.js",
            ".min.css",
            ".map",
            ".png",
            ".jpg",
            ".jpeg",
            ".gif",
            ".svg",
            ".ico",
            ".woff",
            ".woff2",
            ".ttf",
            ".eot",
        )
        skipped_files = []
        if file_list:
            skipped_files = [
                f.get("filename")
                for f in file_list
                if f.get("filename", "").endswith(skip_extensions) or f.get("status") == "removed"
            ]
            if skipped_files:
                logger.info(f"跳过 {len(skipped_files)} 个非文本/已删除文件: {', '.join(skipped_files[:5])}...")

        # 实际从 diff 中移除跳过的文件（按文件边界切割）
        if skipped_files:
            diff_lines = diff_text.split("\n")
            filtered_lines = []
            current_file = None
            skip_current = False
            i = 0
            while i < len(diff_lines):
                line = diff_lines[i]
                # 检测新文件开始: diff --git a/xxx b/xxx
                if line.startswith("diff --git "):
                    # 提取文件名
                    parts = line.split(" ")
                    if len(parts) >= 4:
                        # b/ 后面的是目标文件名
                        current_file = parts[3][2:] if parts[3].startswith("b/") else parts[3]
                        skip_current = any(current_file == sf for sf in skipped_files) or any(
                            current_file.endswith(ext) for ext in skip_extensions
                        )
                    else:
                        skip_current = False
                if not skip_current:
                    filtered_lines.append(line)
                i += 1
            original_len = len(diff_text)
            diff_text = "\n".join(filtered_lines)
            logger.info(f"Diff 过滤后: {original_len} -> {len(diff_text)} 字符 (减少 {original_len - len(diff_text)})")

        # 4. 截断过大的 diff
        diff_text, was_truncated = truncate_diff(diff_text, MAX_DIFF_CHARS)
        if was_truncated:
            logger.warning(f"Diff 过大，已截断至 {len(diff_text)} 字符")

        # 5. 如果 diff 为空，直接跳过
        if not diff_text.strip():
            logger.info("Diff 为空，无需审查")
            sys.exit(0)

        # 6. 调用 LLM 审查
        review_result = call_llm_for_review(
            diff_text=diff_text,
            pr_number=pr_number,
            file_list=file_list,
            llm_base_url=llm_base_url,
            llm_api_key=llm_api_key,
            llm_model=llm_model,
            coze_bot_id=coze_bot_id,
        )

        if not review_result:
            logger.error("LLM 审查失败")
            sys.exit(0)  # fail-open: LLM调用失败不阻塞合并

        # 7. 加上审查时间和标识（便于识别是自动审查）
        from datetime import datetime

        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        marker = "<!-- AI_CODE_REVIEW_AUTO_COMMENT -->"
        full_comment = f"""{review_result}

---
<sub>🤖 由 AI 代码审查机器人自动生成 | {timestamp} | 模型: {llm_model}</sub>

{marker}
"""

        # 8. 输出审查结果到日志
        logger.info("=" * 60)
        logger.info("审查结果:")
        for line in review_result.split("\n")[:30]:
            logger.info(line)
        if len(review_result.split("\n")) > 30:
            logger.info(f"... 共 {len(review_result.split(chr(10)))} 行")
        logger.info("=" * 60)

        # 9. 发布评论（先删除旧的审查评论，避免刷屏）
        if args.dry_run:
            logger.info("--dry-run 模式，跳过发布评论")
            print(full_comment)
        else:
            # 去重：删除之前的 AI 审查评论
            old_comments = gitea.get_existing_review_comments(pr_number, marker)
            if old_comments:
                logger.info(f"找到 {len(old_comments)} 条旧的 AI 审查评论，先删除")
                for cid in old_comments:
                    gitea.delete_pr_comment(pr_number, cid)
            # 发布新评论
            success = gitea.post_pr_comment(pr_number, full_comment)
            if not success:
                logger.error("评论发布失败")
                sys.exit(1)

        # 10. 解析阻塞级问题，用退出码决定 job 状态
        # 有阻塞级问题 → exit 1 → job失败 → Gitea自动打failure status → 门禁拦截
        # 无阻塞级问题 → exit 0 → job成功 → Gitea自动打success status
        # LLM调用失败等异常 → exit 0 → fail-open，不阻塞正常开发
        has_blocker, blocker_count = parse_blocker_result(review_result)

        if has_blocker:
            logger.error(f"检测到 {blocker_count} 个阻塞级问题，审查不通过")
            logger.info("代码审查完成（失败）")
            sys.exit(1)
        else:
            logger.info("无阻塞级问题，审查通过")
            logger.info("代码审查完成（通过）")
            sys.exit(0)

    except Exception as e:
        logger.exception(f"审查脚本发生未预期的异常: {e}")
        sys.exit(0)  # fail-open: 异常不阻塞正常开发


if __name__ == "__main__":
    main()
