#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""D14: Procedural Sleep Healer — Self-Correcting Workflow Brain.
FILTER → DETECT → PROMOTE → PERSIST. Every 12h via orchestrator (before sleep_cycle).
Pre-consolidation quality gate: strips low-success evidence, flags capture gaps,
promotes proven procedures to auto_patterns.jsonl.
"""
from __future__ import annotations

import io
import json
import os
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

if os.name == "nt" and __name__ == "__main__":
    if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(encoding='utf-8', errors='replace')
    if hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(encoding='utf-8', errors='replace')

from pulse.core.brain_utils import (
    HIPPOCAMPUS_DIR, STATE_DIR, PATTERNS_JSONL, EVIDENCE_METRICS_DIR, now_iso,
)
from pulse.core.brain_io import _acquire, save_json

STATE_FILE = STATE_DIR / "healer_state.json"
PROCEDURAL_LOG = EVIDENCE_METRICS_DIR / "procedural_log.json"
WANTS_FILE = HIPPOCAMPUS_DIR / "Intents" / "wants.json"
DONT_WANTS_FILE = HIPPOCAMPUS_DIR / "Intents" / "dont_wants.json"
CAPTURE_STATE = STATE_DIR / "auto_capture_state.json"

# Thresholds (from OG DMN vision — Session 035018)
FAIL_THRESHOLD = 0.60       # procedures below this = low-success
MIN_EXECUTIONS = 3          # minimum runs to judge a procedure
PROMOTE_CONFIDENCE = 0.90   # procedures above this get promoted
PROMOTE_MIN_RUNS = 5        # minimum executions for promotion
CAPTURE_GAP_SECONDS = 7200  # 2h — evidence during gaps is tainted
HISTORY_CAP = 50            # rolling sweep history
# Quality tightening (stricter than _clean_wants 3w/10c)
TIGHT_MIN_WORDS = 5
TIGHT_MIN_CHARS = 25


def _load_json(path: Path) -> dict | list:
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def _load_state() -> dict:
    s = _load_json(STATE_FILE)
    if not isinstance(s, dict):
        s = {}
    s.setdefault("runs", 0)
    s.setdefault("sweep_history", [])
    s.setdefault("promoted_ids", [])
    s.setdefault("tainted_items", [])
    s.setdefault("lifetime", {"stripped": 0, "tainted": 0, "promoted": 0})
    return s

def _save_state(state: dict):
    state["sweep_history"] = state.get("sweep_history", [])[-HISTORY_CAP:]
    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
    with _acquire(STATE_FILE):
        save_json(STATE_FILE, state)


def _load_procedures() -> list[dict]:
    """Load procedure list from procedural_log.json."""
    data = _load_json(PROCEDURAL_LOG)
    return data.get("procedures", []) if isinstance(data, dict) else []

def _read_intents(path: Path) -> list[dict]:
    data = _load_json(path)
    return data if isinstance(data, list) else []

def _write_intents(path: Path, items: list[dict]):
    path.parent.mkdir(parents=True, exist_ok=True)
    with _acquire(path):
        path.write_text(json.dumps(items, indent=2, ensure_ascii=False), encoding="utf-8")

def _sweep_low_success(verbose: bool = False, dry_run: bool = False) -> dict:
    """Strip evidence from domains with failing procedures + quality-tighten."""
    vp = print if verbose else lambda *a: None
    procedures = _load_procedures()
    # Find failing procedures
    failing = [p for p in procedures
               if p.get("executions", 0) >= MIN_EXECUTIONS
               and p.get("success_rate", 1.0) < FAIL_THRESHOLD]
    if not failing:
        vp("[D14] Pillar 1: No failing procedures (or insufficient data)")
        return {"stripped": 0, "failing_procedures": [], "quality_tightened": False}
    failing_names = [p.get("procedure_id", "?") for p in failing]
    failing_domains = {p.get("domain", "general") for p in failing} - {"general"}
    vp(f"[D14] Pillar 1: {len(failing)} failing procedures, "
       f"domains={failing_domains or '{quality-tighten only}'}")
    total_stripped = 0
    for intent_file in (WANTS_FILE, DONT_WANTS_FILE):
        items = _read_intents(intent_file)
        if not items:
            continue
        before = len(items)
        filtered = []
        for item in items:
            domain = item.get("domain", "general")
            # PASS 1: domain-match removal for non-general failing domains
            if domain in failing_domains:
                total_stripped += 1
                continue
            # PASS 2: quality tightening (when failures exist)
            text = item.get("want") or item.get("dont_want") or ""
            words = text.split()
            if len(words) < TIGHT_MIN_WORDS or len(text) < TIGHT_MIN_CHARS:
                total_stripped += 1
                continue
            filtered.append(item)
        if len(filtered) < before and not dry_run:
            _write_intents(intent_file, filtered)
        vp(f"  {intent_file.name}: {before} → {len(filtered)} "
           f"(-{before - len(filtered)})")
    return {"stripped": total_stripped, "failing_procedures": failing_names,
            "quality_tightened": True}

def _detect_tainted(verbose: bool = False, dry_run: bool = False) -> dict:
    """Flag evidence captured during capture daemon gaps."""
    vp = print if verbose else lambda *a: None
    capture = _load_json(CAPTURE_STATE)
    if not isinstance(capture, dict) or "last_log_batch" not in capture:
        vp("[D14] Pillar 2: No capture state — skipping taint detection")
        return {"tainted": 0, "capture_gap_hours": 0}
    try:
        last_batch = datetime.fromisoformat(
            capture["last_log_batch"].replace("Z", "+00:00"))
    except (ValueError, TypeError):
        return {"tainted": 0, "capture_gap_hours": 0}
    now_dt = datetime.now(timezone.utc)
    gap_seconds = (now_dt - last_batch).total_seconds()
    if gap_seconds <= CAPTURE_GAP_SECONDS:
        vp(f"[D14] Pillar 2: Capture fresh ({gap_seconds:.0f}s ago) — no taint")
        return {"tainted": 0, "capture_gap_hours": round(gap_seconds / 3600, 1)}
    # Capture has been stale — find items in the gap window
    gap_start = last_batch
    gap_end = now_dt
    vp(f"[D14] Pillar 2: Capture gap {gap_seconds / 3600:.1f}h "
       f"({gap_start.isoformat()[:19]} → now)")
    tainted = []
    for intent_file in (WANTS_FILE, DONT_WANTS_FILE):
        items = _read_intents(intent_file)
        for item in items:
            try:
                ts = datetime.fromisoformat(
                    item.get("timestamp", "").replace("Z", "+00:00"))
                if gap_start <= ts <= gap_end and item.get("_hash"):
                    tainted.append({
                        "file": intent_file.name,
                        "hash": item["_hash"],
                        "timestamp": item["timestamp"],
                        "reason": "capture_gap",
                    })
            except (ValueError, TypeError):
                continue
    vp(f"  Found {len(tainted)} items in capture gap window")
    if tainted and not dry_run:
        state = _load_state()
        state["tainted_items"] = tainted  # overwrite — latest scan only
        _save_state(state)
    return {"tainted": len(tainted),
            "capture_gap_hours": round(gap_seconds / 3600, 1),
            "gap_window": [gap_start.isoformat()[:19], gap_end.isoformat()[:19]]}

def _promote_proven(verbose: bool = False, dry_run: bool = False) -> dict:
    """Promote high-confidence procedures to auto_patterns.jsonl."""
    from pulse.brain.save_writers import append_knowledge_md
    vp = print if verbose else lambda *a: None
    procedures = _load_procedures()
    state = _load_state()
    already = set(state.get("promoted_ids", []))
    candidates = [p for p in procedures
                  if p.get("confidence", 0) >= PROMOTE_CONFIDENCE
                  and p.get("executions", 0) >= PROMOTE_MIN_RUNS
                  and p.get("procedure_id") not in already]
    if not candidates:
        vp("[D14] Pillar 3: No procedures qualify for promotion")
        return {"promoted": 0, "procedure_ids": []}
    promoted_ids = []
    for proc in candidates:
        pid = proc.get("procedure_id", "unknown")
        name = proc.get("name", pid.replace("_", " ").title())
        desc = proc.get("description", "")
        seq = proc.get("event_sequence", [])
        seq_str = " → ".join(seq) if seq else "observed workflow"
        domain = proc.get("domain", "general")
        conf = proc.get("confidence", 0.9)
        pattern_text = (f"Proven Workflow: {name} — {desc or seq_str}. "
                        f"Sequence: {seq_str}. "
                        f"Success rate: {proc.get('success_rate', 0):.0%} "
                        f"over {proc.get('executions', 0)} executions")
        vp(f"  PROMOTE: {pid} (conf={conf:.2f}, runs={proc.get('executions', 0)})")
        if not dry_run:
            append_knowledge_md(
                PATTERNS_JSONL, pattern_text, "procedure_healer",
                now_iso(), domain, conf, salience=8.0)
        promoted_ids.append(pid)
    if promoted_ids and not dry_run:
        state = _load_state()
        state["promoted_ids"] = list(set(state.get("promoted_ids", []))
                                     | set(promoted_ids))
        _save_state(state)
    return {"promoted": len(promoted_ids), "procedure_ids": promoted_ids}

def run_once(verbose: bool = False, dry_run: bool = False) -> dict:
    """Full D14 cycle: FILTER → DETECT → PROMOTE → PERSIST."""
    t0 = time.time()
    vp = print if verbose else lambda *a: None
    vp("[D14] Procedural Sleep Healer starting...")
    r1 = _sweep_low_success(verbose, dry_run)
    r2 = _detect_tainted(verbose, dry_run)
    r3 = _promote_proven(verbose, dry_run)
    if dry_run:
        vp("[D14] Dry run — no state persisted")
        return {"stripped": r1, "tainted": r2, "promoted": r3}
    state = _load_state()
    lt = state.get("lifetime", {"stripped": 0, "tainted": 0, "promoted": 0})
    lt["stripped"] = lt.get("stripped", 0) + r1.get("stripped", 0)
    lt["tainted"] = lt.get("tainted", 0) + r2.get("tainted", 0)
    lt["promoted"] = lt.get("promoted", 0) + r3.get("promoted", 0)
    state.update(last_run=now_iso(), runs=state.get("runs", 0) + 1,
                 last_sweep={"stripped": r1, "tainted": r2, "promoted": r3},
                 lifetime=lt, last_elapsed_s=round(time.time() - t0, 2))
    state["sweep_history"].append({
        "ts": now_iso()[:19],
        "strip": r1.get("stripped", 0),
        "taint": r2.get("tainted", 0),
        "promote": r3.get("promoted", 0),
    })
    _save_state(state)
    vp(f"[D14] Done in {state['last_elapsed_s']}s — "
       f"strip={r1.get('stripped', 0)} taint={r2.get('tainted', 0)} "
       f"promote={r3.get('promoted', 0)}")
    return {"stripped": r1, "tainted": r2, "promoted": r3}


def show_status():
    """Print D14 sweep metrics and promoted workflows."""
    state = _load_state()
    last = (state.get("last_run") or "never")[:19]
    lt = state.get("lifetime", {})
    sw = state.get("last_sweep", {})
    s1, s2, s3 = sw.get("stripped", {}), sw.get("tainted", {}), sw.get("promoted", {})
    print(f"\n  D14 Procedural Sleep Healer\n  {'=' * 40}")
    print(f"  Runs:           {state.get('runs', 0)}")
    print(f"  Last run:       {last}")
    tq = " (quality-tightened)" if s1.get("quality_tightened") else ""
    print(f"\n  Last Sweep:     strip={s1.get('stripped', 0)}{tq} | "
          f"taint={s2.get('tainted', 0)} (gap={s2.get('capture_gap_hours', 0):.1f}h) | "
          f"promote={s3.get('promoted', 0)}")
    print(f"  Lifetime:       strip={lt.get('stripped', 0)} | "
          f"taint={lt.get('tainted', 0)} | promote={lt.get('promoted', 0)}")
    promoted = state.get("promoted_ids", [])
    if promoted:
        procs = _load_procedures()
        pm = {p["procedure_id"]: p for p in procs if "procedure_id" in p}
        print(f"\n  Promoted Workflows:")
        for pid in promoted:
            p = pm.get(pid, {})
            print(f"    {pid:<35} ({p.get('confidence', 0):.2f} conf, "
                  f"{p.get('executions', 0)} runs)")
    print()


def get_last_sweep() -> dict:
    """Read last sweep results from state file."""
    s = _load_json(STATE_FILE)
    return s.get("last_sweep", {}) if isinstance(s, dict) else {}

def get_total_actions() -> dict:
    """Read lifetime aggregate stats."""
    s = _load_json(STATE_FILE)
    return s.get("lifetime", {}) if isinstance(s, dict) else {}


def cmd_healer(args: list):
    """CLI entry: pulse healer [--dry-run] [-v] [status]."""
    if args and args[0] == "status":
        return show_status()
    verbose = "-v" in args or "--verbose" in args
    r = run_once(verbose=verbose, dry_run="--dry-run" in args)
    if not verbose:
        print(f"[D14] strip={r['stripped'].get('stripped', 0)} "
              f"taint={r['tainted'].get('tainted', 0)} "
              f"promote={r['promoted'].get('promoted', 0)}")


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