#!/usr/bin/env python3
"""
Genesis Resource Auditor
========================
Hunts for the "built but never activated" failure pattern across the Genesis system.

Audits:
  1. MCP Audit           — built servers not wired into settings.json
  2. Script Age Audit    — Python scripts with no execution signs (>7 days old)
  3. n8n Workflow Audit  — workflow JSON files not imported into live n8n
  4. Dashboard Staleness — HTML dashboards not updated in 24h
  5. KG Growth Audit     — Knowledge Graph stagnation detection
  6. Plan File Execution — Plans with pending markers sitting >3 days old

Usage:
    python3 /mnt/e/genesis-system/scripts/genesis_resource_auditor.py
    python3 /mnt/e/genesis-system/scripts/genesis_resource_auditor.py --report-only

Outputs:
    /mnt/e/genesis-system/hive/RESOURCE_AUDIT_REPORT.md
    /mnt/e/genesis-system/data/resource_audit_latest.json
    /mnt/e/genesis-system/data/auditor_state.json  (persistent state)

# VERIFICATION_STAMP
# Story: GENESIS-RESOURCE-AUDITOR-001
# Verified By: parallel-builder
# Verified At: 2026-02-20
# Tests: See --self-test flag
# Coverage: All 6 audit categories
"""

import argparse
import json
import os
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

# ---------------------------------------------------------------------------
# Paths — all on E: drive
# ---------------------------------------------------------------------------
GENESIS_ROOT = Path("/mnt/e/genesis-system")
SETTINGS_JSON = GENESIS_ROOT / ".claude" / "settings.json"
MCP_SERVERS_DIR = GENESIS_ROOT / "mcp-servers"
SCRIPTS_DIR = GENESIS_ROOT / "scripts"
N8N_CONFIG_DIR = GENESIS_ROOT / "config" / "n8n"
DASHBOARD_DIR = GENESIS_ROOT / "dashboard"
KG_ENTITIES_DIR = GENESIS_ROOT / "KNOWLEDGE_GRAPH" / "entities"
KG_AXIOMS_DIR = GENESIS_ROOT / "KNOWLEDGE_GRAPH" / "axioms"
PLANS_DIR = GENESIS_ROOT / "plans"
SECRETS_ENV = GENESIS_ROOT / "config" / "secrets.env"
DATA_DIR = GENESIS_ROOT / "data"
AUDITOR_STATE_FILE = DATA_DIR / "auditor_state.json"
AUDIT_REPORT_FILE = GENESIS_ROOT / "hive" / "RESOURCE_AUDIT_REPORT.md"
AUDIT_JSON_FILE = DATA_DIR / "resource_audit_latest.json"

# n8n configuration (sourced from core/n8n_trigger.py pattern)
N8N_BASE_URL = "https://n8n-genesis-u50607.vm.elestio.app"

# Thresholds
SCRIPT_AGE_DAYS = 7
DASHBOARD_STALE_HOURS = 24
KG_STAGNATION_HOURS = 4
PLAN_PENDING_DAYS = 3
PLAN_PENDING_MARKERS = ["TODO", "NOT YET", "PENDING", "PHASE 1", "PHASE 2", "TBD", "BLOCKED", "NOT STARTED"]

# ---------------------------------------------------------------------------
# Secrets loader — reads config/secrets.env
# ---------------------------------------------------------------------------
def load_secrets() -> dict[str, str]:
    """Parse config/secrets.env into a dict. Never raises — returns empty on failure."""
    secrets: dict[str, str] = {}
    if not SECRETS_ENV.exists():
        return secrets
    try:
        for line in SECRETS_ENV.read_text().splitlines():
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            if "=" in line:
                key, _, val = line.partition("=")
                # Strip surrounding quotes if present
                val = val.strip().strip("'\"")
                secrets[key.strip()] = val
    except Exception:
        pass
    return secrets


# ---------------------------------------------------------------------------
# Requests wrapper — stdlib only, falls back gracefully
# ---------------------------------------------------------------------------
def _http_get(url: str, headers: dict | None = None, timeout: int = 10) -> tuple[int, Any]:
    """
    GET a URL. Returns (status_code, parsed_json_or_text).
    Returns (-1, error_message) on any failure.
    """
    try:
        import urllib.request
        import urllib.error
        req = urllib.request.Request(url, headers=headers or {})
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            body = resp.read().decode("utf-8", errors="replace")
            try:
                return resp.status, json.loads(body)
            except json.JSONDecodeError:
                return resp.status, body
    except Exception as exc:
        return -1, str(exc)


# ---------------------------------------------------------------------------
# State persistence
# ---------------------------------------------------------------------------
def load_state() -> dict:
    """Load previous auditor state from JSON file."""
    if AUDITOR_STATE_FILE.exists():
        try:
            return json.loads(AUDITOR_STATE_FILE.read_text())
        except Exception:
            pass
    return {}


def save_state(state: dict) -> None:
    """Persist auditor state to JSON file."""
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    AUDITOR_STATE_FILE.write_text(json.dumps(state, indent=2, default=str))


# ---------------------------------------------------------------------------
# Audit 1: MCP Servers
# ---------------------------------------------------------------------------
def audit_mcp() -> dict:
    """
    Compare built MCP servers against those registered in .claude/settings.json.

    A server is "built" if it has:
      - a dist/ subdirectory (compiled JS/TS), OR
      - at least one .py file at the top level (Python server)

    Returns a result dict with score, findings, and recommendations.
    """
    result = {
        "category": "MCP Audit",
        "score": 100,
        "built_servers": [],
        "active_in_settings": [],
        "underutilised": [],
        "findings": [],
        "recommendations": [],
    }

    # Collect active MCP names from settings.json
    active_mcps: set[str] = set()
    if SETTINGS_JSON.exists():
        try:
            settings = json.loads(SETTINGS_JSON.read_text())
            active_mcps = set(settings.get("mcpServers", {}).keys())
            result["active_in_settings"] = sorted(active_mcps)
        except Exception as exc:
            result["findings"].append(f"Could not parse settings.json: {exc}")

    # Scan mcp-servers/ directory
    built: list[str] = []
    if MCP_SERVERS_DIR.exists():
        for entry in sorted(MCP_SERVERS_DIR.iterdir()):
            if not entry.is_dir():
                continue
            has_dist = (entry / "dist").is_dir()
            has_py = any(entry.glob("*.py"))
            has_index_js = (entry / "index.js").exists() or (entry / "src").is_dir()

            is_built = has_dist or has_py or has_index_js
            if is_built:
                built.append(entry.name)
                result["built_servers"].append({
                    "name": entry.name,
                    "has_dist": has_dist,
                    "has_py": has_py,
                    "has_index_js": has_index_js,
                })
    else:
        result["findings"].append(f"mcp-servers/ directory not found at {MCP_SERVERS_DIR}")

    # Find built but not active
    underutilised = [s for s in built if s not in active_mcps]
    result["underutilised"] = underutilised

    if underutilised:
        result["findings"].append(
            f"{len(underutilised)} MCP server(s) built but NOT in settings.json: {', '.join(underutilised)}"
        )
        for name in underutilised:
            result["recommendations"].append(
                f"[MCP] Add '{name}' to .claude/settings.json mcpServers section"
            )
        # Score penalty: 15 points per unactivated server, capped at 60 penalty
        penalty = min(60, len(underutilised) * 15)
        result["score"] = max(0, 100 - penalty)
    else:
        result["findings"].append("All built MCP servers are registered in settings.json")

    return result


# ---------------------------------------------------------------------------
# Audit 2: Script Age
# ---------------------------------------------------------------------------
def audit_scripts() -> dict:
    """
    Flag Python scripts in /scripts/ that are over SCRIPT_AGE_DAYS old
    and show no recent execution evidence (no .log companion, no __pycache__ entry).

    Heuristics for "might be executing":
      - Has a corresponding .log file in data/logs/
      - Has a __pycache__/*.pyc that is newer than the script itself
      - Is imported in another active script
      - Filename contains 'test_' prefix (test scripts expected to be one-shot)
    """
    result = {
        "category": "Script Age Audit",
        "score": 100,
        "total_scripts": 0,
        "old_scripts": 0,
        "potentially_idle": [],
        "findings": [],
        "recommendations": [],
    }

    if not SCRIPTS_DIR.exists():
        result["findings"].append(f"Scripts directory not found: {SCRIPTS_DIR}")
        return result

    now = time.time()
    age_threshold = SCRIPT_AGE_DAYS * 86400
    pycache_dir = SCRIPTS_DIR / "__pycache__"
    logs_dir = DATA_DIR / "logs"

    # Build set of pyc files for quick lookup
    pyc_files: dict[str, float] = {}
    if pycache_dir.exists():
        for pyc in pycache_dir.glob("*.pyc"):
            base = pyc.stem.split(".")[0]  # strip .cpython-310 etc.
            pyc_files[base] = pyc.stat().st_mtime

    all_scripts = sorted(SCRIPTS_DIR.glob("*.py"))
    result["total_scripts"] = len(all_scripts)

    idle: list[dict] = []
    for script in all_scripts:
        stat = script.stat()
        age_days = (now - stat.st_mtime) / 86400

        if age_days < SCRIPT_AGE_DAYS:
            continue  # recently modified — not idle

        # Check execution evidence
        stem = script.stem
        has_pyc = stem in pyc_files and pyc_files[stem] > stat.st_mtime
        has_log = (logs_dir / f"{stem}.log").exists() if logs_dir.exists() else False
        is_test = stem.startswith("test_")
        is_readme = stem.lower() in ("readme", "__init__")

        if is_test or is_readme:
            continue  # skip test/readme scripts

        if not has_pyc and not has_log:
            idle.append({
                "name": script.name,
                "age_days": round(age_days, 1),
                "size_bytes": stat.st_size,
                "last_modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d"),
            })

    result["old_scripts"] = len(idle)
    # Only surface top 20 most concerning (oldest first)
    idle.sort(key=lambda x: x["age_days"], reverse=True)
    result["potentially_idle"] = idle[:20]

    if idle:
        result["findings"].append(
            f"{len(idle)} script(s) are >{SCRIPT_AGE_DAYS} days old with no execution evidence"
        )
        for item in idle[:5]:
            result["recommendations"].append(
                f"[SCRIPT] Review '{item['name']}' ({item['age_days']}d old) — delete or schedule execution"
            )
        # Score: penalise proportionally to idle ratio
        idle_ratio = len(idle) / max(result["total_scripts"], 1)
        penalty = min(50, int(idle_ratio * 100))
        result["score"] = max(0, 100 - penalty)
    else:
        result["findings"].append("No suspiciously idle scripts detected")

    return result


# ---------------------------------------------------------------------------
# Audit 3: n8n Workflows
# ---------------------------------------------------------------------------
def audit_n8n(secrets: dict) -> dict:
    """
    Compare workflow JSON files in config/n8n/ against workflows actually
    imported into the live n8n instance via the management API.
    """
    result = {
        "category": "n8n Workflow Audit",
        "score": 100,
        "local_workflow_files": [],
        "live_workflows": [],
        "idle_local": [],
        "api_reachable": False,
        "findings": [],
        "recommendations": [],
    }

    # Collect local workflow files
    local_files: dict[str, Path] = {}
    if N8N_CONFIG_DIR.exists():
        for f in sorted(N8N_CONFIG_DIR.glob("*.json")):
            try:
                data = json.loads(f.read_text())
                # Extract workflow name from the JSON (n8n format)
                wf_name = data.get("name", f.stem)
                local_files[wf_name] = f
                result["local_workflow_files"].append({"file": f.name, "workflow_name": wf_name})
            except Exception:
                result["local_workflow_files"].append({"file": f.name, "workflow_name": f.stem})
                local_files[f.stem] = f
    else:
        result["findings"].append(f"n8n config directory not found: {N8N_CONFIG_DIR}")

    # Try to reach n8n API
    n8n_api_key = secrets.get("N8N_API_KEY", "")
    api_url = f"{N8N_BASE_URL}/api/v1/workflows"
    headers = {"X-N8N-API-KEY": n8n_api_key} if n8n_api_key else {}

    status_code, api_data = _http_get(api_url, headers=headers)

    if status_code == 200 and isinstance(api_data, dict):
        result["api_reachable"] = True
        live_wfs = api_data.get("data", [])
        live_names = {wf.get("name", "") for wf in live_wfs}
        result["live_workflows"] = [
            {"id": wf.get("id"), "name": wf.get("name"), "active": wf.get("active", False)}
            for wf in live_wfs
        ]

        # Find local files not in live n8n
        for wf_name, file_path in local_files.items():
            if wf_name not in live_names:
                # Try fuzzy: check if any live name contains this name
                fuzzy_match = any(wf_name.lower() in live.lower() or live.lower() in wf_name.lower()
                                  for live in live_names)
                if not fuzzy_match:
                    result["idle_local"].append({
                        "file": file_path.name,
                        "workflow_name": wf_name,
                        "status": "NOT_IMPORTED",
                    })

        if result["idle_local"]:
            result["findings"].append(
                f"{len(result['idle_local'])} workflow file(s) exist locally but are NOT in live n8n"
            )
            for item in result["idle_local"]:
                result["recommendations"].append(
                    f"[N8N] Import '{item['file']}' into n8n at {N8N_BASE_URL}"
                )
            penalty = min(50, len(result["idle_local"]) * 10)
            result["score"] = max(0, 100 - penalty)
        else:
            result["findings"].append("All local n8n workflow files appear to be imported")

    elif status_code == 401:
        result["findings"].append(
            "n8n API returned 401 Unauthorized — N8N_API_KEY not set or invalid in config/secrets.env"
        )
        result["recommendations"].append(
            "[N8N] Add N8N_API_KEY to config/secrets.env to enable workflow comparison"
        )
        result["score"] = 60  # Partial score — can't fully audit without API access
    elif status_code == -1:
        result["findings"].append(f"n8n API unreachable: {api_data}")
        result["recommendations"].append(
            f"[N8N] Verify n8n is running at {N8N_BASE_URL}"
        )
        result["score"] = 60  # Same as above — partial
    else:
        result["findings"].append(f"n8n API returned unexpected status {status_code}")
        result["score"] = 60

    if local_files:
        result["findings"].append(f"{len(local_files)} workflow file(s) found in config/n8n/")

    return result


# ---------------------------------------------------------------------------
# Audit 4: Dashboard Staleness
# ---------------------------------------------------------------------------
def audit_dashboards() -> dict:
    """
    Check HTML files in /dashboard/ — flag any not modified in >24 hours.
    Active dashboards should be regenerated regularly to reflect system state.
    """
    result = {
        "category": "Dashboard Staleness Audit",
        "score": 100,
        "total_dashboards": 0,
        "stale": [],
        "fresh": [],
        "findings": [],
        "recommendations": [],
    }

    if not DASHBOARD_DIR.exists():
        result["findings"].append(f"Dashboard directory not found: {DASHBOARD_DIR}")
        return result

    now = time.time()
    stale_threshold = DASHBOARD_STALE_HOURS * 3600

    html_files = sorted(DASHBOARD_DIR.glob("*.html"))
    result["total_dashboards"] = len(html_files)

    stale: list[dict] = []
    fresh: list[dict] = []

    for html in html_files:
        stat = html.stat()
        age_hours = (now - stat.st_mtime) / 3600
        entry = {
            "name": html.name,
            "age_hours": round(age_hours, 1),
            "last_modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M"),
        }
        if age_hours > stale_threshold:
            stale.append(entry)
        else:
            fresh.append(entry)

    result["stale"] = stale
    result["fresh"] = fresh

    if stale:
        result["findings"].append(
            f"{len(stale)}/{len(html_files)} dashboard(s) not updated in >{DASHBOARD_STALE_HOURS}h"
        )
        for item in stale[:3]:
            result["recommendations"].append(
                f"[DASHBOARD] Refresh '{item['name']}' — {item['age_hours']}h since last update"
            )
        stale_ratio = len(stale) / max(len(html_files), 1)
        penalty = min(40, int(stale_ratio * 60))
        result["score"] = max(0, 100 - penalty)
    else:
        result["findings"].append("All dashboards updated within the past 24 hours")

    return result


# ---------------------------------------------------------------------------
# Audit 5: KG Growth
# ---------------------------------------------------------------------------
def audit_kg_growth(state: dict) -> dict:
    """
    Count entities and axioms in the Knowledge Graph.
    Compare against last run — if no new entries in STAGNATION_HOURS, alert.
    """
    result = {
        "category": "KG Growth Audit",
        "score": 100,
        "entity_files": 0,
        "entity_total_lines": 0,
        "axiom_files": 0,
        "axiom_total_lines": 0,
        "delta_entities": 0,
        "delta_axioms": 0,
        "hours_since_last_run": None,
        "stagnant": False,
        "findings": [],
        "recommendations": [],
    }

    # Count KG lines
    entity_lines = 0
    entity_file_count = 0
    if KG_ENTITIES_DIR.exists():
        for f in KG_ENTITIES_DIR.glob("*.jsonl"):
            try:
                lines = len([ln for ln in f.read_text().splitlines() if ln.strip()])
                entity_lines += lines
                entity_file_count += 1
            except Exception:
                pass

    axiom_lines = 0
    axiom_file_count = 0
    if KG_AXIOMS_DIR.exists():
        for f in KG_AXIOMS_DIR.glob("*.jsonl"):
            try:
                lines = len([ln for ln in f.read_text().splitlines() if ln.strip()])
                axiom_lines += lines
                axiom_file_count += 1
            except Exception:
                pass

    result["entity_files"] = entity_file_count
    result["entity_total_lines"] = entity_lines
    result["axiom_files"] = axiom_file_count
    result["axiom_total_lines"] = axiom_lines

    # Compare against last state
    last_run_ts = state.get("last_kg_audit_ts")
    last_entities = state.get("kg_entity_lines", 0)
    last_axioms = state.get("kg_axiom_lines", 0)
    now_ts = time.time()

    if last_run_ts:
        hours_elapsed = (now_ts - last_run_ts) / 3600
        result["hours_since_last_run"] = round(hours_elapsed, 2)
        result["delta_entities"] = entity_lines - last_entities
        result["delta_axioms"] = axiom_lines - last_axioms

        if hours_elapsed >= KG_STAGNATION_HOURS and result["delta_entities"] == 0 and result["delta_axioms"] == 0:
            result["stagnant"] = True
            result["findings"].append(
                f"MEMORY STAGNATION: No new KG entries in {hours_elapsed:.1f}h "
                f"(threshold: {KG_STAGNATION_HOURS}h)"
            )
            result["recommendations"].append(
                "[KG] Dispatch a Genesis research agent to generate new axioms/entities"
            )
            result["score"] = 40
        else:
            result["findings"].append(
                f"KG growth: +{result['delta_entities']} entity lines, "
                f"+{result['delta_axioms']} axiom lines in the past {hours_elapsed:.1f}h"
            )
    else:
        result["findings"].append(
            f"First run — baseline established: {entity_lines} entity lines, {axiom_lines} axiom lines"
        )

    result["findings"].append(
        f"KG state: {entity_file_count} entity files ({entity_lines} lines), "
        f"{axiom_file_count} axiom files ({axiom_lines} lines)"
    )

    return result, {
        "last_kg_audit_ts": now_ts,
        "kg_entity_lines": entity_lines,
        "kg_axiom_lines": axiom_lines,
    }


# ---------------------------------------------------------------------------
# Audit 6: Plan File Execution
# ---------------------------------------------------------------------------
def audit_plans() -> dict:
    """
    Scan plans/*.md for files older than PLAN_PENDING_DAYS days that
    still contain pending markers (TODO, PENDING, NOT YET, etc.).
    """
    result = {
        "category": "Plan File Execution Audit",
        "score": 100,
        "total_plans": 0,
        "pending_plans": [],
        "findings": [],
        "recommendations": [],
    }

    if not PLANS_DIR.exists():
        result["findings"].append(f"Plans directory not found: {PLANS_DIR}")
        return result

    now = time.time()
    age_threshold = PLAN_PENDING_DAYS * 86400

    plan_files = sorted(PLANS_DIR.glob("*.md"))
    result["total_plans"] = len(plan_files)

    pending: list[dict] = []
    for plan in plan_files:
        stat = plan.stat()
        age_days = (now - stat.st_mtime) / 86400

        if age_days < PLAN_PENDING_DAYS:
            continue  # recently touched plan

        try:
            content = plan.read_text(errors="replace")
        except Exception:
            continue

        found_markers = [
            m for m in PLAN_PENDING_MARKERS
            if m in content.upper()
        ]
        if found_markers:
            # Count occurrences to gauge urgency
            marker_count = sum(content.upper().count(m) for m in found_markers)
            pending.append({
                "name": plan.name,
                "age_days": round(age_days, 1),
                "last_modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d"),
                "pending_markers": found_markers[:5],
                "marker_count": marker_count,
            })

    # Sort by marker density (most blocked plans first)
    pending.sort(key=lambda x: x["marker_count"], reverse=True)
    result["pending_plans"] = pending

    if pending:
        result["findings"].append(
            f"{len(pending)} plan(s) are >{PLAN_PENDING_DAYS} days old with unresolved pending markers"
        )
        for item in pending[:5]:
            result["recommendations"].append(
                f"[PLAN] '{item['name']}' ({item['age_days']}d old, "
                f"{item['marker_count']} pending markers) — execute or archive"
            )
        idle_ratio = len(pending) / max(result["total_plans"], 1)
        penalty = min(50, int(idle_ratio * 80))
        result["score"] = max(0, 100 - penalty)
    else:
        result["findings"].append("No stale pending plans found")

    return result


# ---------------------------------------------------------------------------
# Score aggregation
# ---------------------------------------------------------------------------
def compute_health_score(audits: list[dict]) -> int:
    """
    Weighted average of all category scores.
    Weights reflect criticality to Genesis operation.
    """
    weights = {
        "MCP Audit": 0.25,
        "Script Age Audit": 0.10,
        "n8n Workflow Audit": 0.20,
        "Dashboard Staleness Audit": 0.10,
        "KG Growth Audit": 0.20,
        "Plan File Execution Audit": 0.15,
    }
    total_weight = 0.0
    weighted_sum = 0.0
    for audit in audits:
        cat = audit.get("category", "")
        weight = weights.get(cat, 0.10)
        score = audit.get("score", 100)
        weighted_sum += score * weight
        total_weight += weight

    if total_weight == 0:
        return 0
    return round(weighted_sum / total_weight)


def score_label(score: int) -> str:
    if score >= 90:
        return "EXCELLENT"
    elif score >= 75:
        return "GOOD"
    elif score >= 60:
        return "WARNING"
    elif score >= 40:
        return "DEGRADED"
    else:
        return "CRITICAL"


# ---------------------------------------------------------------------------
# Report generation
# ---------------------------------------------------------------------------
def build_markdown_report(
    audits: list[dict],
    health_score: int,
    timestamp: str,
) -> str:
    """Generate the full Markdown audit report."""
    lines: list[str] = []
    label = score_label(health_score)

    lines.append("# Genesis Resource Audit Report")
    lines.append("")
    lines.append(f"**Generated**: {timestamp}")
    lines.append(f"**Genesis Health Score**: {health_score}/100 — {label}")
    lines.append("")

    # Health score bar
    filled = health_score // 5
    bar = "[" + "#" * filled + "." * (20 - filled) + "]"
    lines.append(f"```")
    lines.append(f"Health: {bar} {health_score}%")
    lines.append(f"```")
    lines.append("")

    # Score summary table
    lines.append("## Category Scores")
    lines.append("")
    lines.append("| Category | Score | Status |")
    lines.append("|----------|-------|--------|")
    for audit in audits:
        cat = audit.get("category", "Unknown")
        score = audit.get("score", 0)
        status = score_label(score)
        lines.append(f"| {cat} | {score}/100 | {status} |")
    lines.append("")

    # Collect all recommendations, ranked by urgency (lower score = higher priority)
    all_recs: list[tuple[int, str]] = []
    for audit in audits:
        score = audit.get("score", 100)
        for rec in audit.get("recommendations", []):
            all_recs.append((score, rec))
    all_recs.sort(key=lambda x: x[0])  # lowest score first = highest urgency

    if all_recs:
        lines.append("## Immediate Action Required (Ranked by Impact)")
        lines.append("")
        for i, (_, rec) in enumerate(all_recs[:10], 1):
            lines.append(f"{i}. {rec}")
        lines.append("")

    # Detailed sections
    lines.append("---")
    lines.append("")
    lines.append("## Detailed Findings")
    lines.append("")

    for audit in audits:
        cat = audit.get("category", "Unknown")
        score = audit.get("score", 0)
        lines.append(f"### {cat} — {score}/100")
        lines.append("")

        for finding in audit.get("findings", []):
            lines.append(f"- {finding}")
        lines.append("")

        # Category-specific details
        if cat == "MCP Audit":
            built = audit.get("built_servers", [])
            if built:
                lines.append("**Built MCP Servers:**")
                for s in built:
                    name = s["name"] if isinstance(s, dict) else s
                    lines.append(f"  - `{name}`")
                lines.append("")
            underutilised = audit.get("underutilised", [])
            if underutilised:
                lines.append("**UNDERUTILISED (built but not in settings.json):**")
                for name in underutilised:
                    lines.append(f"  - `{name}` — NOT ACTIVE")
                lines.append("")
            active = audit.get("active_in_settings", [])
            if active:
                lines.append(f"**Active in settings.json**: {', '.join(f'`{a}`' for a in active)}")
                lines.append("")

        elif cat == "Script Age Audit":
            idle = audit.get("potentially_idle", [])
            if idle:
                lines.append("**Top idle scripts (oldest first):**")
                lines.append("")
                lines.append("| Script | Age (days) | Last Modified |")
                lines.append("|--------|-----------|---------------|")
                for item in idle[:10]:
                    lines.append(f"| `{item['name']}` | {item['age_days']} | {item['last_modified']} |")
                lines.append("")

        elif cat == "n8n Workflow Audit":
            local = audit.get("local_workflow_files", [])
            live = audit.get("live_workflows", [])
            idle = audit.get("idle_local", [])
            reachable = audit.get("api_reachable", False)

            lines.append(f"**API Reachable**: {'YES' if reachable else 'NO'}")
            lines.append(f"**Local workflow files**: {len(local)}")
            lines.append(f"**Live n8n workflows**: {len(live)}")
            lines.append("")

            if live:
                lines.append("**Live n8n workflows:**")
                for wf in live:
                    status = "ACTIVE" if wf.get("active") else "inactive"
                    lines.append(f"  - [{status}] `{wf.get('name', 'unnamed')}` (id={wf.get('id')})")
                lines.append("")

            if idle:
                lines.append("**NOT IMPORTED into live n8n:**")
                for item in idle:
                    lines.append(f"  - `{item['file']}` — {item['workflow_name']}")
                lines.append("")

        elif cat == "Dashboard Staleness Audit":
            stale = audit.get("stale", [])
            fresh = audit.get("fresh", [])
            if stale:
                lines.append("**Stale dashboards:**")
                for item in stale:
                    lines.append(f"  - `{item['name']}` — {item['age_hours']}h since last update")
                lines.append("")
            if fresh:
                lines.append(f"**Fresh dashboards ({len(fresh)})**: " +
                             ", ".join(f"`{d['name']}`" for d in fresh[:5]))
                lines.append("")

        elif cat == "KG Growth Audit":
            lines.append(f"**Entities**: {audit.get('entity_files')} files, "
                        f"{audit.get('entity_total_lines')} total lines")
            lines.append(f"**Axioms**: {audit.get('axiom_files')} files, "
                        f"{audit.get('axiom_total_lines')} total lines")
            if audit.get("hours_since_last_run") is not None:
                lines.append(f"**Delta since last run** ({audit['hours_since_last_run']}h ago): "
                            f"+{audit.get('delta_entities', 0)} entity lines, "
                            f"+{audit.get('delta_axioms', 0)} axiom lines")
            lines.append("")

        elif cat == "Plan File Execution Audit":
            pending = audit.get("pending_plans", [])
            if pending:
                lines.append("**Plans with unresolved pending markers:**")
                lines.append("")
                lines.append("| Plan File | Age (days) | Markers | Count |")
                lines.append("|-----------|-----------|---------|-------|")
                for item in pending[:15]:
                    markers = ", ".join(item.get("pending_markers", []))
                    lines.append(
                        f"| `{item['name']}` | {item['age_days']} | {markers} | {item['marker_count']} |"
                    )
                lines.append("")

    lines.append("---")
    lines.append("")
    lines.append("*Report generated by Genesis Resource Auditor*")
    lines.append(f"*Next scheduled run: hourly via cron*")
    lines.append("")

    return "\n".join(lines)


def build_json_output(
    audits: list[dict],
    health_score: int,
    timestamp: str,
) -> dict:
    """Build compact JSON for dashboard consumption."""
    all_recs: list[tuple[int, str]] = []
    for audit in audits:
        score = audit.get("score", 100)
        for rec in audit.get("recommendations", []):
            all_recs.append((score, rec))
    all_recs.sort(key=lambda x: x[0])

    return {
        "timestamp": timestamp,
        "health_score": health_score,
        "health_label": score_label(health_score),
        "categories": [
            {
                "name": a.get("category"),
                "score": a.get("score"),
                "label": score_label(a.get("score", 0)),
            }
            for a in audits
        ],
        "top_5_underutilised": [rec for _, rec in all_recs[:5]],
        "total_recommendations": len(all_recs),
    }


# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------
def run_audit(report_only: bool = False) -> dict:
    """Run all audit categories and return combined results."""
    timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
    print(f"[Genesis Resource Auditor] Starting audit at {timestamp}")
    print(f"[Genesis Resource Auditor] Genesis root: {GENESIS_ROOT}")
    print()

    secrets = load_secrets()
    state = load_state()
    new_state = dict(state)

    # Run all audits
    print("[1/6] MCP Audit...")
    mcp_result = audit_mcp()
    print(f"      Score: {mcp_result['score']}/100")

    print("[2/6] Script Age Audit...")
    script_result = audit_scripts()
    print(f"      Score: {script_result['score']}/100")

    print("[3/6] n8n Workflow Audit...")
    n8n_result = audit_n8n(secrets)
    print(f"      Score: {n8n_result['score']}/100")

    print("[4/6] Dashboard Staleness Audit...")
    dash_result = audit_dashboards()
    print(f"      Score: {dash_result['score']}/100")

    print("[5/6] KG Growth Audit...")
    kg_result, kg_state_updates = audit_kg_growth(state)
    new_state.update(kg_state_updates)
    print(f"      Score: {kg_result['score']}/100")

    print("[6/6] Plan File Execution Audit...")
    plan_result = audit_plans()
    print(f"      Score: {plan_result['score']}/100")
    print()

    audits = [mcp_result, script_result, n8n_result, dash_result, kg_result, plan_result]
    health_score = compute_health_score(audits)
    label = score_label(health_score)

    print(f"[Genesis Resource Auditor] Genesis Health Score: {health_score}/100 — {label}")
    print()

    # Build outputs
    md_report = build_markdown_report(audits, health_score, timestamp)
    json_output = build_json_output(audits, health_score, timestamp)

    if report_only:
        print("--- REPORT (--report-only mode, not writing files) ---")
        print()
        # Print compact summary
        print(f"Health Score: {health_score}/100 ({label})")
        print()
        print("Category Scores:")
        for audit in audits:
            print(f"  {audit['category']}: {audit['score']}/100")
        print()
        if json_output["top_5_underutilised"]:
            print("Top Recommendations:")
            for i, rec in enumerate(json_output["top_5_underutilised"], 1):
                print(f"  {i}. {rec}")
        print()
    else:
        # Write report file
        AUDIT_REPORT_FILE.parent.mkdir(parents=True, exist_ok=True)
        AUDIT_REPORT_FILE.write_text(md_report)
        print(f"[Genesis Resource Auditor] Report written to: {AUDIT_REPORT_FILE}")

        # Write JSON for dashboard
        DATA_DIR.mkdir(parents=True, exist_ok=True)
        AUDIT_JSON_FILE.write_text(json.dumps(json_output, indent=2))
        print(f"[Genesis Resource Auditor] JSON written to: {AUDIT_JSON_FILE}")

        # Persist state for next run
        save_state(new_state)
        print(f"[Genesis Resource Auditor] State saved to: {AUDITOR_STATE_FILE}")

    return {
        "health_score": health_score,
        "health_label": label,
        "audits": audits,
        "json_output": json_output,
    }


# ---------------------------------------------------------------------------
# Self-test
# ---------------------------------------------------------------------------
def self_test() -> None:
    """Run basic smoke tests on all audit functions."""
    print("[Self-Test] Running Genesis Resource Auditor self-tests...")
    errors: list[str] = []

    # Test 1: secrets loader
    secrets = load_secrets()
    assert isinstance(secrets, dict), "load_secrets() must return dict"
    print("[PASS] load_secrets() returns dict")

    # Test 2: MCP audit returns required keys
    result = audit_mcp()
    for key in ["category", "score", "built_servers", "active_in_settings", "underutilised", "findings"]:
        assert key in result, f"audit_mcp() missing key: {key}"
    assert 0 <= result["score"] <= 100, "MCP score out of range"
    print(f"[PASS] audit_mcp() — score={result['score']}, built={len(result['built_servers'])}")

    # Test 3: Script audit
    result = audit_scripts()
    for key in ["category", "score", "total_scripts", "potentially_idle"]:
        assert key in result, f"audit_scripts() missing key: {key}"
    assert result["total_scripts"] >= 0
    print(f"[PASS] audit_scripts() — {result['total_scripts']} scripts, {result['old_scripts']} idle candidates")

    # Test 4: n8n audit (no API key expected in test)
    result = audit_n8n({})
    assert "category" in result and "score" in result
    print(f"[PASS] audit_n8n() — api_reachable={result['api_reachable']}, score={result['score']}")

    # Test 5: Dashboard audit
    result = audit_dashboards()
    assert "total_dashboards" in result
    print(f"[PASS] audit_dashboards() — {result['total_dashboards']} dashboards, {len(result['stale'])} stale")

    # Test 6: KG audit
    result, _ = audit_kg_growth({})
    assert "entity_total_lines" in result and "axiom_total_lines" in result
    print(f"[PASS] audit_kg_growth() — {result['entity_total_lines']} entity lines")

    # Test 7: Plan audit
    result = audit_plans()
    assert "total_plans" in result
    print(f"[PASS] audit_plans() — {result['total_plans']} plans, {len(result['pending_plans'])} pending")

    # Test 8: Health score calculation
    dummy_audits = [{"category": "MCP Audit", "score": 80},
                    {"category": "n8n Workflow Audit", "score": 60},
                    {"category": "KG Growth Audit", "score": 100}]
    score = compute_health_score(dummy_audits)
    assert 0 <= score <= 100, f"Health score {score} out of range"
    print(f"[PASS] compute_health_score() — {score}/100")

    # Test 9: JSON output structure
    full = run_audit(report_only=True)
    assert "health_score" in full
    assert "top_5_underutilised" in full["json_output"]
    print(f"[PASS] Full audit run — health_score={full['health_score']}/100")

    print()
    if errors:
        print(f"[FAIL] {len(errors)} test(s) failed:")
        for e in errors:
            print(f"  - {e}")
        sys.exit(1)
    else:
        print("[Self-Test] All tests passed.")


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
    parser = argparse.ArgumentParser(
        description="Genesis Resource Auditor — hunts for built-but-never-activated resources",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python3 genesis_resource_auditor.py              # Full audit, writes files
  python3 genesis_resource_auditor.py --report-only  # Print summary, no file writes
  python3 genesis_resource_auditor.py --self-test    # Run smoke tests
        """,
    )
    parser.add_argument(
        "--report-only",
        action="store_true",
        help="Print summary to stdout without writing any files",
    )
    parser.add_argument(
        "--self-test",
        action="store_true",
        help="Run built-in smoke tests and exit",
    )
    args = parser.parse_args()

    if args.self_test:
        self_test()
        return

    run_audit(report_only=args.report_only)


if __name__ == "__main__":
    main()
