"""
impact_analyzer.py - 변경된 파일 목록을 입력받아 영향받는 함수/endpoint/모듈을 추출하는 정적 분석 도구.

사용법:
    python3 impact_analyzer.py --files "file1.py,file2.py" --output impact.json [--workspace /path/to/search]

제약:
    - LLM 호출 없음 (AST + grep + 정규식만 사용)
    - 실행 시간 30초 이내
    - Python 파일만 대상 (.py)
    - 존재하지 않는 파일은 에러 없이 무시
    - 빈 파일 목록은 빈 결과 반환
"""

from __future__ import annotations

import argparse
import ast
import json
import os
import re
import subprocess
from pathlib import Path

# ---------------------------------------------------------------------------
# 1. extract_functions
# ---------------------------------------------------------------------------


def extract_functions(file_path: str) -> list[str]:
    """
    Python AST로 함수/클래스 정의를 파싱하여 이름 목록 반환.

    - ast.FunctionDef, ast.AsyncFunctionDef → 함수 이름
    - ast.ClassDef → 클래스 이름 + 클래스명.메서드명
    - 중첩 함수/메서드도 포함 (클래스명.메서드명 형태)
    """
    if not os.path.isfile(file_path):
        return []

    try:
        source = Path(file_path).read_text(encoding="utf-8")
        tree = ast.parse(source, filename=file_path)
    except (SyntaxError, OSError, UnicodeDecodeError):
        return []

    results: list[str] = []

    for node in ast.walk(tree):
        if isinstance(node, ast.ClassDef):
            results.append(node.name)
            # 클래스 직속 메서드만 추출 (클래스명.메서드명)
            for item in node.body:
                if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    results.append(f"{node.name}.{item.name}")

    # 최상위 함수 (클래스 바깥)
    for node in ast.iter_child_nodes(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            results.append(node.name)

    return results


# ---------------------------------------------------------------------------
# 2. extract_imports
# ---------------------------------------------------------------------------


def extract_imports(file_path: str) -> list[str]:
    """
    ast.Import, ast.ImportFrom 노드로 import 목록 추출.
    모듈 이름만 반환 (from X import Y → X 반환).
    """
    if not os.path.isfile(file_path):
        return []

    try:
        source = Path(file_path).read_text(encoding="utf-8")
        tree = ast.parse(source, filename=file_path)
    except (SyntaxError, OSError, UnicodeDecodeError):
        return []

    results: list[str] = []

    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                # "import foo.bar" → "foo.bar", "import foo" → "foo"
                results.append(alias.name)
        elif isinstance(node, ast.ImportFrom):
            if node.module:
                results.append(node.module)

    return list(dict.fromkeys(results))  # 중복 제거, 순서 유지


# ---------------------------------------------------------------------------
# 3. extract_endpoints
# ---------------------------------------------------------------------------

# 지원하는 endpoint 패턴
_ENDPOINT_PATTERNS: list[re.Pattern[str]] = [
    # self.path == "/api/..."
    re.compile(r'self\.path\s*==\s*["\']([^"\']+)["\']'),
    # self.path.startswith("/api/...")
    re.compile(r'self\.path\.startswith\(["\']([^"\']+)["\']\)'),
    # Flask: @app.route("/path")
    re.compile(r'@\w+\.route\(["\']([^"\']+)["\']'),
    # FastAPI: @router.get("/path"), @router.post("/path"), etc.
    re.compile(r'@\w+\.(get|post|put|patch|delete|head|options)\(["\']([^"\']+)["\']'),
]


def extract_endpoints(file_path: str) -> list[str]:
    """
    파일에서 HTTP endpoint 경로 문자열 추출.

    지원 패턴:
    - self.path == "..."
    - self.path.startswith("...")
    - @app.route("...")
    - @router.get/post/put/patch/delete("...")
    """
    if not os.path.isfile(file_path):
        return []

    try:
        source = Path(file_path).read_text(encoding="utf-8")
    except (OSError, UnicodeDecodeError):
        return []

    results: list[str] = []

    for pattern in _ENDPOINT_PATTERNS:
        for match in pattern.finditer(source):
            # FastAPI 패턴은 group(2), 나머지는 group(1)
            if match.lastindex and match.lastindex >= 2:
                endpoint = match.group(2)
            else:
                endpoint = match.group(1)
            if endpoint not in results:
                results.append(endpoint)

    return results


# ---------------------------------------------------------------------------
# 4. find_reverse_imports
# ---------------------------------------------------------------------------


def find_reverse_imports(module_name: str, workspace: str) -> list[str]:
    """
    workspace 내 모든 .py 파일에서 module_name을 import하는 파일 목록 반환.

    검색 패턴:
    - from {module_name} import ...
    - import {module_name}
    """
    if not module_name or not os.path.isdir(workspace):
        return []

    # grep 패턴: "from module_name import" 또는 "import module_name"
    pattern = rf"(from\s+{re.escape(module_name)}\s+import|import\s+{re.escape(module_name)}(\s|$|,))"

    try:
        proc = subprocess.run(
            ["grep", "-rl", "--include=*.py", "-E", pattern, workspace],
            capture_output=True,
            text=True,
            timeout=10,
        )
        if proc.returncode not in (0, 1):  # 0=found, 1=not found, 2=error
            return []
        files = [line.strip() for line in proc.stdout.splitlines() if line.strip()]
        return files
    except (subprocess.TimeoutExpired, FileNotFoundError):
        return []


# ---------------------------------------------------------------------------
# 5. build_dependency_chain
# ---------------------------------------------------------------------------


def build_dependency_chain(changed_files: list[str], workspace: str) -> dict[str, list[str]]:
    """
    각 변경 파일에 대해 역방향 import를 추적하여 딕셔너리 반환.

    반환 형식: {파일경로: [이 파일을 import하는 파일들]}
    """
    chain: dict[str, list[str]] = {}

    for file_path in changed_files:
        # 모듈 이름 = 파일명에서 .py 제거
        module_name = Path(file_path).stem
        importers = find_reverse_imports(module_name, workspace)
        # 자기 자신은 제외
        importers = [p for p in importers if os.path.abspath(p) != os.path.abspath(file_path)]
        chain[file_path] = importers

    return chain


# ---------------------------------------------------------------------------
# 6. analyze
# ---------------------------------------------------------------------------


def analyze(files: list[str], workspace: str = ".") -> dict:
    """
    변경된 파일 목록을 받아 영향 분석 결과 딕셔너리 반환.

    반환 형식:
    {
        "changed_files": [...],
        "affected_functions": [...],
        "affected_endpoints": [...],
        "affected_imports": [...],
        "dependency_chain": {...}
    }
    """
    # 빈 목록 처리
    if not files:
        return {
            "changed_files": [],
            "affected_functions": [],
            "affected_endpoints": [],
            "affected_imports": [],
            "dependency_chain": {},
        }

    # 존재하는 .py 파일만 필터링
    valid_files = [f for f in files if f.endswith(".py") and os.path.isfile(f)]

    all_functions: list[str] = []
    all_endpoints: list[str] = []
    all_imports: list[str] = []

    for file_path in valid_files:
        funcs = extract_functions(file_path)
        for f in funcs:
            if f not in all_functions:
                all_functions.append(f)

        endpoints = extract_endpoints(file_path)
        for e in endpoints:
            if e not in all_endpoints:
                all_endpoints.append(e)

        imports = extract_imports(file_path)
        for i in imports:
            if i not in all_imports:
                all_imports.append(i)

    dependency_chain = build_dependency_chain(valid_files, workspace)

    return {
        "changed_files": files,
        "affected_functions": all_functions,
        "affected_endpoints": all_endpoints,
        "affected_imports": all_imports,
        "dependency_chain": dependency_chain,
    }


# ---------------------------------------------------------------------------
# 7. main (CLI 진입점)
# ---------------------------------------------------------------------------


def main() -> None:
    parser = argparse.ArgumentParser(description="정적 분석으로 변경된 파일의 영향 범위를 파악합니다.")
    parser.add_argument(
        "--files",
        required=True,
        help="분석할 파일 목록 (쉼표 구분) 또는 단일 파일 경로",
    )
    parser.add_argument(
        "--output",
        default="impact.json",
        help="결과를 저장할 JSON 파일 경로 (기본값: impact.json)",
    )
    parser.add_argument(
        "--workspace",
        default=".",
        help="역방향 import 탐색할 루트 디렉토리 (기본값: 현재 디렉토리)",
    )

    args = parser.parse_args()

    # 파일 목록 파싱: 쉼표 구분 또는 단일 경로
    raw = args.files.strip()
    if "," in raw:
        files = [f.strip() for f in raw.split(",") if f.strip()]
    else:
        files = [raw] if raw else []

    result = analyze(files, workspace=args.workspace)

    output_path = Path(args.output)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    with open(output_path, "w", encoding="utf-8") as f:
        json.dump(result, f, ensure_ascii=False, indent=2)

    print(f"Impact analysis written to: {output_path}")
    print(f"  Changed files    : {len(result['changed_files'])}")
    print(f"  Affected funcs   : {len(result['affected_functions'])}")
    print(f"  Affected endpoints: {len(result['affected_endpoints'])}")
    print(f"  Affected imports : {len(result['affected_imports'])}")


if __name__ == "__main__":
    main()
