"""
Git Worktree Manager - 멀티봇 환경에서의 안전한 worktree 관리 스크립트.

사용법:
    python3 worktree_manager.py <command> [args]

명령어:
    create   <project_path> <task_id> <team_id>        - worktree 생성
    finish   <project_path> <task_id> <team_id> <action> - worktree 완료 처리
    list     <project_path>                             - 활성 worktree 목록
    status   <project_path> <task_id>                  - worktree 상태 확인
"""

import argparse
import functools
import json
import logging
import re
import subprocess
import sys
import time
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Rate limit 트래커
# ---------------------------------------------------------------------------


def _check_rate_limit() -> dict[str, str]:
    """Gemini 무료 한도 (일 33건) 체크. 초과 시 경고 반환."""
    tracker_path = Path(__file__).parent / "gemini_rate_tracker.json"
    today = time.strftime("%Y-%m-%d")
    data = {}
    if tracker_path.exists():
        try:
            data = json.loads(tracker_path.read_text())
        except (json.JSONDecodeError, OSError):
            data = {}
    if data.get("date") != today:
        data = {"date": today, "count": 0}
    data["count"] += 1
    tracker_path.write_text(json.dumps(data))
    if data["count"] > 33:
        return {"warning": f"Gemini daily limit exceeded: {data['count']}/33"}
    return {}


# ---------------------------------------------------------------------------
# Git 헬퍼
# ---------------------------------------------------------------------------


def _run(
    args: list[str],
    cwd: str | None = None,
    check: bool = True,
    capture: bool = True,
) -> subprocess.CompletedProcess[str]:
    """git 명령을 실행하고 CompletedProcess 반환.

    Args:
        args: 실행할 명령어 리스트.
        cwd: 작업 디렉터리.
        check: 실패 시 CalledProcessError 발생 여부.
        capture: stdout/stderr 캡처 여부.

    Returns:
        완료된 프로세스 객체.
    """
    return subprocess.run(
        args,
        cwd=cwd,
        check=check,
        text=True,
        stdout=subprocess.PIPE if capture else None,
        stderr=subprocess.PIPE if capture else None,
    )


def _out(args: list[str], cwd: str | None = None) -> str:
    """명령 실행 후 stdout을 strip하여 반환."""
    result = _run(args, cwd=cwd, check=True)
    return result.stdout.strip()


def detect_main_branch(project_path: str) -> str:
    """메인 브랜치명 감지.

    git symbolic-ref refs/remotes/origin/HEAD → 실패 시 main/master fallback.

    Args:
        project_path: git 저장소 루트 경로.

    Returns:
        메인 브랜치명 (예: "main" 또는 "master").
    """
    # 1차: origin/HEAD 심볼릭 레퍼런스로 감지
    try:
        ref = _out(
            ["git", "symbolic-ref", "refs/remotes/origin/HEAD"],
            cwd=project_path,
        )
        # "refs/remotes/origin/main" → "main"
        return ref.split("/")[-1]
    except subprocess.CalledProcessError:
        pass

    # 2차: 로컬 브랜치 목록에서 main/master 확인
    try:
        branches_raw = _out(
            ["git", "branch", "--format=%(refname:short)"],
            cwd=project_path,
        )
        branches = [b.strip() for b in branches_raw.splitlines()]
        for candidate in ("main", "master"):
            if candidate in branches:
                return candidate
    except subprocess.CalledProcessError:
        pass

    # 3차: 현재 체크아웃된 브랜치 반환 (최후 수단)
    try:
        return _out(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=project_path)
    except subprocess.CalledProcessError:
        return "main"


def list_worktrees_raw(project_path: str) -> list[dict[str, str]]:
    """git worktree list --porcelain 파싱.

    Args:
        project_path: git 저장소 루트 경로.

    Returns:
        각 worktree 정보 딕셔너리 리스트.
        키: "worktree", "HEAD", "branch" (branch는 없을 수 있음)
    """
    output = _out(
        ["git", "worktree", "list", "--porcelain"],
        cwd=project_path,
    )

    worktrees: list[dict[str, str]] = []
    current: dict[str, str] = {}

    for line in output.splitlines():
        if not line.strip():
            if current:
                worktrees.append(current)
                current = {}
            continue
        if " " in line:
            key, _, value = line.partition(" ")
            current[key] = value
        else:
            # "bare" 같은 단독 키워드
            current[line] = ""

    if current:
        worktrees.append(current)

    return worktrees


# ---------------------------------------------------------------------------
# 브랜치 / 경로 네이밍
# ---------------------------------------------------------------------------


def branch_name(task_id: str, team_id: str) -> str:
    """worktree 브랜치명 생성."""
    return f"task/{task_id}-{team_id}"


def worktree_path(project_path: str, task_id: str, team_id: str) -> str:
    """worktree 디렉터리 경로 생성."""
    return str(Path(project_path) / ".worktrees" / f"{task_id}-{team_id}")


# ---------------------------------------------------------------------------
# 안전 검증 데코레이터
# ---------------------------------------------------------------------------


def validate_worktree_safety(func):  # type: ignore[no-untyped-def]
    @functools.wraps(func)
    def wrapper(project_path: str, *args, **kwargs) -> dict[str, Any]:
        # read_only 모드에서는 git 검증 skip (worktree 생성 불필요)
        if kwargs.get("read_only", False):
            return func(project_path, *args, **kwargs)
        git_dir = Path(project_path) / ".git"
        if not git_dir.exists():
            return {"status": "error", "message": f"Not a git repository: {project_path}"}

        gitignore_path = Path(project_path) / ".gitignore"
        gitignore_updated = False

        check_result = subprocess.run(
            ["git", "check-ignore", "-q", ".worktrees/"],
            cwd=project_path,
            capture_output=True,
        )

        if check_result.returncode != 0:
            with open(gitignore_path, "a") as f:
                if gitignore_path.exists() and gitignore_path.stat().st_size > 0:
                    f.write("\n.worktrees/\n")
                else:
                    f.write(".worktrees/\n")
            gitignore_updated = True

        result = func(project_path, *args, **kwargs)

        if isinstance(result, dict) and gitignore_updated:
            result["gitignore_updated"] = True

        return result

    return wrapper


# ---------------------------------------------------------------------------
# 명령어 구현
# ---------------------------------------------------------------------------


@validate_worktree_safety
def cmd_create(project_path: str, task_id: str, team_id: str, read_only: bool = False) -> dict[str, Any]:
    """worktree 생성 또는 재사용.

    Args:
        project_path: git 저장소 루트 경로.
        task_id: 태스크 식별자.
        team_id: 팀 식별자.
        read_only: True이면 worktree 미생성 (read 에이전트 전용).

    Returns:
        {"status": "created"|"reused"|"skipped", "worktree_path": str|None, "branch": str|None}
    """
    if read_only:
        return {
            "status": "skipped",
            "worktree_path": None,
            "branch": None,
            "reason": "read-only agent: worktree 미생성",
        }

    branch = branch_name(task_id, team_id)
    wt_path = worktree_path(project_path, task_id, team_id)

    # 기존 worktree 존재 여부 확인
    existing = list_worktrees_raw(project_path)
    for wt in existing:
        if wt.get("worktree") == wt_path:
            return {
                "status": "reused",
                "worktree_path": wt_path,
                "branch": branch,
            }

    # .worktrees 디렉터리 생성
    Path(wt_path).parent.mkdir(parents=True, exist_ok=True)

    # 브랜치가 이미 있는지 확인
    try:
        _out(
            ["git", "rev-parse", "--verify", branch],
            cwd=project_path,
        )
        branch_exists = True
    except subprocess.CalledProcessError:
        branch_exists = False

    if branch_exists:
        # 브랜치가 존재하면 해당 브랜치로 worktree 추가
        _run(
            ["git", "worktree", "add", wt_path, branch],
            cwd=project_path,
        )
    else:
        # 새 브랜치 생성하며 worktree 추가
        _run(
            ["git", "worktree", "add", "-b", branch, wt_path],
            cwd=project_path,
        )

    # 원격 브랜치 생성 (push, 실패 시 무시 - origin 없는 프로젝트 등)
    _run(
        ["git", "push", "-u", "origin", branch],
        cwd=wt_path,
        check=False,
    )

    return {
        "status": "created",
        "worktree_path": wt_path,
        "branch": branch,
    }


def _resolve_task_level(task_id: str) -> int:
    """task 파일에서 level을 읽어 반환. 없으면 0."""
    tasks_dir = Path(__file__).parent.parent / "memory" / "tasks"
    # task 파일 패턴: task-{id}.md 또는 task-{id}로 시작하는 파일
    candidates = list(tasks_dir.glob(f"{task_id}.md")) + list(tasks_dir.glob(f"{task_id}*.md"))
    for p in candidates:
        try:
            text = p.read_text(encoding="utf-8")
            if text.startswith("---"):
                # YAML frontmatter 파싱
                end = text.find("---", 3)
                if end > 0:
                    try:
                        import yaml  # lazy import

                        fm = yaml.safe_load(text[3:end])
                        if isinstance(fm, dict) and "level" in fm:
                            return int(fm["level"])
                    except ImportError:
                        import re

                        m = re.search(r"^level:\s*(\d+)", text[3:end], re.MULTILINE)
                        if m:
                            return int(m.group(1))
        except (OSError, ValueError, TypeError):
            continue

    # fallback: task-timers.json에서 work_level 읽기
    timers_path = Path(__file__).parent.parent / "memory" / "task-timers.json"
    try:
        timers = json.loads(timers_path.read_text(encoding="utf-8"))
        entry = timers.get(task_id, {})
        wl = entry.get("work_level", "")
        if wl:
            # "lv2" -> 2, "lv3" -> 3 등
            return int(wl.replace("lv", "").strip())
    except (OSError, json.JSONDecodeError, ValueError):
        pass

    return 0


def _get_blast_radius_summary(project_path: str, branch: str, main_branch: str) -> str:
    """AST 의존성 분석으로 PR blast radius 요약 생성.

    Args:
        project_path: git 저장소 루트 경로.
        branch: 분석할 브랜치명.
        main_branch: 비교 기준 메인 브랜치명.

    Returns:
        Blast radius 요약 마크다운 문자열. 실패 시 빈 문자열.
    """
    import logging

    logger = logging.getLogger(__name__)

    try:
        # git diff로 변경된 .py 파일 목록 추출
        diff_result = subprocess.run(
            ["git", "diff", "--name-only", f"{main_branch}...{branch}", "--", "*.py"],
            cwd=project_path,
            check=False,
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=30,
        )
        if diff_result.returncode != 0:
            logger.warning(f"[blast_radius] git diff 실패: {diff_result.stderr.strip()}")
            return ""

        changed_files = [f.strip() for f in diff_result.stdout.strip().splitlines() if f.strip()]
        if not changed_files:
            return ""

        # AST 스크립트 호출
        ast_script = "/home/jay/workspace/scripts/ast_dependency_map.py"
        ast_cmd = ["python3", ast_script, "--root", project_path, "--files"] + changed_files + ["--json"]
        ast_result = subprocess.run(
            ast_cmd,
            cwd=project_path,
            check=False,
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=30,
        )
        if ast_result.returncode != 0:
            logger.warning(f"[blast_radius] AST 스크립트 실패: {ast_result.stderr.strip()}")
            return ""

        data = json.loads(ast_result.stdout)
        direct_importers: list[str] = data.get("direct_importers", [])
        test_files: list[str] = data.get("test_files", [])

        # 요약 형식 생성
        changed_str = ", ".join(changed_files)
        importers_str = ", ".join(direct_importers) if direct_importers else "없음"
        tests_str = ", ".join(test_files) if test_files else "없음"

        summary = (
            "## Blast Radius\n"
            f"- 변경 파일: {len(changed_files)}개 ({changed_str})\n"
            f"- 영향받는 파일: {len(direct_importers)}개 ({importers_str})\n"
            f"- 관련 테스트: {len(test_files)}개 ({tests_str})"
        )
        return summary

    except subprocess.TimeoutExpired:
        logger.warning("[blast_radius] 타임아웃 (30초 초과)")
        return ""
    except (json.JSONDecodeError, KeyError) as e:
        logger.warning(f"[blast_radius] JSON 파싱 실패: {e}")
        return ""
    except Exception as e:
        logger.warning(f"[blast_radius] 예상치 못한 오류: {e}")
        return ""


def _parse_gemini_comments(pr_number: str, owner: str, repo_name: str, cwd: str) -> list[dict[str, str]]:
    """GitHub API로 PR 코멘트를 파싱하여 severity 분류된 리스트 반환.

    Returns:
        [{"severity": "high"|"medium"|"low", "path": str, "body": str, "line": int}]
    """
    comments_result = _run(
        ["gh", "api", f"repos/{owner}/{repo_name}/pulls/{pr_number}/comments"],
        cwd=cwd,
        check=False,
    )
    if comments_result.returncode != 0:
        return []

    try:
        comments = json.loads(comments_result.stdout)
    except (json.JSONDecodeError, KeyError):
        return []

    parsed: list[dict[str, str]] = []
    for comment in comments:
        user = comment.get("user", {}).get("login", "")
        if "gemini" not in user.lower():
            continue
        body_text = comment.get("body", "")
        severity = "low"
        body_lower = body_text.lower()
        # Gemini 마크다운 이미지 alt text 패턴: ![severity-label](URL)
        _high_img_re = re.compile(r'!\[(security-critical|critical|high)\]', re.IGNORECASE)
        _medium_img_re = re.compile(r'!\[(medium)\]', re.IGNORECASE)
        if (
            "severity: high" in body_lower
            or "severity: critical" in body_lower
            or "🔴" in body_text
            or "HIGH" in body_text
            or "CRITICAL" in body_text
            or _high_img_re.search(body_text)
            or "high-priority.svg" in body_lower
            or "critical.svg" in body_lower
        ):
            severity = "high"
        elif (
            "severity: medium" in body_lower
            or "⚠️" in body_text
            or "MEDIUM" in body_text
            or "WARNING" in body_text
            or _medium_img_re.search(body_text)
            or "medium-priority.svg" in body_lower
        ):
            severity = "medium"
        parsed.append(
            {
                "severity": severity,
                "path": comment.get("path", ""),
                "body": body_text[:500],
                "line": str(comment.get("line", 0)),
            }
        )
    return parsed


def _auto_fix_high_comments(
    comments: list[dict[str, str]], worktree_path: str, collect_mode: bool = False
) -> dict[str, Any]:
    """HIGH severity 코멘트를 기반으로 자동 수정 프롬프트 생성 및 실행.

    collect_mode=False(기본값)이면 실제 수정 실행. True이면 프롬프트만 생성.

    Returns:
        {"prompts": [...], "executed": bool, "diff_stat": str}
    """
    high_comments = [c for c in comments if c["severity"] == "high"]
    if not high_comments:
        return {"prompts": [], "executed": False, "diff_stat": ""}

    prompts: list[str] = []
    for c in high_comments:
        prompt = f"Fix the following HIGH severity issue in {c['path']} " f"(line {c['line']}):\n{c['body']}"
        prompts.append(prompt)

    if collect_mode:
        return {"prompts": prompts, "executed": False, "diff_stat": ""}

    # 실제 수정 모드 (활성화됨)
    combined_prompt = "\n\n---\n\n".join(prompts)
    _run(
        ["claude", "-p", combined_prompt, "--dangerously-skip-permissions"],
        cwd=worktree_path,
        check=False,
    )

    # git add + commit
    _run(["git", "add", "-A"], cwd=worktree_path, check=False)
    _run(
        ["git", "commit", "-m", "[auto-fix] Gemini HIGH severity issues"],
        cwd=worktree_path,
        check=False,
    )

    # diff stat
    diff_result = _run(
        ["git", "diff", "--stat", "HEAD~1"],
        cwd=worktree_path,
        check=False,
    )
    diff_stat = diff_result.stdout.strip() if diff_result.returncode == 0 else ""

    # push
    _run(["git", "push"], cwd=worktree_path, check=False)

    return {"prompts": prompts, "executed": True, "diff_stat": diff_stat}


MEDIUM_FIX_PATTERNS: list[str] = [
    # 보안 (5개)
    "sql injection",
    "xss",
    "csrf",
    "auth",
    "permission",
    # 안정성 (3개)
    "null pointer",
    "race condition",
    "deadlock",
    # 데이터 (2개)
    "data loss",
    "corruption",
    # 환경 (2개)
    "env variable",
    "config",
    # 프론트 (3개)
    "accessibility",
    "a11y",
    "responsive",
]

MEDIUM_SKIP_PATTERNS: list[str] = [
    # 스타일 (4개)
    "naming convention",
    "code style",
    "formatting",
    "whitespace",
    # 타입 (1개)
    "type annotation",
    # 컨벤션 (1개)
    "docstring",
]


def _classify_medium_comments(comments: list[dict[str, str]], collect_mode: bool = False) -> list[dict[str, str]]:
    """MEDIUM 코멘트를 FIX/SKIP/DEFER 3종으로 자동 분류.

    collect_mode=False(기본값)이면 패턴 매칭으로 분류. True이면 모든 MEDIUM을 DEFER로 분류하고 로그만 수집.

    Returns:
        [{"severity": "medium", "classification": "FIX"|"SKIP"|"DEFER", "body": str, "path": str, "pattern_matched": str}]
    """
    medium_comments = [c for c in comments if c["severity"] == "medium"]
    classified: list[dict[str, str]] = []

    for c in medium_comments:
        body_lower = c["body"].lower()

        if collect_mode:
            # Week 1-2: 모든 MEDIUM은 DEFER (수집 모드)
            classification = "DEFER"
            pattern_matched = "collect_mode"
        else:
            # 정상 모드: 패턴 매칭 기반 분류
            classification = "DEFER"
            pattern_matched = ""

            for pattern in MEDIUM_FIX_PATTERNS:
                if pattern in body_lower:
                    classification = "FIX"
                    pattern_matched = pattern
                    break

            if classification == "DEFER":
                for pattern in MEDIUM_SKIP_PATTERNS:
                    if pattern in body_lower:
                        classification = "SKIP"
                        pattern_matched = pattern
                        break

        classified.append(
            {
                "severity": "medium",
                "classification": classification,
                "body": c["body"],
                "path": c.get("path", ""),
                "pattern_matched": pattern_matched,
            }
        )

    return classified


def _log_medium_comments(classified: list[dict[str, str]]) -> None:
    """MEDIUM 분류 결과를 로그 파일에 기록."""
    log_path = Path(__file__).resolve().parent.parent / "dashboard" / "data" / "medium-comments-log.json"

    existing: list[dict[str, str]] = []
    if log_path.exists():
        try:
            existing = json.loads(log_path.read_text())
        except (json.JSONDecodeError, OSError):
            existing = []

    for c in classified:
        entry = {
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
            "classification": c["classification"],
            "body": c["body"][:200],
            "path": c.get("path", ""),
            "pattern_matched": c.get("pattern_matched", ""),
        }
        existing.append(entry)

    log_path.parent.mkdir(parents=True, exist_ok=True)
    log_path.write_text(json.dumps(existing, indent=2, ensure_ascii=False))


@validate_worktree_safety
def cmd_finish(
    project_path: str,
    task_id: str,
    team_id: str,
    action: str,
    pr_title: str = "",
    pr_body: str = "",
    gemini_timeout: int = 300,
    collect_mode: bool = False,
) -> dict[str, Any]:
    """worktree 작업 완료 처리.

    Args:
        project_path: git 저장소 루트 경로.
        task_id: 태스크 식별자.
        team_id: 팀 식별자.
        action: "merge" | "discard" | "keep" | "pr" | "auto" (auto: task level에 따라 자동 결정)
        pr_title: PR 제목 (기본값: "[{task_id}] worktree finish").
        pr_body: PR 본문.
        gemini_timeout: Gemini 리뷰 대기 최대 시간(초).
        collect_mode: False이면 실제 수정 루프 실행(기본값), True이면 프롬프트 수집만.

    Returns:
        {"status": "merged"|"discarded"|"kept"|"pending"|"merge_failed"|"blocked_by_high_severity", "branch": str}
    """
    # auto 액션 해소
    if action == "auto":
        level = _resolve_task_level(task_id)
        action = "pr" if level >= 2 else "keep"

    branch = branch_name(task_id, team_id)
    wt_path = worktree_path(project_path, task_id, team_id)

    # worktree 존재 확인
    existing = list_worktrees_raw(project_path)
    wt_found = any(wt.get("worktree") == wt_path for wt in existing)

    if action == "keep":
        sync_status = "skipped"
        if wt_found:
            main_branch = detect_main_branch(project_path)
            _run(["git", "fetch", "origin"], cwd=wt_path, check=False)
            sync_result = _run(
                ["git", "merge", main_branch, "--no-edit"],
                cwd=wt_path,
                check=False,
            )
            if sync_result.returncode == 0:
                sync_status = "synced"
            else:
                _run(["git", "merge", "--abort"], cwd=wt_path, check=False)
                sync_status = "conflict"
        return {"status": "kept", "branch": branch, "sync_with_main": sync_status}

    if action == "merge":
        if not wt_found:
            raise RuntimeError(f"worktree not found: {wt_path}")

        main_branch = detect_main_branch(project_path)

        # ★ main 최신화: worktree 브랜치에서 main을 merge하여 최신화
        _run(["git", "fetch", "origin"], cwd=wt_path, check=False)  # remote 없으면 무시
        sync_result = _run(
            ["git", "merge", main_branch, "--no-edit"],
            cwd=wt_path,
            check=False,
        )
        if sync_result.returncode != 0:
            _run(["git", "merge", "--abort"], cwd=wt_path, check=False)
            conflict_info = sync_result.stdout or ""
            raise RuntimeError(
                f"main 최신화 중 충돌 발생. 수동 해결 필요.\n" f"브랜치: {branch}\n" f"충돌 파일:\n{conflict_info}"
            )

        # 메인 브랜치로 전환 후 머지
        _run(["git", "checkout", main_branch], cwd=project_path)

        merge_result = _run(
            ["git", "merge", "--no-ff", branch],
            cwd=project_path,
            check=False,
        )

        if merge_result.returncode != 0:
            # 충돌 발생 시 머지 중단 후 에러 반환 (자동 해결 시도 금지)
            _run(["git", "merge", "--abort"], cwd=project_path, check=False)
            raise RuntimeError(
                f"Merge conflict detected while merging '{branch}' into "
                f"'{main_branch}'. Resolve conflicts manually."
            )

        # worktree 제거
        _run(["git", "worktree", "remove", "--force", wt_path], cwd=project_path)
        # 브랜치 삭제
        _run(["git", "branch", "-d", branch], cwd=project_path, check=False)
        # 원격 wip 브랜치 삭제 (실패 시 무시)
        _run(
            ["git", "push", "origin", "--delete", branch],
            cwd=project_path,
            check=False,
        )

        return {"status": "merged", "branch": branch}

    if action == "discard":
        if not wt_found:
            raise RuntimeError(f"worktree not found: {wt_path}")

        # 변경 사항 초기화 후 worktree 제거
        _run(["git", "checkout", "--", "."], cwd=wt_path, check=False)
        _run(["git", "clean", "-fd"], cwd=wt_path, check=False)
        _run(["git", "worktree", "remove", "--force", wt_path], cwd=project_path)
        # 브랜치 강제 삭제 (미머지 상태)
        _run(["git", "branch", "-D", branch], cwd=project_path, check=False)
        # 원격 wip 브랜치 삭제 (실패 시 무시)
        _run(
            ["git", "push", "origin", "--delete", branch],
            cwd=project_path,
            check=False,
        )

        return {"status": "discarded", "branch": branch}

    if action == "pr":
        if not wt_found:
            raise RuntimeError(f"worktree not found: {wt_path}")

        main_branch = detect_main_branch(project_path)

        # 1. Sync with main first
        _run(["git", "fetch", "origin"], cwd=wt_path, check=False)
        sync_result = _run(
            ["git", "merge", main_branch, "--no-edit"],
            cwd=wt_path,
            check=False,
        )
        if sync_result.returncode != 0:
            _run(["git", "merge", "--abort"], cwd=wt_path, check=False)
            raise RuntimeError(f"main 최신화 중 충돌 발생. 수동 해결 필요.\n브랜치: {branch}")

        # 2. Push branch to remote
        _run(["git", "push", "-u", "origin", branch], cwd=wt_path, check=False)

        # 3. Create PR
        title = pr_title or f"[{task_id}] worktree finish"
        body = pr_body or f"Task {task_id} by team {team_id}"
        # AST blast radius 요약 삽입 (task-1869_2.2+1)
        blast_summary = _get_blast_radius_summary(project_path, branch, main_branch)
        if blast_summary:
            body = body + "\n\n" + blast_summary

        rate_limit_result = _check_rate_limit()

        pr_result = _run(
            ["gh", "pr", "create", "--title", title, "--body", body, "--base", main_branch, "--head", branch],
            cwd=wt_path,
            check=False,
        )

        if pr_result.returncode != 0:
            # PR already exists or other error — try to find existing PR
            pr_url = ""
            pr_number = ""
            find_pr = _run(
                ["gh", "pr", "view", branch, "--json", "number,url"],
                cwd=wt_path,
                check=False,
            )
            if find_pr.returncode == 0:
                pr_info = json.loads(find_pr.stdout)
                pr_number = str(pr_info.get("number", ""))
                pr_url = pr_info.get("url", "")
            if not pr_number:
                raise RuntimeError(f"PR 생성 실패: {pr_result.stderr}")
        else:
            # Extract PR URL and number from creation output
            pr_url = pr_result.stdout.strip()
            # Get PR number
            pr_view = _run(
                ["gh", "pr", "view", branch, "--json", "number"],
                cwd=wt_path,
                check=False,
            )
            if pr_view.returncode == 0:
                pr_number = str(json.loads(pr_view.stdout).get("number", ""))
            else:
                pr_number = ""

        # 4. Wait for Gemini review (up to gemini_timeout seconds)
        gemini_found = False
        elapsed = 0
        interval = 30

        # Get repo owner/name
        repo_info = _run(
            ["gh", "repo", "view", "--json", "owner,name"],
            cwd=wt_path,
            check=False,
        )
        owner = ""
        repo_name = ""
        if repo_info.returncode == 0:
            ri = json.loads(repo_info.stdout)
            owner = ri.get("owner", {}).get("login", "")
            repo_name = ri.get("name", "")

        while elapsed < gemini_timeout and pr_number:
            reviews_result = _run(
                ["gh", "api", f"repos/{owner}/{repo_name}/pulls/{pr_number}/reviews"],
                cwd=wt_path,
                check=False,
            )
            if reviews_result.returncode == 0 and "gemini-code-assist" in reviews_result.stdout.lower():
                gemini_found = True
                break
            time.sleep(interval)
            elapsed += interval

        # 5. Parse Gemini comments + HIGH auto-fix loop + MEDIUM classification
        high_severity_count = 0
        review_summary: list[dict[str, str]] = []
        auto_fix_attempts = 0
        max_auto_fix_attempts = 3

        parsed: list[dict[str, str]] = []
        medium_classified: list[dict[str, str]] = []

        # [gemini-retry] 타임아웃 시 60초 추가 대기 후 1회 재확인
        if not gemini_found and pr_number:
            logger.info("[gemini-retry] 타임아웃 후 60초 추가 대기...")
            time.sleep(60)
            retry_result = _run(
                ["gh", "api", f"repos/{owner}/{repo_name}/pulls/{pr_number}/reviews"],
                cwd=wt_path,
                check=False,
            )
            if retry_result.returncode == 0 and "gemini-code-assist" in retry_result.stdout.lower():
                gemini_found = True
                logger.info("[gemini-retry] 재확인 성공 — Gemini 리뷰 감지됨")

        if gemini_found and pr_number:
            # HIGH 자동 수정 루프 (최대 3회)
            for attempt in range(max_auto_fix_attempts):
                parsed = _parse_gemini_comments(pr_number, owner, repo_name, wt_path)
                high_comments = [c for c in parsed if c["severity"] == "high"]
                high_severity_count = len(high_comments)

                if high_severity_count == 0 or attempt == max_auto_fix_attempts - 1:
                    review_summary = [
                        {"severity": c["severity"], "path": c["path"], "body": c["body"][:200]} for c in parsed
                    ]
                    break

                # 자동 수정 시도
                auto_fix_attempts += 1
                fix_result = _auto_fix_high_comments(high_comments, wt_path, collect_mode=collect_mode)

                if collect_mode:
                    # 수집 모드: 프롬프트만 생성, 루프 종료
                    review_summary = [
                        {"severity": c["severity"], "path": c["path"], "body": c["body"][:200]} for c in parsed
                    ]
                    break

                # 실행 모드: push 후 재리뷰 대기
                if fix_result.get("executed"):
                    # 재리뷰 대기 (최대 gemini_timeout초)
                    re_elapsed = 0
                    while re_elapsed < gemini_timeout:
                        re_reviews = _run(
                            ["gh", "api", f"repos/{owner}/{repo_name}/pulls/{pr_number}/reviews"],
                            cwd=wt_path,
                            check=False,
                        )
                        if re_reviews.returncode == 0 and "gemini-code-assist" in re_reviews.stdout.lower():
                            break
                        time.sleep(30)
                        re_elapsed += 30

            # MEDIUM 분류 + 로깅
            medium_classified = _classify_medium_comments(parsed, collect_mode=collect_mode)
            if medium_classified:
                _log_medium_comments(medium_classified)

        # 6. Merge if no unresolved High issues; block on timeout or HIGH
        merge_status = "pending"
        if not gemini_found:
            gemini_verdict = "TIMEOUT"
        elif high_severity_count == 0:
            gemini_verdict = "PASS"
        else:
            gemini_verdict = "BLOCKED"

        if not gemini_found:
            # 타임아웃 시 머지 차단 — 수동 확인 필요
            merge_status = "blocked_by_timeout"
            logger.warning("Gemini review timed out for PR #%s. Merge blocked. Manual review required.", pr_number)
        elif high_severity_count == 0:
            merge_result = _run(
                ["gh", "pr", "merge", pr_number, "--merge", "--delete-branch"],
                cwd=wt_path,
                check=False,
            )
            if merge_result.returncode == 0:
                merge_status = "merged"
                # Clean up worktree
                _run(["git", "checkout", main_branch], cwd=project_path, check=False)
                _run(["git", "pull"], cwd=project_path, check=False)
                _run(["git", "worktree", "remove", "--force", wt_path], cwd=project_path, check=False)
            else:
                merge_status = "merge_failed"
        else:
            merge_status = "blocked_by_high_severity"

        result: dict[str, Any] = {
            "status": merge_status,
            "branch": branch,
            "pr_url": pr_url,
            "pr_number": pr_number,
            "gemini_reviewed": gemini_found,
            "gemini_timeout": not gemini_found,
            "gemini_verdict": gemini_verdict,
            "high_severity_count": high_severity_count,
            "review_summary": review_summary,
            "auto_fix_attempts": auto_fix_attempts,
            "medium_classified": [
                {
                    "classification": c["classification"],
                    "path": c.get("path", ""),
                    "pattern_matched": c.get("pattern_matched", ""),
                }
                for c in medium_classified
            ] if medium_classified else [],
        }
        if rate_limit_result:
            result["rate_limit_warning"] = rate_limit_result.get("warning", "")
        return result

    raise ValueError(f"Unknown action: '{action}'. Use merge|discard|keep|pr.")


def cmd_list(project_path: str) -> dict[str, Any]:
    """활성 worktree 목록 반환.

    task/<task_id>-<team_id> 패턴 브랜치를 가진 worktree만 포함.

    Args:
        project_path: git 저장소 루트 경로.

    Returns:
        {"worktrees": [{"path": str, "branch": str, "task_id": str, "team_id": str}]}
    """
    raw = list_worktrees_raw(project_path)
    result: list[dict[str, str]] = []

    for wt in raw:
        branch_ref = wt.get("branch", "")
        # refs/heads/task/<task_id>-<team_id> 형태
        if not branch_ref.startswith("refs/heads/task/"):
            continue

        short_branch = branch_ref.removeprefix("refs/heads/")
        # "task/<task_id>-<team_id>" → task_id, team_id 분리
        # task/ 이후 첫 번째 '-' 기준으로 분리
        task_part = short_branch.removeprefix("task/")
        # task_id는 '.' 포함 가능 (예: 329.1), team_id는 '-' 뒤
        # 오른쪽에서 '-' 하나로 분리
        dash_idx = task_part.rfind("-")
        if dash_idx == -1:
            continue

        t_id = task_part[:dash_idx]
        tm_id = task_part[dash_idx + 1 :]

        result.append(
            {
                "path": wt.get("worktree", ""),
                "branch": short_branch,
                "task_id": t_id,
                "team_id": tm_id,
            }
        )

    return {"worktrees": result}


def cmd_cleanup(project_path: str) -> dict[str, Any]:
    """머지 완료된 워크트리를 자동 정리.

    main 브랜치에 이미 머지된 task/ 브랜치의 워크트리만 제거한다.
    머지되지 않은 워크트리는 건드리지 않는다 (안전장치).

    Args:
        project_path: git 저장소 루트 경로.

    Returns:
        {"cleaned": [...], "skipped": [...]}
    """
    listed = cmd_list(project_path)
    main_branch = detect_main_branch(project_path)

    # main에 머지된 브랜치 목록 조회
    try:
        merged_raw = _out(
            ["git", "branch", "--merged", main_branch, "--format=%(refname:short)"],
            cwd=project_path,
        )
        merged_branches = {b.strip() for b in merged_raw.splitlines() if b.strip()}
    except subprocess.CalledProcessError:
        merged_branches = set()

    cleaned: list[dict[str, str]] = []
    skipped: list[dict[str, str]] = []

    for wt in listed["worktrees"]:
        branch = wt["branch"]
        wt_path = wt["path"]
        task_id = wt["task_id"]
        team_id = wt["team_id"]

        if branch in merged_branches:
            # 머지 완료 → 워크트리 제거 + 브랜치 삭제
            _run(
                ["git", "worktree", "remove", "--force", wt_path],
                cwd=project_path,
            )
            _run(
                ["git", "branch", "-d", branch],
                cwd=project_path,
                check=False,
            )
            _run(
                ["git", "push", "origin", "--delete", branch],
                cwd=project_path,
                check=False,
            )
            cleaned.append(
                {
                    "path": wt_path,
                    "branch": branch,
                    "task_id": task_id,
                    "team_id": team_id,
                }
            )
        else:
            skipped.append(
                {
                    "path": wt_path,
                    "branch": branch,
                    "task_id": task_id,
                    "team_id": team_id,
                    "reason": "not_merged",
                }
            )

    return {"cleaned": cleaned, "skipped": skipped}


def cmd_status(project_path: str, task_id: str) -> dict[str, Any]:
    """특정 task_id에 해당하는 모든 worktree 상태 반환.

    Args:
        project_path: git 저장소 루트 경로.
        task_id: 태스크 식별자 (team_id 무관하게 검색).

    Returns:
        {"worktrees": [{"path": str, "branch": str, "changed_files": int, "commits_ahead": int}]}
    """
    listed = cmd_list(project_path)
    matching = [w for w in listed["worktrees"] if w["task_id"] == task_id]

    result: list[dict[str, Any]] = []
    main_branch = detect_main_branch(project_path)

    for wt in matching:
        wt_path = wt["path"]
        branch = wt["branch"]

        # 변경 파일 수 (staged + unstaged + untracked)
        try:
            status_out = _out(
                ["git", "status", "--porcelain"],
                cwd=wt_path,
            )
            changed_files = len([line for line in status_out.splitlines() if line.strip()])
        except subprocess.CalledProcessError:
            changed_files = 0

        # 메인 브랜치 대비 앞선 커밋 수
        try:
            ahead_out = _out(
                [
                    "git",
                    "rev-list",
                    "--count",
                    f"{main_branch}..{branch}",
                ],
                cwd=wt_path,
            )
            commits_ahead = int(ahead_out) if ahead_out.isdigit() else 0
        except subprocess.CalledProcessError:
            commits_ahead = 0

        result.append(
            {
                "path": wt_path,
                "branch": branch,
                "changed_files": changed_files,
                "commits_ahead": commits_ahead,
            }
        )

    return {"worktrees": result}


# ---------------------------------------------------------------------------
# CLI 진입점
# ---------------------------------------------------------------------------


def build_parser() -> argparse.ArgumentParser:
    """argparse 파서 구성."""
    parser = argparse.ArgumentParser(
        prog="worktree_manager.py",
        description="Git Worktree 관리 스크립트 (멀티봇 환경용)",
    )
    sub = parser.add_subparsers(dest="command", required=True)

    # create
    p_create = sub.add_parser("create", help="새 worktree 생성")
    p_create.add_argument("project_path", help="git 저장소 루트 경로")
    p_create.add_argument("task_id", help="태스크 ID (예: 329.1)")
    p_create.add_argument("team_id", help="팀 ID (예: dev1)")

    # finish
    p_finish = sub.add_parser("finish", help="worktree 작업 완료")
    p_finish.add_argument("project_path", help="git 저장소 루트 경로")
    p_finish.add_argument("task_id", help="태스크 ID")
    p_finish.add_argument("team_id", help="팀 ID")
    p_finish.add_argument(
        "--action",
        required=True,
        choices=["merge", "discard", "keep", "pr", "auto"],
        help="완료 액션: merge|discard|keep|pr|auto",
    )
    p_finish.add_argument("--pr-title", default="", help="PR 제목")
    p_finish.add_argument("--pr-body", default="", help="PR 본문")
    p_finish.add_argument("--gemini-timeout", type=int, default=300, help="Gemini 리뷰 대기 시간(초)")

    # cleanup
    p_cleanup = sub.add_parser("cleanup", help="머지 완료된 worktree 자동 정리")
    p_cleanup.add_argument("project_path", help="git 저장소 루트 경로")

    # list
    p_list = sub.add_parser("list", help="활성 worktree 목록")
    p_list.add_argument("project_path", help="git 저장소 루트 경로")

    # status
    p_status = sub.add_parser("status", help="worktree 상태 확인")
    p_status.add_argument("project_path", help="git 저장소 루트 경로")
    p_status.add_argument("task_id", help="태스크 ID")

    return parser


def main() -> None:
    """CLI 메인 진입점."""
    parser = build_parser()
    args = parser.parse_args()

    try:
        result: dict[str, Any]

        if args.command == "create":
            result = cmd_create(args.project_path, args.task_id, args.team_id)
        elif args.command == "finish":
            result = cmd_finish(
                args.project_path,
                args.task_id,
                args.team_id,
                args.action,
                pr_title=getattr(args, "pr_title", ""),
                pr_body=getattr(args, "pr_body", ""),
                gemini_timeout=getattr(args, "gemini_timeout", 300),
            )
        elif args.command == "cleanup":
            result = cmd_cleanup(args.project_path)
        elif args.command == "list":
            result = cmd_list(args.project_path)
        elif args.command == "status":
            result = cmd_status(args.project_path, args.task_id)
        else:
            result = {"status": "error", "message": f"Unknown command: {args.command}"}

    except (subprocess.CalledProcessError, RuntimeError, ValueError, OSError) as exc:
        result = {"status": "error", "message": str(exc)}

    print(json.dumps(result, ensure_ascii=False, indent=2))

    if result.get("status") == "error":
        sys.exit(1)


if __name__ == "__main__":
    main()
