/* Al Moasis · Dashboard v3 — Memory tab
   KG entities (skill / cron / policy / tool / conversation) — searchable
   + filterable. Reads /api/knowledge/graph?tenant_id=… and renders cards. */

'use strict';

(function () {
  const $  = (s) => document.querySelector(s);
  const $$ = (s) => Array.from(document.querySelectorAll(s));
  const api = () => window.__moasis?.api;
  const applyIcons = () => window.__moasis?.applyIcons?.();

  let ENTITIES = [];
  let LINKS = [];          // kg_relationships, fetched alongside entities
  let activeKind = 'all';
  let graphNetwork = null; // vis-network instance (lazy-created)
  let graphMounted = false;
  // Anti-glitch state: the 12s auto-refresh used to call setData() every tick,
  // which restarts vis-network's physics (the graph "jumps/restarts"), and it
  // did so even when nothing changed — most jarring mid-hover. We now (a) skip
  // re-render when the data signature is unchanged, and (b) defer re-render
  // while the user is hovering/dragging a node.
  let _lastGraphSig = '';
  let _graphInteracting = false;
  let _pendingGraphRender = false;
  function graphSignature(ents, links) {
    const e = ents.map((x) => `${x.id}~${x.episode_count || 0}`).sort().join(',');
    const l = links.map((x) => x.id).sort().join(',');
    return `${ents.length}:${links.length}:${e}|${l}`;
  }
  function flushPendingGraph() {
    if (!_pendingGraphRender) return;
    _pendingGraphRender = false;
    setTimeout(() => { if (!_graphInteracting) renderGraph(); }, 300);
  }

  const KIND_ICONS = {
    skill: 'sparkles',
    cron: 'repeat',
    policy: 'shield',
    tool: 'wrench',
    conversation: 'message-square',
    research: 'binoculars',
    // v3 — journal-mirror kinds
    action: 'activity',
    lesson: 'graduation-cap',
    decision: 'gavel',
    goal: 'target',
    document: 'file-text',
  };
  const KIND_LABELS = {
    skill: 'مهارة',
    cron: 'مهمة دورية',
    policy: 'سياسة',
    tool: 'أداة',
    conversation: 'من محادثة',
    research: 'بحث',
    action: 'إجراء',
    lesson: 'درس',
    decision: 'قرار',
    goal: 'هدف',
    document: 'مستند',
  };

  async function loadEntities() {
    const grid = $('[data-bind="memory-grid"]');
    if (!grid) return;
    try {
      const tid = window.CURRENT_TENANT_ID;
      if (!tid) { grid.innerHTML = empty('لم يُحدَّد المستأجر بعد.'); return; }
      const r = await api()(`/api/knowledge/graph?tenant_id=${encodeURIComponent(tid)}`);
      // The endpoint returns { success, data: { nodes, links, stats } }
      ENTITIES = r?.data?.nodes || r?.nodes || [];
      LINKS    = r?.data?.links || r?.links || [];
      // If nothing changed since the last load, leave the live graph alone —
      // re-rendering identical data just restarts the physics for no reason
      // (the every-12s "glitch"). Only the first mount bypasses this.
      const sig = graphSignature(ENTITIES, LINKS);
      if (sig === _lastGraphSig && graphMounted) return;
      _lastGraphSig = sig;
      render();
      // Defer the graph re-render while the user is hovering/dragging so we
      // never yank a node out from under the cursor. It runs right after.
      if (graphMounted && _graphInteracting) { _pendingGraphRender = true; return; }
      renderGraph();
    } catch (err) {
      grid.innerHTML = empty(`تعذّر التحميل: ${err.message}`);
    }
  }

  function render() {
    const grid = $('[data-bind="memory-grid"]');
    if (!grid) return;
    const q = ($('#memory-search')?.value || '').trim().toLowerCase();
    const list = ENTITIES
      .filter((e) => activeKind === 'all' || e.kind === activeKind)
      .filter((e) => !q
        || (e.name || '').toLowerCase().includes(q)
        || (e.type || '').toLowerCase().includes(q)
        || JSON.stringify(e.properties || {}).toLowerCase().includes(q));
    if (list.length === 0) {
      grid.innerHTML = empty(q ? 'لا توجد نتائج للبحث.' : 'لا توجد عناصر في الذاكرة بعد.');
      return;
    }
    grid.innerHTML = list.map(card).join('');
    applyIcons();
  }

  function card(e) {
    const icon = KIND_ICONS[e.kind] || 'circle';
    const kindLabel = KIND_LABELS[e.kind] || e.kind;
    const props = e.properties || {};
    const summary = props.summary || props.description || props.schedule || '';
    return `
      <div class="bg-white rounded-xl border border-line shadow-card p-4 hover:shadow-cardHover transition">
        <div class="flex items-start gap-3 mb-2">
          <span class="w-8 h-8 rounded-lg bg-emerald-50 flex items-center justify-center shrink-0">
            <i data-lucide="${icon}" class="w-4 h-4 text-emerald-600"></i>
          </span>
          <div class="flex-1 min-w-0">
            <div class="font-semibold text-[14px] text-ink truncate">${esc(e.name)}</div>
            <div class="text-[11px] text-muted">${esc(kindLabel)} · ${esc(e.type || '')}</div>
          </div>
        </div>
        ${summary ? `<div class="text-[12.5px] text-body leading-[1.6] line-clamp-3">${esc(summary)}</div>` : ''}
      </div>`;
  }

  function empty(msg) {
    return `<div class="col-span-2 text-center text-[13px] text-muted py-12">${esc(msg)}</div>`;
  }
  function esc(s) { return String(s ?? '').replace(/[&<>"']/g,(c)=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c])); }

  // ── Obsidian vault rebuild + open ──
  async function rebuildVault(btn) {
    const tid = window.CURRENT_TENANT_ID;
    if (!tid) return;
    const origLabel = btn.innerHTML;
    btn.disabled = true;
    btn.innerHTML = '<i data-lucide="loader-circle" class="w-4 h-4 animate-spin"></i><span>جارٍ المزامنة…</span>';
    applyIcons();
    try {
      const r = await api()('/api/knowledge/vault/rebuild', {
        method: 'POST',
        body: JSON.stringify({ tenant_id: tid }),
      });
      const d = r?.data || {};
      const flash = document.querySelector('[data-bind="memory-vault-flash"]');
      if (flash) {
        flash.innerHTML = `✓ صُدّر ${d.entities || 0} عنصراً + ${d.relationships || 0} علاقة إلى:<br><code class="text-[11px] font-num" dir="ltr">${escapeHtmlInner(d.path || '')}</code><br>افتح هذا المجلد في تطبيق Obsidian لرؤية الرسم البياني.`;
        flash.classList.remove('hidden');
      }
    } catch (err) {
      const flash = document.querySelector('[data-bind="memory-vault-flash"]');
      if (flash) {
        flash.innerHTML = `<span class="text-rose-600">فشل: ${escapeHtmlInner(err.message)}</span>`;
        flash.classList.remove('hidden');
      }
    } finally {
      btn.disabled = false;
      btn.innerHTML = origLabel;
      applyIcons();
    }
  }
  async function openVaultFolder(btn) {
    const tid = window.CURRENT_TENANT_ID;
    if (!tid) return;
    try {
      const r = await api()(`/api/knowledge/vault/path?tenant_id=${encodeURIComponent(tid)}`);
      const path = r?.data?.path;
      if (!path) return;
      // Open via Tauri opener if available (system Finder), else show alert
      if (window.__TAURI__?.opener?.openPath) window.__TAURI__.opener.openPath(path);
      else if (window.__TAURI__?.opener?.openUrl) window.__TAURI__.opener.openUrl(`file://${path}`);
      else {
        const flash = document.querySelector('[data-bind="memory-vault-flash"]');
        if (flash) {
          flash.innerHTML = `مسار المكتبة:<br><code class="text-[11px] font-num" dir="ltr">${escapeHtmlInner(path)}</code><br>افتحه يدوياً في Obsidian → File → Open Vault.`;
          flash.classList.remove('hidden');
        }
      }
    } catch (err) {
      alert(`فشل: ${err.message}`);
    }
  }
  function escapeHtmlInner(s) { return esc(s); }

  // ─── Obsidian-style graph view ──────────────────────────────────
  // Dark canvas, pale nodes, glow halo on focus, whisper-thin edges.
  // Real KG data only — kg_entities + kg_relationships from your DB.
  // Color is a subtle accent (kind classification) on otherwise-pale
  // nodes, so the overall feel stays Obsidian-clean rather than rainbow.
  const KIND_COLORS = {
    skill:        { bg: '#D1FAE5', border: '#A7F3D0', highlight: '#10B981' }, // pale mint
    cron:         { bg: '#DBEAFE', border: '#BFDBFE', highlight: '#3B82F6' }, // pale sky
    policy:       { bg: '#EDE9FE', border: '#DDD6FE', highlight: '#8B5CF6' }, // pale violet
    tool:         { bg: '#FEF3C7', border: '#FDE68A', highlight: '#F59E0B' }, // pale amber
    conversation: { bg: '#FCE7F3', border: '#FBCFE8', highlight: '#EC4899' }, // pale rose
    research:     { bg: '#CFFAFE', border: '#A5F3FC', highlight: '#06B6D4' }, // pale cyan
  };
  const DEFAULT_NODE_COLOR  = { bg: '#E5E7EB', border: '#9CA3AF', highlight: '#FFFFFF' };
  const EDGE_COLOR_DIM      = 'rgba(255, 255, 255, 0.08)';
  const EDGE_COLOR_HIGHLIGHT = 'rgba(16, 185, 129, 0.85)';

  function renderGraph() {
    const host = document.getElementById('memory-graph');
    if (!host) return;
    if (!window.vis || !window.vis.Network) {
      // vis-network still loading — retry shortly
      host.innerHTML = '<div class="flex items-center justify-center h-full text-muted text-[13px]">جارٍ تحميل محرّك الرسم…</div>';
      setTimeout(renderGraph, 500);
      return;
    }
    if (ENTITIES.length === 0) {
      host.innerHTML = '<div class="flex items-center justify-center h-full text-muted text-[13px]">لا توجد عناصر في الذاكرة بعد.</div>';
      return;
    }
    // vis-network builds a canvas sized to its host. If we create it while the
    // pane has just been un-hidden (layout not flushed → height 0), it makes a
    // 0×0 canvas that never resizes → the graph shows as a blank dark box even
    // though data is present. Wait until the host is actually measured.
    if (host.clientHeight < 40 || host.clientWidth < 40) { setTimeout(renderGraph, 200); return; }

    // Filter by active chip + search
    const q = ($('#memory-search')?.value || '').trim().toLowerCase();
    const visible = ENTITIES.filter((e) =>
      (activeKind === 'all' || e.kind === activeKind) &&
      (!q
        || (e.name || '').toLowerCase().includes(q)
        || (e.type || '').toLowerCase().includes(q))
    );
    const visibleIds = new Set(visible.map((e) => e.id));

    const nodes = visible.map((e) => {
      const c = KIND_COLORS[e.kind] || DEFAULT_NODE_COLOR;
      // Obsidian uses 8-32px range scaled by connection count
      const size = 8 + Math.min(20, Math.log2((e.episode_count || 1) + 1) * 4);
      return {
        id: e.id,
        label: (e.name || '').length > 22 ? (e.name).slice(0, 20) + '…' : (e.name || '?'),
        title: `${e.name}\n${e.type} · ${e.kind}\n${e.episode_count || 0} episode(s)`,
        color: {
          background: c.bg,
          border:     c.border,
          highlight:  { background: c.highlight, border: '#FFFFFF' },
          hover:      { background: c.highlight, border: '#FFFFFF' },
        },
        font: {
          color: '#E5E7EB',                      // pale labels on dark canvas
          size: 11,
          face: 'Noto Sans Arabic, Inter, sans-serif',
          strokeWidth: 0,
          background: 'rgba(14,17,23,0.45)',     // label backdrop so they're readable over nodes
        },
        size,
        shape: 'dot',
        borderWidth: 1,
        borderWidthSelected: 2,
        // Glow halo — Obsidian's signature look. Subtle pale ring around
        // each node, bright when focused.
        shadow: { enabled: true, color: 'rgba(255,255,255,0.18)', size: 14, x: 0, y: 0 },
      };
    });

    // /api/knowledge/graph returns links with `source` / `target` fields
    // (NOT `source_id` / `target_id` — that's the DB column naming).
    const edges = (LINKS || [])
      .filter((l) => {
        const s = l.source || l.source_id || l.from;
        const t = l.target || l.target_id || l.to;
        return visibleIds.has(s) && visibleIds.has(t);
      })
      .map((l, i) => ({
        id: `e${i}`,
        from:  l.source || l.source_id || l.from,
        to:    l.target || l.target_id || l.to,
        // No edge labels by default — Obsidian's graph is whisper-clean.
        color: {
          color:     EDGE_COLOR_DIM,
          highlight: EDGE_COLOR_HIGHLIGHT,
          hover:     EDGE_COLOR_HIGHLIGHT,
        },
        width: 0.8,
        smooth: { enabled: true, type: 'continuous', roundness: 0.3 },
      }));

    const data = {
      nodes: new window.vis.DataSet(nodes),
      edges: new window.vis.DataSet(edges),
    };
    // (v3.0.4) Physics tuned to match Obsidian's signature drag feel —
    // springy edges, satisfying inertia, graceful settle. Higher
    // springLength gives an airier layout (less node overlap), lower
    // springConstant makes connected nodes follow the drag with that
    // elastic-string feeling, higher damping kills excessive bouncing
    // so the network settles cleanly when you release.
    const options = {
      autoResize: true,
      physics: {
        enabled: true,
        solver: 'forceAtlas2Based',  // smoother than barnesHut for graph viz
        forceAtlas2Based: {
          gravitationalConstant: -60,    // node repulsion
          centralGravity: 0.005,         // very gentle pull toward center
          springLength: 120,             // resting edge length — airy
          springConstant: 0.06,          // edge stiffness — bouncy
          damping: 0.55,                 // settle cleanly after drag
          avoidOverlap: 1,
        },
        maxVelocity: 50,
        minVelocity: 0.5,                // stop early when settling
        stabilization: {
          iterations: 200,
          fit: true,
          updateInterval: 25,
        },
        timestep: 0.45,                  // smoother per-frame motion
      },
      interaction: {
        hover: true,
        tooltipDelay: 100,
        zoomView: true,
        dragView: true,
        dragNodes: true,
        hoverConnectedEdges: true,
        selectConnectedEdges: true,
        navigationButtons: false,
      },
      nodes: {
        shape: 'dot',
        scaling: { min: 8, max: 32 },
        shadow: { enabled: true, color: 'rgba(15,23,42,0.15)', size: 6, x: 0, y: 2 },
      },
      edges: {
        smooth: { enabled: true, type: 'continuous', roundness: 0.4 },
        hoverWidth: 2.5,
        selectionWidth: 2.5,
      },
    };

    if (graphNetwork) {
      graphNetwork.setData(data);
    } else {
      graphNetwork = new window.vis.Network(host, data, options);

      // Pre-compute neighborhood index once (avoids recomputing on every
      // hover event — graph stays buttery smooth even with 200+ nodes).
      const neighborMap = new Map();
      edges.forEach((e) => {
        if (!neighborMap.has(e.from)) neighborMap.set(e.from, new Set());
        if (!neighborMap.has(e.to))   neighborMap.set(e.to,   new Set());
        neighborMap.get(e.from).add(e.to);
        neighborMap.get(e.to).add(e.from);
      });
      const allNodeIds = nodes.map((n) => n.id);

      const focusNode = (id) => {
        const connected = new Set([id, ...(neighborMap.get(id) || [])]);
        // Dim non-neighbors way down (Obsidian goes to ~0.12), brighten
        // the focused node by intensifying its halo shadow.
        const updates = allNodeIds.map((nid) => ({
          id: nid,
          opacity: connected.has(nid) ? 1 : 0.12,
          // Bigger glow on the focused node itself
          shadow: nid === id
            ? { enabled: true, color: 'rgba(255,255,255,0.55)', size: 28, x: 0, y: 0 }
            : { enabled: true, color: 'rgba(255,255,255,0.18)', size: 14, x: 0, y: 0 },
        }));
        data.nodes.update(updates);
        // Edges: bright emerald for in-neighborhood, ghosted otherwise.
        const edgeUpdates = edges.map((e) => {
          const inFocus = connected.has(e.from) && connected.has(e.to);
          return {
            id: e.id,
            color: { color: inFocus ? EDGE_COLOR_HIGHLIGHT : 'rgba(255,255,255,0.04)' },
            width: inFocus ? 1.6 : 0.6,
          };
        });
        data.edges.update(edgeUpdates);
      };

      const clearFocus = () => {
        data.nodes.update(allNodeIds.map((nid) => ({
          id: nid,
          opacity: 1,
          shadow: { enabled: true, color: 'rgba(255,255,255,0.18)', size: 14, x: 0, y: 0 },
        })));
        data.edges.update(edges.map((e) => ({
          id: e.id,
          color: { color: EDGE_COLOR_DIM },
          width: 0.8,
        })));
      };

      // Hover focus — Obsidian's signature behavior. Hover any node and
      // the rest of the graph fades to give you a clear sub-graph view.
      // hover/drag also flag interaction so the auto-refresh defers its
      // re-render instead of restarting the physics under the cursor.
      graphNetwork.on('hoverNode', (params) => { _graphInteracting = true; focusNode(params.node); });
      graphNetwork.on('blurNode',  ()       => { _graphInteracting = false; clearFocus(); flushPendingGraph(); });
      graphNetwork.on('dragStart', () => { _graphInteracting = true; });
      graphNetwork.on('dragEnd',   () => { _graphInteracting = false; flushPendingGraph(); });

      // Click: persistent focus on the clicked node until clicking blank space
      graphNetwork.on('click', (params) => {
        if (params.nodes.length > 0) focusNode(params.nodes[0]);
        else clearFocus();
      });

      // Cursor: pointer on hover so the affordance is obvious
      graphNetwork.on('hoverNode', () => host.style.cursor = 'pointer');
      graphNetwork.on('blurNode',  () => host.style.cursor = 'default');

      // Keep the canvas sized to the container across pane show/hide + window
      // resizes (vis autoResize only watches the window, not the element).
      if (window.ResizeObserver) {
        const ro = new ResizeObserver(() => {
          try { graphNetwork.setSize('100%', '100%'); graphNetwork.redraw(); } catch { /* */ }
        });
        ro.observe(host);
      }
      // Fit the whole graph into view once physics settles.
      graphNetwork.once('stabilizationIterationsDone', () => { try { graphNetwork.fit({ animation: { duration: 400 } }); } catch { /* */ } });
    }
    graphMounted = true;
  }

  function switchView(view) {
    $$('[data-mem-view]').forEach((b) => b.classList.toggle('wa-method-active', b.dataset.memView === view));
    $$('[data-mem-pane]').forEach((p) => p.classList.toggle('hidden', p.dataset.memPane !== view));
    if (view === 'graph') {
      // Defer one tick so the pane is visible before we measure
      setTimeout(renderGraph, 50);
    }
  }

  function wire() {
    // Filter chips
    $$('[data-mem-kind]').forEach((btn) => {
      btn.addEventListener('click', () => {
        activeKind = btn.dataset.memKind;
        $$('[data-mem-kind]').forEach((b) => b.classList.toggle('wa-method-active', b === btn));
        render();
        if (graphMounted && !document.querySelector('[data-mem-pane="graph"]')?.classList.contains('hidden')) {
          renderGraph();
        }
      });
    });
    // Search input
    const input = $('#memory-search');
    if (input) input.addEventListener('input', () => {
      render();
      if (graphMounted && !document.querySelector('[data-mem-pane="graph"]')?.classList.contains('hidden')) {
        renderGraph();
      }
    });
    // View toggle (cards / graph)
    $$('[data-mem-view]').forEach((btn) => {
      btn.addEventListener('click', () => switchView(btn.dataset.memView));
    });
    // Obsidian actions
    document.addEventListener('click', (e) => {
      const rebuildBtn = e.target.closest?.('[data-action="memory-rebuild-vault"]');
      if (rebuildBtn) rebuildVault(rebuildBtn);
      const openBtn = e.target.closest?.('[data-action="memory-open-vault"]');
      if (openBtn) openVaultFolder(openBtn);
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', wire);
  } else { wire(); }

  // (v3.0.5) Live refresh — poll the graph endpoint every 12s while the
  // Memory pane is visible so new KG nodes (goals, tool calls, lessons,
  // decisions) appear without a manual reload. Cleared when the user
  // navigates away to avoid burning bandwidth on background tabs.
  let _refreshTimer = null;
  function startAutoRefresh() {
    stopAutoRefresh();
    _refreshTimer = setInterval(() => {
      // Only refresh if the Memory pane is the visible one. The pane
      // router toggles a class on each panel; we check visibility via
      // the same selector the router uses.
      const memPanel = document.querySelector('[data-pane="memory"]:not(.hidden)');
      if (!memPanel) return;
      loadEntities();
    }, 12_000);
  }
  function stopAutoRefresh() {
    if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
  }

  function onShow() {
    loadEntities();
    startAutoRefresh();
    // Re-showing the pane: the canvas may have been sized to 0 while hidden —
    // force it to re-measure + repaint now that it's visible again.
    if (graphNetwork) setTimeout(() => { try { graphNetwork.setSize('100%', '100%'); graphNetwork.redraw(); graphNetwork.fit(); } catch { /* */ } }, 150);
  }
  function onHide() { stopAutoRefresh(); }
  window.__moasisMemory = { onShow, onHide };
})();
