"""
Interface analyzer for Gar

Python AST를 사용하여 모듈의 공개 인터페이스(함수/클래스 시그니처)를
가볍게 요약해 LLM 에이전트가 참고할 수 있게 함.

문법 오류가 있는 파일도 정규식 기반 fallback으로 최소한의 정보 추출
"""

from __future__ import annotations

import ast
import logging
import re
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)


@dataclass
class FunctionSignature:
    name: str
    args: list[str]
    returns: str | None
    lineno: int
    docstring: str | None


@dataclass
class ClassSignature:
    name: str
    methods: list[FunctionSignature]
    lineno: int
    docstring: str | None


@dataclass
class ModuleInterface:
    path: str
    functions: list[FunctionSignature]
    classes: list[ClassSignature]
    # 분석 상태 정보
    parse_error: str | None = None
    raw_source_preview: str | None = None  # 파싱 실패 시 원본 코드 일부
    is_partial: bool = False  # 부분 분석 여부


@dataclass
class InterfaceSummary:
    """인터페이스 분석 결과 전체."""

    modules: list[dict[str, Any]] = field(default_factory=list)
    parse_failures: list[dict[str, Any]] = field(default_factory=list)
    stats: dict[str, int] = field(default_factory=dict)


def _expr_to_str(expr: ast.expr | None) -> str | None:
    if expr is None:
        return None
    try:
        # Python 3.9+에서 ast.unparse 사용 가능
        if hasattr(ast, "unparse"):
            return ast.unparse(expr)
        # Python 3.8 이하에서는 간단한 문자열 변환 시도
        if isinstance(expr, ast.Name):
            return expr.id
        if isinstance(expr, ast.Constant):
            return str(expr.value)
        # 복잡한 타입은 None 반환 (타입힌트가 없음으로 간주)
        return None
    except Exception:
        return None


def _extract_raw_interface_hints(source: str) -> dict[str, Any]:
    """AST 없이 정규식/문자열 파싱으로 기본 인터페이스 정보를 추출한다.

    문법 오류가 있는 파일에서도 최소한의 정보를 얻기 위함.
    """
    hints: dict[str, Any] = {
        "functions": [],
        "classes": [],
        "imports": [],
    }

    # 함수 정의 패턴
    func_pattern = re.compile(
        r"^(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)\s*(?:->\s*([^:]+))?:",
        re.MULTILINE,
    )
    for match in func_pattern.finditer(source):
        name, args, returns = match.groups()
        if not name.startswith("_"):
            hints["functions"].append(
                {
                    "name": name,
                    "args": [a.strip().split(":")[0].split("=")[0].strip() 
                             for a in args.split(",") if a.strip()],
                    "args_raw": args.strip() if args else "",
                    "returns": returns.strip() if returns else None,
                    "lineno": source[: match.start()].count("\n") + 1,
                    "docstring": None,
                }
            )

    # 클래스 정의 패턴
    class_pattern = re.compile(r"^class\s+(\w+)\s*(?:\([^)]*\))?:", re.MULTILINE)
    for match in class_pattern.finditer(source):
        name = match.group(1)
        if not name.startswith("_"):
            hints["classes"].append(
                {
                    "name": name,
                    "lineno": source[: match.start()].count("\n") + 1,
                    "methods": [],
                    "docstring": None,
                }
            )

    return hints


def _analyze_source(path: Path, source: str) -> dict[str, Any]:
    try:
        tree = ast.parse(source, filename=str(path))
    except SyntaxError as e:
        raw_hints = _extract_raw_interface_hints(source)
        preview = source[:500] + ("..." if len(source) > 500 else "")
        return {
            "path": str(path),
            "functions": raw_hints["functions"],
            "classes": raw_hints["classes"],
            "parse_error": f"SyntaxError at line {e.lineno}: {e.msg}",
            "raw_source_preview": preview,
            "is_partial": True,
        }
    except Exception as e:
        raw_hints = _extract_raw_interface_hints(source)
        preview = source[:500] + ("..." if len(source) > 500 else "")
        return {
            "path": str(path),
            "functions": raw_hints["functions"],
            "classes": raw_hints["classes"],
            "parse_error": f"파싱 실패: {e}",
            "raw_source_preview": preview,
            "is_partial": True,
        }

    # 정상 AST 분석
    functions: list[FunctionSignature] = []
    classes: list[ClassSignature] = []

    for node in tree.body:
        if isinstance(node, ast.FunctionDef) and not node.name.startswith("_"):
            args = [arg.arg for arg in node.args.args]
            sig = FunctionSignature(
                name=node.name,
                args=args,
                returns=_expr_to_str(node.returns),
                lineno=node.lineno,
                docstring=ast.get_docstring(node),
            )
            functions.append(sig)
        elif isinstance(node, ast.AsyncFunctionDef) and not node.name.startswith("_"):
            # async 함수도 처리
            args = [arg.arg for arg in node.args.args]
            sig = FunctionSignature(
                name=node.name,
                args=args,
                returns=_expr_to_str(node.returns),
                lineno=node.lineno,
                docstring=ast.get_docstring(node),
            )
            functions.append(sig)
        elif isinstance(node, ast.ClassDef) and not node.name.startswith("_"):
            methods: list[FunctionSignature] = []
            for item in node.body:
                if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    # 공개 메서드 + __init__, __call__ 포함
                    if not item.name.startswith("_") or item.name in (
                        "__init__",
                        "__call__",
                    ):
                        args = [arg.arg for arg in item.args.args]
                        methods.append(
                            FunctionSignature(
                                name=item.name,
                                args=args,
                                returns=_expr_to_str(item.returns),
                                lineno=item.lineno,
                                docstring=ast.get_docstring(item),
                            )
                        )
            classes.append(
                ClassSignature(
                    name=node.name,
                    methods=methods,
                    lineno=node.lineno,
                    docstring=ast.get_docstring(node),
                )
            )

    module = ModuleInterface(
        path=str(path),
        functions=functions,
        classes=classes,
    )
    return asdict(module)


def analyze_interface(path: Path) -> dict[str, Any]:
    """단일 Python 파일의 공개 인터페이스를 요약한다."""
    try:
        source = path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        try:
            source = path.read_text(encoding="latin-1")
            logger.warning(f"UTF-8 디코딩 실패, latin-1로 읽음: {path}")
        except Exception as e:
            logger.error(f"파일 읽기 실패: {path} - {e}")
            return asdict(
                ModuleInterface(
                    path=str(path),
                    functions=[],
                    classes=[],
                    parse_error=f"파일 읽기 실패: {e}",
                    is_partial=True,
                )
            )
    except Exception as e:
        logger.error(f"파일 읽기 실패: {path} - {e}")
        return asdict(
            ModuleInterface(
                path=str(path),
                functions=[],
                classes=[],
                parse_error=f"파일 읽기 실패: {e}",
                is_partial=True,
            )
        )
    return _analyze_source(path, source)


def analyze_interface_from_source(path: Path, source: str) -> dict[str, Any]:
    """입력된 소스 문자열로 인터페이스를 요약한다."""
    return _analyze_source(path, source)


def build_interface_summary(files: list[Path], file_entries: list[dict[str, Any]] | None = None) -> dict[str, Any]:
    """여러 파일에 대한 인터페이스 요약을 구성한다.

    모든 파일을 분석하며, 파싱 실패한 파일도 결과에 포함한다.
    """
    modules: list[dict[str, Any]] = []
    parse_failures: list[dict[str, Any]] = []

    content_map = {entry.get("path"): entry.get("content", "") for entry in (file_entries or [])}

    for path in files:
        if path.suffix != ".py":
            logger.debug(f"Python 파일이 아님, 건너뜀: {path}")
            continue

        key = str(path)
        if key in content_map:
            module_summary = analyze_interface_from_source(path, content_map[key])
        else:
            module_summary = analyze_interface(path)

        if module_summary.get("parse_error"):
            # 파싱 실패한 파일 기록
            parse_failures.append(module_summary)
            # 부분 분석 결과가 있으면 모듈에도 추가 (함수/클래스 정보가 있는 경우)
            if module_summary.get("functions") or module_summary.get("classes"):
                modules.append(module_summary)
            elif module_summary.get("is_partial"):
                # 함수/클래스 정보는 없지만 파싱 오류 파일임을 알리기 위해 추가
                modules.append(module_summary)
        else:
            modules.append(module_summary)

    # 통계 정보
    stats = {
        "total_files": len(files),
        "analyzed_modules": len(modules),
        "parse_failures": len(parse_failures),
        "total_functions": sum(len(m.get("functions", [])) for m in modules),
        "total_classes": sum(len(m.get("classes", [])) for m in modules),
    }

    return {
        "modules": modules,
        "parse_failures": parse_failures,
        "stats": stats,
    }
