#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
DMN Daemon V7.1 — Multi-Model Ideation Matrix

5 frontier LLMs dream in parallel (ThreadPoolExecutor). Two modes:
  symposium — All models see same files (diverse perspectives)
  chaos     — Each model sees different files (diverse inputs)

Output: Prefrontal_Cortex/Working_Memory/Proposals/session_*.json
Review: pulse proposals
"""
from __future__ import annotations

import json
import random
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from pathlib import Path

from pulse.core.paths import get_root_dir

ROOT = get_root_dir()
PROPOSALS_DIR = ROOT / "Prefrontal_Cortex" / "Working_Memory" / "Proposals"
CODE_DIR = ROOT / "pulse"

# The Panel of Geniuses — 6 frontier models
DMN_PANEL = [
    ("ollama", "qwen3-coder:480b-cloud"),        # 480B — strongest free reasoning
    ("ollama", "kimi-k2.5:cloud"),               # Kimi — thinking + tools
    ("ollama", "minimax-m2.5:cloud"),            # MiniMax — thinking mode
    ("anthropic", "claude-opus-4-6"),            # Opus — deepest reasoning (CLI sub)
    ("anthropic", "claude-sonnet-4-6"), # Sonnet — fast premium (CLI sub)
    ("google", "gemini-3.1-pro-preview"),        # Google Gemini Preview
]

DMN_PROMPT = """You are the Default Mode Network of PULSE, a Neural Operating System for AI agent swarms.

You have been given {file_count} random modules from the PULSE codebase. Your job is DIVERGENT THINKING — imagine a novel architectural feature, daemon, or capability that merges concepts from these modules in a way nobody has thought of.

{file_sections}

## Your Task
1. Identify the core concepts in each module.
2. Perform DIVERGENT THINKING: What novel feature emerges from combining these concepts?
3. Describe the feature in 3-5 sentences. Be specific — name the daemon/module, describe what it does, and explain WHY it would make PULSE more intelligent.
4. Give it a compelling title (under 60 chars).

Respond in this exact JSON format:
{{
  "title": "Short compelling title",
  "idea": "3-5 sentence description of the novel feature",
  "modules_merged": {file_names_json},
  "impact": "One sentence on why this matters"
}}"""

def _sample_code_files(n: int = 2) -> list[Path]:
    """Sample n random Python files from the pulse/ package."""
    py_files = [f for f in CODE_DIR.rglob("*.py")
                if "__pycache__" not in str(f)
                and f.name != "__init__.py"
                and f.stat().st_size > 500]
    if len(py_files) < n:
        return py_files
    return random.sample(py_files, n)


def _read_truncated(filepath: Path, max_chars: int = 4000) -> str:
    """Read file content, truncated to fit in context window."""
    try:
        text = filepath.read_text(encoding="utf-8", errors="replace")
        return text[:max_chars]
    except OSError:
        return ""


def _build_prompt(files: list[Path]) -> str:
    """Build the DMN prompt from a set of files."""
    sections = []
    names = []
    for i, f in enumerate(files):
        rel = str(f.relative_to(ROOT))
        names.append(rel)
        code = _read_truncated(f)
        sections.append(f"## Module {chr(65 + i)}: {rel}\n```python\n{code}\n```")

    return DMN_PROMPT.format(
        file_count=len(files),
        file_sections="\n\n".join(sections),
        file_names_json=json.dumps(names),
    )


def _call_model(provider: str, model: str, prompt: str, max_attempts: int = 2) -> dict | None:
    """Call a single model with retry. Returns parsed idea or None."""
    from pulse.core.llm_router import dispatch_to, extract_json

    for attempt in range(max_attempts):
        try:
            raw = dispatch_to(provider, model, prompt, temperature=0.9)
            parsed = extract_json(raw)
            if parsed.get("title") and parsed.get("idea"):
                return {
                    "title": parsed["title"],
                    "idea": parsed["idea"],
                    "modules_merged": parsed.get("modules_merged", []),
                    "impact": parsed.get("impact", ""),
                    "model": model,
                    "provider": provider,
                    "status": "pending",
                }
            # Bad JSON — retry if attempts remain
            if attempt < max_attempts - 1:
                continue
            # Last attempt — wrap raw response if non-empty
            if not raw or not raw.strip():
                return None
            return {
                "title": f"DMN: {model}",
                "idea": raw[:1000],
                "modules_merged": [],
                "impact": "Auto-generated (unstructured response).",
                "model": model,
                "provider": provider,
                "status": "pending",
            }
        except Exception:
            if attempt < max_attempts - 1:
                continue
            return None
    return None


def _get_available_panel() -> list[tuple[str, str]]:
    """Filter DMN_PANEL to only available providers."""
    from pulse.core.provider_state import check_availability
    avail = check_availability()
    return [(p, m) for p, m in DMN_PANEL if avail.get(p, False)]


def _save_session(session: dict) -> Path:
    """Write a session file to the Proposals directory."""
    PROPOSALS_DIR.mkdir(parents=True, exist_ok=True)
    filename = f"{session['id']}.json"
    filepath = PROPOSALS_DIR / filename
    filepath.write_text(
        json.dumps(session, indent=2, ensure_ascii=False), encoding="utf-8"
    )
    return filepath


def dream_session(files_count: int = 3, mode: str = "symposium") -> dict | None:
    """Execute one dream session with the full panel in parallel."""
    panel = _get_available_panel()
    if not panel:
        print("  No LLM providers available. Check API keys or Ollama.")
        return None

    now = datetime.now(timezone.utc)
    session_id = f"session_{now.strftime('%Y%m%dT%H%M%S')}"

    # Build prompts based on mode
    if mode == "chaos":
        # Each model gets its own unique file pair
        prompts = {}
        all_source_files = set()
        for provider, model in panel:
            files = _sample_code_files(files_count)
            all_source_files.update(str(f.relative_to(ROOT)) for f in files)
            prompts[(provider, model)] = _build_prompt(files)
        source_files = sorted(all_source_files)
    else:
        # Symposium: all models see the same files
        files = _sample_code_files(files_count)
        source_files = [str(f.relative_to(ROOT)) for f in files]
        prompt = _build_prompt(files)
        prompts = {(p, m): prompt for p, m in panel}

    print(f"  Session: {session_id}")
    print(f"  Mode: {mode} | Files: {files_count} | Panel: {len(panel)} models")
    print(f"  Models: {', '.join(m for _, m in panel)}")
    print()

    # Execute all models in parallel via ThreadPoolExecutor
    ideas = []
    with ThreadPoolExecutor(max_workers=len(panel)) as executor:
        futures = {}
        for (provider, model), p in prompts.items():
            fut = executor.submit(_call_model, provider, model, p)
            futures[fut] = (provider, model)

        for fut in as_completed(futures):
            provider, model = futures[fut]
            try:
                result = fut.result()
                if result:
                    ideas.append(result)
                    print(f"  [{model}] {result['title']}")
                else:
                    print(f"  [{model}] Failed (no response)")
            except Exception as e:
                print(f"  [{model}] Error: {e}")

    session = {
        "id": session_id,
        "created": now.isoformat(),
        "mode": mode,
        "file_count": files_count,
        "source_files": source_files,
        "panel_size": len(panel),
        "ideas": ideas,
    }

    filepath = _save_session(session)
    print(f"\n  {len(ideas)}/{len(panel)} ideas generated -> {filepath.name}")
    return session


def dream(count: int = 1, files_count: int = 3, mode: str = "symposium"):
    """Run N dream sessions. Each session uses the full panel in parallel."""
    print(f"\n  [DMN] Default Mode Network V7.1 — {count} session(s)\n")
    total_ideas = 0
    for i in range(count):
        if count > 1:
            print(f"  === Session {i + 1}/{count} ===\n")
        session = dream_session(files_count=files_count, mode=mode)
        if session:
            total_ideas += len(session.get("ideas", []))
        print()

    print(f"  Done. {total_ideas} total ideas. Review with: pulse proposals\n")

def _interactive_picker() -> tuple[int, int, str]:
    """Ask the user for session count, files count, and mode."""
    print("\n  [DMN] Default Mode Network V7.1 — Interactive Dream\n")
    try:
        raw = input("  How many code files to sample? [3]: ").strip()
        files_count = max(1, min(int(raw) if raw else 3, 10))
    except (ValueError, EOFError):
        files_count = 2
    try:
        print("  Modes: 1=symposium (same files, diverse perspectives)")
        print("         2=chaos (different files per model)")
        raw = input("  Choose mode [1]: ").strip()
        mode = "chaos" if raw == "2" else "symposium"
    except EOFError:
        mode = "symposium"
    try:
        raw = input("  How many sessions? [1]: ").strip()
        count = max(1, min(int(raw) if raw else 1, 10))
    except (ValueError, EOFError):
        count = 1
    return count, files_count, mode

def cmd_dream(args: list):
    """CLI entry: pulse dream [count] [--files N] [--mode symposium|chaos]."""
    from pulse.core.paths import _is_dev_mode
    if not _is_dev_mode():
        print("[PULSE] Dream Mode is coming soon. Stay tuned: https://pulseos.dev")
        return
    count = 1
    files_count = 3
    mode = "symposium"
    has_flags = False

    # Parse args
    i = 0
    while i < len(args):
        arg = args[i]
        if arg == "--files" and i + 1 < len(args):
            try:
                files_count = max(1, min(int(args[i + 1]), 10))
            except ValueError:
                print("  --files requires a number (1-10).")
                return
            has_flags = True
            i += 2
        elif arg == "--mode" and i + 1 < len(args):
            mode = args[i + 1].lower()
            has_flags = True
            i += 2
        elif arg.isdigit():
            count = int(arg)
            has_flags = True
            i += 1
        else:
            print(f"  Unknown flag: {arg}")
            print("  Usage: pulse dream [count] [--files N] [--mode symposium|chaos]")
            return

    if mode not in ("symposium", "chaos"):
        print("  Mode must be 'symposium' or 'chaos'.")
        return

    # If no flags at all, run interactive picker
    if not has_flags:
        count, files_count, mode = _interactive_picker()

    dream(count=count, files_count=files_count, mode=mode)
