#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Semantic Sleep Cycle — V6 RAG + V5 fallback.
V6: Embed → find new → top-5 vector search → surgical LLM merge (<2k tokens/call)
V5: Full brain → single Premium LLM consolidation (100k+ tokens)
Usage: python -m pulse.daemons.synthesis.sleep_cycle [--dry-run|--dispatch|--v5]
"""
from __future__ import annotations

import io, json, logging, os, sys, 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')

SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent.parent.parent
if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))

from pulse.core.brain_utils import (HIPPOCAMPUS_DIR, LESSONS_JSONL, FAILS_JSONL,
    PATTERNS_JSONL, read_jsonl_entries, STATE_DIR, now_iso as _now_iso)
log = logging.getLogger("pulse.sleep_cycle")

BRAIN_FILES = {
    "lessons": LESSONS_JSONL,
    "failures": FAILS_JSONL,
    "patterns": PATTERNS_JSONL,
}
ARCHIVE_DIR = STATE_DIR / "brain_archives"

MERGE_PROMPT = """You are the PULSE Brain performing a surgical rule merge.
NEW RULES (not yet consolidated):
{new_rules}
SIMILAR EXISTING RULES (from vector search):
{existing_rules}
For each new rule: MERGE if an existing rule covers same concept (produce ONE improved version), or APPEND if genuinely novel.
OUTPUT strict JSON only (no markdown fences):
{{"actions": [{{"new_rule": "text", "action": "merge"|"append", "result": "final text", "merged_with_idx": N|null}}]}}"""

V5_PROMPT = """You are the PULSE Brain Consolidation Engine. Produce a CONSOLIDATED version:
1. MERGE redundant rules 2. REMOVE contradictions 3. COMBINE overly-specific into general
4. PRESERVE unique insights 5. KEEP bullet format (- prefix). Reduce 30-60%, lose ZERO knowledge.
## Lessons ({lessons_count}): {lessons}
## Failures ({failures_count}): {failures}
## Patterns ({patterns_count}): {patterns}
OUTPUT strict JSON: {{"lessons": ["..."], "failures": ["..."], "patterns": ["..."],
"stats": {{"original_count": N, "consolidated_count": M, "reduction_pct": X}}}}"""

_KTYPE_MAP = {"lessons": "lesson", "failures": "failure", "patterns": "pattern"}

def _replay_priority_sort(items: list) -> list:
    """F11 (Item 20): Sort items by confidence (proxy for reward) descending - replay high-reward first."""
    try:
        items.sort(key=lambda x: float(x.get("confidence", 0) or x.get("reward_credit", 0) or 0), reverse=True)
    except Exception:
        pass  # fail-open
    return items

def _read_brain_section(filepath: Path) -> tuple[str, int]:
    """Read a JSONL brain file, return (bullet text for LLM, item_count)."""
    rows = read_jsonl_entries(filepath)
    if not rows:
        return "", 0
    items = [(r.get("content") or r.get("title", "")) for r in rows]
    text = "\n".join(f"- {item}" for item in items if len(item.strip()) >= 20)
    return text[:30000], len(items)

def _write_consolidated(filepath: Path, items: list[str], section_name: str):
    # P47: rewrite_jsonl routes to bulk_upsert (SQLite-first). With busy_timeout=60s this is reliable.
    """Write LLM-consolidated items back to JSONL brain."""
    from pulse.brain.save_writers import build_jsonl_entry, rewrite_jsonl
    ktype = _KTYPE_MAP.get(section_name.lower(), "lesson")
    entries = []
    for item in items:
        if not isinstance(item, str) or len(item.strip()) < 20:
            continue
        entries.append(build_jsonl_entry(
            description=item.strip(), source="sleep_cycle",
            timestamp=_now_iso(), domain="general",
            confidence=0.7, salience=2.0, knowledge_type=ktype,
            provenance={"stage": "sleep_consolidation"},
        ))
    rewrite_jsonl(filepath, entries)

def _archive_brain():
    """Archive current JSONL brain files before overwrite."""
    import shutil
    ARCHIVE_DIR.mkdir(parents=True, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    for key, src in BRAIN_FILES.items():
        if src.exists() and src.stat().st_size > 200:
            shutil.copy2(src, ARCHIVE_DIR / f"{src.stem}_presleep_{ts}.jsonl")

def _post_write_cleanup(verbose: bool = False):
    """Clear dedup cache + rebuild indexes after brain write."""
    try:
        from pulse.brain.save_writers import _DEDUP_CACHE
        _DEDUP_CACHE.clear()
    except Exception:
        pass
    try:
        from pulse.brain.index.search_index import build_index
        build_index(verbose=verbose)
    except Exception:
        pass
    try:
        from pulse.brain.index.rule_embeddings import build_rule_index
        build_rule_index(force=True, verbose=verbose)
    except Exception:
        pass

# Phase functions live in sleep_cycle_phases to keep this file focused on orchestration.
from pulse.daemons.synthesis.sleep.sleep_cycle_phases import (
    _run_rag_cycle,
    _run_full_brain_cycle,
    _run_nrem_phase,
    _run_rem_phase,
)

def run_sleep_cycle(dry_run: bool = False, verbose: bool = False, force_v5: bool = False,
                    phase: str = "full") -> dict:
    """Main entry: run sleep cycle with optional phase control.

    Args:
        phase: "nrem" — only NREM (declarative consolidation, task_type=fast)
               "rem"  — only REM (associative synthesis, task_type=standard)
               "full" — both phases in order (default)
    """
    phase = phase.lower().strip()
    if phase == "nrem":
        return _run_nrem_phase(dry_run, verbose, force_v5)
    if phase == "rem":
        return _run_rem_phase(dry_run, verbose)
    # "full": NREM first, then REM
    start = time.time()
    nrem_result = _run_nrem_phase(dry_run, verbose, force_v5)
    rem_result = _run_rem_phase(dry_run, verbose)
    return {
        "phase": "full",
        "status": "completed",
        "nrem": nrem_result,
        "rem": rem_result,
        "elapsed_seconds": round(time.time() - start, 2),
    }

def dispatch_sleep_task() -> str:
    """Post the Sleep Cycle as a task to the Swarm board."""
    from pulse.tasks.pulse_dispatcher import TaskDispatcher
    tid = f"sleep_{int(time.time())}"
    task = {
        "task_id": tid, "title": "[SLEEP_CYCLE] Nightly Brain Consolidation (V6 RAG)",
        "description": "Vectorized RAG consolidation: embed → find new → surgical merge.",
        "domain": "brain_consolidation", "complexity": "high",
        "recommended_tier": "premium", "status": "open",
        "required_capabilities": [], "depends_on": [],
        "claimed_by": None, "claimed_at": None, "files_touched": [],
        "outcome": None, "memory_data": {"type": "SLEEP_CYCLE"},
    }
    TaskDispatcher(verbose=False).dispatch(
        prompt="[SLEEP_CYCLE] Nightly brain consolidation (V6 RAG)",
        tasks=[task], founder_override=True)
    return tid

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(asctime)s [SLEEP] %(message)s")
    dry_run = "--dry-run" in sys.argv
    verbose = "--verbose" in sys.argv or "-v" in sys.argv or dry_run
    if "--dispatch" in sys.argv:
        print(f"Dispatched Sleep Cycle task: {dispatch_sleep_task()}")
    else:
        _phase = "full"
        for _arg in sys.argv:
            if _arg.startswith("--phase="):
                _phase = _arg.split("=", 1)[1]
                break
        print(json.dumps(run_sleep_cycle(dry_run=dry_run, verbose=verbose,
                                          force_v5="--v5" in sys.argv,
                                          phase=_phase), indent=2))
