// simulator.jsx — Eliza chat + admin overlay
// Mount target: #sim-app (the .sim-wrap section)

const { useState, useEffect, useRef, useMemo } = React;

// A persona's opening line becomes the first message in the stream.
function greetingMsg(text) {
  return text ? [{ from: 'eliza', t: 'just now', body: text }] : [];
}

// Stable anonymous visitor id, so the persona remembers you across visits.
function getVisitorId() {
  try {
    let id = localStorage.getItem('gosum.visitor');
    if (!id) {
      id = 'v-' + Math.random().toString(16).slice(2) + Date.now().toString(16);
      localStorage.setItem('gosum.visitor', id);
    }
    return id;
  } catch (e) {
    return 'v-anon';
  }
}

async function callEliza(payload) {
  const r = await fetch('/api/eliza', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(payload),
  });
  return r.json();
}

// ── Time machine: client-side decay, matching functions/lib/memory.js ──
const DECAY = { H: 14, rho: 0.30, theta: 0.15, rDetail: 1.5 };
const LN2 = Math.log(2);
function salienceAt(a, days) {
  const floor = DECAY.rho * Math.abs(a.valence || 0);
  return Math.min(1, floor + Math.max(0, (a.salience || 0) - floor) * Math.exp(-LN2 * days / DECAY.H));
}
function brightnessAt(b, days) {
  return b * Math.exp(-DECAY.rDetail * LN2 * days / DECAY.H);
}
function kindAt(s) {
  return s < DECAY.theta ? 'forgotten' : s > 0.8 ? 'defining' : s < 0.3 ? 'faint' : 'clear';
}

const JUMPS = [
  { key: 'now',    label: 'now',      days: 0 },
  { key: 'day',    label: 'a day',    days: 1 },
  { key: 'week',   label: 'a week',   days: 7 },
  { key: 'month',  label: 'a month',  days: 30 },
  { key: 'year',   label: 'a year',   days: 365 },
  { key: 'decade', label: 'a decade', days: 3650 },
];

function seasonOf(days) {
  const m = new Date(Date.now() + days * 86400000).getMonth();
  if (m === 11 || m <= 1) return 'winter';
  if (m <= 4) return 'spring';
  if (m <= 7) return 'summer';
  return 'autumn';
}
const SKY = {
  winter: { dawn: '#dcc9c4', day: '#bcd2e6', dusk: '#c6a6ad', night: '#0e1730' },
  spring: { dawn: '#e2cdd8', day: '#bcd9e8', dusk: '#d2b29c', night: '#101d36' },
  summer: { dawn: '#f0d6ad', day: '#9fccef', dusk: '#df9f78', night: '#11284a' },
  autumn: { dawn: '#e6c098', day: '#c6d4dc', dusk: '#ce9870', night: '#111a30' },
};
function hexRgb(h) { h = h.replace('#', ''); return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16)); }
function mix(a, b, t) { const A = hexRgb(a), B = hexRgb(b); return `rgb(${A.map((v, i) => Math.round(v + (B[i] - v) * t)).join(',')})`; }
function seasonPeak(season) { return season === 'summer' ? 14 : season === 'winter' ? 46 : 30; }

// Map a time offset (days) to a position 0..1 along the day's arc — log-scaled,
// because the jumps are exponential (a day → a decade).
function aFromDays(days) {
  return Math.min(0.97, 0.1 + 0.86 * (Math.log10(Math.max(0, days) + 1) / Math.log10(3651)));
}
// The sun sits EXACTLY on the dotted quadratic arc M8,82 Q50,(2*peak-82) 92,82,
// so it glides along the dotted line as time advances — no falling, no jumping.
function skyFrame(days, season) {
  const a = aFromDays(days);
  const peak = seasonPeak(season);
  const t = a, mt = 1 - a;
  const sunLeft = mt * mt * 8 + 2 * mt * t * 50 + t * t * 92;
  const sunTop = mt * mt * 82 + 2 * mt * t * (2 * peak - 82) + t * t * 82;
  const pal = SKY[season];
  const color = a < 0.5 ? mix(pal.dawn, pal.day, a / 0.5)
    : a < 0.85 ? mix(pal.day, pal.dusk, (a - 0.5) / 0.35)
      : mix(pal.dusk, pal.night, (a - 0.85) / 0.15);
  return { color, sunLeft, sunTop };
}

const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
function faceFor(days) {
  const d = new Date(Date.now() + days * 86400000);
  return { day: d.getDate(), wd: WD[d.getDay()], mon: MON[d.getMonth()], year: d.getFullYear() };
}
function humanizeDays(d) {
  d = Math.round(d);
  if (d <= 0) return 'now';
  if (d < 7) return d === 1 ? 'a day later' : d + ' days later';
  if (d < 30) { const w = Math.round(d / 7); return w === 1 ? 'a week later' : w + ' weeks later'; }
  if (d < 365) { const m = Math.round(d / 30); return m === 1 ? 'a month later' : m + ' months later'; }
  const y = Math.round(d / 365);
  if (y === 1) return 'a year later';
  if (y >= 10) return Math.round(y / 10) * 10 + ' years later';
  return y + ' years later';
}


// ─── App ────────────────────────────────────────────────────────────────────
function App() {
  const [messages, setMessages] = useState([]);
  const [draft, setDraft] = useState('');
  const [state, setState] = useState('idle'); // idle | listening | thinking | speaking
  const [memory, setMemory] = useState([]); // real atoms from the runtime (used by admin view, added later)
  const [lastProvider, setLastProvider] = useState(null);
  const [personas, setPersonas] = useState([]);
  const [personaId, setPersonaId] = useState('eliza');
  const [displayDays, setDisplayDays] = useState(0); // time-machine offset
  const [activeJump, setActiveJump] = useState('now');
  const [jumping, setJumping] = useState(false);
  const [pages, setPages] = useState([]); // falling calendar pages
  const [theme, setTheme] = useState(() => (typeof document !== 'undefined' && document.documentElement.dataset.theme) || 'cafe');
  const visitorId = useMemo(getVisitorId, []);
  const streamRef = useRef(null);
  const inputRef = useRef(null);
  const jumpTimer = useRef(null);
  const pageId = useRef(0);
  const pageTimer = useRef(null);

  const persona = personas.find((p) => p.id === personaId) || null;

  // Track the café/atelier theme so the sky follows it.
  useEffect(() => {
    const el = document.documentElement;
    const obs = new MutationObserver(() => setTheme(el.dataset.theme || 'cafe'));
    obs.observe(el, { attributes: true, attributeFilter: ['data-theme'] });
    return () => obs.disconnect();
  }, []);

  // Instant return to "now" — used whenever the memory itself changes.
  function backToNow() {
    cancelAnimationFrame(jumpTimer.current);
    setJumping(false);
    setDisplayDays(0);
    setActiveJump('now');
  }

  // Tear off calendar pages for the days that pass during a jump.
  function spawnPages(from, target) {
    const span = Math.abs(target - from);
    if (span < 0.5) return;
    const k = Math.max(2, Math.min(8, Math.round(Math.log10(span + 1) * 2) + 1));
    const dir = target >= from ? 'fall' : 'rise';
    const arr = [];
    for (let i = 0; i < k; i++) {
      const d = from + (target - from) * ((i + 1) / (k + 1));
      arr.push({ id: pageId.current++, dir, delay: i * 95, face: faceFor(d) });
    }
    setPages(arr);
    clearTimeout(pageTimer.current);
    pageTimer.current = setTimeout(() => setPages([]), k * 95 + 780);
  }

  // Animate a jump: the sun glides along the dotted arc and the calendar sheds
  // pages while the memory fades, in real time, to the target point.
  function jumpTo(target, key) {
    if (jumping) return;
    const from = displayDays;
    setActiveJump(key);
    if (target === from) return;
    setJumping(true);
    spawnPages(from, target);
    const dur = 1100 + Math.min(1400, Math.log10(Math.abs(target - from) + 1) * 700);
    const t0 = performance.now();
    cancelAnimationFrame(jumpTimer.current);
    const tick = (t) => {
      const p = Math.min(1, (t - t0) / dur);
      const e = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2; // easeInOut
      setDisplayDays(from + (target - from) * e);
      if (p < 1) jumpTimer.current = requestAnimationFrame(tick);
      else { setDisplayDays(target); setJumping(false); }
    };
    jumpTimer.current = requestAnimationFrame(tick);
  }

  useEffect(() => () => { cancelAnimationFrame(jumpTimer.current); clearTimeout(pageTimer.current); }, []);

  // Auto-scroll on new message
  useEffect(() => {
    const el = streamRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [messages, state]);

  // On mount: load the persona library, then greet and load memory.
  useEffect(() => {
    fetch(`/api/eliza?visitorId=${encodeURIComponent(visitorId)}&personaId=${encodeURIComponent(personaId)}`)
      .then((r) => r.json())
      .then((d) => {
        if (d.personas) setPersonas(d.personas);
        const pid = d.defaultPersona || personaId;
        setPersonaId(pid);
        const p = (d.personas || []).find((x) => x.id === pid);
        setMessages(greetingMsg(p && p.greeting));
        if (d.memories) setMemory(d.memories);
      })
      .catch(() => {});
  }, [visitorId]);

  // Switch persona: re-greet, and load that persona's own memory of you.
  async function switchPersona(pid) {
    if (pid === personaId) return;
    setPersonaId(pid);
    setMemory([]);
    backToNow();
    const p = personas.find((x) => x.id === pid);
    setMessages(greetingMsg(p && p.greeting));
    try {
      const r = await fetch(`/api/eliza?visitorId=${encodeURIComponent(visitorId)}&personaId=${encodeURIComponent(pid)}`);
      const d = await r.json();
      if (d.memories) setMemory(d.memories);
    } catch (e) {}
  }

  // Send handler — talks to the real runtime.
  async function send() {
    const text = draft.trim();
    if (!text || state === 'thinking') return;
    setDraft('');
    setMessages((prev) => prev.concat([{ from: 'user', t: 'just now', body: text }]));
    setState('thinking');
    try {
      const d = await callEliza({ visitorId, personaId, message: text, atOffsetDays: Math.round(displayDays) });
      if (d.error) {
        setMessages((prev) => prev.concat([{ from: 'eliza', t: 'just now', body: d.error }]));
      } else {
        setState('speaking');
        setLastProvider(d.fallback ? d.provider + ' (fallback)' : d.provider);
        if (d.memories) setMemory(d.memories);
        backToNow();
        setMessages((prev) => prev.concat([{ from: 'eliza', t: 'just now', body: d.reply }]));
      }
    } catch (e) {
      setMessages((prev) => prev.concat([{ from: 'eliza', t: 'just now', body: 'The line went quiet for a moment. Try again.' }]));
    } finally {
      setState('idle');
    }
  }

  // Right to erasure — wipe what this persona remembers about this visitor.
  async function forgetMe() {
    await callEliza({ visitorId, personaId, action: 'forget' });
    setMemory([]);
    backToNow();
    setMessages(greetingMsg(persona && persona.greeting));
  }

  function onKey(e) {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      send();
    }
  }

  return (
    <>
      {/* ──── Chat column: conversation + persona / your-data ──── */}
      <div className="chat-col">
      <div className="chat" role="region" aria-label="conversation with Eliza">
        <div className="chat-head">
          <div className={`chat-orb is-${state}`} aria-hidden="true"></div>
          <div className="chat-meta">
            <span className="chat-name">{persona ? persona.name.split(',')[0] : 'Eliza'}</span>
            <span className="chat-sub">{persona ? persona.name.replace(/^[^,]+,\s*/, '') : 'of the Lighthouse Café'}</span>
          </div>
          <div className="chat-toolbar">
            <span className={`chat-state is-${state}`}>
              <span className="dot"></span>{labelForState(state)}
            </span>
            <button type="button" onClick={() => setMessages(greetingMsg(persona && persona.greeting))}>reset</button>
          </div>
        </div>

        <div className="chat-stream" ref={streamRef}>
          {messages.map((m, i) => (
            <div key={i} className={`msg from-${m.from}`}>
              <span className="msg-meta">
                {m.from === 'eliza' ? (persona ? persona.name.split(',')[0].toLowerCase() : 'eliza') : 'you'} · {m.t}
              </span>
              <div className="msg-body">{m.body}</div>
            </div>
          ))}
          {state === 'thinking' && (
            <div className="msg from-eliza">
              <span className="msg-meta">{(persona ? persona.name.split(',')[0].toLowerCase() : 'eliza')} · thinking</span>
              <div className="msg-body">
                <span className="typing"><span></span><span></span><span></span></span>
              </div>
            </div>
          )}
        </div>

        <div className="chat-composer">
          <textarea
            ref={inputRef}
            className="chat-input"
            placeholder={'say something to ' + (persona ? persona.name.split(',')[0].toLowerCase() : 'eliza') + '…'}
            value={draft}
            rows={1}
            onChange={(e) => setDraft(e.target.value)}
            onFocus={() => setState((s) => s === 'idle' ? 'listening' : s)}
            onBlur={() => setState((s) => s === 'listening' ? 'idle' : s)}
            onKeyDown={onKey}
          />
          <button className="chat-send" onClick={send} disabled={!draft.trim()}>
            send
          </button>
        </div>
      </div>

        <div className="under-chat">
          <div className="card" style={persona && persona.tint ? { background: `color-mix(in oklab, ${persona.tint} 22%, var(--surface))`, borderLeft: `2px solid ${persona.tint}` } : undefined}>
            <h3><em>The persona</em></h3>
            <div className="ix">
              <div className="row">
                <span className="k">voice</span>
                <select value={personaId} onChange={(e) => switchPersona(e.target.value)} aria-label="choose a voice"
                  style={{ background: 'transparent', border: '1px solid var(--border)', color: 'var(--ink)', font: '400 12px var(--mono)', padding: '3px 6px', borderRadius: 2, maxWidth: 180 }}>
                  {personas.map((p) => (<option key={p.id} value={p.id}>{p.name}</option>))}
                </select>
              </div>
              <div className="row"><span className="k">running on</span><span className="v tab">{lastProvider || 'free · workers-ai'}</span></div>
            </div>
            <p className="small" style={{ marginTop: 'var(--space-4)' }}>
              {persona ? persona.blurb + ' ' : ''}An AI persona, plainly — sourced from
              public-domain literary tradition, not from a person.
            </p>
            {persona && persona.lineage && (
              <p className="small" style={{ marginTop: 'var(--space-3)', fontStyle: 'italic', opacity: 0.85 }}>
                {persona.lineage}
              </p>
            )}
          </div>

          <div className="card">
            <h3><em>Your data</em></h3>
            <p className="small">
              She runs on a free, on-platform model. What you say stays within the demo.
            </p>
            <button type="button" onClick={forgetMe}
              style={{ marginTop: 'var(--space-4)', width: '100%', background: 'transparent', border: '1px solid var(--border)', color: 'var(--ink)', font: '400 13px var(--mono)', padding: '8px 12px', cursor: 'pointer', borderRadius: 2 }}>
              forget me — erase everything
            </button>
            <p className="small" style={{ marginTop: 'var(--space-2)', opacity: 0.7 }}>
              Runs the redaction protocol: every memory of you is deleted.
            </p>
          </div>
        </div>
      </div>

      {/* ──── Side rail ──── */}
      <aside className="rail" aria-label="about this persona">
        <div className="card card-intro">
          <p className="lede">
            <em>Have a conversation.</em> Then use the <strong>Time &amp; memory</strong> panel
            below to jump ahead — a day, a year, a decade — and watch what is kept: the vivid,
            feeling-laden memories endure, the details blur, and the faint ones fade away.
          </p>
        </div>
        <div className="card">
          <h3><em>Time &amp; memory</em></h3>
          {(() => {
            const season = seasonOf(displayDays);
            const f = skyFrame(displayDays, season);
            const peak = seasonPeak(season);
            const arc = `M8,82 Q50,${2 * peak - 82} 92,82`;
            const filter = theme === 'atelier' ? 'brightness(0.66) saturate(1.05)' : 'none';
            const sunGlide = jumping ? 'none' : 'left 500ms ease, top 500ms ease';
            return (
              <div className="sky" style={{ backgroundColor: f.color, filter }}>
                <div className="sky-overlay" />
                <svg className="sky-arc" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
                  <path d={arc} fill="none" stroke="var(--accent)" strokeWidth="0.9"
                    strokeDasharray="0.5 3.6" strokeLinecap="round" opacity="0.55" />
                </svg>
                <div className="sky-sun" style={{
                  left: f.sunLeft + '%', top: f.sunTop + '%', transition: sunGlide,
                  background: 'radial-gradient(circle at 40% 38%, #ffffff, var(--accent-warm))',
                  boxShadow: '0 0 22px 6px var(--accent-warm)',
                }} />
                <div className="sky-horizon" />
                <div className="sky-lighthouse" />
                <div className="sky-season">{season}</div>
                <div className="sky-label">{humanizeDays(displayDays)}</div>
              </div>
            );
          })()}

          <div className="cal-wrap">
            <div className="cal">
              {(() => { const fc = faceFor(displayDays); return (
                <div className="cal-page">
                  <div className="cal-top">{fc.mon} {fc.year}</div>
                  <div className="cal-day">{fc.day}</div>
                  <div className="cal-wd">{fc.wd}</div>
                </div>
              ); })()}
              {pages.map((p) => (
                <div key={p.id} className={`cal-page cal-${p.dir}`} style={{ animationDelay: p.delay + 'ms' }}>
                  <div className="cal-top">{p.face.mon} {p.face.year}</div>
                  <div className="cal-day">{p.face.day}</div>
                  <div className="cal-wd">{p.face.wd}</div>
                </div>
              ))}
            </div>
          </div>

          <div className="jump-row">
            {JUMPS.map((j) => (
              <button key={j.key} className="jump-btn" aria-current={activeJump === j.key}
                disabled={jumping} onClick={() => jumpTo(j.days, j.key)}>
                {j.label}
              </button>
            ))}
          </div>
          <p className="small" style={{ marginTop: 'var(--space-3)' }}>
            Jump forward and watch what she keeps. Vivid, feeling-laden memories
            endure; the details blur first; faint ones fade away.
          </p>
          <div className="mtl">
            {memory.length === 0 && (
              <div className="mtl-empty">No memories yet — tell her a few things first.</div>
            )}
            {memory.map((a) => {
              const s = salienceAt(a, displayDays);
              const k = kindAt(s);
              const op = k === 'forgotten' ? 0.3 : 0.4 + 0.6 * s;
              return (
                <div key={a.id} className={`mem mem-${k}`} style={{ opacity: op }}>
                  <div className="mem-gist">{a.gist}</div>
                  {a.details && a.details.length > 0 && (
                    <div className="mem-details">
                      {a.details.map((d, i) => {
                        const b = brightnessAt(d.brightness, displayDays);
                        return b >= 0.1
                          ? <span key={i} className="mem-detail" style={{ opacity: Math.min(1, b + 0.25) }}>{d.content}</span>
                          : null;
                      })}
                    </div>
                  )}
                  <div className="mem-bar"><i style={{ width: Math.round(s * 100) + '%', background: k === 'defining' ? 'var(--accent-warm)' : 'var(--accent)' }} /></div>
                  <div className="mem-tag">{k} · {Math.round(s * 100)}%</div>
                </div>
              );
            })}
          </div>
        </div>

      </aside>

    </>
  );
}

function labelForState(s) {
  return ({
    idle: 'ready',
    listening: 'listening',
    thinking: 'thinking',
    speaking: 'speaking',
  })[s] || 'ready';
}


ReactDOM.createRoot(document.getElementById('sim-app')).render(<App />);
