#!/usr/bin/env python3
"""외부 AI API 를 부르는 자리를 AST 로 전수 조사하고, 그 자리가 Opik 에
남는지 판정한다.

「있는 것을 센 것」과 「필요한 자리에 있는지 본 것」은 다르다 —
호출 지점마다 그 함수 안에 기록 호출이 있는지를 본다.
"""
import ast
import sys
from pathlib import Path

ROOT = Path("/Users/manta/Documents/Projects/TheRoad-I1/backend")

# 외부로 나가는 호출로 보는 것
HTTP_ATTRS = {"post", "get", "request", "stream"}
HTTP_MODS = {"requests", "httpx", "aiohttp", "urllib"}
# Opik 에 남기는 통로
LOG_NAMES = {"record_provider_call", "log_llm_call", "completion",
             "acompletion", "ImageTracer", "log"}


class Visitor(ast.NodeVisitor):
    def __init__(self):
        self.funcs = []          # (func_name, lineno, http_calls, log_calls)
        self._stack = []

    def _enter(self, node):
        self._stack.append({"name": node.name, "line": node.lineno,
                            "http": [], "log": []})
        self.generic_visit(node)
        f = self._stack.pop()
        if f["http"]:
            self.funcs.append(f)

    visit_FunctionDef = _enter
    visit_AsyncFunctionDef = _enter

    def visit_Call(self, node):
        if self._stack:
            f = self._stack[-1]
            src = ast.unparse(node.func)
            # 이 저장소가 밖으로 나가는 통로는 둘뿐이다 (실측)
            if src.endswith("urlopen") or src.endswith("litellm.completion") \
                    or src.endswith("litellm.acompletion"):
                kind = "litellm" if "litellm" in src else "urlopen"
                f["http"].append((kind, node.lineno))
            fn = node.func
            name = (fn.attr if isinstance(fn, ast.Attribute)
                    else getattr(fn, "id", ""))
            if name in LOG_NAMES:
                f["log"].append((name, node.lineno))
        self.generic_visit(node)


def scan(path: Path):
    try:
        tree = ast.parse(path.read_text(encoding="utf-8"))
    except SyntaxError:
        return []
    v = Visitor()
    v.visit(tree)
    return v.funcs


targets = sorted(p for p in ROOT.joinpath("app").rglob("*.py"))
rows = []
for p in targets:
    src = p.read_text(encoding="utf-8", errors="ignore")
    # AI 엔드포인트를 실제로 가리키는 파일만
    if "urlopen" not in src and "litellm.completion" not in src and "litellm.acompletion" not in src:
        continue
    file_has_log = "record_provider_call" in src
    for f in scan(p):
        rows.append((p.relative_to(ROOT), f["name"], f["line"],
                     len(f["http"]), len(f["log"]), file_has_log))

print(f"{'파일':<50} {'함수':<34} {'줄':>5} {'HTTP':>5} {'기록':>5} {'파일내기록'}")
print("-" * 118)
missing = []
for rel, fn, line, nh, nl, fhl in rows:
    mark = "" if (nl or fhl) else "  ← 기록 없음"
    if not (nl or fhl):
        missing.append((str(rel), fn, line))
    print(f"{str(rel):<50} {fn:<34} {line:>5} {nh:>5} {nl:>5} "
          f"{'O' if fhl else 'X'}{mark}")

print(f"\n호출 함수 {len(rows)}개 · 기록 없는 함수 {len(missing)}개")
for r, fn, line in missing:
    print(f"  ✗ {r}:{line}  {fn}")
