# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""D27: Autonomic Cognitive Triage — Emergency Resource Redirection.
Suspends low-priority daemons on critical failure, injects emergency context.
Source: DMN session_20260311T162910 | Usage: pulse triage
"""

import json
import logging
from datetime import datetime, timezone, timedelta
from pathlib import Path

_log = logging.getLogger("pulse.d27_triage")

try:
    from pulse.core.brain_utils import STATE_DIR
except Exception:
    STATE_DIR = Path("state")

HEALTH_DIR = STATE_DIR / "health"
DAEMON_HEALTH_FILE = HEALTH_DIR / "daemon_health.json"
OVERRIDE_FILE = STATE_DIR / "homeostatic_overrides.json"
METRICS_FILE = HEALTH_DIR / "cognitive_triage.json"

_SEVERITY_THRESHOLD = 0.6      # enter triage above this
_TRIAGE_DURATION_MIN = 30      # auto-clear after N minutes
_TRIAGE_MAX_HOURS = 2          # hard cap on triage duration
_EMERGENCY_SALIENCE = 8.0      # hot cache salience for emergency items
_SUSPEND_MULTIPLIER = 10       # multiply low-priority intervals by this

# Low-priority daemons safe to suspend during triage
_SUSPENDABLE = {
    "sleep_cycle_scheduler", "epistemic_forager_scheduler",
    "skill_synthesizer_scheduler", "profile_distiller_scheduler",
    "prune_scheduler", "schema_daemon_scheduler",
    "procedure_daemon_scheduler", "intent_daemon_scheduler",
    "synthesize_scheduler", "phylogeny_scheduler",
    "agent_proc_synth_scheduler",
}


def _load_daemon_health() -> dict:
    """Read daemon heartbeat data."""
    if not DAEMON_HEALTH_FILE.exists():
        return {}
    try:
        return json.loads(DAEMON_HEALTH_FILE.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def _detect_failures() -> list[dict]:
    """Detect critical failures from daemon health + agent signals."""
    failures = []
    health = _load_daemon_health()
    now = datetime.now(timezone.utc)
    stale_cutoff = now - timedelta(minutes=10)

    for name, info in health.items():
        if not isinstance(info, dict):
            continue
        # Check for error status
        if info.get("status") == "error":
            failures.append({
                "type": "daemon_error", "daemon": name,
                "severity": 0.7,
            })
        # Check for stale heartbeat (>10 min without update)
        ts_str = info.get("timestamp", "")
        if ts_str:
            try:
                ts = datetime.fromisoformat(ts_str)
                if ts < stale_cutoff:
                    failures.append({
                        "type": "daemon_stale", "daemon": name,
                        "severity": 0.5, "last_seen": ts_str,
                    })
            except (ValueError, TypeError):
                pass
        # Check exit code in metrics
        metrics = info.get("metrics", {})
        if isinstance(metrics, dict) and metrics.get("exit_code", 0) != 0:
            failures.append({
                "type": "daemon_exit_error", "daemon": name,
                "severity": 0.6,
            })

    # Check contention for extreme waits
    contention_file = HEALTH_DIR / "contention_tracker.json"
    if contention_file.exists():
        try:
            ct = json.loads(contention_file.read_text(encoding="utf-8"))
            for fname, data in ct.items():
                if data.get("max_wait", 0) > 30:
                    failures.append({
                        "type": "extreme_contention", "file": fname,
                        "severity": 0.8, "max_wait": data["max_wait"],
                    })
        except (json.JSONDecodeError, OSError):
            pass

    return failures


def _compute_max_severity(failures: list[dict]) -> float:
    """Compute overall severity from failure list."""
    if not failures:
        return 0.0
    return max(f.get("severity", 0) for f in failures)


def _write_triage_overrides(suspend: bool):
    """Write interval overrides to suspend/resume low-priority daemons."""
    import os
    try:
        # Read existing overrides (from D13)
        existing = {}
        if OVERRIDE_FILE.exists():
            try:
                existing = json.loads(
                    OVERRIDE_FILE.read_text(encoding="utf-8"))
            except (json.JSONDecodeError, OSError):
                pass

        overrides = existing.get("overrides", {})
        if suspend:
            for sched in _SUSPENDABLE:
                # Get current interval or default, multiply by suspend factor
                current = overrides.get(sched, 3600)
                overrides[sched] = current * _SUSPEND_MULTIPLIER
        else:
            # Remove triage overrides (let D13 recalculate naturally)
            for sched in _SUSPENDABLE:
                overrides.pop(sched, None)

        data = {
            "overrides": overrides,
            "stress": existing.get("stress", 0.5),
            "zone": existing.get("zone", "elevated"),
            "updated": datetime.now(timezone.utc).isoformat(),
            "_triage_active": suspend,
        }
        OVERRIDE_FILE.parent.mkdir(parents=True, exist_ok=True)
        tmp = OVERRIDE_FILE.with_suffix(".tmp")
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2, ensure_ascii=False)
        os.replace(str(tmp), str(OVERRIDE_FILE))
    except OSError as e:
        _log.warning("Failed to write triage overrides: %s", e)


def _inject_emergency_context(failures: list[dict]) -> int:
    """Deep brain search for failure context, inject into hot cache."""
    try:
        from pulse.brain.brain_search import brain_search
        from pulse.brain.hot_cache import write_hot
    except ImportError:
        return 0

    # Build query from failure types
    terms = set()
    for f in failures[:5]:
        terms.add(f.get("daemon", f.get("file", "system")))
        terms.add(f.get("type", ""))
    query = " ".join(terms) + " recovery fix prevent"

    results = brain_search(query, max_per_source=3)
    items = []
    for source_items in results.get("results", {}).values():
        if isinstance(source_items, list):
            items.extend(source_items)
    items.sort(key=lambda x: x.get("relevance", 0), reverse=True)

    injected = 0
    now = datetime.now(timezone.utc).isoformat()
    for item in items[:5]:
        entry = {
            "content": item.get("text", "")[:300],
            "title": f"[TRIAGE] {item.get('title', '')[:60]}",
            "type": "triage",
            "salience": _EMERGENCY_SALIENCE,
            "confidence": item.get("confidence", 0.7),
            "source": "cognitive_triage",
            "domain": item.get("domain", "system"),
            "timestamp": now,
        }
        if write_hot(entry):
            injected += 1
    return injected


def _load_state() -> dict:
    if not METRICS_FILE.exists():
        return {"runs": 0, "triage_active": False, "total_events": 0}
    try:
        return json.loads(METRICS_FILE.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {"runs": 0, "triage_active": False, "total_events": 0}


def _save_state(state: dict):
    import os
    METRICS_FILE.parent.mkdir(parents=True, exist_ok=True)
    tmp = METRICS_FILE.with_suffix(".tmp")
    try:
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(state, f, indent=2, ensure_ascii=False)
        os.replace(str(tmp), str(METRICS_FILE))
    except OSError:
        if tmp.exists():
            tmp.unlink()


def run_once(verbose: bool = False) -> dict:
    """Run one triage cycle: detect → assess → respond."""
    state = _load_state()
    failures = _detect_failures()
    severity = _compute_max_severity(failures)

    # Check triage expiry
    if state.get("triage_active"):
        started = state.get("triage_started", "")
        if started:
            try:
                ts = datetime.fromisoformat(started)
                elapsed = (datetime.now(timezone.utc) - ts).total_seconds()
                if elapsed > _TRIAGE_MAX_HOURS * 3600:
                    if verbose:
                        _log.info("Triage expired (>%dh) — clearing",
                                  _TRIAGE_MAX_HOURS)
                    state["triage_active"] = False
                    _write_triage_overrides(suspend=False)
            except (ValueError, TypeError):
                pass

    now = datetime.now(timezone.utc).isoformat()
    injected = 0

    if severity >= _SEVERITY_THRESHOLD and not state.get("triage_active"):
        # Enter triage
        if verbose:
            _log.info("TRIAGE ACTIVATED — severity=%.2f, %d failure(s)",
                      severity, len(failures))
        _write_triage_overrides(suspend=True)
        injected = _inject_emergency_context(failures)
        state["triage_active"] = True
        state["triage_started"] = now
        state["current_severity"] = severity
        state["current_failures"] = [f.get("type", "") for f in failures[:5]]
        state["total_events"] = state.get("total_events", 0) + 1
        if verbose:
            _log.info("  Suspended %d low-priority daemons, injected %d items",
                      len(_SUSPENDABLE), injected)
    elif severity < _SEVERITY_THRESHOLD * 0.5 and state.get("triage_active"):
        # Clear triage (severity dropped significantly)
        if verbose:
            _log.info("Triage cleared — severity=%.2f (below threshold)",
                      severity)
        _write_triage_overrides(suspend=False)
        state["triage_active"] = False
        state["triage_started"] = None
    elif state.get("triage_active"):
        # Re-apply overrides (D13 may have overwritten them)
        _write_triage_overrides(suspend=True)
        injected = _inject_emergency_context(failures) if failures else 0

    state["runs"] = state.get("runs", 0) + 1
    state["last_run"] = now
    state["last_severity"] = severity
    state["emergency_injections"] = state.get("emergency_injections", 0) + injected
    _save_state(state)

    return {"failures": len(failures), "severity": severity,
            "triage_active": state.get("triage_active", False),
            "injected": injected, "status": "ok"}


def cmd_triage(args: list):
    """CLI entry point for `pulse triage`."""
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    if args and args[0] == "status":
        s = _load_state()
        active = "ACTIVE" if s.get("triage_active") else "inactive"
        print(f"[D27] Cognitive Triage — {active}, "
              f"Runs: {s.get('runs', 0)}, "
              f"Events: {s.get('total_events', 0)}")
        print(f"  Last severity: {s.get('last_severity', 0):.2f}, "
              f"Emergency injections: {s.get('emergency_injections', 0)}")
        if s.get("triage_active"):
            print(f"  Triage started: {s.get('triage_started', '?')}")
            print(f"  Failures: {s.get('current_failures', [])}")
        print(f"  Last run: {s.get('last_run', 'never')}")
        return
    result = run_once(verbose=True)
    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    import sys
    cmd_triage(sys.argv[1:])
