"""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) -> FastAPI:
    """Create FastAPI app with all dashboard routes."""
    app = FastAPI(title="Neo Cortex Dashboard", docs_url=None, redoc_url=None)

    # --- Stats ---

    @app.get("/api/stats")
    def get_stats():
        if cortex._index:
            return cortex._index.stats(cortex.dream_count)
        return cortex.stats().model_dump()

    # --- 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)})

    @app.post("/api/rebuild")
    async def trigger_rebuild(confirm: bool = False):
        if not confirm:
            return JSONResponse(
                status_code=400,
                content={"error": "Rebuild is destructive. Pass confirm=true to proceed."},
            )
        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()
            cortex._dream_count = 0
            return {"status": "rebuilt", "cleared": cleared}
        except Exception as e:
            return JSONResponse(status_code=500, content={"error": str(e)})

    # --- HTML frontend ---

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

    return app


def start_dashboard(cortex, host: str = "127.0.0.1", port: int = 0) -> int:
    """Start dashboard in a daemon thread. Returns assigned port.

    Pre-binds a socket with port=0 to let the OS assign a port,
    then passes the bound socket to uvicorn. This way the actual port
    is known synchronously before the thread starts.

    Args:
        cortex: CortexService instance
        host: Bind address (default localhost only)
        port: Port number (0 = OS auto-assign)

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

    # Pre-bind socket so we know the port before starting the thread
    sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
    sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
    sock.bind((host, port))
    actual_port = sock.getsockname()[1]

    app = create_app(cortex)
    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/d3@7"></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; }
  #graph-svg { width:100%; height:500px; background:#0d1117; border:1px solid #30363d; border-radius:6px; }
  #graph-svg circle { cursor:pointer; }
  #graph-svg text { fill:#8b949e; font-size:10px; pointer-events:none; }
  #graph-svg line { stroke:#30363d; stroke-opacity:0.6; }
  .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>
  <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 -->
<div class="panel" id="panel-graph">
  <svg id="graph-svg"></svg>
</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>Clear all storage and rebuild from scratch. This is destructive!</p>
      <button class="btn btn-danger" id="btn-rebuild" onclick="triggerRebuild()">Rebuild</button>
      <span id="rebuild-status"></span>
    </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 ---
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');
  });
});

// --- 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');
  document.getElementById('stats-bar').innerHTML =
    'Memories: <span>' + (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||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(\'' + m.id + '\')">' +
    '<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 (d3-force) ---
async function loadGraph() {
  const data = await api('/api/graph');
  if (!data.nodes.length) return;
  const svg = d3.select('#graph-svg');
  const width = svg.node().clientWidth, height = svg.node().clientHeight || 500;
  svg.selectAll('*').remove();
  const g = svg.append('g');
  const zoom = d3.zoom().on('zoom', function(e) { g.attr('transform', e.transform); });
  svg.call(zoom);
  const maxMentions = Math.max.apply(null, data.nodes.map(function(n) { return n.mentions; }).concat([1]));
  const sim = d3.forceSimulation(data.nodes)
    .force('link', d3.forceLink(data.edges).id(function(d) { return d.name; }).distance(80))
    .force('charge', d3.forceManyBody().strength(-120))
    .force('center', d3.forceCenter(width/2, height/2));
  const link = g.selectAll('line').data(data.edges).join('line')
    .attr('stroke-width', function(d) { return Math.sqrt(d.weight); });
  const node = g.selectAll('circle').data(data.nodes).join('circle')
    .attr('r', function(d) { return 4 + (d.mentions / maxMentions) * 16; })
    .attr('fill', '#58a6ff').attr('stroke', '#0d1117').attr('stroke-width', 1.5)
    .call(d3.drag()
      .on('start', function(e,d) { if(!e.active) sim.alphaTarget(0.3).restart(); d.fx=d.x; d.fy=d.y; })
      .on('drag', function(e,d) { d.fx=e.x; d.fy=e.y; })
      .on('end', function(e,d) { if(!e.active) sim.alphaTarget(0); d.fx=null; d.fy=null; }));
  const label = g.selectAll('text').data(data.nodes).join('text')
    .text(function(d) { return d.name; }).attr('dx', 12).attr('dy', 4);
  sim.on('tick', function() {
    link.attr('x1',function(d){return d.source.x;}).attr('y1',function(d){return d.source.y;})
        .attr('x2',function(d){return d.target.x;}).attr('y2',function(d){return d.target.y;});
    node.attr('cx',function(d){return d.x;}).attr('cy',function(d){return d.y;});
    label.attr('x',function(d){return d.x;}).attr('y',function(d){return d.y;});
  });
}

// --- 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;
}
async function triggerRebuild() {
  if (!confirm('This will CLEAR all memory storage. Are you sure?')) return;
  document.getElementById('btn-rebuild').disabled = true;
  document.getElementById('rebuild-status').textContent = ' Rebuilding...';
  try {
    const r = await (await fetch('/api/rebuild?confirm=true', {method:'POST'})).json();
    document.getElementById('rebuild-status').textContent = r.error ? ' Error: ' + r.error : ' Done!';
    toast('Rebuild completed'); loadStats();
  } catch(e) { document.getElementById('rebuild-status').textContent = ' Failed'; }
  document.getElementById('btn-rebuild').disabled = false;
}

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