#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Intent Daemon — Brain V6 Phase 2

Reads lessons, wants, and patterns from the brain, uses LLM (Ollama cloud → paid
fallback) to synthesize strategic intents — high-level goals and directions.
Writes to Hippocampus/Evidence/Intents/strategic_intents.json.

Usage: python -m pulse.daemons.bridge.intent_daemon [--once] [--batch] [--verbose] [--dry-run]
  --batch: Sweep entire brain in chunks (multiple LLM calls). Use for initial population.
  --once:  Single chunk extraction then exit.
  Daemon mode (default): Sliding window — advances through brain each 6h cycle.
"""
from __future__ import annotations

import json
import logging
import sys
import time
from pathlib import Path

from pulse.core.brain_utils import HIPPOCAMPUS_DIR, now_iso, save_json
from pulse.core.llm_router import dispatch, extract_json

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

INTENTS_FILE = HIPPOCAMPUS_DIR / "Evidence" / "Intents" / "strategic_intents.json"
WANTS_FILE = HIPPOCAMPUS_DIR / "Evidence" / "Intents" / "wants.json"
DONT_WANTS_FILE = HIPPOCAMPUS_DIR / "Evidence" / "Intents" / "dont_wants.json"
LESSONS_FILE = HIPPOCAMPUS_DIR / "Lessons" / "auto_lessons.md"
PATTERNS_FILE = HIPPOCAMPUS_DIR / "Patterns" / "auto_patterns.md"
STATE_FILE = HIPPOCAMPUS_DIR / ".intent_daemon_state.json"
CYCLE_INTERVAL = 6 * 3600  # 6 hours
CHUNK_SIZE = 80  # titles per LLM call

EXTRACT_PROMPT = """You are analyzing a knowledge brain to synthesize STRATEGIC INTENTS — high-level goals and directions the system is moving toward.

Analyze the wants (what the user desires), dont_wants (what to avoid), lessons learned, and patterns to identify 5-15 strategic intents.

A strategic intent is a DIRECTIONAL GOAL, not a task. Examples:
- "Minimize cloud API costs by prioritizing free Ollama models"
- "Achieve 100% brain-first compliance across all agents"
- "Build self-healing infrastructure that survives process crashes"
- "Keep all code under 300 lines per file via SoC enforcement"

WANTS (what the user desires):
{wants}

DONT WANTS (what to avoid):
{dont_wants}

LESSONS (batch {batch_label}):
{lessons}

PATTERNS (batch {batch_label}):
{patterns}

EXISTING INTENTS (do NOT duplicate):
{existing}

Output ONLY valid JSON:
{{"intents": [{{"title": "Short goal statement", "description": "Why this matters and evidence", "priority": "high|medium|low", "domain": "category"}}]}}

Rules:
- Synthesize from MULTIPLE sources (wants + lessons + patterns)
- Each intent should represent a strategic direction, not a single task
- Priority: high = mentioned repeatedly, medium = clear evidence, low = emerging trend
- Domain: architecture, cost, reliability, brain, agents, security, ux, general
- Do NOT duplicate existing intents
- Output ONLY the JSON object, nothing else"""


def _get_all_titles(filepath: Path) -> list[str]:
    """Extract ALL section titles from a markdown brain file."""
    if not filepath.exists():
        return []
    text = filepath.read_text(encoding="utf-8", errors="replace")
    sections = text.split("\n## ")
    return [s.split("\n")[0].strip() for s in sections[1:] if s.strip()]


def _titles_to_text(titles: list[str]) -> str:
    return "\n".join(f"- {t}" for t in titles)


def _read_json_items(filepath: Path, key: str, max_items: int = 50) -> str:
    if not filepath.exists():
        return ""
    try:
        data = json.loads(filepath.read_text(encoding="utf-8"))
        items = data if isinstance(data, list) else data.get(key, data.get("intents", []))
        if not isinstance(items, list):
            return ""
        return "\n".join(f"- {item.get(key, item.get('want', item.get('title', str(item)[:100])))}"
                         for item in items[:max_items])
    except Exception:
        return ""


def _load_state() -> dict:
    if STATE_FILE.exists():
        try:
            return json.loads(STATE_FILE.read_text(encoding="utf-8"))
        except Exception:
            pass
    return {"lesson_offset": 0, "pattern_offset": 0}


def _save_state(state: dict):
    try:
        STATE_FILE.write_text(json.dumps(state), encoding="utf-8")
    except Exception:
        pass


def _load_intents() -> list:
    if not INTENTS_FILE.exists():
        return []
    try:
        data = json.loads(INTENTS_FILE.read_text(encoding="utf-8"))
        if isinstance(data, list):
            return data
        return data.get("intents", [])
    except Exception:
        return []


def _save_intents(intents: list):
    INTENTS_FILE.parent.mkdir(parents=True, exist_ok=True)
    output = {"synthesized_at": now_iso(), "intent_count": len(intents), "intents": intents}
    save_json(INTENTS_FILE, output)


def _extract_one_chunk(lesson_titles: list[str], pattern_titles: list[str],
                       wants_text: str, dont_wants_text: str,
                       batch_label: str, current: list, existing_titles: set,
                       dry_run: bool, verbose: bool) -> int:
    """Run one LLM extraction on a chunk of titles. Returns count of items added."""
    if not lesson_titles and not pattern_titles and not wants_text:
        return 0

    existing_summary = "\n".join(
        f"- {i['title']}" for i in current[-30:] if i.get("title")
    ) or "(none yet)"

    prompt = EXTRACT_PROMPT.format(
        wants=wants_text or "(none)", dont_wants=dont_wants_text or "(none)",
        lessons=_titles_to_text(lesson_titles) or "(none)",
        patterns=_titles_to_text(pattern_titles) or "(none)",
        existing=existing_summary, batch_label=batch_label,
    )

    if verbose:
        log.info("Chunk %s: %d lessons + %d patterns (%d chars)",
                 batch_label, len(lesson_titles), len(pattern_titles), len(prompt))

    try:
        raw = dispatch(prompt, task_type="fast")
        result = extract_json(raw)
    except Exception as e:
        log.error("LLM dispatch failed on chunk %s: %s", batch_label, e)
        return 0

    added = 0
    for intent in result.get("intents", []):
        title = intent.get("title", "").strip()
        if not title or title.lower() in existing_titles:
            continue
        intent.setdefault("priority", "medium")
        intent.setdefault("domain", "general")
        intent["created"] = now_iso()
        intent["source"] = "intent_daemon"
        current.append(intent)
        existing_titles.add(title.lower())
        added += 1
    return added


def run_intent_batch(dry_run: bool = False, verbose: bool = False) -> dict:
    """Batch mode: sweep entire brain in chunks with multiple LLM calls."""
    all_lessons = _get_all_titles(LESSONS_FILE)
    all_patterns = _get_all_titles(PATTERNS_FILE)
    wants_text = _read_json_items(WANTS_FILE, "want")
    dont_wants_text = _read_json_items(DONT_WANTS_FILE, "dont_want")
    log.info("Batch mode: %d lessons + %d patterns", len(all_lessons), len(all_patterns))

    current = _load_intents()
    existing_titles = {i.get("title", "").lower() for i in current}
    total_added = 0

    lesson_chunks = [all_lessons[i:i + CHUNK_SIZE] for i in range(0, max(len(all_lessons), 1), CHUNK_SIZE)]
    pattern_chunks = [all_patterns[i:i + CHUNK_SIZE] for i in range(0, max(len(all_patterns), 1), CHUNK_SIZE)]
    n_chunks = max(len(lesson_chunks), len(pattern_chunks))

    for i in range(n_chunks):
        l_chunk = lesson_chunks[i] if i < len(lesson_chunks) else []
        p_chunk = pattern_chunks[i] if i < len(pattern_chunks) else []
        label = f"{i + 1}/{n_chunks}"
        added = _extract_one_chunk(l_chunk, p_chunk, wants_text, dont_wants_text,
                                   label, current, existing_titles, dry_run, verbose)
        total_added += added
        log.info("Chunk %s: +%d intents (running total: %d)", label, added, total_added)

        if added > 0 and not dry_run:
            _save_intents(current)

    log.info("Batch complete: %d new intents (total: %d)", total_added, len(current))
    return {"status": "ok", "new": total_added, "total": len(current), "chunks": n_chunks}


def run_intent_extraction(dry_run: bool = False, verbose: bool = False) -> dict:
    """Single chunk extraction with sliding window for daemon mode."""
    all_lessons = _get_all_titles(LESSONS_FILE)
    all_patterns = _get_all_titles(PATTERNS_FILE)
    wants_text = _read_json_items(WANTS_FILE, "want")
    dont_wants_text = _read_json_items(DONT_WANTS_FILE, "dont_want")

    if not all_lessons and not wants_text:
        log.info("No brain data to synthesize intents from")
        return {"status": "empty", "new": 0}

    state = _load_state()
    l_off = state.get("lesson_offset", 0) % max(len(all_lessons), 1)
    p_off = state.get("pattern_offset", 0) % max(len(all_patterns), 1)

    l_chunk = all_lessons[l_off:l_off + CHUNK_SIZE]
    p_chunk = all_patterns[p_off:p_off + CHUNK_SIZE]

    current = _load_intents()
    existing_titles = {i.get("title", "").lower() for i in current}

    label = f"offset L:{l_off} P:{p_off}"
    added = _extract_one_chunk(l_chunk, p_chunk, wants_text, dont_wants_text,
                               label, current, existing_titles, dry_run, verbose)

    if added > 0 and not dry_run:
        _save_intents(current)
        log.info("Added %d new intents (total: %d)", added, len(current))

    # Advance window
    new_l = (l_off + CHUNK_SIZE) if (l_off + CHUNK_SIZE) < len(all_lessons) else 0
    new_p = (p_off + CHUNK_SIZE) if (p_off + CHUNK_SIZE) < len(all_patterns) else 0
    _save_state({"lesson_offset": new_l, "pattern_offset": new_p})

    return {"status": "ok", "new": added, "total": len(current),
            "window": {"lessons": f"{l_off}-{l_off + len(l_chunk)}/{len(all_lessons)}",
                       "patterns": f"{p_off}-{p_off + len(p_chunk)}/{len(all_patterns)}"}}


def run_daemon(verbose: bool = False, dry_run: bool = False):
    """Main daemon loop — sliding window extraction every CYCLE_INTERVAL."""
    log.info("Intent Daemon started (interval=%dh, chunk=%d)", CYCLE_INTERVAL // 3600, CHUNK_SIZE)
    while True:
        try:
            result = run_intent_extraction(dry_run=dry_run, verbose=verbose)
            log.info("Cycle complete: %s", result)
        except Exception as e:
            log.error("Cycle failed: %s", e)
        time.sleep(CYCLE_INTERVAL)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(asctime)s [INTENT] %(message)s")
    once = "--once" in sys.argv
    batch = "--batch" in sys.argv
    verbose = "--verbose" in sys.argv
    dry_run = "--dry-run" in sys.argv

    if batch:
        result = run_intent_batch(dry_run=dry_run, verbose=verbose)
        print(json.dumps(result, indent=2))
    elif once:
        result = run_intent_extraction(dry_run=dry_run, verbose=verbose)
        print(json.dumps(result, indent=2))
    else:
        run_daemon(verbose=verbose, dry_run=dry_run)
