#!/usr/bin/env node
// scripts/dev-server.js — local manual test harness for Silver demos.
//
// Boots the bot with the in-memory WhatsApp adapter and serves a web UI
// at http://localhost:7331/dev.
//
// What you can do here (used for live demos):
//   - Pick a user from the website DB (admin/src/db/mentees) — falls back to
//     2 dummy users if the DB path isn't reachable
//   - Set the simulated clock to any specific date + time (so you can prove
//     the 08/14/18 triggers fire at the right moments)
//   - Onboard via fake Hotmart webhook (creates a silver.json record)
//   - Tick the scheduler at 08/14/18 manually
//   - Send messages as the user, observe bot responses (Anthropic Claude
//     when ANTHROPIC_API_KEY is set; canned stub otherwise)
//   - Watch the live state booleans: lesson completion, task status,
//     module completion gates
//
// Run: SILVER_TEST_MODE=1 node scripts/dev-server.js
//   or: npm run dev
//
// SECURITY NOTE: this server is for LOCAL DEV ONLY. It binds to 127.0.0.1
// and the dev UI does not authenticate. Do not expose to the public network.

// Load .env from ecobewhatsapp/.env BEFORE anything imports process.env.
// Without this, putting ANTHROPIC_API_KEY on a separate shell line (no
// `export`) silently won't reach the child process and the bot stays in
// stub mode. Loading dotenv here makes it work regardless.
import { config as dotenvConfig } from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, join, resolve } from 'path';
import http from 'http';
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';

dotenvConfig({ path: new URL('../.env', import.meta.url).pathname });
import {
  configureTest, sendText, injectInbound, onInbound, subscribe, readOutbox, clearOutbox,
} from '../lib/whatsappAdapter.js';
import * as Clock from '../lib/clock.js';
import {
  startSilver, tickSilverUser, handleSilverMessage,
} from '../lib/silverCourse.js';
import { handleHotmartWebhook } from '../lib/hotmartWebhook.js';
import {
  loadSilver, saveSilver, resetSilver, loadCatalog, loadTaskRegistry,
} from '../lib/silverStore.js';

const __filename = fileURLToPath(import.meta.url);
const SCRIPT_DIR = dirname(dirname(__filename));

process.env.SILVER_TEST_MODE = '1';
configureTest();

// Resolve the website DB location. Two layouts are supported:
//   A. Directory of per-mentee subfolders (legacy ecobot/website-mirror)
//      e.g. admin/src/db/mentees/<slug>/career-strategy.json
//   B. Single mentees.json file (current ecobewebsite layout)
//      e.g. admin/src/db/mentees.json containing an array of mentee objects
// Returns { kind: 'dir' | 'file', path: string } or null.
//
// Resolution order:
//   1. Explicit env var WEBSITE_MENTEES_DB (file or dir, both work)
//   2. ../admin/src/db/mentees{,.json} relative to SCRIPT_DIR
//   3. website-mirror/ecobe/admin/src/db/mentees{,.json} (legacy)
function resolveWebsiteDB() {
  const tryPath = (p) => {
    if (!p || !existsSync(p)) return null;
    return { kind: statSync(p).isDirectory() ? 'dir' : 'file', path: p };
  };
  if (process.env.WEBSITE_MENTEES_DB) return tryPath(resolve(process.env.WEBSITE_MENTEES_DB));
  const candidates = [
    resolve(SCRIPT_DIR, '..', 'admin', 'src', 'db', 'mentees.json'),
    resolve(SCRIPT_DIR, '..', 'admin', 'src', 'db', 'mentees'),
    resolve(SCRIPT_DIR, 'website-mirror', 'ecobe', 'admin', 'src', 'db', 'mentees.json'),
    resolve(SCRIPT_DIR, 'website-mirror', 'ecobe', 'admin', 'src', 'db', 'mentees'),
  ];
  for (const c of candidates) {
    const r = tryPath(c);
    if (r) return r;
  }
  return null;
}

const DUMMY_USERS = [
  { slug: 'demo-anna',  name: 'Anna Demo',  email: 'anna+demo@example.com',  phone: '41791111111' },
  { slug: 'demo-bruno', name: 'Bruno Demo', email: 'bruno+demo@example.com', phone: '5511991234567' },
];

function ensureDummyUsers() {
  for (const u of DUMMY_USERS) {
    const dir = join(SCRIPT_DIR, 'mentees', u.slug);
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
    const profilePath = join(dir, 'profile.json');
    if (!existsSync(profilePath)) {
      writeFileSync(profilePath, JSON.stringify({ slug: u.slug, name: u.name, email: u.email, phone: u.phone }, null, 2));
    }
  }
}

const WEBSITE_DB = resolveWebsiteDB();
if (!WEBSITE_DB) {
  console.log('[dev] website DB not found — using 2 dummy users');
  ensureDummyUsers();
} else {
  console.log(`[dev] website DB (${WEBSITE_DB.kind}) at ${WEBSITE_DB.path}`);
}

function listAvailableUsers() {
  const out = [];
  if (WEBSITE_DB?.kind === 'dir') {
    for (const slug of readdirSync(WEBSITE_DB.path)) {
      const csPath = join(WEBSITE_DB.path, slug, 'career-strategy.json');
      const formPath = join(WEBSITE_DB.path, slug, 'form.json');
      let name = slug, email = '', phone = '';
      try {
        if (existsSync(csPath)) {
          const cs = JSON.parse(readFileSync(csPath, 'utf8'));
          name = cs.leadName || cs.name || slug;
          email = cs.email || '';
        }
        if (existsSync(formPath)) {
          const form = JSON.parse(readFileSync(formPath, 'utf8'));
          name = name || form.answers?.name || slug;
          phone = form.answers?.phone || '';
        }
      } catch {}
      out.push({ slug, name, email, phone, source: 'website-db' });
    }
  } else if (WEBSITE_DB?.kind === 'file') {
    // mentees.json: an array of objects, OR a wrapper object { mentees: [...] }
    try {
      const raw = JSON.parse(readFileSync(WEBSITE_DB.path, 'utf8'));
      const arr = Array.isArray(raw) ? raw : (Array.isArray(raw.mentees) ? raw.mentees : []);
      for (const m of arr) {
        const slug = m.slug || m.id || (m.name || '').toLowerCase().replace(/[^a-z0-9]+/g, '-');
        if (!slug) continue;
        out.push({
          slug,
          name: m.leadName || m.name || slug,
          email: m.email || '',
          phone: m.phone || m.clientPhone || '',
          source: 'website-db-file',
        });
      }
    } catch (e) {
      console.warn('[dev] failed to parse mentees.json:', e.message);
    }
  } else {
    for (const u of DUMMY_USERS) out.push({ ...u, source: 'dummy' });
  }
  const localMentees = join(SCRIPT_DIR, 'mentees');
  if (existsSync(localMentees)) {
    for (const slug of readdirSync(localMentees)) {
      if (out.find(u => u.slug === slug)) continue;
      const sp = join(localMentees, slug, 'silver.json');
      if (!existsSync(sp)) continue;
      try {
        const s = JSON.parse(readFileSync(sp, 'utf8'));
        out.push({ slug, name: s.leadName || slug, email: s.email || '', phone: s.clientPhone || '', source: 'local-silver' });
      } catch {}
    }
  }
  return out;
}

let currentSlug = process.env.TEST_SLUG || 'test-silver';
const groupJidFor = (slug) => `${slug}@g.us`;

onInbound(async (msg) => {
  const state = loadSilver(SCRIPT_DIR, currentSlug);
  if (!state) {
    await sendText(msg.jid, `[bot] No active Silver user — pick a user and click *Onboard* first.`);
    return;
  }
  await handleSilverMessage(SCRIPT_DIR, currentSlug, { text: msg.text, fromNum: msg.fromNum });
});

const sseClients = new Set();
subscribe((evt) => {
  for (const res of sseClients) res.write(`data: ${JSON.stringify(evt)}\n\n`);
});

const PORT = Number(process.env.PORT || 7331);

const server = http.createServer(async (req, res) => {
  try {
    const url = new URL(req.url, `http://localhost:${PORT}`);
    if (req.method === 'GET'  && url.pathname === '/dev')              return serveUI(res);
    if (req.method === 'GET'  && url.pathname === '/dev/state')        return serveState(res);
    if (req.method === 'GET'  && url.pathname === '/dev/stream')       return serveStream(req, res);
    if (req.method === 'GET'  && url.pathname === '/dev/users')        return json(res, 200, { users: listAvailableUsers(), current: currentSlug });
    if (req.method === 'POST' && url.pathname === '/dev/select-user')  return await selectUser(req, res);
    if (req.method === 'POST' && url.pathname === '/dev/onboard')      return await onboard(req, res);
    if (req.method === 'POST' && url.pathname === '/dev/say')          return await say(req, res);
    if (req.method === 'POST' && url.pathname === '/dev/tick')         return await tick(req, res);
    if (req.method === 'POST' && url.pathname === '/dev/ff')           return await fastForward(req, res);
    if (req.method === 'POST' && url.pathname === '/dev/set-clock')    return await setClock(req, res);
    if (req.method === 'POST' && url.pathname === '/dev/reset')        return await reset(req, res);
    if (req.method === 'POST' && url.pathname === '/dev/webhook')      return await fakeWebhook(req, res);
    if (req.method === 'POST' && url.pathname === '/webhooks/hotmart') return await realWebhook(req, res);
    res.writeHead(404); res.end('not found');
  } catch (err) {
    console.error('[dev-server]', err);
    res.writeHead(500, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: String(err) }));
  }
});

server.listen(PORT, '127.0.0.1', () => {
  console.log(`\n  🟣 Silver dev harness ready --> http://localhost:${PORT}/dev\n`);
});

async function readBody(req) {
  const chunks = [];
  for await (const c of req) chunks.push(c);
  return Buffer.concat(chunks);
}

function json(res, status, obj) {
  res.writeHead(status, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(obj));
}

async function selectUser(req, res) {
  const body = JSON.parse((await readBody(req)).toString('utf8') || '{}');
  if (!body.slug) return json(res, 400, { error: 'slug required' });
  currentSlug = body.slug;
  json(res, 200, { ok: true, currentSlug });
}

async function onboard(req, res) {
  const body = JSON.parse((await readBody(req)).toString('utf8') || '{}');
  const slug = body.slug || currentSlug;
  resetSilver(SCRIPT_DIR, slug);
  mkdirSync(join(SCRIPT_DIR, 'mentees', slug), { recursive: true });
  clearOutbox();
  await startSilver(SCRIPT_DIR, slug, {
    groupJid: groupJidFor(slug),
    clientPhone: body.phone || '491700000000',
    leadName: body.name || 'Test User',
  });
  currentSlug = slug;
  json(res, 200, { ok: true, slug });
}

async function say(req, res) {
  const body = JSON.parse((await readBody(req)).toString('utf8') || '{}');
  injectInbound({
    jid: groupJidFor(currentSlug),
    fromNum: '491700000000',
    text: body.text || '',
  });
  setTimeout(() => json(res, 200, { ok: true }), 50);
}

async function tick(req, res) {
  const body = JSON.parse((await readBody(req)).toString('utf8') || '{}');
  const slot = Number(body.slot) || 8;
  const r = await tickSilverUser(SCRIPT_DIR, currentSlug, slot);
  json(res, 200, { ok: true, result: r });
}

async function fastForward(req, res) {
  const body = JSON.parse((await readBody(req)).toString('utf8') || '{}');
  const ms = Number(body.ms) || 86_400_000;
  Clock.advance(ms);
  json(res, 200, { ok: true, offsetMs: Clock.getOffsetMs() });
}

async function setClock(req, res) {
  const body = JSON.parse((await readBody(req)).toString('utf8') || '{}');
  if (!body.iso) return json(res, 400, { error: 'iso required (YYYY-MM-DDTHH:mm or full ISO)' });
  try {
    const offset = Clock.setTime(body.iso);
    json(res, 200, { ok: true, offsetMs: offset, now: Clock.nowIso(), zurich: Clock.zurichParts() });
  } catch (e) {
    json(res, 400, { error: e.message });
  }
}

async function reset(req, res) {
  resetSilver(SCRIPT_DIR, currentSlug);
  clearOutbox();
  Clock.reset();
  json(res, 200, { ok: true });
}

async function fakeWebhook(req, res) {
  const body = JSON.parse((await readBody(req)).toString('utf8') || '{}');
  const hottok = process.env.HOTMART_HOTTOK || 'TEST_HOTTOK';
  process.env.HOTMART_HOTTOK = hottok;

  const users = listAvailableUsers();
  const user = users.find(u => u.slug === currentSlug) || { slug: currentSlug, name: 'Demo User', email: `${currentSlug}@example.com`, phone: '491700000000' };

  const payload = {
    id: `dev-evt-${Date.now()}`,
    creation_date: Date.now(),
    event: 'PURCHASE_APPROVED',
    version: '2.0.0',
    hottok,
    data: {
      product: { id: 4567890, name: 'Ecobe Silver — 30-Day Job Hunt Reset' },
      buyer: {
        name: body.name || user.name,
        email: body.email || user.email || `${currentSlug}@example.com`,
        checkout_phone: body.phone || `+${user.phone || '491700000000'}`,
      },
      purchase: {
        transaction: body.transaction || `DEV_TX_${Date.now()}`,
        status: 'APPROVED',
        offer: { code: 'OFFER_DEV' },
        price: { value: 990.0, currency_value: 'CHF' },
      },
    },
  };
  const raw = Buffer.from(JSON.stringify(payload), 'utf8');
  const result = await handleHotmartWebhook(SCRIPT_DIR, { rawBody: raw, headers: { 'content-type': 'application/json' } });
  if (result.body?.slug) currentSlug = result.body.slug;
  json(res, 200, { ok: true, result });
}

async function realWebhook(req, res) {
  const raw = await readBody(req);
  const result = await handleHotmartWebhook(SCRIPT_DIR, { rawBody: raw, headers: req.headers });
  res.writeHead(result.status, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(result.body));
}

function serveState(res) {
  const state = loadSilver(SCRIPT_DIR, currentSlug);
  const journeyPath = join(SCRIPT_DIR, 'mentees', currentSlug, 'journey.json');
  const journey = existsSync(journeyPath) ? JSON.parse(readFileSync(journeyPath, 'utf8')) : null;
  const catalog = loadCatalog(SCRIPT_DIR);
  const taskRegistry = loadTaskRegistry(SCRIPT_DIR);
  const lessonView = catalog.lessons.map(l => ({
    id: l.id, day: l.day, week: l.week, module: l.module, title: l.title,
    completed: !!state?.lessons?.[l.id]?.completedAt,
    completedAt: state?.lessons?.[l.id]?.completedAt || null,
  }));
  const taskView = taskRegistry.tasks.map(t => ({
    id: t.id, week: t.week,
    module: typeof t.gatesModule === 'number' ? t.gatesModule : t.week,
    title: t.title,
    status: state?.tasks?.[t.id]?.status || 'pending',
    askedAt: state?.tasks?.[t.id]?.askedAt || null,
    recordedAt: state?.tasks?.[t.id]?.recordedAt || null,
  }));
  const moduleView = [1, 2, 3, 4].map(n => ({
    n,
    completed: !!state?.modulesCompleted?.[n],
    completedAt: state?.[`module${n}CompletedAt`] || null,
  }));
  json(res, 200, {
    state, journey,
    outbox: readOutbox(),
    clockOffsetMs: Clock.getOffsetMs(),
    now: Clock.nowIso(),
    zurich: Clock.zurichParts(),
    currentSlug,
    websiteDb: WEBSITE_DB,
    lessons: lessonView, tasks: taskView, modules: moduleView,
    aiMode: process.env.ANTHROPIC_API_KEY ? 'live (Anthropic Claude)' : 'stub (no ANTHROPIC_API_KEY)',
  });
}

function serveStream(req, res) {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  });
  res.write('\n');
  sseClients.add(res);
  req.on('close', () => sseClients.delete(res));
}

function serveUI(res) {
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  res.end(DEV_HTML);
}

const DEV_HTML = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Silver — Dev Harness</title>
<style>
  :root { color-scheme: dark; }
  body { font: 14px/1.45 -apple-system, system-ui, Segoe UI, sans-serif; margin: 0; background:#0e1116; color:#e6edf3; }
  header { padding: 10px 16px; background:#161b22; border-bottom:1px solid #30363d; display:flex; align-items:center; gap:14px; flex-wrap: wrap; }
  header h1 { font-size:14px; margin:0; font-weight:600; letter-spacing:.3px; }
  header .clock { font-family: ui-monospace, monospace; color:#7ee787; font-size:12px; }
  header .ai { font-family: ui-monospace, monospace; font-size:11px; padding:2px 6px; border-radius:4px; }
  header .ai.live { background:#238636; }
  header .ai.stub { background:#6e7681; }
  .grid { display:grid; grid-template-columns: 1fr 380px; grid-template-rows: 1fr 320px; gap:8px; height: calc(100vh - 60px); padding:8px; }
  .pane { background:#161b22; border:1px solid #30363d; border-radius:6px; display:flex; flex-direction:column; min-height:0; }
  .pane h2 { font-size:11px; text-transform:uppercase; letter-spacing:.7px; color:#8b949e; margin:0; padding:8px 12px; border-bottom:1px solid #30363d; }
  #chat { grid-column:1; grid-row:1; }
  #chat .body { flex:1; overflow:auto; padding:10px; display:flex; flex-direction:column; gap:6px; }
  .msg { max-width:78%; padding:8px 10px; border-radius:10px; white-space:pre-wrap; word-break:break-word; }
  .msg.bot { background:#1f6feb22; border:1px solid #1f6feb55; align-self:flex-start; }
  .msg.user { background:#238636aa; align-self:flex-end; }
  .msg.meta { align-self:center; font-size:11px; color:#8b949e; background:transparent; }
  #compose { display:flex; gap:6px; padding:8px; border-top:1px solid #30363d; }
  #compose input { flex:1; padding:8px 10px; background:#0d1117; color:#e6edf3; border:1px solid #30363d; border-radius:6px; font:inherit; }
  #compose button { padding:8px 12px; border-radius:6px; border:1px solid #30363d; background:#238636; color:#fff; cursor:pointer; }
  #controls { grid-column:2; grid-row:1; }
  #controls .body { padding:10px; display:flex; flex-direction:column; gap:8px; overflow:auto; }
  #controls fieldset { border:1px solid #30363d; border-radius:6px; padding:8px 10px; margin:0; }
  #controls legend { font-size:11px; color:#8b949e; padding:0 4px; }
  #controls button { display:block; width:100%; margin-top:6px; padding:6px 8px; background:#21262d; color:#e6edf3; border:1px solid #30363d; border-radius:6px; cursor:pointer; text-align:left; font:inherit; }
  #controls button:hover { background:#30363d; }
  #controls .row { display:flex; gap:6px; }
  #controls .row button { margin-top:0; flex:1; }
  #controls input, #controls select { width:100%; padding:6px 8px; background:#0d1117; color:#e6edf3; border:1px solid #30363d; border-radius:6px; font:inherit; box-sizing:border-box; }
  #booltable { grid-column:1 / span 2; grid-row:2; }
  #booltable .body { display:grid; grid-template-columns: 1.4fr 1.4fr 1fr 1fr; gap:0; flex:1; min-height:0; }
  #booltable .col { border-right:1px solid #30363d; min-height:0; display:flex; flex-direction:column; }
  #booltable .col:last-child { border-right:none; }
  #booltable h3 { margin:0; padding:6px 10px; font-size:11px; text-transform:uppercase; color:#8b949e; border-bottom:1px solid #30363d; }
  #booltable .scroll { overflow:auto; padding:6px 10px; font: 12px/1.4 ui-monospace, monospace; }
  #booltable .row-item { display:grid; grid-template-columns: auto 1fr auto; gap:6px; align-items:center; padding:2px 0; border-bottom:1px solid #21262d; }
  .pill { display:inline-block; min-width:14px; text-align:center; padding:1px 6px; border-radius:4px; font-size:10px; font-weight:bold; }
  .pill.true { background:#238636; color:#fff; }
  .pill.false { background:#30363d; color:#8b949e; }
  small { color:#8b949e; }
  hr.sep { border:none; border-top:1px solid #30363d; margin:6px 0; }
</style>
</head>
<body>
<header>
  <h1>Silver — Dev Harness</h1>
  <span class="clock" id="clock">--:--</span>
  <small id="offset"></small>
  <span class="ai stub" id="aiBadge">stub</span>
  <small>user: <b id="slug">—</b></small>
  <small id="dbsource"></small>
</header>
<div class="grid">
  <section class="pane" id="chat">
    <h2>WhatsApp (mock) <span class="pill" id="slugLabel"></span></h2>
    <div class="body" id="chatBody"></div>
    <form id="compose">
      <input id="composeInput" placeholder="Type as the user — e.g. 'Yes', 'send the CV template', 'how many apps per week?'" />
      <button type="submit">Send</button>
    </form>
  </section>

  <section class="pane" id="controls">
    <h2>Controls</h2>
    <div class="body">
      <fieldset>
        <legend>User picker</legend>
        <select id="userSel"></select>
        <div class="row">
          <button id="btnSelectUser">Use selected</button>
          <button id="btnOnboard">Onboard (fresh silver.json)</button>
        </div>
        <button id="btnReset">Reset (wipe + clear)</button>
        <button id="btnWebhook">Fire fake Hotmart purchase.approved</button>
      </fieldset>

      <fieldset>
        <legend>Clock — pick exact date + time</legend>
        <div class="row">
          <input id="datePicker" type="date" />
          <input id="timePicker" type="time" />
        </div>
        <button id="btnSetClock">Set clock to selected</button>
        <hr class="sep" />
        <div class="row">
          <button data-ff="3600000">+1 hour</button>
          <button data-ff="86400000">+1 day</button>
          <button data-ff="604800000">+1 week</button>
        </div>
      </fieldset>

      <fieldset>
        <legend>Scheduler tick (manual)</legend>
        <div class="row">
          <button data-tick="8">08:00 send</button>
          <button data-tick="14">14:00 nudge</button>
          <button data-tick="18">18:00 nudge</button>
        </div>
      </fieldset>

      <fieldset>
        <legend>Quick replies</legend>
        <button data-quick="Yes">Reply: Yes</button>
        <button data-quick="No">Reply: No</button>
        <button data-quick="What is the CAR method?">Ask: CAR method</button>
        <button data-quick="Send the CV template">Ask: CV template</button>
        <button data-quick="I need a coach">Ask: human</button>
        <button data-quick="Applied!">Reply: APPLIED</button>
      </fieldset>
    </div>
  </section>

  <section class="pane" id="booltable">
    <h2>State booleans (lessons / tasks / modules / journey)</h2>
    <div class="body">
      <div class="col">
        <h3>Lessons (30)</h3>
        <div class="scroll" id="lessonsView"></div>
      </div>
      <div class="col">
        <h3>Tasks</h3>
        <div class="scroll" id="tasksView"></div>
      </div>
      <div class="col">
        <h3>Modules</h3>
        <div class="scroll" id="modulesView"></div>
      </div>
      <div class="col">
        <h3>Journey events (last 20)</h3>
        <div class="scroll" id="journeyView"></div>
      </div>
    </div>
  </section>
</div>
<script>
const $ = (id) => document.getElementById(id);
const chatBody = $('chatBody');

// HTML escape — every value flowing into innerHTML below goes through this
// to avoid any chance of XSS from a mischievous mentee name / journey event.
function esc(v) {
  if (v == null) return '';
  return String(v).replace(/[&<>"']/g, c => ({
    '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
  })[c]);
}

function clearChildren(el) { while (el.firstChild) el.removeChild(el.firstChild); }

function bubble(kind, text) {
  const div = document.createElement('div');
  div.className = 'msg ' + kind;
  div.textContent = text;
  chatBody.appendChild(div);
  chatBody.scrollTop = chatBody.scrollHeight;
}

function metaLine(t) {
  const div = document.createElement('div');
  div.className = 'msg meta';
  div.textContent = t;
  chatBody.appendChild(div);
  chatBody.scrollTop = chatBody.scrollHeight;
}

async function api(path, body) {
  const res = await fetch(path, { method: body ? 'POST' : 'GET', headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined });
  return res.json().catch(() => ({}));
}

function pill(bool, ch = '✓', falseCh = '·') {
  return '<span class="pill ' + (bool ? 'true' : 'false') + '">' + (bool ? ch : falseCh) + '</span>';
}

async function refreshState() {
  const s = await api('/dev/state');
  $('clock').textContent = s.zurich ? (s.zurich.date + ' ' + String(s.zurich.hour).padStart(2,'0') + ':' + String(s.zurich.minute).padStart(2,'0') + ' Zurich') : '--';
  $('offset').textContent = s.clockOffsetMs ? '(offset ' + (s.clockOffsetMs >= 0 ? '+' : '') + Math.round(s.clockOffsetMs/3600000) + 'h)' : '';
  const ai = $('aiBadge'); ai.textContent = s.aiMode || 'unknown'; ai.className = 'ai ' + (s.aiMode && s.aiMode.startsWith('live') ? 'live' : 'stub');
  $('slug').textContent = s.currentSlug || '—';
  $('slugLabel').textContent = s.currentSlug ? (s.currentSlug + (s.state ? ' · day ' + s.state.currentDay : ' · no silver.json')) : 'no user';
  $('dbsource').textContent = s.websiteDb ? 'website DB: ' + s.websiteDb : 'using dummy users';

  $('lessonsView').innerHTML = (s.lessons || []).map(l =>
    '<div class="row-item">' + pill(l.completed) + '<span>' + esc(l.id) + ' D' + esc(l.day) + ' M' + esc(l.module || '?') + ' — ' + esc(l.title) + '</span></div>'
  ).join('');

  $('tasksView').innerHTML = (s.tasks || []).map(t => {
    const ok = t.status === 'claimed';
    const partial = t.status === 'partial';
    const cls = ok ? 'true' : 'false';
    const ch = ok ? '✓' : (partial ? '~' : '·');
    return '<div class="row-item">' +
      '<span class="pill ' + cls + '">' + ch + '</span>' +
      '<span>' + esc(t.id) + ' M' + esc(t.module || '?') + ' — ' + esc(t.title) + '</span>' +
      '<small>' + esc(t.status || '—') + '</small></div>';
  }).join('');

  $('modulesView').innerHTML = (s.modules || []).map(m =>
    '<div class="row-item">' + pill(m.completed) + '<span>Module ' + esc(m.n) + '</span><small>' + esc((m.completedAt || '—').slice(0,10)) + '</small></div>'
  ).join('');

  const events = (s.journey && s.journey.events ? s.journey.events.slice(-20).reverse() : []);
  $('journeyView').innerHTML = events.map(e => {
    const rest = Object.fromEntries(Object.entries(e).filter(([k]) => !['ts','event'].includes(k)));
    const restJson = Object.keys(rest).length ? JSON.stringify(rest) : '';
    return '<div>' + esc((e.ts || '').slice(11,19)) + ' <b>' + esc(e.event) + '</b> ' + (restJson ? '<small>' + esc(restJson) + '</small>' : '') + '</div>';
  }).join('') || '<small>(no events)</small>';
}

async function refreshUsers() {
  const r = await api('/dev/users');
  const sel = $('userSel');
  sel.innerHTML = (r.users || []).map(u => {
    const isSelected = u.slug === r.current ? ' selected' : '';
    return '<option value="' + esc(u.slug) + '"' + isSelected + '>' + esc(u.slug) + ' — ' + esc(u.name) + ' [' + esc(u.source) + ']</option>';
  }).join('');
}

async function selectUser() {
  const slug = $('userSel').value;
  if (!slug) return;
  await api('/dev/select-user', { slug });
  metaLine('switched user → ' + slug);
  clearChildren(chatBody);
  await refreshState();
}

async function onboard() {
  clearChildren(chatBody);
  metaLine('— onboarding fresh silver.json for ' + $('userSel').value + ' —');
  await api('/dev/onboard', { slug: $('userSel').value });
  await refreshUsers();
  await refreshState();
}

async function tick(slot) {
  metaLine('tick ' + slot + ':00');
  const r = await api('/dev/tick', { slot });
  if (r.result?.skipped) metaLine('  → ' + r.result.skipped);
  await refreshState();
}

async function ff(ms) {
  metaLine('time → +' + (ms/3600000) + 'h');
  await api('/dev/ff', { ms });
  await refreshState();
}

async function setClockFromPickers() {
  const d = $('datePicker').value;
  const t = $('timePicker').value || '08:00';
  if (!d) { metaLine('pick a date first'); return; }
  const iso = d + 'T' + t + ':00';
  const r = await api('/dev/set-clock', { iso });
  if (r.error) metaLine('  → ' + r.error);
  else metaLine('clock → ' + r.zurich.date + ' ' + String(r.zurich.hour).padStart(2,'0') + ':' + String(r.zurich.minute).padStart(2,'0') + ' Zurich');
  await refreshState();
}

async function reset() {
  clearChildren(chatBody);
  metaLine('— reset —');
  await api('/dev/reset');
  await refreshState();
}

async function say() {
  const t = $('composeInput').value.trim();
  if (!t) return;
  $('composeInput').value = '';
  await api('/dev/say', { text: t });
  setTimeout(refreshState, 300);
}

function quick(t) { $('composeInput').value = t; say(); }

async function webhook() {
  metaLine('Hotmart purchase.approved fired (creates silver.json)');
  await api('/dev/webhook', {});
  await refreshUsers();
  await refreshState();
}

document.getElementById('compose').addEventListener('submit', (e) => { e.preventDefault(); say(); });
document.getElementById('btnOnboard').addEventListener('click', (e) => { e.preventDefault(); onboard(); });
document.getElementById('btnSelectUser').addEventListener('click', (e) => { e.preventDefault(); selectUser(); });
document.getElementById('btnReset').addEventListener('click', (e) => { e.preventDefault(); reset(); });
document.getElementById('btnWebhook').addEventListener('click', (e) => { e.preventDefault(); webhook(); });
document.getElementById('btnSetClock').addEventListener('click', (e) => { e.preventDefault(); setClockFromPickers(); });
document.querySelectorAll('[data-tick]').forEach(b => b.addEventListener('click', (e) => { e.preventDefault(); tick(Number(b.dataset.tick)); }));
document.querySelectorAll('[data-ff]').forEach(b => b.addEventListener('click', (e) => { e.preventDefault(); ff(Number(b.dataset.ff)); }));
document.querySelectorAll('[data-quick]').forEach(b => b.addEventListener('click', (e) => { e.preventDefault(); quick(b.dataset.quick); }));

// Default the date picker to today
const today = new Date();
$('datePicker').value = today.toISOString().slice(0,10);
$('timePicker').value = '08:00';

const es = new EventSource('/dev/stream');
es.onmessage = (e) => {
  try {
    const evt = JSON.parse(e.data);
    if (evt.kind === 'out') bubble('bot', evt.msg.text);
    if (evt.kind === 'in')  bubble('user', evt.msg.text);
    setTimeout(refreshState, 100);
  } catch {}
};

refreshUsers();
refreshState();
setInterval(refreshState, 3000);
</script>
</body>
</html>`;
