"""Cortex Dashboard — lightweight web UI for memory navigation.

Embedded in the MCP process as a daemon thread.
FastAPI app on dynamic port, discoverable via MCP tool.
"""

import asyncio
import threading
from functools import partial

import uvicorn
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse

from neo_cortex._log import boot_log

_bl = partial(boot_log, "DASHBOARD")

# Track running servers for cleanup
_servers: list[uvicorn.Server] = []


def create_app(cortex, digestor=None, conv_log=None, version: str = "unknown") -> FastAPI:
    """Create FastAPI app with all dashboard routes."""
    app = FastAPI(title="Neo Cortex Dashboard", docs_url=None, redoc_url=None)

    # --- Version ---

    @app.get("/api/version")
    def get_version():
        return {"version": version}

    # --- Stats ---

    @app.get("/api/stats")
    def get_stats():
        if cortex._index:
            d = cortex._index.stats(cortex.dream_count)
            d["version"] = version
            return d
        d = cortex.stats().model_dump()
        d["version"] = version
        return d

    # --- Memory search / timeline / detail ---

    @app.get("/api/memories")
    def search_memories(
        q: str = "",
        project: str | None = None,
        activity: str | None = None,
        n: int = 20,
    ):
        if not cortex._index:
            return {"results": [], "error": "no index"}
        results = cortex._index.search_fts(query=q, project=project, activity=activity, n=n)
        return {"results": [r.model_dump() for r in results], "count": len(results)}

    @app.get("/api/timeline")
    def get_timeline(n: int = 20, project: str | None = None):
        if not cortex._index:
            return {"memories": []}
        memories = cortex._index.timeline(n=n, project=project)
        return {"memories": [m.model_dump() for m in memories]}

    @app.get("/api/memory/{memory_id}")
    def get_memory(memory_id: str):
        if not cortex._index:
            return {"error": "no index"}
        record = cortex._index.get_by_id(memory_id)
        if not record:
            raise HTTPException(status_code=404, detail="Memory not found")
        structured = cortex._index.get_structured(memory_id)
        return {
            "record": record.model_dump(),
            "structured": structured.model_dump() if structured else None,
        }

    # --- Concept graph + energy ---

    @app.get("/api/graph")
    def get_graph():
        if not cortex._graph:
            return {"nodes": [], "edges": [], "memory_count": 0}
        G = cortex._graph._graph
        nodes = [
            {"name": name, "mentions": data.get("mentions", 0)}
            for name, data in G.nodes(data=True)
        ]
        edges = [
            {"source": u, "target": v, "weight": data.get("weight", 1)}
            for u, v, data in G.edges(data=True)
        ]
        return {
            "nodes": nodes,
            "edges": edges,
            "memory_count": len(cortex._graph._memory_concepts),
        }

    @app.get("/api/graph/concept/{concept}")
    def get_concept(concept: str):
        if not cortex._graph:
            return {"concept": concept, "memories": [], "related": {}}
        memory_ids = cortex._graph.memories_for(concept)
        related = cortex._graph.spreading_activation([concept])
        memories = []
        if cortex._index and memory_ids:
            memories = [r.model_dump() for r in cortex._index.get_by_ids(memory_ids)]
        return {"concept": concept, "memories": memories, "related": related}

    @app.get("/api/energy")
    def get_energy():
        if not cortex._index:
            return {"histogram": [0] * 10, "by_project": {}}
        all_mem = cortex._index.get_all_for_dream()
        histogram = [0] * 10
        by_project: dict[str, int] = {}
        for m in all_mem:
            bucket = min(int(m["energy"] * 10), 9)
            histogram[bucket] += 1
            proj = m.get("project", "general")
            by_project[proj] = by_project.get(proj, 0) + 1
        return {"histogram": histogram, "by_project": by_project, "total": len(all_mem)}

    # --- Actions ---

    @app.post("/api/dream")
    async def trigger_dream():
        try:
            result = await cortex.dream()
            return {
                "status": result.status,
                "dream_count": result.dream_count,
                "high_energy_count": len(result.cluster),
            }
        except Exception as e:
            return JSONResponse(status_code=500, content={"error": str(e)})

    import random
    _rebuild_codes: dict[str, int] = {}

    @app.post("/api/rebuild/request")
    async def rebuild_request():
        """Generate a random confirmation code. User must type it to confirm."""
        code = random.randint(1000, 9999)
        _rebuild_codes["current"] = code
        return {"code": code, "message": f"Type {code} to confirm rebuild."}

    @app.post("/api/rebuild/confirm")
    async def rebuild_confirm(code: int = 0):
        expected = _rebuild_codes.pop("current", None)
        if expected is None or code != expected:
            return JSONResponse(
                status_code=400,
                content={"error": "Invalid or expired confirmation code."},
            )
        try:
            if digestor:
                digestor.pause()
            try:
                cleared = {}
                cleared["chromadb"] = cortex._store.clear()
                if cortex._index:
                    cleared["memory_index"] = cortex._index.clear()
                if cortex._dedup:
                    cleared["dedup"] = cortex._dedup.clear()
                if cortex._graph:
                    cleared["concept_graph"] = cortex._graph.clear()
                if cortex._index:
                    cleared["pending_merges"] = cortex._index.clear_pending_merges()
                if conv_log:
                    cleared["turns_reset"] = conv_log.reset_digested()
                    cleared["total_turns"] = conv_log.count()
                cortex._dream_count = 0
                return {"status": "rebuilt", "cleared": cleared}
            finally:
                if digestor:
                    digestor.resume()
        except Exception as e:
            return JSONResponse(status_code=500, content={"error": str(e)})

    # --- Rebuild progress ---

    @app.get("/api/rebuild/progress")
    def rebuild_progress():
        total_turns = conv_log.count() if conv_log else 0
        undigested = len(conv_log.get_undigested(limit=99999)) if conv_log else 0
        digested = total_turns - undigested
        memories = cortex._store.count() if cortex._store else 0
        return {
            "total_turns": total_turns,
            "digested": digested,
            "undigested": undigested,
            "memories": memories,
            "progress_pct": round(digested / total_turns * 100, 1) if total_turns else 100,
        }

    # --- HTML frontend ---

    @app.get("/", response_class=HTMLResponse)
    def index():
        return _DASHBOARD_HTML

    return app


_DEFAULT_PORT = 43559


def start_dashboard(cortex, host: str = "0.0.0.0", port: int = 0,
                    digestor=None, conv_log=None, version: str = "unknown") -> int:
    """Start dashboard in a daemon thread. Returns assigned port.

    Tries _DEFAULT_PORT first, then increments until a free port is found.

    Args:
        cortex: CortexService instance
        host: Bind address
        port: Port number (0 = use _DEFAULT_PORT with fallback)

    Returns:
        Actual port number the server is listening on.
    """
    import socket as _socket

    base = port if port > 0 else _DEFAULT_PORT
    sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
    sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
    for attempt in range(10):
        try:
            sock.bind((host, base + attempt))
            break
        except OSError:
            if attempt == 9:
                raise
    actual_port = sock.getsockname()[1]

    app = create_app(cortex, digestor=digestor, conv_log=conv_log, version=version)
    config = uvicorn.Config(app, host=host, port=actual_port, log_level="warning")
    server = uvicorn.Server(config)
    _servers.append(server)

    def _thread_target():
        try:
            asyncio.run(server.serve(sockets=[sock]))
        except Exception as e:
            _bl(f"CRASHED: {e}")

    t = threading.Thread(target=_thread_target, daemon=True, name="cortex-dashboard")
    t.start()
    _bl(f"listening on http://{host}:{actual_port}")
    return actual_port


# --- Full HTML dashboard (single page, all inline) ---

_DASHBOARD_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neo Cortex Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/3d-force-graph@1"></script>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
         background:#0d1117; color:#c9d1d9; }
  .header { background:#161b22; padding:16px 24px; border-bottom:1px solid #30363d;
            display:flex; align-items:center; gap:16px; }
  .header h1 { font-size:20px; color:#58a6ff; }
  .header .stats-bar { display:flex; gap:24px; margin-left:auto; font-size:13px; color:#8b949e; }
  .header .stats-bar span { color:#c9d1d9; font-weight:600; }
  nav { background:#161b22; border-bottom:1px solid #30363d; display:flex; gap:0; }
  nav button { background:none; border:none; color:#8b949e; padding:12px 20px;
               cursor:pointer; font-size:14px; border-bottom:2px solid transparent; }
  nav button:hover { color:#c9d1d9; }
  nav button.active { color:#58a6ff; border-bottom-color:#58a6ff; }
  .panel { display:none; padding:24px; max-width:1200px; margin:0 auto; }
  .panel.active { display:block; }
  .card { background:#161b22; border:1px solid #30363d; border-radius:6px; padding:16px; }
  .stats-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:16px; }
  .stat-card { text-align:center; }
  .stat-card .value { font-size:36px; font-weight:700; color:#58a6ff; }
  .stat-card .label { font-size:13px; color:#8b949e; margin-top:4px; }
  .search-box { display:flex; gap:8px; margin-bottom:16px; }
  .search-box input, .search-box select { background:#0d1117; border:1px solid #30363d;
    color:#c9d1d9; padding:8px 12px; border-radius:6px; font-size:14px; }
  .search-box input { flex:1; }
  .search-box button { background:#238636; color:#fff; border:none; padding:8px 16px;
    border-radius:6px; cursor:pointer; font-size:14px; }
  .search-box button:hover { background:#2ea043; }
  .memory-list { display:flex; flex-direction:column; gap:8px; }
  .memory-item { padding:12px; cursor:pointer; transition:background 0.15s; }
  .memory-item:hover { background:#1c2128; }
  .memory-item .title { color:#58a6ff; font-weight:600; }
  .memory-item .meta { font-size:12px; color:#8b949e; margin-top:4px; }
  #panel-graph { padding:0; max-width:none; position:fixed; inset:0; z-index:50; background:#0d1117; overflow:hidden; }
  #panel-graph.active { display:flex; flex-direction:column; }
  body.graph-active { overflow:hidden; }
  #graph-3d { flex:1; width:100vw; height:100vh; background:#0d1117; overflow:hidden; }
  #graph-3d canvas { outline:none; }
  .graph-back { position:absolute; top:16px; left:16px; z-index:60; background:#161b22;
    border:1px solid #30363d; color:#58a6ff; padding:8px 16px; border-radius:6px;
    cursor:pointer; font-size:14px; font-weight:600; }
  .graph-back:hover { background:#21262d; }
  .graph-controls { position:absolute; top:16px; right:16px; z-index:60; display:flex; flex-direction:column; gap:8px; }
  .graph-controls button { background:#161b22; border:1px solid #30363d; color:#c9d1d9;
    padding:6px 12px; border-radius:4px; cursor:pointer; font-size:12px; }
  .graph-controls button:hover { background:#21262d; }
  .graph-hint { position:absolute; bottom:24px; left:50%; transform:translateX(-50%); z-index:60;
    background:rgba(22,27,34,0.9); border:1px solid #30363d; color:#8b949e; padding:12px 24px;
    border-radius:8px; font-size:13px; text-align:center; transition:opacity 1s; }
  .graph-tooltip { position:absolute; background:#161b22; border:1px solid #58a6ff;
    color:#c9d1d9; padding:8px 12px; border-radius:6px; font-size:12px; pointer-events:none;
    display:none; z-index:60; max-width:300px; }
  .graph-info { position:absolute; top:0; right:0; width:340px; height:100%; z-index:60;
    background:rgba(22,27,34,0.95); border-left:1px solid #30363d; overflow-y:auto;
    padding:16px; display:none; font-size:13px; }
  .graph-info.active { display:block; }
  .graph-info h3 { color:#58a6ff; margin:0 0 4px; font-size:16px; }
  .graph-info .mentions { color:#8b949e; font-size:12px; margin-bottom:12px; }
  .graph-info .section-title { color:#d2a8ff; font-size:11px; text-transform:uppercase;
    letter-spacing:1px; margin:12px 0 6px; }
  .graph-info .related-item { display:flex; justify-content:space-between; padding:3px 0;
    color:#c9d1d9; cursor:pointer; }
  .graph-info .related-item:hover { color:#58a6ff; }
  .graph-info .related-item .weight { color:#8b949e; font-size:11px; }
  .graph-info .memory-item-mini { padding:6px 8px; margin:2px 0; background:#161b22;
    border:1px solid #21262d; border-radius:4px; cursor:pointer; }
  .graph-info .memory-item-mini:hover { border-color:#58a6ff; }
  .graph-info .memory-item-mini .title { color:#58a6ff; font-size:12px; }
  .graph-info .memory-item-mini .meta { color:#8b949e; font-size:11px; margin-top:2px; }
  .graph-info .close-btn { position:absolute; top:8px; right:12px; color:#8b949e;
    cursor:pointer; font-size:18px; background:none; border:none; }
  .graph-info .close-btn:hover { color:#c9d1d9; }
  .graph-loading { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); z-index:60;
    color:#58a6ff; font-size:16px; text-align:center; }
  .graph-loading .spinner { width:40px; height:40px; border:3px solid #30363d; border-top:3px solid #58a6ff;
    border-radius:50%; animation:spin 1s linear infinite; margin:0 auto 12px; }
  @keyframes spin { to { transform:rotate(360deg); } }
  .chart-container { max-width:600px; margin:16px auto; }
  .actions-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(300px, 1fr)); gap:16px; }
  .action-card h3 { margin-bottom:8px; }
  .action-card p { font-size:13px; color:#8b949e; margin-bottom:12px; }
  .btn { padding:8px 16px; border:none; border-radius:6px; cursor:pointer; font-size:14px; }
  .btn-primary { background:#238636; color:#fff; }
  .btn-primary:hover { background:#2ea043; }
  .btn-danger { background:#da3633; color:#fff; }
  .btn-danger:hover { background:#f85149; }
  .btn:disabled { opacity:0.5; cursor:not-allowed; }
  .modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.7);
    z-index:100; justify-content:center; align-items:center; }
  .modal-overlay.active { display:flex; }
  .modal { background:#161b22; border:1px solid #30363d; border-radius:8px;
    padding:24px; max-width:700px; width:90%; max-height:80vh; overflow-y:auto; }
  .modal h2 { margin-bottom:12px; color:#58a6ff; }
  .modal pre { background:#0d1117; padding:12px; border-radius:4px; overflow-x:auto;
    font-size:13px; white-space:pre-wrap; }
  .modal .close { float:right; cursor:pointer; color:#8b949e; font-size:20px; }
  .toast { position:fixed; bottom:24px; right:24px; background:#238636; color:#fff;
    padding:12px 20px; border-radius:6px; display:none; z-index:200; }
</style>
</head>
<body>

<div class="header">
  <h1>Neo Cortex Dashboard</h1>
  <span id="version-tag" style="font-size:11px;color:#8b949e;background:#21262d;padding:2px 8px;border-radius:4px"></span>
  <div class="stats-bar" id="stats-bar">Loading...</div>
</div>

<nav id="nav">
  <button class="active" data-panel="stats">Stats</button>
  <button data-panel="memories">Memories</button>
  <button data-panel="graph">Graph</button>
  <button data-panel="energy">Energy</button>
  <button data-panel="actions">Actions</button>
</nav>

<!-- Stats Panel -->
<div class="panel active" id="panel-stats">
  <div class="stats-grid" id="stats-grid"></div>
</div>

<!-- Memories Panel (Search + Timeline) -->
<div class="panel" id="panel-memories">
  <div class="search-box">
    <input type="text" id="search-input" placeholder="Search memories...">
    <select id="search-project"><option value="">All projects</option></select>
    <button onclick="doSearch()">Search</button>
  </div>
  <div class="memory-list card" id="memory-results">
    <p style="color:#8b949e">Search or browse timeline below</p>
  </div>
  <h3 style="margin:24px 0 12px">Recent Timeline</h3>
  <div class="memory-list card" id="timeline-list"></div>
</div>

<!-- Graph Panel (fullscreen overlay) -->
<div class="panel" id="panel-graph">
  <button class="graph-back" onclick="exitGraph()">Back</button>
  <div class="graph-loading" id="graph-loading">
    <div class="spinner"></div>
    Loading concept graph...
  </div>
  <div id="graph-3d"></div>
  <div class="graph-controls">
    <button onclick="resetGraphCamera()">Reset View</button>
    <button onclick="toggleLabels()">Labels</button>
    <button onclick="toggleAutoRotate()">Rotate</button>
  </div>
  <div class="graph-hint" id="graph-hint">
    Left drag = Rotate &nbsp; | &nbsp; Right drag = Pan &nbsp; | &nbsp; Scroll = Zoom &nbsp; | &nbsp; Click node = Focus &nbsp; | &nbsp; ESC = Back
  </div>
  <div class="graph-tooltip" id="graph-tooltip"></div>
  <div class="graph-info" id="graph-info">
    <button class="close-btn" onclick="closeGraphInfo()">&times;</button>
    <h3 id="gi-name"></h3>
    <div class="mentions" id="gi-mentions"></div>
    <div class="section-title">Related concepts</div>
    <div id="gi-related"></div>
    <div class="section-title">Memories</div>
    <div id="gi-memories"></div>
  </div>
</div>

<!-- Energy Panel -->
<div class="panel" id="panel-energy">
  <div class="chart-container"><canvas id="energy-histogram"></canvas></div>
  <div class="chart-container"><canvas id="project-pie"></canvas></div>
</div>

<!-- Actions Panel -->
<div class="panel" id="panel-actions">
  <div class="actions-grid">
    <div class="card action-card">
      <h3>Dream Cycle</h3>
      <p>Run energy decay/boost cycle. Reinforces recalled memories, decays forgotten ones.</p>
      <button class="btn btn-primary" id="btn-dream" onclick="triggerDream()">Run Dream</button>
      <span id="dream-status"></span>
    </div>
    <div class="card action-card">
      <h3>Rebuild Index</h3>
      <p>Wipe all memories and re-process from conversation log. The digestor will re-ingest everything.</p>
      <button class="btn btn-danger" id="btn-rebuild-start" onclick="startRebuild()">Rebuild...</button>
      <div id="rebuild-confirm" style="display:none;margin-top:12px">
        <p style="color:#f85149;font-weight:600" id="rebuild-prompt"></p>
        <input type="text" id="rebuild-code-input" placeholder="Type the code" style="background:#0d1117;border:1px solid #da3633;color:#c9d1d9;padding:8px;border-radius:4px;width:100px;margin:8px 8px 0 0">
        <button class="btn btn-danger" onclick="confirmRebuild()">Confirm</button>
        <button class="btn" style="background:#30363d;color:#c9d1d9" onclick="cancelRebuild()">Cancel</button>
      </div>
      <span id="rebuild-status"></span>
      <div id="rebuild-progress" style="display:none;margin-top:12px">
        <div style="display:flex;justify-content:space-between;font-size:12px;color:#8b949e;margin-bottom:4px">
          <span id="rebuild-progress-text">Digesting...</span>
          <span id="rebuild-progress-pct">0%</span>
        </div>
        <div style="background:#21262d;border-radius:4px;height:8px;overflow:hidden">
          <div id="rebuild-progress-bar" style="background:#58a6ff;height:100%;width:0%;transition:width 0.5s"></div>
        </div>
        <div style="font-size:11px;color:#8b949e;margin-top:4px">
          <span id="rebuild-progress-detail"></span>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- Memory Detail Modal -->
<div class="modal-overlay" id="modal" onclick="if(event.target===this)closeModal()">
  <div class="modal">
    <span class="close" onclick="closeModal()">&times;</span>
    <h2 id="modal-title">Memory Detail</h2>
    <pre id="modal-body"></pre>
  </div>
</div>

<div class="toast" id="toast"></div>

<script>
// --- Navigation ---
var _graphLoaded = false;
document.querySelectorAll('nav button').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('nav button').forEach(b => b.classList.remove('active'));
    document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
    btn.classList.add('active');
    document.getElementById('panel-' + btn.dataset.panel).classList.add('active');
    // Lazy-load 3D graph when panel becomes visible
    if (btn.dataset.panel === 'graph') {
      document.body.classList.add('graph-active');
      // Show hint fresh each time
      var hint = document.getElementById('graph-hint');
      if (hint) { hint.style.display = ''; hint.style.opacity = '1'; }
      setTimeout(fadeGraphHint, 5000);
      if (!_graphLoaded) {
        _graphLoaded = true;
        setTimeout(loadGraph, 100);
      }
      // Resize 3D graph
      if (_graph3d) {
        _graph3d.width(window.innerWidth).height(window.innerHeight);
      }
    } else {
      document.body.classList.remove('graph-active');
    }
  });
});

// --- API helpers ---
async function api(path) { return (await fetch(path)).json(); }
function toast(msg) {
  const t = document.getElementById('toast');
  t.textContent = msg; t.style.display = 'block';
  setTimeout(() => t.style.display = 'none', 3000);
}

// --- Stats ---
async function loadStats() {
  const s = await api('/api/stats');
  if (s.version) document.getElementById('version-tag').textContent = 'v' + s.version;
  document.getElementById('stats-bar').innerHTML =
    'Memories: <span>' + (s.total_memories||s.total||0) + '</span> | Sessions: <span>' + (s.sessions||0) + '</span> | ' +
    'Avg Energy: <span>' + ((s.avg_energy||0).toFixed(2)) + '</span> | Dreams: <span>' + (s.dream_count||0) + '</span>';
  const grid = document.getElementById('stats-grid');
  const cards = [
    {v: s.total_memories||s.total||0, l: 'Total Memories'},
    {v: s.sessions||0, l: 'Sessions'},
    {v: (s.avg_energy||0).toFixed(2), l: 'Avg Energy'},
    {v: s.dream_count||0, l: 'Dream Cycles'},
  ];
  grid.innerHTML = cards.map(c =>
    '<div class="card stat-card"><div class="value">' + c.v + '</div><div class="label">' + c.l + '</div></div>'
  ).join('');
  // Populate project filter
  if (s.projects) {
    const sel = document.getElementById('search-project');
    Object.keys(s.projects).forEach(p => {
      const opt = document.createElement('option');
      opt.value = p; opt.textContent = p + ' (' + s.projects[p] + ')';
      sel.appendChild(opt);
    });
  }
}

// --- Search ---
async function doSearch() {
  const q = document.getElementById('search-input').value;
  const p = document.getElementById('search-project').value;
  const url = '/api/memories?q=' + encodeURIComponent(q) + '&n=30' + (p ? '&project=' + encodeURIComponent(p) : '');
  const data = await api(url);
  renderMemories(data.results || [], 'memory-results');
}
document.getElementById('search-input').addEventListener('keydown', function(e) { if(e.key==='Enter') doSearch(); });

// --- Timeline ---
async function loadTimeline() {
  const data = await api('/api/timeline?n=20');
  renderMemories(data.memories || [], 'timeline-list');
}

function renderMemories(list, targetId) {
  const el = document.getElementById(targetId);
  if (!list.length) { el.innerHTML = '<p style="color:#8b949e">No results</p>'; return; }
  el.innerHTML = list.map(m =>
    '<div class="memory-item" onclick="showMemory(\\x27' + m.id + '\\x27)">' +
    '<div class="title">' + (m.topic || m.title || m.id) + '</div>' +
    '<div class="meta">' + (m.project||'') + ' | energy: ' + ((m.energy||0).toFixed(2)) + ' | ' + (m.timestamp||'') + '</div>' +
    '</div>'
  ).join('');
}

// --- Memory Detail Modal ---
async function showMemory(id) {
  const data = await api('/api/memory/' + id);
  document.getElementById('modal-title').textContent = (data.structured && data.structured.title) || id;
  document.getElementById('modal-body').textContent = JSON.stringify(data, null, 2);
  document.getElementById('modal').classList.add('active');
}
function closeModal() { document.getElementById('modal').classList.remove('active'); }

// --- Graph (3D force) ---
var _graph3d = null;
var _showLabels = true;
var _autoRotate = false;
var _selectedNode = null;
var _neighbors = new Set();
var _projectColors = {
  'neo-cortex': '#d2a8ff',
  'neo-ram': '#58a6ff',
  'neo-reloaded': '#3fb950',
  'neo-vscode': '#f0883e',
  'general': '#8b949e',
  'soul': '#f778ba'
};
function getNodeColor(node) {
  // Color by most connected project or by mention count
  if (node.project) return _projectColors[node.project] || '#58a6ff';
  var m = node.mentions || 1;
  if (m > 20) return '#f778ba';
  if (m > 10) return '#d2a8ff';
  if (m > 5) return '#58a6ff';
  return '#3fb950';
}
function makeLabel(text, mentions) {
  // Canvas-based sprite label — works everywhere, no external deps
  var canvas = document.createElement('canvas');
  var size = mentions > 10 ? 512 : mentions > 5 ? 256 : 192;
  canvas.width = size; canvas.height = size / 2;
  var ctx = canvas.getContext('2d');
  var fontSize = mentions > 10 ? 48 : mentions > 5 ? 32 : 22;
  ctx.font = (mentions > 10 ? 'bold ' : '') + fontSize + 'px -apple-system, BlinkMacSystemFont, sans-serif';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  // Dark outline for readability
  ctx.strokeStyle = '#0d1117'; ctx.lineWidth = 4; ctx.strokeText(text, size/2, size/4);
  ctx.fillStyle = mentions > 10 ? '#d2a8ff' : mentions > 5 ? '#c9d1d9' : '#8b949e';
  ctx.fillText(text, size/2, size/4);
  var texture = new THREE.CanvasTexture(canvas);
  var sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false, depthTest: false }));
  sprite.renderOrder = 999;
  var scale = mentions > 10 ? 24 : mentions > 5 ? 16 : 10;
  sprite.scale.set(scale, scale/2, 1);
  return sprite;
}
async function loadGraph() {
  var loading = document.getElementById('graph-loading');
  loading.style.display = 'block';
  var data = await api('/api/graph');
  if (!data.nodes || !data.nodes.length) {
    loading.innerHTML = 'No graph data yet';
    return;
  }
  var el = document.getElementById('graph-3d');
  if (!el.clientWidth) return;
  var maxM = Math.max.apply(null, data.nodes.map(function(n){ return n.mentions || 1; }));
  var gData = {
    nodes: data.nodes.map(function(n) {
      return { id: n.name, mentions: n.mentions || 1, val: 1 + (n.mentions / maxM) * 8 };
    }),
    links: data.edges.map(function(e) {
      return { source: e.source, target: e.target, weight: e.weight || 1 };
    })
  };
  loading.innerHTML = '<div class="spinner"></div>' + gData.nodes.length + ' concepts, ' + gData.links.length + ' connections';
  await new Promise(function(r) { requestAnimationFrame(r); });
  // Clear previous graph
  el.innerHTML = '';
  _graph3d = ForceGraph3D()(el)
    .backgroundColor('#0d1117')
    .graphData(gData)
    .nodeVal('val')
    .nodeColor(function(n) { return getNodeColor(n); })
    .nodeOpacity(0.9)
    .nodeResolution(8)
    // Links: visible!
    .linkWidth(function(l) { return 0.15 + Math.sqrt(l.weight) * 0.2; })
    .linkColor(function() { return '#444c56'; })
    .linkOpacity(0.35)
    // Labels: always on all nodes
    .nodeThreeObject(function(node) {
      var group = new THREE.Group();
      // Sphere
      var geo = new THREE.SphereGeometry(1 + (node.mentions / maxM) * 4, 8, 8);
      var mat = new THREE.MeshLambertMaterial({ color: getNodeColor(node), transparent: true, opacity: 0.9 });
      group.add(new THREE.Mesh(geo, mat));
      // Label
      if (_showLabels) {
        var label = makeLabel(node.id, node.mentions);
        label.position.y = 3 + (node.mentions / maxM) * 5;
        group.add(label);
      }
      return group;
    })
    .nodeThreeObjectExtend(false)
    .onNodeHover(function(node) {
      el.style.cursor = node ? 'pointer' : 'default';
    })
    .onNodeClick(function(node) {
      _graph3d.cameraPosition({x:node.x+80, y:node.y+40, z:node.z+80}, node, 800);
      selectNode(node);
    })
    .onBackgroundClick(function() { deselectNode(); })
    .warmupTicks(0)
    .cooldownTime(5000)
    .onEngineStop(function() {
      loading.style.display = 'none';
    });
  loading.style.display = 'none';
  _graph3d.d3Force('charge').strength(-50);
  _graph3d.d3Force('link').distance(function(l) { return 20 + 40 / (l.weight || 1); });
}
async function selectNode(node) {
  _selectedNode = node;
  // Find neighbors
  _neighbors = new Set();
  var gd = _graph3d.graphData();
  gd.links.forEach(function(l) {
    var src = typeof l.source === 'object' ? l.source.id : l.source;
    var tgt = typeof l.target === 'object' ? l.target.id : l.target;
    if (src === node.id) _neighbors.add(tgt);
    if (tgt === node.id) _neighbors.add(src);
  });
  _neighbors.add(node.id);
  // Dim non-neighbors
  _graph3d.nodeOpacity(function(n) { return _neighbors.has(n.id) ? 0.95 : 0.08; });
  _graph3d.linkOpacity(function(l) {
    var src = typeof l.source === 'object' ? l.source.id : l.source;
    var tgt = typeof l.target === 'object' ? l.target.id : l.target;
    return (src === node.id || tgt === node.id) ? 0.6 : 0.03;
  });
  // Load concept info
  var panel = document.getElementById('graph-info');
  document.getElementById('gi-name').textContent = node.id;
  document.getElementById('gi-mentions').textContent = (node.mentions || 0) + ' mentions | ' + _neighbors.size + ' connections';
  document.getElementById('gi-related').innerHTML = '<div style="color:#8b949e">Loading...</div>';
  document.getElementById('gi-memories').innerHTML = '';
  panel.classList.add('active');
  try {
    var data = await api('/api/graph/concept/' + encodeURIComponent(node.id));
    // Related concepts
    var related = Object.entries(data.related || {}).sort(function(a,b) { return b[1]-a[1]; }).slice(0, 15);
    document.getElementById('gi-related').innerHTML = related.length ? related.map(function(r) {
      return '<div class="related-item" onclick="jumpToConcept(\\x27' + r[0].replace(/'/g,'\\x27') + '\\x27)">' +
        '<span>' + r[0] + '</span><span class="weight">' + r[1].toFixed(2) + '</span></div>';
    }).join('') : '<div style="color:#8b949e">None</div>';
    // Memories
    var mems = data.memories || [];
    document.getElementById('gi-memories').innerHTML = mems.length ? mems.map(function(m) {
      return '<div class="memory-item-mini" onclick="showMemory(\\x27' + m.id + '\\x27)">' +
        '<div class="title">' + (m.topic || m.id) + '</div>' +
        '<div class="meta">' + (m.project || '') + ' | energy: ' + ((m.energy||0).toFixed(2)) + '</div></div>';
    }).join('') : '<div style="color:#8b949e">No memories</div>';
  } catch(e) {
    document.getElementById('gi-related').innerHTML = '<div style="color:#da3633">Error loading</div>';
  }
}
function deselectNode() {
  _selectedNode = null;
  _neighbors.clear();
  if (_graph3d) {
    _graph3d.nodeOpacity(0.9);
    _graph3d.linkOpacity(0.35);
  }
  document.getElementById('graph-info').classList.remove('active');
}
function closeGraphInfo() { deselectNode(); }
function jumpToConcept(name) {
  var gd = _graph3d.graphData();
  var node = gd.nodes.find(function(n) { return n.id === name; });
  if (node) {
    _graph3d.cameraPosition({x:node.x+80, y:node.y+40, z:node.z+80}, node, 800);
    selectNode(node);
  }
}
function resetGraphCamera() {
  if (_graph3d) _graph3d.cameraPosition({x:0, y:0, z:300}, {x:0,y:0,z:0}, 1000);
}
function toggleLabels() {
  _showLabels = !_showLabels;
  if (_graph3d) {
    _graphLoaded = false;
    loadGraph();
  }
}
function toggleAutoRotate() {
  _autoRotate = !_autoRotate;
  if (_graph3d) {
    var controls = _graph3d.controls();
    if (controls) controls.autoRotate = _autoRotate;
  }
}
function exitGraph() {
  document.body.classList.remove('graph-active');
  document.querySelectorAll('nav button').forEach(function(b) { b.classList.remove('active'); });
  document.querySelectorAll('.panel').forEach(function(p) { p.classList.remove('active'); });
  document.querySelector('nav button[data-panel="stats"]').classList.add('active');
  document.getElementById('panel-stats').classList.add('active');
}
// Hint auto-fade after 5s
function fadeGraphHint() {
  var hint = document.getElementById('graph-hint');
  if (hint) { hint.style.opacity = '0'; setTimeout(function() { hint.style.display = 'none'; }, 1000); }
}
// ESC key to exit graph
document.addEventListener('keydown', function(e) {
  if (e.key === 'Escape' && document.getElementById('panel-graph').classList.contains('active')) {
    exitGraph();
  }
});

// --- Energy Charts (Chart.js) ---
async function loadEnergy() {
  const data = await api('/api/energy');
  const labels = ['0-.1','.1-.2','.2-.3','.3-.4','.4-.5','.5-.6','.6-.7','.7-.8','.8-.9','.9-1'];
  new Chart(document.getElementById('energy-histogram'), {
    type: 'bar',
    data: { labels: labels, datasets: [{label:'Memories', data:data.histogram, backgroundColor:'#58a6ff'}] },
    options: { responsive:true, plugins:{title:{display:true,text:'Energy Distribution',color:'#c9d1d9'}},
      scales: {x:{ticks:{color:'#8b949e'}},y:{ticks:{color:'#8b949e'}}} }
  });
  const projects = Object.keys(data.by_project || {});
  const colors = ['#58a6ff','#3fb950','#d2a8ff','#f0883e','#da3633','#8b949e','#f778ba'];
  new Chart(document.getElementById('project-pie'), {
    type: 'doughnut',
    data: { labels: projects,
      datasets: [{data: projects.map(function(p){return data.by_project[p];}),
        backgroundColor: projects.map(function(_,i){return colors[i % colors.length];})}] },
    options: { responsive:true, plugins:{title:{display:true,text:'Memories by Project',color:'#c9d1d9'},
      legend:{labels:{color:'#8b949e'}}} }
  });
}

// --- Actions ---
async function triggerDream() {
  document.getElementById('btn-dream').disabled = true;
  document.getElementById('dream-status').textContent = ' Running...';
  try {
    const r = await (await fetch('/api/dream', {method:'POST'})).json();
    document.getElementById('dream-status').textContent = r.error ? ' Error: ' + r.error : ' Done!';
    toast('Dream cycle completed'); loadStats();
  } catch(e) { document.getElementById('dream-status').textContent = ' Failed'; }
  document.getElementById('btn-dream').disabled = false;
}
var _rebuildCode = null;
async function startRebuild() {
  try {
    const r = await (await fetch('/api/rebuild/request', {method:'POST'})).json();
    _rebuildCode = r.code;
    document.getElementById('rebuild-prompt').textContent = 'Type ' + r.code + ' to confirm rebuild:';
    document.getElementById('rebuild-confirm').style.display = 'block';
    document.getElementById('btn-rebuild-start').style.display = 'none';
    document.getElementById('rebuild-code-input').value = '';
    document.getElementById('rebuild-code-input').focus();
  } catch(e) { document.getElementById('rebuild-status').textContent = ' Failed to request code'; }
}
function cancelRebuild() {
  document.getElementById('rebuild-confirm').style.display = 'none';
  document.getElementById('btn-rebuild-start').style.display = '';
  _rebuildCode = null;
}
async function confirmRebuild() {
  var typed = document.getElementById('rebuild-code-input').value.trim();
  if (typed !== String(_rebuildCode)) {
    document.getElementById('rebuild-status').textContent = ' Wrong code!';
    return;
  }
  document.getElementById('rebuild-status').textContent = ' Clearing...';
  try {
    var r = await (await fetch('/api/rebuild/confirm?code=' + typed, {method:'POST'})).json();
    if (r.error) {
      document.getElementById('rebuild-status').textContent = ' Error: ' + r.error;
      cancelRebuild();
      return;
    }
    document.getElementById('rebuild-status').textContent = ' Cleared! Digestor re-ingesting...';
    cancelRebuild();
    startProgressPolling(r.cleared.turns_reset || 0);
  } catch(e) { document.getElementById('rebuild-status').textContent = ' Failed'; cancelRebuild(); }
}

var _progressTimer = null;
function startProgressPolling(totalTurns) {
  var el = document.getElementById('rebuild-progress');
  el.style.display = 'block';
  _progressTimer = setInterval(async function() {
    try {
      var p = await api('/api/rebuild/progress');
      var pct = p.progress_pct || 0;
      document.getElementById('rebuild-progress-bar').style.width = pct + '%';
      document.getElementById('rebuild-progress-pct').textContent = pct + '%';
      document.getElementById('rebuild-progress-text').textContent =
        pct >= 100 ? 'Complete!' : 'Digestor re-ingesting...';
      document.getElementById('rebuild-progress-detail').textContent =
        p.digested + '/' + p.total_turns + ' turns digested | ' + p.memories + ' memories';
      loadStats();
      if (pct >= 100) {
        clearInterval(_progressTimer);
        _progressTimer = null;
        toast('Rebuild complete — ' + p.memories + ' memories');
        document.getElementById('rebuild-status').textContent = '';
        loadTimeline();
      }
    } catch(e) { /* ignore polling errors */ }
  }, 3000);
}

// --- Init ---
loadStats(); loadTimeline(); loadEnergy();
</script>
</body>
</html>"""
