#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Sleep Cycle Phases — V6 RAG + V5 fallback sub-cycles.

Contains:
  _run_rag_cycle       — V6: embed → find new → surgical LLM merge (<2k tokens/call)
  _run_full_brain_cycle — V5: full brain → single Premium LLM consolidation
  _run_nrem_phase      — NREM: declarative fact consolidation (fast task_type)
  _run_rem_phase       — REM: creative/associative synthesis (standard task_type)
"""
from __future__ import annotations

import logging
import time

log = logging.getLogger("pulse.sleep_cycle")


def _run_rag_cycle(dry_run: bool = False, verbose: bool = False) -> dict:
    """V6 RAG path: embed → find new rules → surgical LLM merge."""
    from pulse.brain.index.rule_embeddings import build_rule_index, get_new_rules, search_similar
    from pulse.core.llm_router import dispatch, extract_json
    from pulse.daemons.synthesis.sleep.sleep_cycle import (
        BRAIN_FILES, MERGE_PROMPT,
        _replay_priority_sort, _archive_brain, _read_brain_section,
        _write_consolidated, _post_write_cleanup,
    )

    start = time.time()
    if verbose:
        log.info("V6 RAG Sleep Cycle starting...")

    # 1. Build/refresh embedding index
    idx_result = build_rule_index(verbose=verbose)
    if idx_result.get("status") == "empty":
        return {"status": "skipped", "reason": "No brain rules to embed", "mode": "rag"}

    # 2. Find new (un-indexed) rules
    new_rules = get_new_rules()
    if not new_rules:
        # Nothing new — rebuild index anyway to stay fresh
        build_rule_index(force=True, verbose=verbose)
        return {"status": "skipped", "reason": "No new rules since last cycle", "mode": "rag",
                "elapsed_seconds": round(time.time() - start, 2)}

    if verbose:
        log.info("Found %d new rules to consolidate", len(new_rules))

    # 3. Process in adaptive micro-batches (Item 127: sleep pressure scaling)
    new_rules = _replay_priority_sort(new_rules)
    merged_count = 0
    appended_count = 0
    all_results: list[dict] = []

    try:
        from pulse.daemons.synthesis.sleep.sleep_pressure import get_adaptive_batch_size
        batch_size = get_adaptive_batch_size(BRAIN_FILES)
    except Exception:
        batch_size = 10

    for i in range(0, len(new_rules), batch_size):
        batch = new_rules[i:i + batch_size]

        # Find similar existing rules for each new rule in batch
        existing_set: dict[str, dict] = {}
        for rule in batch:
            similar = search_similar(rule["text"], top_k=5)
            for s in similar:
                existing_set[s["text"]] = s

        new_text = "\n".join(f"- {r['text']}" for r in batch)
        existing_text = "\n".join(
            f"{j+1}. [{s['source']}] (sim={s['similarity']}) {s['text']}"
            for j, s in enumerate(existing_set.values())
        ) or "(no similar rules found)"

        prompt = MERGE_PROMPT.format(new_rules=new_text, existing_rules=existing_text)

        if verbose:
            log.info("Batch %d: %d new rules, %d similar, ~%d tokens",
                     i // 10 + 1, len(batch), len(existing_set), len(prompt) // 4)

        raw = dispatch(prompt, task_type="fast")
        result = extract_json(raw)

        for action in result.get("actions", []):
            if not isinstance(action, dict):
                continue
            act = action.get("action", "append")
            text = action.get("result", "")
            if isinstance(text, str) and len(text.strip()) >= 20:
                all_results.append({"action": act, "text": text.strip(),
                                    "source": batch[0]["source"] if batch else "lessons"})
                if act == "merge":
                    merged_count += 1
                else:
                    appended_count += 1

    if verbose:
        log.info("RAG results: %d merged, %d appended", merged_count, appended_count)

    if dry_run:
        if verbose:
            for r in all_results[:10]:
                log.info("  [%s] %s", r["action"].upper(), r["text"][:80])
        return {"status": "dry_run", "mode": "rag", "merged": merged_count,
                "appended": appended_count, "elapsed_seconds": round(time.time() - start, 2)}

    # 4. Apply results: archive, merge, write
    _archive_brain()
    name_map = {"lessons": "Lessons", "failures": "Failures", "patterns": "Patterns"}

    for section_key, filepath in BRAIN_FILES.items():
        text, count = _read_brain_section(filepath)
        current_items = [line.strip()[2:] for line in text.splitlines() if line.strip().startswith("- ")]
        new_for_section = [r["text"] for r in all_results if r["source"] == section_key and r["action"] == "append"]
        merged_for_section = [r["text"] for r in all_results if r["source"] == section_key and r["action"] == "merge"]
        final_items = current_items + new_for_section + merged_for_section
        # Dedup by exact text
        seen = set()
        deduped = []
        for item in final_items:
            if item not in seen:
                seen.add(item)
                deduped.append(item)
        if deduped:
            _write_consolidated(filepath, deduped, name_map[section_key])
            if verbose:
                log.info("  %s: %d -> %d items", section_key, count, len(deduped))

    _post_write_cleanup(verbose=verbose)
    elapsed = round(time.time() - start, 2)
    if verbose:
        log.info("V6 RAG Sleep Cycle complete in %ss", elapsed)
    return {"status": "completed", "mode": "rag", "merged": merged_count,
            "appended": appended_count, "elapsed_seconds": elapsed}


def _run_full_brain_cycle(dry_run: bool = False, verbose: bool = False) -> dict:
    """V5 fallback: full brain → single Premium LLM consolidation."""
    from pulse.core.llm_router import dispatch, extract_json
    from pulse.daemons.synthesis.sleep.sleep_cycle import (
        BRAIN_FILES, V5_PROMPT,
        _archive_brain, _read_brain_section, _write_consolidated, _post_write_cleanup,
    )

    start = time.time()
    sections, total_items = {}, 0
    for name, fp in BRAIN_FILES.items():
        text, count = _read_brain_section(fp)
        sections[name] = {"text": text, "count": count, "path": fp}
        total_items += count
    if total_items < 20:
        return {"status": "skipped", "reason": f"Brain too small ({total_items})", "mode": "v5"}
    prompt = V5_PROMPT.format(
        lessons=sections["lessons"]["text"], lessons_count=sections["lessons"]["count"],
        failures=sections["failures"]["text"], failures_count=sections["failures"]["count"],
        patterns=sections["patterns"]["text"], patterns_count=sections["patterns"]["count"])
    result = extract_json(dispatch(prompt, task_type="premium"))
    consolidated = {}
    for key in ("lessons", "failures", "patterns"):
        items = result.get(key, [])
        consolidated[key] = [s.strip() for s in (items if isinstance(items, list) else [])
                             if isinstance(s, str) and len(s.strip()) >= 20]
    new_total = sum(len(v) for v in consolidated.values())
    reduction = round((1 - new_total / total_items) * 100, 1) if total_items > 0 else 0
    if reduction > 80:
        return {"status": "refused", "reason": f"{reduction}% reduction too aggressive", "mode": "v5"}
    if not dry_run:
        _archive_brain()
        nm = {"lessons": "Lessons", "failures": "Failures", "patterns": "Patterns"}
        for key, items in consolidated.items():
            if items:
                _write_consolidated(sections[key]["path"], items, nm[key])
        _post_write_cleanup(verbose=verbose)
    return {"status": "dry_run" if dry_run else "completed", "mode": "v5",
            "original_items": total_items, "consolidated_items": new_total,
            "reduction_pct": reduction, "elapsed_seconds": round(time.time() - start, 2)}


def _run_nrem_phase(dry_run: bool = False, verbose: bool = False, force_v5: bool = False) -> dict:
    """NREM phase: declarative fact consolidation.

    Runs: session_replay, pattern_mining, schema_promotion, reconsolidation.
    Uses task_type='fast' (cheaper, deterministic).
    """
    if verbose:
        log.info("=== NREM Phase: declarative consolidation ===")
    # Core consolidation — V6 RAG (session replay + pattern mining) or V5 fallback
    if force_v5:
        result = _run_full_brain_cycle(dry_run, verbose)
    else:
        try:
            result = _run_rag_cycle(dry_run, verbose)
        except Exception as e:
            log.warning("NREM V6 RAG failed (%s), falling back to V5", e)
            result = _run_full_brain_cycle(dry_run, verbose)

    # Schema promotion (declarative: promote high-confidence patterns)
    try:
        from pulse.daemons.consolidation.consolidation_promotion import stage_schema_promotion
        schema_result = stage_schema_promotion([])
        result["nrem_schema_promotion"] = schema_result
    except Exception as e:
        log.debug("NREM schema_promotion skipped: %s", e)

    result["phase"] = "nrem"
    return result


def _run_rem_phase(dry_run: bool = False, verbose: bool = False) -> dict:
    """REM phase: creative/associative synthesis.

    Runs: associative_replay, DMN dreaming, cross-domain transfer.
    Uses task_type='standard' (higher temperature creative synthesis).
    """
    if verbose:
        log.info("=== REM Phase: associative/creative synthesis ===")
    start = time.time()
    result: dict = {"phase": "rem", "status": "completed"}

    # Associative replay via DMN daemon
    try:
        from pulse.daemons.synthesis.core.dmn_daemon import dream_session as run_dmn_cycle
        dmn_result = run_dmn_cycle()
        result["rem_dmn"] = dmn_result if dmn_result else {"skipped": True, "reason": "no result"}
    except Exception as e:
        log.debug("REM DMN dreaming skipped: %s", e)
        result["rem_dmn"] = {"skipped": True, "reason": str(e)}

    # Cross-domain transfer — stub until dedicated cross-domain synthesizer is implemented
    def _stub_cross_domain():
        log.debug("cross-domain transfer not yet implemented")
        return {"skipped": True, "reason": "cross-domain transfer not yet implemented"}
    result["rem_cross_domain"] = _stub_cross_domain()

    result["elapsed_seconds"] = round(time.time() - start, 2)
    return result
