// Personal Memory — PERSONAL-mode "ask your memory" surface.
//
// A retrieval page: ask a question and Parcle searches everything your
// connected sources have synced (Google / Notion / GitHub / Local Folder).
// All of that connector data lands, per-user, in the backend memory store;
// this page calls the one unified search endpoint that reads across it:
//
//   POST /v1/memories/search   { query }
//     → { answer: string, confidence: number, citations: [{ type, id }] }
//
// The endpoint resolves the caller from the bearer token: a first-party
// session (what this console sends) searches its OWN memory, so `user_id` is
// omitted from the body — supplying it is only for developer (pmem_) API keys
// querying an end user. No connector/source filter is sent, so a single query
// spans every source. The response is a synthesized answer plus the citations
// (source type + id) it drew from — there are no raw per-chunk hits anymore.
// When there's no live backend (PARCLE_API_BASE unset) we fall back to a
// canned mock so the page still demos standalone, mirroring the other
// personal surfaces.
//
// History: every successful search is cached client-side in localStorage and
// listed in the left sidebar. Past results are READ-ONLY — selecting one
// re-displays its stored answer + citations, but there's no way to continue or
// edit that conversation; only "New search" starts a fresh query. Nothing
// here is persisted server-side.

const { useState: useMemState, useRef: useMemRef, useEffect: useMemEffect } = React;

// A citation's `type` is the backing source's kind (e.g. "file", "record",
// "session") → display label + accent for the source chip.
const MEM_CITATION_META = {
  file:    { label: 'File',    color: '#475569' },
  record:  { label: 'Record',  color: '#1A73E8' },
  session: { label: 'Session', color: '#7C3AED' },
};

function memCitationMeta(citation) {
  const key = ((citation && citation.type) || '').toLowerCase();
  return MEM_CITATION_META[key] || { label: (citation && citation.type) || 'Source', color: '#64748B' };
}

// Citation ids are opaque UUIDs — show a short, stable head for reference.
function memCitationId(id) {
  const s = String(id || '');
  return s.length > 12 ? s.slice(0, 12) + '…' : s;
}

// Normalize a stored/live result into the current citation contract. Live and
// fresh-mock results carry `citations`; older cached history entries predate
// the unified endpoint and only have `hits`, so derive a best-effort citation
// list from those so they keep rendering instead of looking empty.
function memCitations(result) {
  if (!result) return [];
  if (Array.isArray(result.citations)) return result.citations;
  if (Array.isArray(result.hits)) {
    return result.hits.map(h => {
      const m = (h && h.metadata) || {};
      return { type: m.source_type || m.connector || m.source_kind || 'source', id: h.source_id || h.chunk_id };
    });
  }
  return [];
}

// ── History (localStorage) ───────────────────────────────────────────────────
// One entry per successful search: { id, query, result, ts }. Newest first,
// capped so the store can't grow without bound. Read-only once written.
const MEM_HISTORY_KEY = 'parcle.memory.history';
const MEM_SIDEBAR_KEY = 'parcle.memory.sidebar';
const MEM_HISTORY_MAX = 50;

function memHistoryLoad() {
  try {
    const raw = JSON.parse(localStorage.getItem(MEM_HISTORY_KEY));
    return Array.isArray(raw) ? raw.filter(e => e && e.id && e.result) : [];
  } catch (e) { return []; }
}

function memNewId() {
  return 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
}

// Compact relative time for sidebar entries.
function memWhen(ts) {
  if (!ts) return '';
  const diff = Math.max(0, Date.now() - ts);
  const m = Math.floor(diff / 60000);
  if (m < 1) return 'just now';
  if (m < 60) return m + 'm ago';
  const h = Math.floor(m / 60);
  if (h < 24) return h + 'h ago';
  const d = Math.floor(h / 24);
  if (d < 7) return d + 'd ago';
  return Math.floor(d / 7) + 'w ago';
}

// ── API ─────────────────────────────────────────────────────────────────────
// Live mode hits the backend; mock mode returns a deterministic-ish canned
// result so the surface demos without a server. pcLive() comes from
// personal_connectors_data.jsx (true only when PARCLE_API_BASE is set).
async function memSearchLive(query) {
  const headers = (typeof parcleAuthHeaders === 'function')
    ? { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', ...parcleAuthHeaders() }
    : { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' };
  // `user_id` is intentionally omitted: the console authenticates with a
  // first-party session token, so the endpoint searches the caller's own
  // memory. It must only be sent by developer (pmem_) API keys.
  //
  // The endpoint streams Server-Sent Events: the search can outlast Cloudflare's
  // 100s timeout, so the response opens immediately and emits a keepalive comment
  // every ~20s until the answer is ready. The cheap prepare phase (auth, query
  // validation, workspace resolution) runs before the stream, so its failures
  // still arrive as real HTTP status codes (401/404/422/503) with a JSON body.
  // Once the stream is open the status is fixed at 200, and a run-phase failure
  // (e.g. prompt-injection block, engine unavailable) is delivered in-band as an
  // `error` event. The successful answer arrives as a single `final` event.
  const res = await fetch(parcleApiBase() + '/v1/memories/search', {
    method: 'POST',
    headers,
    body: JSON.stringify({ query }),
  });
  if (res.status === 401 && typeof parcleNotifyUnauthorized === 'function') parcleNotifyUnauthorized();
  if (!res.ok) {
    let detail = '';
    try { detail = (await res.json()).detail || ''; } catch (e) {}
    throw new Error(`POST /v1/memories/search → ${res.status}${detail ? ' ' + detail : ''}`);
  }

  if (!res.body) throw new Error('Search response had no stream body.');
  const reader = res.body.getReader();
  const dec = new TextDecoder();
  let buf = '';
  let result = null;
  let streamError = null;
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    buf += dec.decode(value, { stream: true });
    let idx;
    while ((idx = buf.indexOf('\n\n')) >= 0) {
      const frame = buf.slice(0, idx);
      buf = buf.slice(idx + 2);
      let event = 'message';
      const dataLines = [];
      for (const raw of frame.split('\n')) {
        if (raw.startsWith('event:')) event = raw.slice(6).trim();
        else if (raw.startsWith('data:')) dataLines.push(raw.slice(5).replace(/^\s/, ''));
      }
      if (dataLines.length === 0) continue; // `: keepalive` comment frames land here
      let parsed;
      try { parsed = JSON.parse(dataLines.join('\n')); }
      catch (e) { console.warn('SSE parse error', e, dataLines); continue; }
      if (event === 'final') result = parsed;
      else if (event === 'error') streamError = parsed;
    }
  }
  if (result) return result;
  if (streamError) {
    const msg = (streamError && streamError.message) || 'Internal error during search.';
    throw new Error(msg);
  }
  throw new Error('Search stream ended before a result was received.');
}

function memSearchMock(query) {
  const q = query.trim();
  const citations = [
    { type: 'file',    id: 'src-notion-q3planning' },
    { type: 'session', id: 'src-gmail-alex-thread' },
    { type: 'record',  id: 'src-github-pr-142' },
  ];
  return new Promise(resolve => setTimeout(() => resolve({
    answer: `Based on your connected sources, "${q}" appears across a Notion planning doc, an email thread with Alex, and an open GitHub pull request. The current consensus is to proceed and circulate a short summary by end of week.`,
    confidence: 0.72,
    citations,
  }), 700));
}

function memLive() {
  try { return typeof pcLive === 'function' && pcLive(); } catch (e) { return false; }
}

const MEM_SUGGESTIONS = [
  'What did I discuss about the Q3 roadmap?',
  'Summarize my recent emails with Alex',
  'Which pull requests are still open?',
  'Find notes about pricing',
];

// ── Page ────────────────────────────────────────────────────────────────────
function PersonalMemory() {
  const [history, setHistory] = useMemState(memHistoryLoad);
  // null = new-search mode (input shown); otherwise the id of the cached
  // history entry being viewed read-only.
  const [currentId, setCurrentId] = useMemState(null);
  const [query, setQuery] = useMemState('');
  const [loading, setLoading] = useMemState(false);
  const [error, setError] = useMemState(null);
  const [sidebarOpen, setSidebarOpen] = useMemState(() => {
    try { const v = localStorage.getItem(MEM_SIDEBAR_KEY); return v == null ? true : v === 'true'; }
    catch (e) { return true; }
  });
  const inputRef = useMemRef(null);
  // The last history payload we wrote (or absorbed from another tab). Lets the
  // save effect skip no-op writes so two tabs can't ping-pong identical writes
  // forever via the `storage` event.
  const lastWrittenRef = useMemRef(null);

  useMemEffect(() => {
    let json = null;
    try { json = JSON.stringify(history); } catch (e) { return; }
    if (json === lastWrittenRef.current) return;
    lastWrittenRef.current = json;
    try { localStorage.setItem(MEM_HISTORY_KEY, json); } catch (e) { /* quota / disabled */ }
  }, [history]);

  // Cross-tab sync: when another tab edits the shared history (e.g. deletes an
  // entry), reload it here so this tab doesn't keep showing — or later clobber —
  // a now-stale list. Mark the absorbed value as last-written so the save
  // effect above doesn't echo it straight back.
  useMemEffect(() => {
    const onStorage = (e) => {
      if (e.key !== MEM_HISTORY_KEY) return;
      const next = memHistoryLoad();
      try { lastWrittenRef.current = JSON.stringify(next); } catch (err) { lastWrittenRef.current = null; }
      setHistory(next);
    };
    window.addEventListener('storage', onStorage);
    return () => window.removeEventListener('storage', onStorage);
  }, []);

  useMemEffect(() => {
    try { localStorage.setItem(MEM_SIDEBAR_KEY, String(sidebarOpen)); } catch (e) {}
  }, [sidebarOpen]);

  const current = currentId ? history.find(h => h.id === currentId) : null;
  // A stale currentId (entry deleted in another tab) falls back to search mode.
  useMemEffect(() => {
    if (currentId && !history.find(h => h.id === currentId)) setCurrentId(null);
  }, [history, currentId]);

  const run = async (raw) => {
    const q = (raw != null ? raw : query).trim();
    if (!q || loading) return;
    setLoading(true);
    setError(null);
    try {
      const data = memLive() ? await memSearchLive(q) : await memSearchMock(q);
      const entry = { id: memNewId(), query: q, result: data, ts: Date.now() };
      setHistory(prev => [entry, ...prev].slice(0, MEM_HISTORY_MAX));
      setCurrentId(entry.id);
      setQuery('');
    } catch (e) {
      // Keep the query in the box so the user can retry / tweak it.
      setError((e && e.message) || String(e));
    } finally {
      setLoading(false);
    }
  };

  const onKeyDown = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); run(); }
  };

  const newSearch = () => {
    setCurrentId(null);
    setError(null);
    setQuery('');
    setTimeout(() => { try { inputRef.current && inputRef.current.focus(); } catch (e) {} }, 0);
  };
  const switchTo = (id) => { setCurrentId(id); setError(null); };
  const deleteEntry = (id) => {
    setHistory(prev => prev.filter(h => h.id !== id));
    if (currentId === id) setCurrentId(null);
  };

  // Results come from the selected cached entry; new-search mode shows none
  // until a search completes (which then switches into viewing mode).
  const result = current ? current.result : null;
  const submitted = current ? current.query : '';
  const citations = memCitations(result);
  const hasAnswer = result && result.answer;

  return (
    <div style={{ display: 'flex', height: '100%', minHeight: 0 }}>
      <MemoryHistorySidebar
        history={history}
        currentId={currentId}
        open={sidebarOpen}
        setOpen={setSidebarOpen}
        onSwitch={switchTo}
        onNew={newSearch}
        onDelete={deleteEntry}
      />

      <div style={{ flex: 1, minWidth: 0, overflowY: 'auto' }}>
        <div style={{ padding: '32px 32px 80px', maxWidth: 880, margin: '0 auto' }}>
          {/* Title */}
          <div style={{ marginBottom: 8 }}>
            <h1 className="t-h1" style={{ margin: 0 }}>Memory</h1>
            <div className="text-secondary t-body" style={{ marginTop: 4 }}>
              Ask a question — Parcle searches across everything your connectors have synced.
            </div>
          </div>

          {/* Viewing a cached result (read-only) — show the question + a way out. */}
          {current ? (
            <div style={{
              marginTop: 24, display: 'flex', alignItems: 'flex-start', gap: 12,
              padding: '12px 14px',
              border: '1px solid var(--border)', borderRadius: 12,
              background: 'var(--surface)',
            }}>
              <Icons.Search size={16} style={{ color: 'var(--text-tertiary)', flexShrink: 0, marginTop: 4 }} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div className="t-body" style={{ fontWeight: 500, color: 'var(--text-primary)' }}>{current.query}</div>
                <div className="t-micro text-tertiary" style={{ marginTop: 3 }}>
                  Saved result · read-only{current.ts ? ` · ${memWhen(current.ts)}` : ''}
                </div>
              </div>
              <button className="btn sm accent" onClick={newSearch} style={{ flexShrink: 0 }}>
                <Icons.Plus size={11} /> New search
              </button>
            </div>
          ) : (
            /* Ask box (new-search mode) */
            <div style={{
              marginTop: 24, display: 'flex', alignItems: 'center', gap: 8,
              padding: '6px 6px 6px 14px',
              border: '1px solid var(--border)', borderRadius: 12,
              background: 'var(--surface)',
            }}>
              <Icons.Search size={16} style={{ color: 'var(--text-tertiary)', flexShrink: 0 }} />
              <input
                ref={inputRef}
                autoFocus
                value={query}
                onChange={e => setQuery(e.target.value)}
                onKeyDown={onKeyDown}
                placeholder="Ask anything across your connected sources…"
                style={{
                  flex: 1, border: 'none', outline: 'none', background: 'transparent',
                  fontSize: 14, color: 'var(--text-primary)', padding: '8px 0',
                }}
              />
              <button className="btn accent sm" onClick={() => run()} disabled={loading || !query.trim()}
                style={(loading || !query.trim()) ? { opacity: 0.6 } : {}}>
                {loading
                  ? <><Icons.Spinner size={13} style={{ animation: 'parcle-spin .8s linear infinite' }} /> Searching</>
                  : <><Icons.Sparkle size={13} /> Ask</>}
              </button>
            </div>
          )}

          {/* Pre-search suggestions (new-search mode, idle) */}
          {!current && !loading && (
            <div style={{ marginTop: 18 }}>
              <div className="t-micro text-tertiary" style={{ marginBottom: 10 }}>TRY ASKING</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
                {MEM_SUGGESTIONS.map(s => (
                  <button key={s} className="btn sm ghost" onClick={() => run(s)}
                    style={{ fontWeight: 400, color: 'var(--text-secondary)' }}>
                    {s}
                  </button>
                ))}
              </div>
              <div className="t-small text-tertiary" style={{ marginTop: 22, display: 'flex', alignItems: 'center', gap: 6 }}>
                <Icons.Plug size={13} style={{ color: 'var(--text-tertiary)' }} />
                Results come from every connected source. Manage them in Connectors.
              </div>
            </div>
          )}

          {/* Loading */}
          {loading && (
            <div style={{ marginTop: 28, display: 'flex', alignItems: 'center', gap: 10, color: 'var(--text-secondary)' }}>
              <Icons.Spinner size={15} style={{ animation: 'parcle-spin .8s linear infinite' }} />
              <span className="mono-sm mono">Searching your memory…</span>
            </div>
          )}

          {/* Error (new-search mode only) */}
          {error && !loading && !current && (
            <div style={{
              marginTop: 24, padding: '14px 16px',
              border: '1px solid var(--error-bg)', borderRadius: 12,
              background: 'var(--surface)', display: 'flex', alignItems: 'center', gap: 10,
            }}>
              <Icons.AlertTri size={15} style={{ color: 'var(--error)', flexShrink: 0 }} />
              <div>
                <div className="t-body" style={{ fontWeight: 500 }}>Search failed</div>
                <div className="mono-sm mono text-secondary" style={{ marginTop: 2 }}>{error}</div>
              </div>
            </div>
          )}

          {/* Results */}
          {result && !loading && (
            <div style={{ marginTop: 24, display: 'flex', flexDirection: 'column', gap: 20 }}>
              {/* AI answer */}
              {hasAnswer && (
                <div style={{
                  padding: 18, borderRadius: 12,
                  border: '1px solid var(--border-subtle)',
                  background: 'var(--bg)', borderLeft: '2px solid var(--accent)',
                }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
                    <Icons.Sparkle size={14} style={{ color: 'var(--accent-text)' }} />
                    <span className="t-micro text-tertiary">ANSWER</span>
                    {result.confidence != null && (
                      <span className="mono-sm mono text-tertiary" style={{ marginLeft: 'auto' }}>
                        {Math.round(result.confidence * 100)}% confidence
                      </span>
                    )}
                  </div>
                  <div className="t-body" style={{ lineHeight: '22px', whiteSpace: 'pre-wrap' }}>
                    {result.answer}
                  </div>
                </div>
              )}

              {/* Citations / sources the answer drew from */}
              <div>
                <div className="t-micro text-tertiary" style={{ marginBottom: 10 }}>
                  {citations.length
                    ? `${citations.length} ${citations.length === 1 ? 'CITATION' : 'CITATIONS'}`
                    : 'NO CITATIONS'}
                </div>

                {citations.length === 0 ? (
                  <div className="t-small text-tertiary" style={{
                    padding: 16, border: '1px solid var(--border-subtle)', borderRadius: 12,
                    background: 'var(--surface)',
                  }}>
                    {hasAnswer
                      ? 'The answer above did not cite any specific source.'
                      : `Nothing matched “${submitted}”. Try rephrasing, or connect more sources in Connectors.`}
                  </div>
                ) : (
                  <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
                    {citations.map((c, i) => <MemoryCitation key={(c.id || '') + i} citation={c} />)}
                  </div>
                )}
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ── History sidebar ───────────────────────────────────────────────────────────
// Mirrors the chat sidebar: collapsible rail of past searches, newest first.
function MemoryHistorySidebar({ history, currentId, open, setOpen, onSwitch, onNew, onDelete }) {
  const width = open ? 240 : 44;
  return (
    <aside style={{
      width, flexShrink: 0, borderRight: '1px solid var(--border-subtle)',
      background: 'var(--bg)', display: 'flex', flexDirection: 'column',
      transition: 'width 160ms cubic-bezier(.2,.8,.2,1)', overflow: 'hidden', minHeight: 0,
    }}>
      {/* Header */}
      <div style={{
        padding: open ? '10px 12px' : 6,
        borderBottom: '1px solid var(--border-subtle)',
        display: 'flex', alignItems: 'center', gap: 6,
      }}>
        {open ? (
          <>
            <span className="t-micro text-tertiary" style={{ flex: 1 }}>History</span>
            <button className="btn sm ghost" style={{ padding: '0 6px', height: 24 }} onClick={() => setOpen(false)} title="Collapse">
              <Icons.ChevronL size={12} />
            </button>
            <button className="btn sm accent" style={{ padding: '0 8px', height: 24 }} onClick={onNew}>
              <Icons.Plus size={11} /> New
            </button>
          </>
        ) : (
          <button className="btn sm accent" style={{ padding: 0, height: 28, width: '100%' }} onClick={onNew} title="New search">
            <Icons.Plus size={12} />
          </button>
        )}
      </div>

      {/* Body */}
      <div style={{ flex: 1, overflowY: 'auto', padding: open ? '6px 8px' : 6 }}>
        {open ? (
          history.length === 0 ? (
            <div className="t-small text-tertiary" style={{ padding: '18px 10px', textAlign: 'center' }}>No searches yet</div>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
              {history.map(h => (
                <MemoryHistoryItem key={h.id}
                  entry={h}
                  active={h.id === currentId}
                  onSwitch={() => onSwitch(h.id)}
                  onDelete={() => onDelete(h.id)} />
              ))}
            </div>
          )
        ) : (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'center' }}>
            {history.slice(0, 12).map(h => {
              const isActive = h.id === currentId;
              return (
                <button key={h.id} onClick={() => onSwitch(h.id)} title={h.query}
                  style={{
                    width: 30, height: 30, borderRadius: 7,
                    background: isActive ? 'var(--selected)' : 'transparent',
                    color: isActive ? 'var(--text-primary)' : 'var(--text-tertiary)',
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                    transition: 'background 80ms',
                  }}
                  onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = 'var(--hover)'; }}
                  onMouseLeave={e => { if (!isActive) e.currentTarget.style.background = 'transparent'; }}
                >
                  <Icons.Search size={13} />
                </button>
              );
            })}
            <button className="btn sm ghost" style={{ padding: 0, width: 30, height: 30, marginTop: 6 }} onClick={() => setOpen(true)} title="Expand">
              <Icons.ChevronR size={12} />
            </button>
          </div>
        )}
      </div>
    </aside>
  );
}

function MemoryHistoryItem({ entry, active, onSwitch, onDelete }) {
  const [hover, setHover] = useMemState(false);
  const title = entry.query || 'Untitled search';
  return (
    <div
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      onClick={onSwitch}
      style={{
        display: 'flex', alignItems: 'center', gap: 6,
        padding: '6px 8px', borderRadius: 7, cursor: 'pointer',
        background: active ? 'var(--selected)' : (hover ? 'var(--hover)' : 'transparent'),
        color: active ? 'var(--text-primary)' : 'var(--text-secondary)',
        minHeight: 30,
      }}
    >
      <Icons.Search size={12} style={{ flexShrink: 0, color: 'var(--text-tertiary)' }} />
      <span style={{
        flex: 1, minWidth: 0, overflow: 'hidden',
        textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 13,
      }}>{title}</span>
      {hover ? (
        <div style={{ display: 'flex', gap: 2, flexShrink: 0 }} onClick={e => e.stopPropagation()}>
          <button className="btn sm ghost" style={{ padding: 0, height: 22, width: 22 }} title="Delete"
            onClick={() => { if (window.confirm(`Delete "${title}"?`)) onDelete(); }}>
            <Icons.Trash size={11} />
          </button>
        </div>
      ) : (
        <span className="t-micro text-tertiary" style={{ flexShrink: 0 }}>{memWhen(entry.ts)}</span>
      )}
    </div>
  );
}

// ── Single citation chip ────────────────────────────────────────────────────
// The unified search endpoint returns citations as opaque { type, id } source
// references — no title, snippet, or score — so each renders as a compact chip.
function MemoryCitation({ citation }) {
  const meta = memCitationMeta(citation);
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 8,
      padding: '6px 10px', borderRadius: 8,
      border: '1px solid var(--border-subtle)', background: 'var(--surface)',
    }}>
      <span style={{ width: 7, height: 7, borderRadius: 2, background: meta.color, flexShrink: 0 }} />
      <span className="mono-sm mono" style={{ color: 'var(--text-secondary)' }}>{meta.label}</span>
      <span className="mono-sm mono text-tertiary">{memCitationId(citation.id)}</span>
    </span>
  );
}

Object.assign(window, { PersonalMemory });
