#!/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 logging
log = logging.getLogger("pulse.admin.brain_ops.project_map")

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 as _exc:
        pass  # QR14: silent — consider log.debug("%s", _exc)
    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 as _exc:
        pass  # QR14: silent — consider log.debug("%s", _exc)
    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 as _exc:
        pass  # QR14: silent — consider log.debug("%s", _exc)
    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()
