#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
Tool Dependency Graph: Visualize which tools import which other tools.

Scans all tool .py files, extracts import statements, and builds
a dependency tree. Detects circular dependencies.

Usage:
    python tool_dependency_graph.py              # Print ASCII tree
    python tool_dependency_graph.py --json       # Output as JSON
    python tool_dependency_graph.py --check      # Check for circular deps

Dependencies: Python stdlib only (Law 4)
"""

import ast
import json
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent


# Tools we track (only internal PULSE tools, not stdlib)
_INTERNAL_MODULES = set()


def _scan_tool_names():
    """Build set of internal tool module names."""
    for py_file in SCRIPT_DIR.glob("*.py"):
        if not py_file.name.startswith("__"):
            _INTERNAL_MODULES.add(py_file.stem)
    # Also add brain_utils from parent
    _INTERNAL_MODULES.add("brain_utils")
    _INTERNAL_MODULES.add("wants_parser")
    _INTERNAL_MODULES.add("pulse_crypto")


def _extract_imports(py_file: Path) -> list[str]:
    """Extract internal PULSE imports from a Python file."""
    imports = []
    try:
        source = py_file.read_text(encoding="utf-8", errors="replace")
        tree = ast.parse(source)

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    module = alias.name.split(".")[0]
                    if module in _INTERNAL_MODULES:
                        imports.append(module)
            elif isinstance(node, ast.ImportFrom):
                if node.module:
                    module = node.module.split(".")[0]
                    if module in _INTERNAL_MODULES:
                        imports.append(module)
    except (SyntaxError, Exception):
        pass

    return sorted(set(imports))


def build_graph() -> dict[str, list[str]]:
    """Build dependency graph: tool → [dependencies]."""
    _scan_tool_names()
    graph = {}
    for py_file in sorted(SCRIPT_DIR.glob("*.py")):
        if py_file.name.startswith("__"):
            continue
        deps = _extract_imports(py_file)
        # Remove self-dependency
        deps = [d for d in deps if d != py_file.stem]
        graph[py_file.stem] = deps
    return graph


def find_circular(graph: dict[str, list[str]]) -> list[list[str]]:
    """Find circular dependencies using DFS."""
    cycles = []
    visited = set()
    path = []
    path_set = set()

    def _dfs(node):
        if node in path_set:
            # Found cycle
            cycle_start = path.index(node)
            cycles.append(path[cycle_start:] + [node])
            return
        if node in visited:
            return

        visited.add(node)
        path.append(node)
        path_set.add(node)

        for dep in graph.get(node, []):
            if dep in graph:  # Only follow internal deps
                _dfs(dep)

        path.pop()
        path_set.discard(node)

    for node in graph:
        _dfs(node)

    return cycles


def _find_roots(graph: dict[str, list[str]]) -> list[str]:
    """Find tools that nothing depends on (entry points)."""
    all_deps = set()
    for deps in graph.values():
        all_deps.update(deps)
    return sorted(n for n in graph if n not in all_deps)


def format_tree(graph: dict[str, list[str]]) -> str:
    """Format dependency graph as ASCII tree."""
    lines = ["Tool Dependency Graph", "=" * 40, ""]

    roots = _find_roots(graph)
    if not roots:
        roots = sorted(graph.keys())[:5]

    printed = set()

    def _print_node(node, prefix="", is_last=True):
        connector = "`--" if is_last else "|--"
        dep_count = len(graph.get(node, []))
        suffix = f" ({dep_count} deps)" if dep_count else ""
        lines.append(f"{prefix}{connector}{node}{suffix}")

        if node in printed:
            if dep_count:
                lines.append(f"{prefix}{'    ' if is_last else '|'}`--(see above)")
            return
        printed.add(node)

        deps = graph.get(node, [])
        child_prefix = prefix + ("    " if is_last else "|")
        for i, dep in enumerate(deps):
            _print_node(dep, child_prefix, i == len(deps) - 1)

    for i, root in enumerate(roots):
        if i > 0:
            lines.append("")
        lines.append(f"{root}")
        deps = graph.get(root, [])
        printed.add(root)
        for j, dep in enumerate(deps):
            _print_node(dep, "", j == len(deps) - 1)

    # Summary
    total_tools = len(graph)
    total_deps = sum(len(d) for d in graph.values())
    no_deps = sum(1 for d in graph.values() if not d)
    lines.append("")
    lines.append(f"Summary: {total_tools} tools, {total_deps} dependencies, {no_deps} standalone")

    return "\n".join(lines)


if __name__ == "__main__":
    graph = build_graph()

    if "--json" in sys.argv:
        print(json.dumps(graph, indent=2))
    elif "--check" in sys.argv:
        cycles = find_circular(graph)
        if cycles:
            print(f"CIRCULAR DEPENDENCIES FOUND ({len(cycles)}):")
            for cycle in cycles:
                print(f"  {'→'.join(cycle)}")
            sys.exit(1)
        else:
            print("No circular dependencies found.")
    else:
        print(format_tree(graph))
