#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""D16: Per-Agent Procedural Memory — Individualized Intelligence.

Pipeline: PRUNE → SWEEP → PROFILE → SEARCH → SYNTHESIZE → STORE.
24h interval via orchestrator. Reads hebbian_matrix.json per-model stats,
finds weak domains, synthesizes custom defensive procedures per model.

Usage: python agent_procedure_synthesizer.py --once -v [--dry-run] | status
"""
from __future__ import annotations
import io, json, os, sys, time

if os.name == "nt" and __name__ == "__main__":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")

from pulse.core.paths import get_root_dir
from pulse.core.brain_utils import now_iso
from pulse.core.brain_io import _acquire

from pulse.daemons.synthesis.agent_synth_pipeline import (
    _load_hebbian, _find_weak_models, _count_existing,
    _search_domain_knowledge, _build_context,
    _synthesize_procedures, _fallback_procedures,
    _store_procedures, _prune_expired, MAX_PROCS_PER_MODEL,
)

ROOT = get_root_dir()
STATE_FILE = ROOT / "state" / "agent_procedure_synth_state.json"

_DEFAULT_STATE = {"runs": 0, "last_run": None, "last_elapsed_s": 0,
                  "total_procedures_created": 0, "total_pruned": 0,
                  "models_processed": []}


def _load_state() -> dict:
    if not STATE_FILE.exists():
        return dict(_DEFAULT_STATE)
    try:
        with _acquire(STATE_FILE):
            return json.loads(STATE_FILE.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return dict(_DEFAULT_STATE)


def _save_state(state: dict):
    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
    with _acquire(STATE_FILE):
        STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8")


def run_once(verbose: bool = False, dry_run: bool = False) -> dict:
    """Full D16 cycle: PRUNE → SWEEP → PROFILE → SEARCH → SYNTHESIZE → STORE."""
    state, t0 = _load_state(), time.time()
    vp = print if verbose else lambda *a: None

    # 1. Prune expired
    pruned = _prune_expired(vp)

    # 2. Sweep hebbian matrix
    matrix = _load_hebbian()
    if not matrix:
        vp("[D16] Hebbian matrix empty — nothing to do")
        state.update(last_run=now_iso(), last_elapsed_s=round(time.time() - t0, 1))
        state["runs"] = state.get("runs", 0) + 1
        _save_state(state)
        return {"models": 0, "procedures": 0, "pruned": pruned}

    # 3. Profile — find weak models
    weak_models = _find_weak_models(matrix)
    vp(f"[D16] {len(matrix)} models scanned, {len(weak_models)} with weak domains")

    if not weak_models:
        vp("[D16] No weak models found — all performing well")
        state.update(last_run=now_iso(), last_elapsed_s=round(time.time() - t0, 1))
        state["runs"] = state.get("runs", 0) + 1
        _save_state(state)
        return {"models": 0, "procedures": 0, "pruned": pruned}

    # 4. Per-model: SEARCH → SYNTHESIZE → STORE
    total_added = 0
    models_done = []
    for model_id, weak_domains in weak_models.items():
        vp(f"[D16] Model: {model_id}")
        for wd in weak_domains:
            vp(f"[D16]   Weak: {wd['domain']} (weight={wd['weight']}, rate={wd['success_rate']:.0%})")

        # Cap check
        existing = _count_existing(model_id)
        if existing >= MAX_PROCS_PER_MODEL:
            vp(f"[D16]   Cap reached ({existing}/{MAX_PROCS_PER_MODEL}) — skipping")
            continue

        # Search brain for domain knowledge
        knowledge = {"lessons": [], "failures": [], "anti_patterns": []}
        for wd in weak_domains[:3]:
            dk = _search_domain_knowledge(wd["domain"])
            knowledge["lessons"].extend(dk.get("lessons", []))
            knowledge["failures"].extend(dk.get("failures", []))
            knowledge["anti_patterns"].extend(dk.get("anti_patterns", []))

        if dry_run:
            vp(f"[D16]   [DRY RUN] Would synthesize for {model_id}")
            models_done.append(model_id)
            continue

        # Synthesize
        context = _build_context(model_id, weak_domains, knowledge)
        procs = _synthesize_procedures(model_id, context)
        if not procs:
            vp(f"[D16]   LLM synthesis failed — using fallback")
            procs = _fallback_procedures(model_id, weak_domains)

        # Store
        added = _store_procedures(model_id, procs, vp)
        vp(f"[D16]   {len(procs)} synthesized, {added} stored")
        total_added += added
        models_done.append(model_id)

    # Update state
    state.update(
        last_run=now_iso(),
        last_elapsed_s=round(time.time() - t0, 1),
        total_procedures_created=state.get("total_procedures_created", 0) + total_added,
        total_pruned=state.get("total_pruned", 0) + pruned,
        models_processed=list(set(state.get("models_processed", []) + models_done)),
    )
    state["runs"] = state.get("runs", 0) + 1
    _save_state(state)

    summary = {"models": len(models_done), "procedures": total_added, "pruned": pruned}
    vp(f"[D16] Done: {summary}")
    return summary


def show_status():
    """Print D16 agent procedure synthesizer stats."""
    s = _load_state()
    last = (s.get("last_run") or "never")[:19]
    print(f"\n  D16 Agent Procedure Synthesizer\n  {'=' * 40}")
    for k, v in [("Runs", s.get("runs", 0)), ("Last run", last),
                 ("Procedures", f"{s.get('total_procedures_created', 0)} created"),
                 ("Pruned", f"{s.get('total_pruned', 0)} expired"),
                 ("Models", ", ".join(s.get("models_processed", [])) or "none")]:
        print(f"  {k + ':':<14} {v}")
    if s.get("last_elapsed_s"):
        print(f"  {'Elapsed:':<14} {s['last_elapsed_s']}s")
    print()


if __name__ == "__main__":
    args = sys.argv[1:]
    if args and args[0] == "status":
        show_status()
    else:
        verbose = "-v" in args or "--verbose" in args
        run_once(verbose=verbose, dry_run="--dry-run" in args)
