#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
Separation of Concerns Checker (Law 7 v2)

Goes beyond line counting. Detects files that violate SoC by having:
1. Too many public functions (>5 = warning, >8 = violation)
2. Too many cross-domain imports (>3 domains = warning, >5 = violation)
3. Too many lines (>300 = violation — original Law 7)

Usage:
    python soc_checker.py              # Check all Python files
    python soc_checker.py --strict     # Treat warnings as violations (CI mode)
    python soc_checker.py --file X.py  # Check a single file
"""
import ast
import sys
from pathlib import Path

SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent.parent

# --- Thresholds ---
MAX_LINES = 300
MAX_PUBLIC_FUNCS_WARN = 3
MAX_PUBLIC_FUNCS_FAIL = 5
MAX_DOMAIN_IMPORTS_WARN = 3
MAX_DOMAIN_IMPORTS_FAIL = 5

# Folders to skip
SKIP = {".git", "web", ".venv", "node_modules", "dist", "__pycache__", "tests", "state", "marketing", "Personal_Plans"}

from pulse.admin.audit.soc_grandfathered import GRANDFATHERED  # noqa: F401 (see soc_grandfathered.py)

# PULSE domain map — each pulse subpackage is a "domain"
PULSE_DOMAINS = {
    "pulse.core", "pulse.brain", "pulse.agents", "pulse.workers",
    "pulse.tasks", "pulse.daemons", "pulse.graph", "pulse.api",
    "pulse.admin", "pulse.cli", "pulse.config",
}


def _get_domain(module_name: str) -> str:
    """Map an import to its domain (pulse subpackage)."""
    for domain in PULSE_DOMAINS:
        if module_name.startswith(domain):
            return domain
    return ""


def _count_public_functions(tree: ast.AST) -> list[str]:
    """Return names of public (non-underscore) functions/classes at module level."""
    public = []
    for node in ast.iter_child_nodes(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            if not node.name.startswith("_"):
                public.append(node.name)
        elif isinstance(node, ast.ClassDef):
            if not node.name.startswith("_"):
                public.append(node.name)
    return public


def _count_domain_imports(tree: ast.AST) -> dict[str, list[str]]:
    """Return {domain: [imported_names]} for cross-domain pulse imports."""
    domains: dict[str, list[str]] = {}
    for node in ast.walk(tree):
        module = ""
        if isinstance(node, ast.Import):
            for alias in node.names:
                module = alias.name
                domain = _get_domain(module)
                if domain:
                    domains.setdefault(domain, []).append(module)
        elif isinstance(node, ast.ImportFrom) and node.module:
            module = node.module
            domain = _get_domain(module)
            if domain:
                names = [a.name for a in node.names] if node.names else [module]
                domains.setdefault(domain, []).extend(names)
    return domains


def check_file(filepath: Path, own_domain: str = "") -> dict:
    """Check a single Python file for SoC violations."""
    result = {
        "file": str(filepath.relative_to(ROOT_DIR)),
        "lines": 0,
        "public_functions": [],
        "domain_imports": {},
        "warnings": [],
        "violations": [],
    }

    try:
        content = filepath.read_text(encoding="utf-8", errors="replace")
    except Exception as e:
        result["violations"].append(f"Cannot read: {e}")
        return result

    lines = content.splitlines()
    result["lines"] = len(lines)

    # Law 7 original: line count
    # IMPORTANT: Compression (removing comments, merging lines) is NOT acceptable.
    # The correct fix is to extract related functions into a new module file.
    if len(lines) > MAX_LINES:
        result["violations"].append(
            f"Lines: {len(lines)} > {MAX_LINES} -- MODULARIZE: split into smaller modules (do NOT compress)")

    # Parse AST
    try:
        tree = ast.parse(content, filename=str(filepath))
    except SyntaxError:
        result["warnings"].append("Cannot parse (syntax error)")
        return result

    # Public function count
    public = _count_public_functions(tree)
    result["public_functions"] = public
    if len(public) > MAX_PUBLIC_FUNCS_FAIL:
        result["violations"].append(
            f"Public functions: {len(public)} > {MAX_PUBLIC_FUNCS_FAIL} "
            f"({', '.join(public[:5])}{'...' if len(public) > 5 else ''})")
    elif len(public) > MAX_PUBLIC_FUNCS_WARN:
        result["warnings"].append(
            f"Public functions: {len(public)} > {MAX_PUBLIC_FUNCS_WARN} "
            f"(MODULARIZE: extract functions into a separate module)")

    # Cross-domain imports
    domains = _count_domain_imports(tree)
    # Exclude own domain
    if own_domain:
        domains.pop(own_domain, None)
    result["domain_imports"] = {k: v for k, v in domains.items()}
    cross_count = len(domains)
    if cross_count > MAX_DOMAIN_IMPORTS_FAIL:
        result["violations"].append(
            f"Cross-domain imports: {cross_count} > {MAX_DOMAIN_IMPORTS_FAIL} "
            f"({', '.join(domains.keys())})")
    elif cross_count > MAX_DOMAIN_IMPORTS_WARN:
        result["warnings"].append(
            f"Cross-domain imports: {cross_count} > {MAX_DOMAIN_IMPORTS_WARN} "
            f"({', '.join(domains.keys())})")

    return result


def _detect_own_domain(filepath: Path) -> str:
    """Detect which pulse domain a file belongs to."""
    try:
        rel = filepath.relative_to(ROOT_DIR / "pulse")
        parts = rel.parts
        if parts:
            return f"pulse.{parts[0]}"
    except ValueError:
        pass
    return ""


def check_all(strict: bool = False) -> int:
    """Check all Python files. Returns exit code (0=pass, 1=fail)."""
    violations_total = 0
    warnings_total = 0
    grandfathered_total = 0

    py_files = sorted(ROOT_DIR.rglob("*.py"))
    for fp in py_files:
        rel = fp.relative_to(ROOT_DIR)
        if any(part in SKIP for part in rel.parts):
            continue

        own_domain = _detect_own_domain(fp)
        result = check_file(fp, own_domain)
        rel_posix = rel.as_posix()
        is_grandfathered = rel_posix in GRANDFATHERED

        if result["violations"]:
            if is_grandfathered:
                # Downgrade to warning — tech debt, not blocking
                grandfathered_total += len(result["violations"])
                print(f"  DEBT  {result['file']} (grandfathered)")
                for v in result["violations"]:
                    print(f"        {v}")
            else:
                violations_total += len(result["violations"])
                print(f"  FAIL  {result['file']}")
                for v in result["violations"]:
                    print(f"        {v}")

        if result["warnings"]:
            warnings_total += len(result["warnings"])
            if strict and not is_grandfathered:
                violations_total += len(result["warnings"])
                print(f"  FAIL  {result['file']} (strict)")
            else:
                print(f"  WARN  {result['file']}")
            for w in result["warnings"]:
                print(f"        {w}")

    # Summary
    total_files = len([f for f in py_files
                       if not any(p in f.relative_to(ROOT_DIR).parts for p in SKIP)])
    print(f"\n  SoC Check: {total_files} files scanned")
    print(f"  Violations: {violations_total}  Warnings: {warnings_total}  Tech Debt: {grandfathered_total}")

    if violations_total > 0:
        print("  FAILED")
        return 1
    if grandfathered_total > 0:
        print(f"  PASSED (with {grandfathered_total} grandfathered items to fix)")
    else:
        print("  PASSED")
    return 0


if __name__ == "__main__":
    strict = "--strict" in sys.argv
    single = None
    for i, arg in enumerate(sys.argv):
        if arg == "--file" and i + 1 < len(sys.argv):
            single = sys.argv[i + 1]

    if single:
        fp = Path(single).resolve()
        own_domain = _detect_own_domain(fp)
        r = check_file(fp, own_domain)
        print(f"File: {r['file']} ({r['lines']} lines)")
        print(f"Public functions ({len(r['public_functions'])}): {', '.join(r['public_functions'])}")
        print(f"Cross-domain imports ({len(r['domain_imports'])}): {', '.join(r['domain_imports'].keys())}")
        if r["violations"]:
            for v in r["violations"]:
                print(f"  VIOLATION: {v}")
        if r["warnings"]:
            for w in r["warnings"]:
                print(f"  WARNING: {w}")
        if not r["violations"] and not r["warnings"]:
            print("  CLEAN")
    else:
        sys.exit(check_all(strict))
