#!/usr/bin/env python3
"""
Tasteby 페르소나 기반 코드 리뷰
4개 페르소나(프론트엔드, 백엔드, 보안, 아키텍처)가 git diff를 리뷰하고 최종 권고를 출력.

사용법:
  # 커밋되지 않은 변경 리뷰
  python3 scripts/code_review.py

  # 최근 N개 커밋 리뷰
  python3 scripts/code_review.py --commits 3

  # 특정 영역만 리뷰 (frontend / backend / all)
  python3 scripts/code_review.py --scope frontend
  python3 scripts/code_review.py --scope backend

  # 특정 커밋 범위 리뷰
  python3 scripts/code_review.py --range abc1234..def5678
"""
import subprocess, json, os, sys, argparse
from pathlib import Path

# .env 파일에서 환경변수 로드
def load_dotenv():
    env_path = Path(__file__).resolve().parent.parent / ".env"
    if env_path.exists():
        with open(env_path) as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                if "=" in line:
                    key, _, val = line.partition("=")
                    os.environ.setdefault(key.strip(), val.strip())

load_dotenv()

OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")
MODEL = os.environ.get("REVIEW_MODEL", "anthropic/claude-haiku-4-5")
API_URL = "https://openrouter.ai/api/v1/chat/completions"

# --- 페르소나 정의 ---

PERSONAS = {
    "frontend": [
        {
            "name": "프론트엔드 UX 전문가",
            "emoji": "\U0001f3a8",
            "prompt": """너는 Next.js/React 전문 프론트엔드 엔지니어야.
이 코드 변경에서 UX, 성능, 접근성 문제를 찾아내.
특히:
- 불필요한 리렌더링이나 useEffect 남용은 없는지
- 로딩/에러 상태 처리가 빠져있는지
- 모바일 반응형이 깨질 수 있는 레이아웃 변경은 없는지
- Tailwind CSS 클래스 사용이 적절한지
- 네이버 지도 API 관련 변경이 올바른지
한국어로 3줄 이내로 핵심만 답변."""
        },
        {
            "name": "타입스크립트 품질 검사관",
            "emoji": "\U0001f9d0",
            "prompt": """너는 TypeScript 타입 안전성과 코드 품질만 보는 엄격한 리뷰어야.
이 코드 변경에서 타입 안전성과 코드 품질 문제를 찾아내.
특히:
- any 타입 사용이나 타입 단언(as) 남용은 없는지
- API 응답 타입이 제대로 정의되어 있는지
- null/undefined 처리가 빠져있는지
- 컴포넌트 props 인터페이스가 명확한지
한국어로 3줄 이내로 핵심만 답변."""
        },
    ],
    "backend": [
        {
            "name": "백엔드 API 설계자",
            "emoji": "\u2699\ufe0f",
            "prompt": """너는 Spring Boot + MyBatis 전문 백엔드 엔지니어야.
이 코드 변경에서 API 설계, 데이터 접근, 성능 문제를 찾아내.
특히:
- MyBatis SQL에 N+1 쿼리나 비효율적인 조인은 없는지
- Oracle DB 특성(인덱스, CLOB 처리, ROWNUM)을 고려했는지
- REST API 설계가 일관적인지 (네이밍, HTTP 메서드, 상태 코드)
- Redis 캐싱 전략이 적절한지 (TTL, 키 설계, 무효화)
- ClobTypeHandler와 @JsonRawValue 사용이 올바른지
한국어로 3줄 이내로 핵심만 답변."""
        },
        {
            "name": "보안 감사관",
            "emoji": "\U0001f6e1\ufe0f",
            "prompt": """너는 OWASP Top 10을 기준으로 보안 취약점만 찾는 보안 전문가야.
이 코드 변경에서 보안 위험 요소를 찾아내.
특히:
- SQL Injection 가능성 (MyBatis ${} 사용 여부)
- XSS 취약점 (사용자 입력이 그대로 렌더링되는지)
- 민감 정보 노출 (API 키, DB 비밀번호, 에러 스택트레이스)
- CORS 설정이 너무 개방적이지 않은지
- JWT/인증 토큰 처리가 안전한지
- 입력값 검증이 빠져있는지
한국어로 3줄 이내로 핵심만 답변."""
        },
    ],
    "common": [
        {
            "name": "아키텍처 리뷰어",
            "emoji": "\U0001f3d7\ufe0f",
            "prompt": """너는 전체 시스템 아키텍처를 보는 테크 리드야.
이 코드 변경이 전체 아키텍처에 미치는 영향을 평가해.
특히:
- 프론트엔드-백엔드 간 API 계약이 맞는지
- 관심사 분리가 제대로 되어 있는지 (컨트롤러/서비스/매퍼)
- 기존 코드 패턴과 일관성이 있는지
- 확장성이나 유지보수성을 해치는 변경은 없는지
한국어로 3줄 이내로 핵심만 답변."""
        },
    ]
}

PROJECT_CONTEXT = """
[프로젝트 컨텍스트 — Tasteby]
- YouTube 맛집 영상에서 식당 정보를 추출하여 지도에 표시하는 서비스
- 스택: Spring Boot 3.3.5 + Java 21 + MyBatis + Oracle 23ai + Redis + Next.js 16 + TypeScript + Tailwind CSS
- 프론트엔드: Next.js App Router, TypeScript, Tailwind CSS, 네이버 지도 API
- 백엔드: Spring Boot REST API, MyBatis XML 매퍼, Oracle ADB, Redis 캐싱
- 주요 패턴: Controller(thin) -> Service(비즈니스 로직) -> Mapper(MyBatis XML)
- 도메인: Lombok @Data/@Builder, Jackson SNAKE_CASE, CLOB은 ClobTypeHandler
- UUID 생성: IdGenerator.newId() (32자 대문자 hex)
- 인증: JWT 토큰 (AuthUtil.requireAdmin())

[리뷰 시 참고]
- MyBatis에서 #{} 는 안전(PreparedStatement), ${} 는 SQL Injection 위험
- Oracle CLOB 필드는 ClobTypeHandler + @JsonRawValue 조합으로 처리
- Oracle 대문자 컬럼명 → JsonUtil.lowerKeys()로 변환
- resultType="map" 쿼리 시 서비스 레이어에서 JsonUtil.lowerKeys() 필수
- UserInfo.isAdmin은 @JsonProperty("is_admin") 필요 (JavaBeans boolean 네이밍)
- CORS 설정: WebConfig.java에서 관리, 새 HTTP 메서드 추가 시 allowedMethods 확인
"""


def get_diff(args):
    """변경사항 diff 가져오기"""
    if args.range:
        cmd = ["git", "diff", args.range]
    elif args.commits:
        cmd = ["git", "diff", f"HEAD~{args.commits}", "HEAD"]
    else:
        diff = subprocess.run(["git", "diff", "HEAD"], capture_output=True, text=True).stdout
        if not diff.strip():
            diff = subprocess.run(["git", "diff", "HEAD~1", "HEAD"], capture_output=True, text=True).stdout
        return diff

    return subprocess.run(cmd, capture_output=True, text=True).stdout


def filter_diff_by_scope(diff, scope):
    """scope에 따라 diff 필터링"""
    if scope == "all":
        return diff

    lines = diff.split("\n")
    filtered = []
    include = False

    for line in lines:
        if line.startswith("diff --git"):
            if scope == "frontend":
                include = "frontend/" in line
            elif scope == "backend":
                include = "backend-java/" in line
        if include:
            filtered.append(line)

    return "\n".join(filtered)


def get_changed_files(args):
    """변경된 파일 목록"""
    if args.range:
        cmd = ["git", "diff", "--name-only", args.range]
    elif args.commits:
        cmd = ["git", "diff", "--name-only", f"HEAD~{args.commits}", "HEAD"]
    else:
        result = subprocess.run(["git", "diff", "--name-only", "HEAD"], capture_output=True, text=True).stdout.strip()
        if not result:
            result = subprocess.run(["git", "diff", "--name-only", "HEAD~1", "HEAD"], capture_output=True, text=True).stdout.strip()
        return result

    return subprocess.run(cmd, capture_output=True, text=True).stdout.strip()


def call_llm(system_prompt, user_content):
    """OpenRouter API 호출"""
    import urllib.request

    body = json.dumps({
        "model": MODEL,
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_content}
        ],
        "max_tokens": 500,
    }).encode()

    req = urllib.request.Request(API_URL, data=body, headers={
        "Authorization": f"Bearer {OPENROUTER_API_KEY}",
        "Content-Type": "application/json",
    })

    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            data = json.loads(resp.read())
            return data["choices"][0]["message"]["content"]
    except Exception as e:
        return f"[API 호출 실패] {e}"


def select_personas(scope, files):
    """변경 파일에 따라 적절한 페르소나 선택"""
    personas = list(PERSONAS["common"])

    has_frontend = any(f.startswith("frontend/") for f in files.split("\n")) if files else False
    has_backend = any(f.startswith("backend-java/") for f in files.split("\n")) if files else False

    if scope == "frontend" or (scope == "all" and has_frontend):
        personas = PERSONAS["frontend"] + personas
    if scope == "backend" or (scope == "all" and has_backend):
        personas = PERSONAS["backend"] + personas
    if scope == "all" and not has_frontend and not has_backend:
        personas = PERSONAS["frontend"] + PERSONAS["backend"] + personas

    return personas


def main():
    parser = argparse.ArgumentParser(description="Tasteby 페르소나 기반 코드 리뷰")
    parser.add_argument("--commits", "-n", type=int, help="최근 N개 커밋 리뷰")
    parser.add_argument("--range", "-r", type=str, help="커밋 범위 (예: abc..def)")
    parser.add_argument("--scope", "-s", choices=["frontend", "backend", "all"], default="all",
                        help="리뷰 범위 (frontend/backend/all)")
    parser.add_argument("--model", "-m", type=str, help="사용할 모델 (기본: anthropic/claude-haiku-4-5)")
    args = parser.parse_args()

    if args.model:
        global MODEL
        MODEL = args.model

    if not OPENROUTER_API_KEY:
        print("ERROR: OPENROUTER_API_KEY 환경변수가 필요합니다")
        print("  .env 파일에 OPENROUTER_API_KEY=sk-or-... 추가하거나")
        print("  export OPENROUTER_API_KEY=sk-or-...")
        sys.exit(1)

    diff = get_diff(args)
    if not diff.strip():
        print("변경사항이 없습니다.")
        sys.exit(0)

    diff = filter_diff_by_scope(diff, args.scope)
    if not diff.strip():
        print(f"'{args.scope}' 범위에 해당하는 변경사항이 없습니다.")
        sys.exit(0)

    files = get_changed_files(args)
    personas = select_personas(args.scope, files)

    max_diff = 8000
    truncated = len(diff) > max_diff
    diff_for_review = diff[:max_diff]

    print("=" * 60)
    print(f"  Tasteby 코드 리뷰 ({args.scope})")
    print("=" * 60)
    print(f"\n  모델: {MODEL}")
    print(f"  변경 파일:\n")
    for f in files.split("\n"):
        if f.strip():
            if f.startswith("frontend/"):
                tag = "[FE]"
            elif f.startswith("backend-java/"):
                tag = "[BE]"
            else:
                tag = "[--]"
            print(f"    {tag} {f}")
    if truncated:
        print(f"\n  (diff가 {max_diff}자로 잘림, 원본 {len(diff)}자)")
    print()

    opinions = []
    for p in personas:
        print(f"  {p['emoji']} [{p['name']}] 리뷰 중...")
        user_msg = f"{PROJECT_CONTEXT}\n\n아래 코드 변경을 리뷰해줘:\n\n```diff\n{diff_for_review}\n```"
        result = call_llm(p["prompt"], user_msg)
        opinions.append({"name": p["name"], "emoji": p["emoji"], "opinion": result})
        print(f"  {result}\n")

    print("=" * 60)
    summary_prompt = """너는 프론트엔드/백엔드 리뷰어들의 의견을 종합하는 테크 리드야.
각 의견을 보고 최종 권고를 내려줘:
- PASS: 문제 없음, 배포 가능
- WARN: 사소한 이슈 있지만 배포 가능, 후속 작업 필요
- FAIL: 수정 필요, 배포 전 반드시 고쳐야 함
한국어로 최종 판정 1줄 + 이유/액션 아이템 2-3줄로 답변."""

    opinions_text = "\n\n".join(
        f"[{o['name']}]\n{o['opinion']}" for o in opinions
    )

    print(f"\n  [최종 권고]")
    final = call_llm(
        summary_prompt,
        f"{PROJECT_CONTEXT}\n\n변경사항 요약:\n{diff_for_review[:3000]}\n\n리뷰 의견:\n{opinions_text}"
    )
    print(f"  {final}")
    print("\n" + "=" * 60)


if __name__ == "__main__":
    main()
