#!/usr/bin/env python3
"""diff-aware-qa.py — Git diff 기반 QA 영향 범위 자동 식별 도구

Usage:
    python3 diff-aware-qa.py [--project-dir DIR] [--base-ref REF] [--output json|text]
"""

import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any


# ---------------------------------------------------------------------------
# 커스텀 예외
# ---------------------------------------------------------------------------
class GitError(Exception):
    """git 관련 오류"""


# ---------------------------------------------------------------------------
# git diff 실행 및 파싱
# ---------------------------------------------------------------------------
def get_changed_files(project_dir: str = ".", base_ref: str = "main") -> list[str]:
    """git diff <base_ref>...HEAD --name-only 를 실행하고 변경 파일 목록을 반환한다."""
    git_dir = Path(project_dir) / ".git"
    if not git_dir.exists():
        raise GitError(f"Not a git repository: {project_dir}")

    cmd = ["git", "-C", project_dir, "diff", f"{base_ref}...HEAD", "--name-only"]
    result = subprocess.run(cmd, capture_output=True, text=True)

    if result.returncode != 0:
        raise GitError(f"git diff failed: {result.stderr.strip()}")

    return parse_diff_output(result.stdout)


def parse_diff_output(raw: str) -> list[str]:
    """git diff --name-only 출력을 파싱하여 파일 경로 리스트로 반환한다."""
    return [line.strip() for line in raw.splitlines() if line.strip()]


# ---------------------------------------------------------------------------
# 파일 분류
# ---------------------------------------------------------------------------
def classify_files(files: list[str]) -> dict[str, list[str]]:
    """파일 목록을 backend / frontend / style / test / other 로 분류한다."""
    categories: dict[str, list[str]] = {
        "backend": [],
        "frontend": [],
        "style": [],
        "test": [],
        "other": [],
    }

    for filepath in files:
        path = Path(filepath)
        name = path.name
        suffix = path.suffix.lower()

        # 테스트 파일: test_ 접두사 또는 .test.* / .spec.* 패턴
        if name.startswith("test_") or re.search(r"\.(test|spec)\.", name):
            categories["test"].append(filepath)
        elif suffix == ".py":
            categories["backend"].append(filepath)
        elif suffix in (".ts", ".tsx", ".jsx", ".js"):
            categories["frontend"].append(filepath)
        elif suffix in (".css", ".scss", ".sass", ".less"):
            categories["style"].append(filepath)
        else:
            categories["other"].append(filepath)

    return categories


# ---------------------------------------------------------------------------
# 라우트 추출 (Python — Flask / FastAPI)
# ---------------------------------------------------------------------------
_ROUTE_PATTERN = re.compile(
    r"""@\w+(?:\.\w+)*\s*\(\s*['"]([^'"]+)['"]""",
)


def extract_routes_from_file(filepath: str) -> list[str]:
    """Python 파일에서 @app.route(), @router.get() 등 라우트 데코레이터를 파싱한다."""
    path = Path(filepath)
    if path.suffix != ".py":
        return []

    try:
        content = path.read_text(encoding="utf-8", errors="ignore")
    except OSError:
        return []

    routes: list[str] = []
    for match in _ROUTE_PATTERN.finditer(content):
        value = match.group(1)
        # 라우트는 / 로 시작
        if value.startswith("/"):
            routes.append(value)

    return routes


# ---------------------------------------------------------------------------
# 컴포넌트명 추론 (TypeScript / React)
# ---------------------------------------------------------------------------
_EXPORT_DEFAULT_PATTERN = re.compile(r"export\s+default\s+(?:function|class|const)\s+([A-Z][A-Za-z0-9_]*)")

_FRONTEND_SUFFIXES = {".ts", ".tsx", ".jsx", ".js"}


def extract_components_from_file(filepath: str) -> list[str]:
    """TS/TSX 파일에서 export default 컴포넌트명을 추출한다. 없으면 파일명을 사용한다."""
    path = Path(filepath)
    if path.suffix not in _FRONTEND_SUFFIXES:
        return []

    try:
        content = path.read_text(encoding="utf-8", errors="ignore")
    except OSError:
        return []

    components: list[str] = []
    for match in _EXPORT_DEFAULT_PATTERN.finditer(content):
        components.append(match.group(1))

    # export default 패턴이 없으면 파일명(확장자 제외)에서 컴포넌트명 추론
    if not components:
        stem = path.stem
        # PascalCase 이름이면 컴포넌트로 간주
        if stem and stem[0].isupper():
            components.append(stem)

    return components


# ---------------------------------------------------------------------------
# 요약 문자열 생성
# ---------------------------------------------------------------------------
def build_summary(categories: dict[str, list[str]]) -> str:
    """변경 분류 결과를 한 줄 요약 문자열로 만든다."""
    parts: list[str] = []
    label_map = {
        "backend": "백엔드 파일",
        "frontend": "프론트엔드 컴포넌트",
        "style": "스타일 파일",
        "test": "테스트 파일",
        "other": "기타 파일",
    }
    for key, label in label_map.items():
        count = len(categories.get(key, []))
        if count > 0:
            parts.append(f"{count}개 {label}")

    if not parts:
        return "변경된 파일 없음"

    return ", ".join(parts) + " 변경"


# ---------------------------------------------------------------------------
# 핵심 분석 함수
# ---------------------------------------------------------------------------
def analyze_changes(changed_files: list[str]) -> dict[str, Any]:
    """변경 파일 목록을 받아 영향 분석 결과를 반환한다."""
    categories = classify_files(changed_files)

    affected_routes: list[str] = []
    for filepath in categories["backend"]:
        affected_routes.extend(extract_routes_from_file(filepath))
    # 중복 제거, 순서 유지
    seen: set[str] = set()
    unique_routes: list[str] = []
    for r in affected_routes:
        if r not in seen:
            seen.add(r)
            unique_routes.append(r)

    affected_components: list[str] = []
    for filepath in categories["frontend"]:
        affected_components.extend(extract_components_from_file(filepath))
    seen_comps: set[str] = set()
    unique_components: list[str] = []
    for c in affected_components:
        if c not in seen_comps:
            seen_comps.add(c)
            unique_components.append(c)

    # QA 타겟: 라우트 + 컴포넌트 + 테스트 파일
    qa_targets: list[str] = []
    qa_targets.extend(unique_routes)
    qa_targets.extend(unique_components)
    for filepath in categories["test"]:
        qa_targets.append(f"테스트: {filepath}")

    summary = build_summary(categories)

    return {
        "changed_files": changed_files,
        "affected_routes": unique_routes,
        "affected_components": unique_components,
        "categories": categories,
        "qa_targets": qa_targets,
        "summary": summary,
    }


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="git diff 기반 QA 영향 범위 자동 식별 도구",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("--project-dir", default=".", help="git 프로젝트 디렉토리 (기본: 현재 디렉토리)")
    parser.add_argument("--base-ref", default="main", help="diff 기준 브랜치/커밋 (기본: main)")
    parser.add_argument("--output", choices=["json", "text"], default="json", help="출력 형식 (기본: json)")
    return parser


def main() -> None:
    parser = build_parser()
    args = parser.parse_args()

    try:
        changed_files = get_changed_files(project_dir=args.project_dir, base_ref=args.base_ref)
    except GitError as exc:
        print(f"Error: {exc}", file=sys.stderr)
        sys.exit(1)

    result = analyze_changes(changed_files)

    if args.output == "json":
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        print(f"변경 파일: {len(result['changed_files'])}개")
        print(f"영향받는 라우트: {', '.join(result['affected_routes']) or '없음'}")
        print(f"영향받는 컴포넌트: {', '.join(result['affected_components']) or '없음'}")
        print(f"요약: {result['summary']}")
        if result["qa_targets"]:
            print("QA 타겟:")
            for target in result["qa_targets"]:
                print(f"  - {target}")


if __name__ == "__main__":
    main()
