"""
Scanner — AST ile otomatik call graph builder.

Projeyi tarar, kim kimi çağırıyor otomatik tespit eder.
Registry'deki called_by listelerini günceller.
Manuel beyana gerek kalmaz.
"""

import ast
from pathlib import Path

try:
    from ..core.registry import Registry
except ImportError:
    from registry import Registry


class CallGraphScanner:
    def __init__(self, registry: Registry):
        self.registry = registry
        self.known_funcs: set = set(registry.functions.keys())

    def scan(self, project_root: str = ".") -> dict:
        """
        Tüm .py dosyalarını AST ile tara.
        Her fonksiyonun kimi çağırdığını tespit et.
        Registry'deki called_by listelerini güncelle.

        Döner: {"updated": int, "calls_found": int}
        """
        self.known_funcs = set(self.registry.functions.keys())
        call_map: dict[str, list[str]] = {name: [] for name in self.known_funcs}
        total_calls = 0

        for path in Path(project_root).rglob("*.py"):
            if any(
                p.startswith((".", "__pycache__", "venv", "env", "node_modules"))
                for p in path.parts
            ):
                continue
            try:
                calls = self._scan_file(str(path))
            except Exception:
                continue
            for caller, callees in calls.items():
                for callee in callees:
                    if callee in call_map:
                        location = f"{path.name}:{caller}"
                        if location not in call_map[callee]:
                            call_map[callee].append(location)
                            total_calls += 1

        updated = 0
        for func_name, callers in call_map.items():
            contract = self.registry.get(func_name)
            if contract and callers:
                contract.called_by = callers
                if len(callers) >= 5 and contract.risk == "low":
                    contract.risk = "medium"
                if len(callers) >= 10:
                    contract.risk = "high"
                updated += 1

        self.registry.save()
        return {"updated": updated, "calls_found": total_calls}

    def _scan_file(self, filepath: str) -> dict[str, list[str]]:
        """Bir dosyadaki tüm fonksiyonları tarar."""
        try:
            source = Path(filepath).read_text(encoding="utf-8")
            tree = ast.parse(source)
        except Exception:
            return {}

        result = {}

        for node in ast.walk(tree):
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            caller_name = node.name
            callees = self._extract_calls(node)
            if callees:
                result[caller_name] = callees

        module_calls = []
        for node in tree.body:
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            for child in ast.walk(node):
                if isinstance(child, ast.Call):
                    name = self._call_name(child)
                    if name and name in self.known_funcs:
                        module_calls.append(name)
        if module_calls:
            result["__module__"] = list(set(module_calls))

        return result

    def _extract_calls(self, func_node) -> list[str]:
        """Fonksiyon body'sinden tüm çağrıları çıkar."""
        calls = []
        for node in ast.walk(func_node):
            if not isinstance(node, ast.Call):
                continue
            name = self._call_name(node)
            if name and name in self.known_funcs:
                calls.append(name)
        return list(set(calls))

    def _call_name(self, call_node: ast.Call) -> str:
        """Call node'undan fonksiyon ismini çıkar."""
        func = call_node.func
        if isinstance(func, ast.Name):
            return func.id
        if isinstance(func, ast.Attribute):
            return func.attr
        return ""

    def find_indirect_calls(self, project_root: str = ".") -> dict:
        """
        Dynamic dispatch tespiti — string bazlı.
        Kaynak kodda fonksiyon ismi geçiyor mu kontrol eder.
        Direkt call değil ama referans var → 'indirect' olarak ekler.
        """
        import re as regex_module

        indirect_map = {name: [] for name in self.known_funcs}

        for path in Path(project_root).rglob("*.py"):
            if any(
                p.startswith((".", "__pycache__", "venv", "env", "node_modules"))
                for p in path.parts
            ):
                continue
            try:
                source = path.read_text(encoding="utf-8")
            except Exception:
                continue

            for name in self.known_funcs:
                if regex_module.search(rf"\b{name}\b", source):
                    location = f"{path.name}:indirect"
                    if location not in indirect_map[name]:
                        indirect_map[name].append(location)

        added = 0
        for func_name, locations in indirect_map.items():
            if not locations:
                continue
            contract = self.registry.get(func_name)
            if contract:
                existing = set(contract.called_by)
                for loc in locations:
                    if loc not in existing:
                        contract.called_by.append(loc)
                        added += 1

        if added:
            self.registry.save()

        return {
            "indirect_calls": sum(1 for item in indirect_map.values() if item),
            "updated": added,
        }

    def impact_analysis(self, func_name: str, depth: int = 3) -> dict:
        """Change impact analizi."""
        visited = set()
        layers = []
        current_layer = {func_name}

        for d in range(depth):
            next_layer = set()
            for name in current_layer:
                if name in visited:
                    continue
                visited.add(name)
                contract = self.registry.get(name)
                if not contract:
                    continue
                for caller_loc in contract.called_by:
                    caller_name = (
                        caller_loc.split(":")[-1] if ":" in caller_loc else caller_loc
                    )
                    if caller_name not in visited:
                        next_layer.add(caller_name)
            if next_layer:
                layers.append(list(next_layer))
            current_layer = next_layer

        total = sum(len(layer) for layer in layers)
        risk = "low" if total < 3 else ("medium" if total < 8 else "high")

        return {
            "func": func_name,
            "direct": layers[0] if layers else [],
            "indirect": layers[1:],
            "total_impact": total,
            "risk": risk,
        }

    def build_report(self, project_root: str = ".") -> str:
        """AI'ın okuyacağı call graph özeti."""
        self.scan(project_root)
        lines = ["## Call Graph Özeti\n"]

        high_impact = []
        for name, contract in self.registry.functions.items():
            count = len(contract.called_by)
            if count > 0:
                high_impact.append((name, count, contract.file))

        high_impact.sort(key=lambda x: x[1], reverse=True)

        if high_impact:
            lines.append("### En çok çağrılan fonksiyonlar:")
            for name, count, filepath in high_impact[:10]:
                impact = self.impact_analysis(name, depth=2)
                lines.append(
                    f"- `{name}()` — {count} caller, etki: {impact['total_impact']}, risk: {impact['risk']}"
                )
            lines.append("")

        orphans = [
            name for name, c in self.registry.functions.items() if not c.called_by
        ]
        if orphans:
            lines.append(f"### Hiç çağrılmayan fonksiyonlar ({len(orphans)} adet):")
            for name in orphans[:10]:
                lines.append(f"- `{name}()`")

        return "\n".join(lines)


def scan_project(project_root: str = ".") -> dict:
    """Modül seviyesi kolay kullanım."""
    registry = Registry(project_root)
    scanner = CallGraphScanner(registry)
    result = scanner.scan(project_root)
    indirect = scanner.find_indirect_calls(project_root)
    result.update(indirect)
    return result
