#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Brain Health Monitor: Automated silent watchdog for knowledge pipeline.

Runs every consolidation cycle (6h). Detects pipeline breaks BEFORE
they become 9-day brain freezes. Logs alerts to state/brain_health.json.

Checks:
  1. Bridge activity: did we write anything in the last 24h?
  2. Pipeline leak: capture logs growing but brain files stagnant?
  3. Quality gate: rejection rate above 90%? (salience miscalibration)
  4. Consolidation freshness: has consolidation run recently?
  5. Orchestrator alive: is the daemon actually running?
"""
import json
import logging
import os
import time
from datetime import datetime, timezone, timedelta
from pathlib import Path

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

from pulse.core.brain_utils import (
    HIPPOCAMPUS_DIR, load_json, save_json,
    LESSONS_JSONL, FAILS_JSONL, PATTERNS_JSONL, read_jsonl_entries,
)

STATE_DIR = ROOT_DIR / "state"
HEALTH_FILE = STATE_DIR / "brain_health.json"
CONSOLIDATION_LOG = HIPPOCAMPUS_DIR / "Evidence" / "Metrics" / "consolidation_log.json"

logger = logging.getLogger("pulse.brain_health")

# Thresholds
STALE_HOURS = 24         # Alert if no brain writes in this window
LEAK_RATIO = 200.0       # Alert if capture grew 200x more than brain (raw→distilled is ~50-100x normal)
REJECTION_CEILING = 0.90  # Alert if quality gate rejects > 90%
CONSOL_STALE_HOURS = 12   # Alert if consolidation hasn't run


def _file_age_hours(path: Path) -> float:
    """Hours since file was last modified. Returns 999 if missing."""
    if not path.exists():
        return 999.0
    mtime = path.stat().st_mtime
    return (time.time() - mtime) / 3600


def _count_entries(path: Path) -> int:
    """Count entries in a JSONL brain file."""
    return len(read_jsonl_entries(path))


def check_brain_health(verbose=False) -> dict:
    """Run all health checks. Returns health report dict."""
    alerts = []
    checks = {}
    now = datetime.now(timezone.utc)

    # 1. Brain file freshness
    brain_files = {
        "lessons": LESSONS_JSONL,
        "failures": FAILS_JSONL,
        "patterns": PATTERNS_JSONL,
    }
    for name, path in brain_files.items():
        age = _file_age_hours(path)
        count = _count_entries(path)
        checks[f"brain_{name}_age_hours"] = round(age, 1)
        checks[f"brain_{name}_entries"] = count
        if age > STALE_HOURS:
            alerts.append(f"STALE: {name} not updated in {age:.0f}h (threshold: {STALE_HOURS}h)")

    # 2. Capture log vs brain growth (pipeline leak detection)
    total_capture_bytes = 0
    for f in STATE_DIR.glob("pulse_captured_*.log"):
        total_capture_bytes += f.stat().st_size
    checks["capture_log_total_mb"] = round(total_capture_bytes / 1024 / 1024, 1)

    total_brain_bytes = sum(p.stat().st_size for p in brain_files.values() if p.exists())
    checks["brain_files_total_kb"] = round(total_brain_bytes / 1024, 1)

    if total_brain_bytes > 0:
        ratio = total_capture_bytes / max(total_brain_bytes, 1)
        checks["capture_to_brain_ratio"] = round(ratio, 1)
        if ratio > LEAK_RATIO * 10:
            alerts.append(f"PIPELINE_LEAK: capture logs are {ratio:.0f}x larger than brain output. Knowledge may not be flowing.")

    # 3. Bridge state check
    bridge_state_file = STATE_DIR / "pulse_bridge_state.json"
    if bridge_state_file.exists():
        bridge_state = load_json(bridge_state_file)
        positions = bridge_state.get("positions", {})
        for log_path_str, offset in positions.items():
            log_path = Path(log_path_str)
            if log_path.exists():
                actual_size = log_path.stat().st_size
                unprocessed = actual_size - offset
                name = log_path.name
                checks[f"bridge_{name}_unprocessed_mb"] = round(unprocessed / 1024 / 1024, 2)
                if unprocessed > 5 * 1024 * 1024:  # 5MB unprocessed
                    alerts.append(f"BRIDGE_BEHIND: {name} has {unprocessed/1024/1024:.1f}MB unprocessed")

    # 4. Consolidation freshness
    consol_age = _file_age_hours(CONSOLIDATION_LOG)
    checks["consolidation_age_hours"] = round(consol_age, 1)
    if consol_age > CONSOL_STALE_HOURS:
        alerts.append(f"CONSOL_STALE: last consolidation was {consol_age:.0f}h ago (threshold: {CONSOL_STALE_HOURS}h)")

    # 5. Orchestrator PID check
    pid_file = STATE_DIR / "orchestrator.pid"
    orch_running = False
    if pid_file.exists():
        try:
            pid = int(pid_file.read_text().strip())
            # Check if process exists (works on both Windows and Unix)
            if os.name == 'nt':
                import ctypes
                kernel32 = ctypes.windll.kernel32
                handle = kernel32.OpenProcess(0x0400, False, pid)  # PROCESS_QUERY_INFORMATION
                if handle:
                    kernel32.CloseHandle(handle)
                    orch_running = True
            else:
                os.kill(pid, 0)
                orch_running = True
        except (ValueError, OSError, PermissionError):
            pass
    checks["orchestrator_running"] = orch_running
    if not orch_running:
        alerts.append("ORCH_DOWN: Orchestrator is not running. Brain pipeline is frozen.")

    # Build report
    report = {
        "timestamp": now.isoformat(),
        "status": "HEALTHY" if not alerts else "DEGRADED",
        "alert_count": len(alerts),
        "alerts": alerts,
        "checks": checks,
    }

    # Save to state
    HEALTH_FILE.parent.mkdir(parents=True, exist_ok=True)
    save_json(HEALTH_FILE, report)

    if verbose or alerts:
        status = "HEALTHY" if not alerts else "DEGRADED"
        logger.info(f"[BRAIN HEALTH] {status} — {len(alerts)} alerts")
        for alert in alerts:
            logger.warning(f"  [ALERT] {alert}")

    return report


if __name__ == "__main__":
    import sys
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    report = check_brain_health(verbose=True)
    print(json.dumps(report, indent=2))
    sys.exit(0 if report["status"] == "HEALTHY" else 1)
