#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Generates a structural project map (state/project_map.json) via AST parsing.

As prescribed by QR8 Panel 6. Maps packages, files, public functions,
and dependencies for agent spatial awareness.
"""
import ast
import json
import time
from pathlib import Path
from typing import Dict, Any

SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent.parent
PULSE_DIR = ROOT_DIR / "pulse"
STATE_DIR = ROOT_DIR / "state"
MAP_FILE = STATE_DIR / "project_map.json"

def get_public_functions(filepath: Path) -> list[str]:
    funcs = []
    try:
        content = filepath.read_text(encoding="utf-8")
        tree = ast.parse(content, filename=str(filepath))
        for node in ast.walk(tree):
            if isinstance(node, ast.FunctionDef) and not node.name.startswith("_"):
                funcs.append(node.name)
            elif isinstance(node, ast.ClassDef) and not node.name.startswith("_"):
                funcs.append(node.name)
    except Exception:
        pass
    return funcs


def extract_purpose(filepath: Path) -> str:
    try:
        content = filepath.read_text(encoding="utf-8")
        tree = ast.parse(content, filename=str(filepath))
        doc = ast.get_docstring(tree)
        if doc:
            return doc.split("\n")[0][:150]
    except Exception:
        pass
    return ""


def extract_imports(filepath: Path) -> list[str]:
    """Extract pulse.* imports from a file via AST."""
    imports = []
    try:
        content = filepath.read_text(encoding="utf-8")
        tree = ast.parse(content, filename=str(filepath))
        for node in ast.walk(tree):
            if isinstance(node, ast.ImportFrom) and node.module:
                if node.module.startswith("pulse."):
                    imports.append(node.module)
            elif isinstance(node, ast.Import):
                for alias in node.names:
                    if alias.name.startswith("pulse."):
                        imports.append(alias.name)
    except Exception:
        pass
    return sorted(set(imports))


def has_main_block(filepath: Path) -> bool:
    """Check if file has if __name__ == '__main__' block."""
    try:
        content = filepath.read_text(encoding="utf-8")
        return '__name__' in content and '__main__' in content
    except Exception:
        return False


def build_map() -> None:
    py_files = [f for f in PULSE_DIR.rglob("*.py") if "__pycache__" not in str(f)]

    total_lines = 0
    packages: Dict[str, Any] = {}
    # Track reverse dependencies: module_path -> list of importers
    imported_by: Dict[str, list] = {}

    for f in py_files:
        try:
            lines = len(f.read_text(encoding="utf-8").splitlines())
            total_lines += lines
        except Exception:
            lines = 0

        rel_path = f.relative_to(ROOT_DIR).as_posix()
        pkg_name = f.parent.relative_to(ROOT_DIR).as_posix()

        if pkg_name not in packages:
            packages[pkg_name] = {
                "purpose": extract_purpose(f.parent / "__init__.py") if (f.parent / "__init__.py").exists() else "",
                "file_count": 0,
                "subpackages": set(),
                "files": {}
            }

        imports = extract_imports(f)
        is_entry = has_main_block(f)

        packages[pkg_name]["file_count"] += 1
        packages[pkg_name]["files"][f.name] = {
            "purpose": extract_purpose(f),
            "lines": lines,
            "public_api": get_public_functions(f),
            "imports_from": imports,
            "is_entry_point": is_entry,
        }

        # Build reverse dependency map
        for imp in imports:
            imported_by.setdefault(imp, []).append(rel_path)

        # Track subpackages
        parts = pkg_name.split("/")
        if len(parts) > 1:
            parent_pkg = "/".join(parts[:-1])
            if parent_pkg in packages:
                packages[parent_pkg]["subpackages"].add(parts[-1])

    # Inject reverse dependencies (imported_by) into each file
    for pkg_name, pkg_data in packages.items():
        for filename, file_data in pkg_data["files"].items():
            # Convert file path to module path for lookup
            rel = f"{pkg_name}/{filename}".replace("/", ".").replace(".py", "")
            dependents = imported_by.get(rel, [])
            if dependents:
                file_data["imported_by"] = dependents[:20]

    # Clean up sets for JSON serialization
    for p in packages.values():
        p["subpackages"] = sorted(list(p["subpackages"]))

    # Identify landmarks (files imported by 10+ others)
    landmarks = []
    for module, importers in imported_by.items():
        if len(importers) >= 10:
            landmarks.append({"module": module, "importer_count": len(importers)})
    landmarks.sort(key=lambda x: x["importer_count"], reverse=True)

    project_map = {
        "version": "2.0",
        "generated": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "file_count": len(py_files),
        "line_count": total_lines,
        "landmarks": landmarks[:20],
        "packages": packages,
    }

    MAP_FILE.parent.mkdir(parents=True, exist_ok=True)
    MAP_FILE.write_text(json.dumps(project_map, indent=2), encoding="utf-8")
    print(f"[PULSE] Project map v2 built: {MAP_FILE} ({len(py_files)} files, {total_lines} lines, {len(landmarks)} landmarks)")

if __name__ == "__main__":
    build_map()
