/* eslint-disable */
// ===========================================================================
// Omnivore UI — Assistant chat (full-screen overlay: thread list + conversation)
//
// Part of the in-browser (no-build) React app. These files are loaded in order
// as separate <script type="text/babel"> tags (see index.html); Babel compiles
// each as a classic global script, so top-level names declared in earlier files
// are visible here. Depends on ui/base.jsx, ui/components.jsx.
//
// This is the GENERAL assistant the user opens from Today — it talks through the
// day and PLANS meals, with the full live context (plan, time-aware metric
// standing, today's meals) handed to Engine.assistantChat. It is distinct from
// the Settings → Coach chat, which only configures the plan/targets.
//
// Conversations persist as returnable threads (Store.listChats / getChatThread /
// createChat / appendChatMessage), so you can leave and come back. A meal the
// assistant proposes renders as a card with a deterministic "est. +N" health-
// score lift (Engine.buildRoughEntry + scoreDelta) and a one-tap "Log it" that
// runs the accurate nutrition pass (Engine.estimateMealEntry) before logging.
// ===========================================================================

// today's records + live score, recomputed from the store (same as TodayView).
function liveDay(profile) {
  const log = Store.readLog();
  const today = localDay();
  const now = new Date();
  // meals packed for later today are "planned" — they don't count toward the
  // live score (nor the base a proposed-meal delta is measured against) yet.
  const recs = log.filter((r) => dayOf(r) === today && !isPlanned(r, now))
    .sort((a, b) => (a.eaten_at < b.eaten_at ? 1 : -1));
  const scored = Scoring.scoreDay(recs.map((r) => r.meal), profile, now);
  return { recs, scored };
}

// Deterministic health-score lift of a proposed meal, formatted for a badge.
// Positive → "est. +N" (green); zero → "keeps you on track"; negative → show the
// projected new score rather than a discouraging minus.
function useMealDelta(meal, profile, records, when) {
  const entry = Engine.buildRoughEntry(meal);
  if (!entry) return { entry: null, label: null };
  const d = Engine.scoreDelta({ profile, records, entry, when });
  let label;
  if (d.delta == null) label = { text: "est. lift", color: C.faint };
  else if (d.delta > 0) label = { text: `est. +${d.delta}`, color: C.good };
  else if (d.delta === 0) label = { text: "keeps you on track", color: C.dim };
  else label = { text: d.after != null ? `→ ${d.after}` : "small effect", color: C.dim };
  return { entry, label, delta: d };
}

// In-chat card for a meal the assistant just proposed: the score lift + ingredient
// summary + a "Log it" that estimates accurate nutrition, logs, and refreshes.
function ProposedMealCard({ meal, profile, toast, onLogged }) {
  const { recs, scored } = liveDay(profile);
  const { label } = useMealDelta(meal, profile, recs, scored.when);
  const [busy, setBusy] = useState(false);
  const [done, setDone] = useState(false);
  async function logIt() {
    if (busy || done) return;
    setBusy(true);
    const r = await Engine.estimateMealEntry({ description: meal.name, ingredients: meal.ingredients });
    setBusy(false);
    if (!r.ok) { toast(r.error || "Couldn't log that."); return; }
    Store.logMeal(r.entry);
    setDone(true);
    toast("Logged ✓");
    onLogged && onLogged();
  }
  const ings = (meal.ingredients || []).filter((i) => i && i.name);
  // Show the AI's per-ingredient portion so the user can see what the estimate
  // is based on (and sanity-check it): "cooked white rice 200 g · chicken 120 g".
  const items = ings.map((i) => (i.grams > 0 ? `${i.name} ${Math.round(i.grams)} g` : i.name));
  const totCal = Math.round(ings.reduce((s, i) => s + (Number(i.calories) || 0), 0));
  return (
    <div style={{ background: C.bg, border: `1px solid ${C.line}`, borderRadius: 14, padding: "12px 14px",
      display: "flex", flexDirection: "column", gap: 8, maxWidth: "92%", alignSelf: "flex-start" }}>
      <div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
        <span style={{ flex: 1, fontSize: 15, color: C.text, fontWeight: 600 }}>{meal.name}</span>
        {label && <span style={{ fontSize: 13, fontWeight: 600, color: label.color, whiteSpace: "nowrap" }}>{label.text}</span>}
      </div>
      {items.length > 0 && (
        <div style={{ fontSize: 13, color: C.dim, lineHeight: 1.45 }}>{items.join(" · ")}</div>
      )}
      {totCal > 0 && (
        <div style={{ fontSize: 12, color: C.faint }}>
          ≈ {totCal} kcal (estimated)
        </div>
      )}
      <button onClick={logIt} disabled={busy || done}
        style={{ ...pillBtn(true), padding: "9px 0", borderRadius: 11, marginTop: 2, opacity: busy ? 0.6 : 1 }}>
        {done ? "Logged ✓" : busy ? "Estimating…" : "Log it"}
      </button>
    </div>
  );
}

// One conversation. A brand-new chat is a DRAFT (no persisted thread yet): the
// thread is only created — and shows up in the list — once the first message is
// actually sent (ensureThread). `existingId` is set when resuming a saved thread.
function ChatThreadView({ existingId, draft, profile, toast, onLogged, autoFirst, onCreated }) {
  const tid = useRef(existingId || null);
  const existing = existingId ? Store.getChatThread(existingId) : null;
  const [msgs, setMsgs] = useState(() => (existing ? existing.messages.slice() : []));
  const [input, setInput] = useState("");
  const [busy, setBusy] = useState(false);
  const endRef = useRef(null);
  const started = useRef(false);

  useEffect(() => { if (endRef.current) endRef.current.scrollIntoView({ behavior: "smooth", block: "end" }); }, [msgs, busy]);

  // create the thread lazily, on first send, so empty chats never get saved.
  function ensureThread() {
    if (tid.current) return tid.current;
    const t = Store.createChat(draft || { kind: "general" });
    tid.current = t.id;
    onCreated && onCreated(t.id);
    return t.id;
  }

  function pushLocal(id, m) {
    const msg = { role: m.role, content: m.content, ts: Date.now(), ...(m.meal ? { meal: m.meal } : {}) };
    setMsgs((prev) => [...prev, msg]);
    Store.appendChatMessage(id, msg);
  }

  async function sendText(text) {
    const t = (text || "").trim();
    if (!t || busy) return;
    const id = ensureThread();
    const history = msgs.slice(); // turns BEFORE this one
    pushLocal(id, { role: "user", content: t });
    setInput("");
    setBusy(true);
    const { recs, scored } = liveDay(profile);
    const r = await Engine.assistantChat({ profile, scored, records: recs, history, message: t });
    setBusy(false);
    const m = { role: "assistant", content: r.ok ? r.reply : r.error || "Something went wrong." };
    if (r.ok && r.proposedMeal) m.meal = r.proposedMeal;
    pushLocal(id, m);
  }

  // auto-send the seeded opener exactly once (meal-plan / "act on this" launches)
  useEffect(() => {
    if (started.current) return;
    started.current = true;
    if (autoFirst && msgs.length === 0) sendText(autoFirst);
  }, []); // eslint-disable-line

  const bubble = (role) => ({
    alignSelf: role === "user" ? "flex-end" : "flex-start",
    background: role === "user" ? C.accent : C.card,
    color: role === "user" ? "#fff" : C.text,
    padding: "10px 13px", fontSize: 14, lineHeight: 1.5, maxWidth: "82%",
    borderRadius: role === "user" ? "14px 14px 4px 14px" : "14px 14px 14px 4px",
  });

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%", minHeight: 0 }}>
      <div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 10, overflowY: "auto",
        overscrollBehavior: "contain", minHeight: 0, padding: "4px 2px" }}>
        {msgs.length === 0 && !busy && (
          <div style={{ color: C.faint, fontSize: 14, textAlign: "center", padding: "30px 10px", lineHeight: 1.5 }}>
            Ask anything about your day, or say what you’re in the mood for and I’ll plan a meal.
          </div>
        )}
        {msgs.map((m, i) => (
          <React.Fragment key={i}>
            <div className="om-fade" style={bubble(m.role)}>
              {m.role === "assistant" ? <Markdown text={m.content} /> : m.content}
            </div>
            {m.meal && (
              <div className="om-fade" style={{ alignSelf: "flex-start", maxWidth: "92%", width: "100%" }}>
                <ProposedMealCard meal={m.meal} profile={profile} toast={toast} onLogged={onLogged} />
              </div>
            )}
          </React.Fragment>
        ))}
        {busy && <div style={{ alignSelf: "flex-start", color: C.faint, fontSize: 13 }}>Thinking…</div>}
        <div ref={endRef} />
      </div>
      <div style={{ display: "flex", gap: 8, paddingTop: 10 }}>
        <input value={input} onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter") sendText(input); }} placeholder="Message your coach…"
          style={{ ...inputStyle, borderRadius: 22, padding: "12px 16px" }} />
        <button onClick={() => sendText(input)} disabled={busy}
          style={{ background: C.accent, border: "none", color: "#fff", borderRadius: 22, width: 44,
            fontSize: 16, cursor: "pointer", flexShrink: 0, opacity: busy ? 0.5 : 1 }}>↑</button>
      </div>
    </div>
  );
}

// relative "2h ago / Yesterday / Jun 3" stamp for the thread list.
function agoLabel(ms) {
  if (!ms) return "";
  const diff = Date.now() - ms;
  const min = Math.floor(diff / 60000);
  if (min < 1) return "just now";
  if (min < 60) return `${min}m ago`;
  const hr = Math.floor(min / 60);
  if (hr < 24) return `${hr}h ago`;
  const d = new Date(ms);
  const day = localDay(d);
  return dayLabel(day) === "Yesterday" ? "Yesterday" : `${MON[d.getMonth()]} ${d.getDate()}`;
}

// The thread list: resume an existing conversation, start a new one, or delete.
function ChatListView({ onOpenThread, onNew }) {
  const [chats, setChats] = useState(() => Store.listChats());
  function del(id, e) { e.stopPropagation(); Store.deleteChat(id); setChats(Store.listChats()); }
  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%", minHeight: 0 }}>
      <button className="om-press" onClick={onNew}
        style={{ ...pillBtn(true), padding: "12px 0", borderRadius: 12, flexShrink: 0, marginBottom: 14 }}>
        + New chat
      </button>
      <div style={{ flex: 1, overflowY: "auto", overscrollBehavior: "contain", minHeight: 0 }}>
        {chats.length === 0 ? (
          <div style={{ color: C.faint, fontSize: 14, textAlign: "center", padding: "40px 10px", lineHeight: 1.5 }}>
            No chats yet. Start one to plan meals or talk through your day.
          </div>
        ) : (
          chats.map((t) => {
            const last = t.messages && t.messages.length ? t.messages[t.messages.length - 1] : null;
            const preview = last ? last.content : (t.kind === "meal_plan" ? "Meal plan" : "New chat");
            return (
              <div key={t.id} className="om-fade" onClick={() => onOpenThread(t.id)}
                style={{ display: "flex", alignItems: "center", gap: 10, padding: "13px 4px",
                  borderBottom: `1px solid ${C.line}`, cursor: "pointer" }}>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 15, color: C.text, fontWeight: 600, whiteSpace: "nowrap",
                    overflow: "hidden", textOverflow: "ellipsis" }}>{t.title || "New chat"}</div>
                  <div style={{ fontSize: 13, color: C.faint, whiteSpace: "nowrap", overflow: "hidden",
                    textOverflow: "ellipsis", marginTop: 2 }}>{preview}</div>
                </div>
                <span style={{ fontSize: 12, color: C.faint, whiteSpace: "nowrap" }}>{agoLabel(t.updatedAt)}</span>
                <button onClick={(e) => del(t.id, e)} title="Delete"
                  style={{ background: "none", border: "none", color: C.faint, fontSize: 16, cursor: "pointer",
                    padding: "0 2px", flexShrink: 0 }}>×</button>
              </div>
            );
          })
        )}
      </div>
    </div>
  );
}

// Full-screen overlay inside the phone frame. `intent` (carrying a unique `key`)
// tells it what to show when (re)opened: the list, a specific thread, or a freshly
// created thread (optionally auto-sending a seeded first message).
//   intent: null | {key, type:"list"}
//                | {key, type:"thread", id}
//                | {key, type:"new", kind?, title?, seed?, firstMessage?}
function ChatOverlay({ open, intent, onClose, profile, toast, onLogged }) {
  const { mounted, shown } = useMountTransition(open, D.sheet);
  const [view, setView] = useState("list"); // "list" | "thread"
  // the conversation currently shown: a resumed thread (existingId) or a fresh
  // DRAFT (draft, no id yet — only persisted once its first message is sent).
  const [existingId, setExistingId] = useState(null);
  const [draft, setDraft] = useState(null);
  const [autoFirst, setAutoFirst] = useState(null);
  const [convKey, setConvKey] = useState("none"); // stable React key per conversation
  const lastKey = useRef(null);
  const localSeq = useRef(0);

  function openThread(id) {
    setExistingId(id); setDraft(null); setAutoFirst(null); setConvKey("t-" + id); setView("thread");
  }
  function openDraft(spec, key) {
    setExistingId(null);
    setDraft({ kind: spec.kind || "general", title: spec.title, seed: spec.seed });
    setAutoFirst(spec.firstMessage || null);
    setConvKey(key);
    setView("thread");
  }

  // react to a new open-intent (once per intent.key)
  useEffect(() => {
    if (!open || !intent || lastKey.current === intent.key) return;
    lastKey.current = intent.key;
    if (intent.type === "thread") openThread(intent.id);
    else if (intent.type === "new") openDraft(intent, "n-" + intent.key);
    else { setView("list"); setExistingId(null); setDraft(null); setAutoFirst(null); }
  }, [open, intent]);

  if (!mounted) return null;
  const onList = view === "list";
  const title = onList
    ? "Chats"
    : (existingId ? (Store.getChatThread(existingId) || {}).title : draft && draft.title) || "Coach";
  const header = (
    <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 14, flexShrink: 0 }}>
      <button className="om-press" onClick={onList ? onClose : () => setView("list")}
        style={{ background: "none", border: "none", color: C.dim, fontSize: 22, cursor: "pointer", padding: "0 6px 0 0", lineHeight: 1 }}>
        {onList ? "✕" : "‹"}
      </button>
      <span style={{ flex: 1, fontSize: 16, color: C.text, fontWeight: 600, whiteSpace: "nowrap",
        overflow: "hidden", textOverflow: "ellipsis" }}>{title}</span>
      {!onList && (
        <button className="om-press" onClick={onClose}
          style={{ background: "none", border: `1px solid ${C.line}`, color: C.dim, borderRadius: 16,
            padding: "5px 12px", fontSize: 12, cursor: "pointer" }}>Done</button>
      )}
    </div>
  );

  return (
    <div style={{ position: "absolute", inset: 0, zIndex: 65, background: C.bg, display: "flex",
      flexDirection: "column",
      padding: "calc(18px + env(safe-area-inset-top)) 18px calc(20px + env(safe-area-inset-bottom))",
      transform: shown ? "translateY(0)" : "translateY(100%)",
      transition: `transform ${D.sheet}ms ${EASE}`, willChange: "transform" }}>
      {header}
      {/* fade/slide each view in as it swaps, so list↔thread feels continuous */}
      <div key={onList ? "list" : convKey} className="om-fade" style={{ flex: 1, minHeight: 0 }}>
        {onList ? (
          <ChatListView onOpenThread={openThread}
            onNew={() => openDraft({ kind: "general" }, "n-local-" + (++localSeq.current))} />
        ) : (
          <ChatThreadView key={convKey} existingId={existingId} draft={draft} profile={profile} toast={toast}
            onLogged={onLogged} autoFirst={autoFirst} onCreated={(id) => setExistingId(id)} />
        )}
      </div>
    </div>
  );
}
