/* simple/tabs.jsx — the three surfaces.
   HOME: your money at Yoshi. 7 elements, no scrolling required.
   YOSHI: the interface. Research, trades, moves, automations, docs,
          support — all conversation, rendered as inline cards.
   ACTIVITY: the audit trail. Everything that touched money, one feed,
             tagged with who acted. */

/* ============================================================
   HOME
   ============================================================ */
const HOME_RANGES = ["Today", "1W", "1M", "6M", "YTD", "1Y", "ALL"];
const HOME_RETS = { "Today": DAY_PCT, "1W": 1.2, "1M": 3.1, "6M": 9.4, "YTD": 11.2, "1Y": 18.9, "ALL": 64.0 };
const homeSeries = (range) => series(101 + HOME_RANGES.indexOf(range) * 31, 36, NET_WORTH / (1 + HOME_RETS[range] / 100), NET_WORTH, range === "Today" ? 0.0016 : 0.006);

const AllocStrip = ({ nav }) => {
  const allHoldings = [...GROUPS.investments.items];
  const investmentValue = allHoldings.reduce((sum, holding) => sum + holding.value, 0);
  const investmentDay = allHoldings.reduce((sum, holding) => sum + holding.dayAbs, 0);
  const accountSummary = [
    {
      id: CASH[0].id,
      name: "Cash",
      icon: "bank",
      value: CASH_TOTAL,
      apy: CASH[0].apr,
    },
    {
      id: "brokerage",
      name: "Investment",
      icon: "trade",
      value: investmentValue,
      dayAbs: investmentDay,
      dayPct: (investmentDay / (investmentValue - investmentDay)) * 100,
    },
  ];
  return (
    <div className="ld-account-list">
      {accountSummary.map((account) => (
        <button key={account.id} className="press ld-account-row" onClick={() => nav.sheet({ type: "accounts", focus: { accountId: account.id } })}>
          <span className="ld-account-tile" aria-hidden="true">
            <img src={account.apy ? "assets/lucid-icon-dollar.svg" : "assets/lucid-icon-data.svg"} alt="" />
          </span>
          <span className="ld-account-copy">
            <span className="ld-account-name">{account.name}</span>
            {account.apy
              ? <span className="ld-account-apy">{account.apy}</span>
              : <span className="ld-account-movement"><Delta abs={account.dayAbs} size="var(--ld-type-caption)" /> <span>{performanceMovementLabel === "today" ? "Today" : performanceMovementLabel}</span></span>}
          </span>
          <span className="ld-account-value"><Money value={account.value} size="var(--ld-type-value)" weight={500} color="var(--ink)" /></span>
          <span className="ld-account-chevron" aria-hidden="true"><img src="assets/lucid-chevron.svg" alt="" /></span>
        </button>
      ))}
    </div>
  );
};


const HoldingMark = ({ h, size = 38, label }) => {
  const color = "var(--accent)";
  const content = label === null ? <Icon name="trade" size={size >= 38 ? 16 : 14} stroke={1.6} color={color} /> : (label || h.ticker.slice(0, 4));
  return (
    <span style={{ width: size, height: size, flex: "none", borderRadius: 12, background: "var(--bg-2)", display: "grid", placeItems: "center", fontFamily: "var(--f-mono)", fontSize: size >= 38 ? 12 : 10, fontWeight: 400, lineHeight: 1, color }}>{content}</span>
  );
};

const HoldingsHeader = ({ nav }) => (
  <button className="press" onClick={() => nav.sheet({ type: "trade", holdings: true })} style={{ width: "100%", display: "flex", alignItems: "center", gap: 8, background: "none", border: "none", padding: "10px 2px", cursor: "pointer" }}>
    <Eyebrow color="var(--accent)">Holdings</Eyebrow>
    <span style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 4, fontFamily: "var(--f-display)", fontSize: 16, fontWeight: 500, color: "var(--accent)" }}>
      View all <Icon name="back" size={13} color="var(--accent)" style={{ transform: "scaleX(-1)" }} />
    </span>
  </button>
);

/* holdings — what you hold, with live day moves. Below the pulse so the
   number up top is the summary and this is the detail beneath it. */
const HoldingRow = ({ h, nav, first, onPick }) => (
  <button className="press ld-holding-row" onClick={() => onPick ? onPick(h.id) : nav.security(h.id)} style={{ borderTop: first ? "none" : "1px solid var(--rule)" }}>
    <span style={{ gridRow: "1 / 3" }}><HoldingMark h={h} size={48} /></span>
    <div className="ld-holding-name">{h.name}</div>
    <span data-home-holding-position-summary="value-only" style={{ textAlign: "right", whiteSpace: "nowrap" }}>
      <Money value={h.value} size="var(--ld-type-body)" weight={500} />
    </span>
    <div className="ld-holding-price">
      <Money value={h.last} size="var(--ld-type-small)" weight={400} color="var(--ld-muted)" />
      <Delta abs={h.last * h.dch / 100} pct={h.dch} size="var(--ld-type-small)" style={{ fontWeight: 400 }} />
      <span className="ld-holding-period" data-home-holding-performance-period="today">{performanceMovementLabel}</span>
    </div>
  </button>
);

const HoldingsList = ({ nav }) => {
  const rows = [...GROUPS.investments.items].sort((a, b) => b.value - a.value);
  return (
    <div>
      <HoldingsHeader nav={nav} />
      <div style={{ marginTop: 4, border: "1px solid var(--rule)", borderRadius: 24, overflow: "hidden", background: "var(--bg-card)", boxShadow: "var(--ld-shadow-card)" }}>
        {rows.map((h, i) => <HoldingRow key={h.id} h={h} nav={nav} first={i === 0} />)}
      </div>
    </div>
  );
};

/* Manual fast paths — same centered icon + label as before, just more
   presence: a hairline border, a touch more height, and an accent-lit icon. */
const FastBtn = ({ icon, label, onClick }) => (
  <button className="press" onClick={onClick} style={{
    display: "flex", alignItems: "center", justifyContent: "center", gap: 8, width: "100%",
    padding: "15px 18px", background: "var(--bg-2)", border: "1px solid var(--accent)",
    borderRadius: 12, cursor: "pointer",
    fontFamily: "var(--f-display)", fontSize: typeSize(16), fontWeight: 500, letterSpacing: "normal", color: "var(--accent)",
  }}>
    <Icon name={icon} size={17} color="var(--accent)" /> {label}
  </button>
);

const INSTANT_FUNDING_DISCLOSURE = "Once your pending deposit settles, the full amount will be available for your use. If your pending deposit doesn't settle, the instant funding amount will be removed, which could cause a negative balance. Instant funding availability isn't guaranteed.";

const PendingFundsTip = () => {
  const [open, setOpen] = useState(false);
  const containerRef = useRef(null);
  const tipId = "pending-funds-tip";
  useEffect(() => {
    if (!open) return undefined;
    const dismissOutside = (event) => {
      if (!containerRef.current?.contains(event.target)) setOpen(false);
    };
    document.addEventListener("pointerdown", dismissOutside);
    return () => document.removeEventListener("pointerdown", dismissOutside);
  }, [open]);
  return (
    <div
      ref={containerRef}
      onBlur={(event) => {
        if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
      }}
      style={{ position: "relative", display: "inline-flex", alignItems: "center" }}
    >
      <button
        className="press"
        type="button"
        aria-label="About instant funding"
        aria-describedby={open ? tipId : undefined}
        aria-expanded={open}
        onClick={() => setOpen((value) => !value)}
        onKeyDown={(event) => {
          if (event.key === "Escape") {
            setOpen(false);
            event.currentTarget.blur();
          } else if (event.key === "Enter" || event.key === " ") {
            event.preventDefault();
            setOpen((value) => !value);
          }
        }}
        style={{
          width: 24, height: 28, display: "grid", placeItems: "center", padding: 0,
          background: "transparent", border: "none", borderRadius: 999,
          color: "var(--ink-3)", cursor: "pointer",
        }}
      >
        <Icon name="info" size={15} stroke={1.5} />
      </button>
      {open && (
        <div
          id={tipId}
          role="dialog"
          aria-label="Instant funding disclosure"
          style={{
            position: "absolute", zIndex: 40, top: "calc(100% + 8px)", left: "50%",
            width: 304, maxWidth: "calc(100vw - 40px)", transform: "translateX(-50%)",
            padding: "11px 13px", background: "var(--ink)", color: "var(--bg)",
            borderRadius: 10, boxShadow: "0 10px 30px rgba(0, 0, 0, 0.18)",
            fontFamily: "var(--f-display)", fontSize: typeSize(12), lineHeight: 1.5,
            textAlign: "left",
          }}
        >
          <span aria-hidden="true" style={{ position: "absolute", top: -5, left: "50%", width: 10, height: 10, background: "var(--ink)", transform: "translateX(-50%) rotate(45deg)" }} />
          <div style={{ fontSize: "var(--ld-type-body)", fontWeight: 500, color: "var(--bg)", marginBottom: 8 }}>Instant funding lets you invest up to $10,000.00 through Yoshi while your deposit is pending.</div>
          <div>
            {INSTANT_FUNDING_DISCLOSURE}{" "}
            <a
              href="https://yoshi.ai/termsofservice/"
              target="_blank"
              rel="noopener noreferrer"
              onClick={() => setOpen(false)}
              style={{ color: "var(--bg)", fontWeight: 500, textDecoration: "underline", textUnderlineOffset: 3 }}
            >
              See terms
            </a>.
          </div>
        </div>
      )}
    </div>
  );
};

const HomeTab = ({ nav, proposals, onReview, hideFastPaths, inflight = [], instantFundingUsed = 0 }) => {
  const [range, setRange] = useState("Today");
  const [scrub, setScrub] = useState(null);
  // money in motion — one-time instructions still in flight ("one balance"
  // means moving money is neither here nor there; say where it is)
  const moving = inflight.filter((x) => x.kind === "once");
  const pendingTransfers = moving.filter((x) => x.category === "Transfer" && !x.fundedBy);
  const pendingIncoming = pendingTransfers.filter((x) => (x.net || 0) > 0).reduce((sum, x) => sum + (x.net || 0), 0);
  const pendingOutgoing = Math.abs(pendingTransfers.filter((x) => (x.net || 0) < 0).reduce((sum, x) => sum + (x.net || 0), 0));
  // The tooltip states the capped amount provisionally available to invest.
  // The pill separately names the remainder included in the balance as
  // instant funding, using the approved compact label.
  const provisionalAvailable = Math.min(pendingIncoming, INSTANT_FUNDING_CAP);
  const instantFundingIncluded = Math.max(0, provisionalAvailable - instantFundingUsed);
  // pending transfers move real money in/out of Yoshi, so the balance reflects
  // them immediately (+ in / − out). A trade is cash ↔ security — net-zero on
  // total value until the position moves after it executes — so it doesn't shift
  // the balance here.
  // a queued one-time automation gated on incoming funds (fundedBy) hasn't run
  // yet, so it doesn't move the balance — only genuinely in-flight transfers do
  const balanceDelta = pendingIncoming - pendingOutgoing;
  const liveNetWorth = NET_WORTH + balanceDelta;
  const data = useMemo(() => homeSeries(range), [range]);
  const ret = HOME_RETS[range];
  const abs = NET_WORTH - NET_WORTH / (1 + ret / 100);
  const shown = proposals.slice(0, 1);

  // drag across the chart → read the value + change at that point in time
  const scrubbing = scrub != null && data[scrub] != null;
  // headline = live balance, which already reflects the inbound transfer via
  // balanceDelta (In motion item f2). Instant funding makes only the capped
  // provisional slice investable before the rest of the deposit lands.
  const dispVal = scrubbing ? data[scrub] : liveNetWorth;
  const dispAbs = scrubbing ? data[scrub] - data[0] : abs;
  const dispPct = scrubbing ? ((data[scrub] - data[0]) / data[0]) * 100 : ret;
  const chartColor = dispAbs < 0 ? "var(--signal-neg)" : "var(--accent-pos)";
  return (
    <div className="ld-home" data-performance={dispAbs < 0 ? "loss" : "gain"} style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }}>
      {/* header: hamburger (settings) · bell (briefs) — logo lives in the Yoshi tab */}
      <div style={{ flex: "none", display: "flex", alignItems: "center", padding: "6px 16px 2px", minHeight: 40 }}>
        <button className="press" onClick={() => nav.sheet({ type: "profile" })} aria-label="Menu" style={{ background: "none", border: "none", padding: 6, marginLeft: -6, color: "var(--ink)", display: "flex", cursor: "pointer" }}>
          <Icon name="menu" size={22} />
        </button>
        <span style={{ marginLeft: "auto" }}><BellButton nav={nav} /></span>
      </div>

      <div className="scroll" style={{ padding: "4px 18px 24px" }}>
        {/* 1+2 · the number and the day — tap to see the accounts behind it
             (isolated up here, away from Move/Trade, so no mis-taps) */}
        <div style={{ display: "flex", alignItems: "flex-start", gap: 4 }}>
          <button className="press" onClick={() => nav.sheet({ type: "accounts" })} style={{ display: "block", width: "auto", textAlign: "left", background: "none", border: "none", padding: 0, cursor: "pointer" }}>
            <span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
              <Eyebrow style={{ fontWeight: 400 }}>Yoshi balance</Eyebrow>
              <Icon name="back" size={12} color="var(--ink-3)" style={{ transform: "scaleX(-1)" }} />
            </span>
            <div style={{ marginTop: 5, lineHeight: 1 }}><Money value={dispVal} size="var(--ld-type-balance)" weight={500} dim="var(--ink)" style={{ letterSpacing: "normal" }} /></div>
            <div style={{ marginTop: 7, display: "flex", alignItems: "baseline", gap: 7 }}>
              <SummaryPerformance size={16} abs={dispAbs} pct={dispPct} label={scrubbing ? chartDateLabel(range, scrub, data.length) : range === "Today" ? performanceMovementLabel : range} split={!scrubbing && range === "Today"} afterAbs={GROUPS.investments.items.reduce((sum, h) => sum + afterHoursHoldingChange(h), 0)} />
            </div>
          </button>

        </div>

        {/* Keep the definition and disclosure control beside the instant-funding
            status so the relationship is explicit. */}
        {instantFundingIncluded > 0 &&
        <div style={{ marginTop: 8, display: "flex", alignItems: "center", gap: 2 }}>
          <div role="status" style={{ display: "inline-flex", alignItems: "center", gap: 8, padding: "7px 11px 7px 9px", background: "var(--bg-2)", border: "1px solid var(--rule-2)", borderRadius: 999, color: "var(--ink-3)" }}>
            <Icon name="bolt" size={12.5} color="var(--accent)" stroke={1.6} />
            <span style={{ fontFamily: "var(--f-display)", fontSize: 14, lineHeight: 1.35 }}>
              Incl.{" "}<span style={{ fontFamily: "var(--f-mono)", fontWeight: 500, color: "var(--accent-pos)", fontVariantNumeric: "proportional-nums" }}>{usd(instantFundingIncluded)}</span>{" "}instant funds
            </span>
          </div>
          {provisionalAvailable > 0 && <PendingFundsTip />}
        </div>}

        {/* 3 · sparkline — drag across to scrub the value over time */}
        <div style={{ margin: "8px 0 0" }}>
          <Chart data={data} height={104} accent={chartColor} fillColor={chartColor} padY={4} scrub onScrub={(idx) => setScrub(idx)} range={range} yLabelFormatter={(n) => "$" + Math.round(n / 1000) + "k"} />
        </div>
        <div style={{ display: "flex", gap: 3, marginTop: 4 }}>
          {HOME_RANGES.map((r) => (
            <button key={r} className="press" onClick={() => { setRange(r); setScrub(null); }} style={{ flex: 1, padding: "7px 0", background: r === range ? "var(--bg-card)" : "transparent", border: "1px solid " + (r === range ? "var(--rule)" : "transparent"), boxShadow: r === range ? "var(--ld-shadow-card)" : "none", borderRadius: 18, fontFamily: "var(--f-mono)", fontSize: 14, fontWeight: 500, letterSpacing: "normal", color: r === range ? "var(--ink)" : "var(--ink-3)", cursor: "pointer", fontVariantNumeric: "proportional-nums" }}>{r}</button>
          ))}
        </div>

        {/* 4 · consolidated account summary */}
        <div style={{ marginTop: 12 }}><AllocStrip nav={nav} /></div>

        {/* 5 · manual fast paths — the two doors, above the fold.
             Original TransferFlow / TradeSheet, unchanged. On web these live
             in the top bar instead, so the shell hides them here. */}
        {!hideFastPaths && (
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, marginTop: 16 }}>
            <FastBtn icon="swap" label="Transfer" onClick={() => nav.sheet({ type: "transfer" })} />
            <FastBtn icon="trade" label="Trade" onClick={() => nav.sheet({ type: "trade" })} />
          </div>
        )}

        {/* 6 · needs you */}
        <div style={{ marginTop: 18 }}>
          <div style={{ display: "flex", alignItems: "baseline" }}>
            <Eyebrow color="var(--accent)">Needs you</Eyebrow>
            {proposals.length > 1 && (
              <button className="press" onClick={() => nav.sheet({ type: "briefs" })} style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 4, background: "none", border: "none", padding: "2px 0", cursor: "pointer", fontFamily: "var(--f-display)", fontSize: "var(--ld-type-small)", fontWeight: 500, color: "var(--accent)" }}>
                +{proposals.length - 1} more <Icon name="back" size={13} color="var(--accent)" style={{ transform: "scaleX(-1)" }} />
              </button>
            )}
          </div>
          {proposals.length > 0 ? (
            <div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 8 }}>
              {shown.map((p) => <PCard key={p.id} p={p} compact onReview={() => onReview(p)} />)}
            </div>
          ) : (
            <div style={{ marginTop: 8, fontFamily: "var(--f-display)", fontSize: "var(--ld-type-small)", fontWeight: 500, color: "var(--ink-2)", lineHeight: 1.45 }}>
              You're all caught up
            </div>
          )}
        </div>

        {/* 7 · holdings — what you hold, live day moves */}
        <div style={{ marginTop: 18 }}><HoldingsList nav={nav} /></div>
      </div>
    </div>
  );
};

/* ============================================================
   YOSHI · the interface. The reply engine renders capability
   cards inline — each one a screen this redesign deleted.
   ============================================================ */
const YOSHI_QUICK = ["What's my full net worth?", "How am I doing vs the S&P?", "Move $2,000 to savings", "Show my automations"];

const yoshiReply = (t, ctx) => {
  const s = t.toLowerCase();
  if (/net worth|everything|total|full picture|external/.test(s)) return {
    t: `Everything together: ${usd(NET_WORTH + 8210.32 + 186400 + 12840 - LIAB_TOTAL)}. That's ${usd(NET_WORTH)} here at Yoshi, plus ${usd(8210.32 + 186400 + 12840)} across Chase, Schwab, and Coinbase, minus ${usd(LIAB_TOTAL)} of debt (the mortgage and two cards). Your biggest lever: the Schwab account is earning almost nothing in its cash sweep — want me to draft a consolidation?`,
  };
  if (/s&p|compare|vs |versus|how am i doing|performance/.test(s)) return {
    t: "You're ahead of the index this year — concentration in NVDA did most of that, which cuts both ways.",
    card: { type: "compare" },
  };
  if (/nvda|nvidia/.test(s)) return { t: securityTake("nvda").take, card: { type: "quote", id: "nvda" } };
  if (/bitcoin|btc/.test(s)) return { t: securityTake("btc").take, card: { type: "quote", id: "btc" } };
  if (/tsla|tesla/.test(s)) return { t: securityTake("tsla").take, card: { type: "quote", id: "tsla" } };
  if (/move|transfer|send/.test(s)) {
    const amt = parseFloat((s.match(/\$?([\d,]+(?:\.\d+)?)/) || [])[1]?.replace(/,/g, "")) || 2000;
    return {
      t: `Drafted. ${usd(amt)} from Cash to Reserve, instant, no fee. Approve when ready.`,
      proposal: {
        id: "pm_" + Date.now(), agent: "Yoshi agent", title: `Move ${usd(amt)} to Reserve`,
        why: `One-time instant transfer from Yoshi Cash ••8841 to Yoshi Reserve ••2207. Both earn 3.30% APY. This will not repeat.`,
        net: 0, settles: "In seconds", settlesVerb: "arrives", kind: "transfer",
        legs: [["FROM", "Yoshi Cash ••8841", `Balance ${usd(18420.55)}`, "−" + usd(amt)], ["TO", "Yoshi Reserve ••2207", `Balance ${usd(64000)}`, "+" + usd(amt)], ["TIMING", "Instant transfer", "Arrives in seconds", "$0.00 fee"]],
        activity: { icon: "arrow-left-right", title: "Reserve", detail: `${usd(amt)} · Instant`, category: "Transfer", by: "Yoshi agent", net: 0, amount: amt },
      },
    };
  }
  if (/automat|rules|recurring/.test(s)) return { t: "Six rules running, all inside your limits. They've earned you $841.00 this quarter between interest, the VTI streak, and harvested losses.", card: { type: "autos" } };
  if (/spend|dining|budget/.test(s)) return { t: "Good news, actually:", card: { type: "insight", i: 0 } };
  if (/1099|tax|statement|document|doc/.test(s)) return { t: "Here you go — I can also send these straight to your accountant.", card: { type: "doc", name: "1099-B · 2025", sub: "Yoshi Securities · consolidated" } };
  if (/pay|sapphire|card|debt/.test(s)) return { t: "Your Sapphire statement is $2,340.18, due Friday at 21.99% APR. Paying in full from your Cash keeps you at zero interest — it's ready below.", proposal: PROPOSALS.find((p) => p.id === "p2") };
  if (/cash|idle|savings rate|yield/.test(s)) return { t: "Your cash is working: every Yoshi cash account earns 3.30% APY, and the buffer rule keeps your spendable Cash lean while the excess sits in your Reserve. One idea — $15,000.00 has sat past your buffer for 9 days; short-term Treasuries would add about $760.00 a year and stay liquid. Want the proposal?" };
  return { t: "On it. I can research anything you hold, move money, place trades, set up rules, pull documents, or just explain what's happening — all from right here. What do you want to do?" };
};

const YoshiCardFor = ({ card, nav, flash }) => {
  if (!card) return null;
  if (card.type === "quote") return <QuoteCard id={card.id} nav={nav} />;
  if (card.type === "compare") return <CompareCard />;
  if (card.type === "autos") return <AutosCard flash={flash} />;
  if (card.type === "insight") { const ins = BRIEF_INSIGHTS[card.i || 0]; return <InsightChip label={ins.label} stat={ins.stat} read={ins.read} />; }
  if (card.type === "doc") return <DocCard name={card.name} sub={card.sub} flash={flash} />;
  return null;
};

const YOSHI_SEED = [
  { from: "agent", t: "Morning. You're up " + perf(DAY_ABS, DAY_PCT) + " today — NVDA and BTC did most of it. One thing needs you: your stocks drifted past the mix you set, so I'd like to even things out.", time: "9:24" },
  { from: "user", t: "what would that do", time: "9:25" },
  { from: "agent", t: "Three trades: trim NVDA and BTC, add VOO. You'd come out about $1,153.00 ahead, done by tomorrow. Approve whenever you're ready.", time: "9:25", proposal: "p1" },
];

const YoshiTab = ({ nav, proposals, onReview, inject, onInjected, flash }) => {
  const [msgs, setMsgs] = useState(YOSHI_SEED);
  const [input, setInput] = useState("");
  const [typing, setTyping] = useState(false);
  const endRef = useRef(null);
  const replyTimer = useRef(null);
  useEffect(() => () => clearTimeout(replyTimer.current), []);
  useEffect(() => { endRef.current && endRef.current.scrollIntoView({ behavior: "smooth" }); }, [msgs, typing]);
  useEffect(() => {
    if (!inject) return;
    setMsgs((m) => [...m, ...(inject.note ? [{ from: "user", t: inject.note, time: "now" }] : []), { from: "agent", t: inject.reply, time: "now" }]);
    onInjected();
  }, [inject]);

  const send = (text) => {
    const t = (text != null ? text : input).trim();
    if (!t) return;
    setInput("");
    setMsgs((m) => [...m, { from: "user", t, time: "now" }]);
    setTyping(true);
    replyTimer.current = setTimeout(() => {
      const r = yoshiReply(t);
      setTyping(false);
      setMsgs((m) => [...m, { from: "agent", t: r.t, time: "now", card: r.card, proposalObj: r.proposal }]);
    }, 10000);
  };
  const stop = () => {
    clearTimeout(replyTimer.current);
    replyTimer.current = null;
    setTyping(false);
  };

  const findP = (idOrObj) => typeof idOrObj === "string" ? proposals.find((p) => p.id === idOrObj) || PROPOSALS.find((p) => p.id === idOrObj) : idOrObj;

  return (
    <div style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }}>
      <div style={{ flex: "none", display: "flex", alignItems: "center", gap: 8, padding: "6px 20px 10px", borderBottom: "1px solid var(--rule)" }}>
        <Logo size={18} />
        <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(16), fontWeight: 500, letterSpacing: "normal" }}>Yoshi</span>
        <button className="press" onClick={() => nav.tab("home")} aria-label="Close" style={{ marginLeft: "auto", background: "none", border: "none", padding: 0, color: "var(--ink-3)", display: "flex", cursor: "pointer" }}>
          <Icon name="close" size={19} />
        </button>
      </div>

      <div className="scroll" style={{ padding: "14px 16px 8px" }}>
        {msgs.map((m, i) => {
          const p = m.proposal ? findP(m.proposal) : m.proposalObj;
          return (
            <div key={i} style={{ marginBottom: 12 }}>
              <div style={{ display: "flex", justifyContent: m.from === "user" ? "flex-end" : "flex-start" }}>
                <div style={{ maxWidth: "86%", padding: "10px 13px", borderRadius: m.from === "user" ? "14px 14px 4px 14px" : "14px 14px 14px 4px", background: m.from === "user" ? "var(--ink)" : "var(--bg-card)", color: m.from === "user" ? "var(--bg)" : "var(--ink)", border: m.from === "user" ? "none" : "1px solid var(--rule)", fontFamily: "var(--f-display)", fontSize: "var(--ld-type-body)", fontWeight: 500, lineHeight: 1.45 }}>
                  {m.t}
                </div>
              </div>
              {p && <div style={{ marginTop: 8 }}><ChatProposal p={p} onReview={() => onReview(p)} /></div>}
              {m.card && <div style={{ marginTop: 8 }}><YoshiCardFor card={m.card} nav={nav} flash={flash} /></div>}
            </div>
          );
        })}
        {typing && (
          <div style={{ display: "inline-flex", gap: 4, padding: "12px 14px", background: "var(--bg-card)", border: "1px solid var(--rule)", borderRadius: "14px 14px 14px 4px" }}>
            {[0, 1, 2].map((i) => <span key={i} className="yo-mk-pulse" style={{ width: 5, height: 5, borderRadius: 999, background: "var(--ink-3)", animationDelay: `${i * 0.2}s` }} />)}
          </div>
        )}
        <div ref={endRef} />
      </div>

      <div style={{ flex: "none", padding: "6px 14px 10px" }}>
        <div className="hcar" style={{ display: "flex", gap: 7, overflowX: "auto", paddingBottom: 8 }}>
          {YOSHI_QUICK.map((qr) => (
            <button key={qr} className="press" onClick={() => send(qr)} style={{ flex: "none", padding: "8px 13px", borderRadius: 999, background: "transparent", border: "1px solid var(--rule-2)", color: "var(--ink-2)", fontFamily: "var(--f-display)", fontSize: typeSize(12), fontWeight: 500, cursor: "pointer" }}>{qr}</button>
          ))}
        </div>
        <YoshiComposer value={input} onChange={setInput} onSend={() => send()} placeholder="Talk to Yoshi…" onAttach={(d) => send(`I attached ${d.name}`)} isWorking={typing} onStop={stop} />
      </div>
    </div>
  );
};

/* ============================================================
   ACTIVITY · the audit trail. One feed, everything, tagged with
   who acted. The trust contract for an agent with authority.
   ============================================================ */
const ACTIVITY_FILTERS = [
  { id: "all", label: "All" },
  { id: "cash", label: "Cash" },
  { id: "investments", label: "Investments" },
];
// a credit-card transaction — detail carries the card, e.g. "Card ··4417"
const cardOf = (tx) => { const m = (tx.detail || "").match(/Card ··(\d{4})/); return m ? "··" + m[1] : null; };
const activityTypeOf = (item) => {
  const scope = item.accountScope || item.scope;
  if (scope && scope !== "yoshi") return null;
  const automation = item.autoId ? AUTOMATIONS.find((entry) => entry.id === item.autoId) : null;
  const category = automation?.runTx?.category || item.category;
  return category === "Investment" || category === "Trade" ? "investments" : "cash";
};

/* Coming up — the live queue (one-time in flight · recurring next runs ·
   predicted events). Data + lifecycle model live in data.jsx (COMING_UP_SEED);
   the stateful list is owned by SimpleApp and arrives via the `coming` prop.
   Chip color says how alive a row is: in-flight → accent, the rest stay calm. */
const inflightChipColor = (item) =>
  item.state === "working" || item.state === "queued" ? "var(--accent)"
  : item.state === "pending" ? "var(--ink-2)"
  : "var(--ink-3)";

const actorOf = (tx) => {
  if (tx.by) return tx.by;
  if (tx.autoId) { const a = AUTOMATIONS.find((x) => x.id === tx.autoId); return a ? a.name : "Yoshi"; }
  if (/Card ··/.test(tx.detail || "")) return "Card";
  if (tx.category === "Income") return "Deposit";
  return "You";
};
const isAgentActed = (tx) => !!tx.autoId || (tx.by && tx.by !== "You" && tx.by !== "Card");

const ActivityTransactionRow = ({ tx, nav }) => {
  const agent = isAgentActed(tx);
  const stamp = transactionStamp(tx);
  // credit-card transactions call out the card; other external
  // transactions call out the account they touched, the same way
  const card = cardOf(tx);
  const acct = card ? `Card ${card}` : (tx.accountScope === "external" ? tx.acct : null);
  const tCat = directionalCategory(tx.category, tx.net);
  const sub = acct ? `${tCat} ${card ? "on" : "with"} ${acct}` : tCat;
  return (
    <button className="press" onClick={() => nav.sheet({ type: "txn", tx })} style={{ width: "100%", textAlign: "left", display: "grid", gridTemplateColumns: "30px 1fr auto", gap: 12, alignItems: "center", padding: "11px 20px", background: tx.fresh ? "color-mix(in srgb, var(--accent) 8%, transparent)" : "none", transition: "background 900ms ease", animation: tx.fresh ? "count-up 420ms ease both" : "none", border: "none", borderBottom: "1px solid var(--rule)", cursor: "pointer" }}>
      <span style={{ width: 30, height: 30, borderRadius: 9, background: "var(--bg-2)", display: "grid", placeItems: "center", color: agent ? "var(--accent)" : "var(--ink-2)" }}>
        <Icon name={tx.icon || "receipt"} size={15} stroke={1.5} />
      </span>
      <div style={{ minWidth: 0 }}>
        {/* title = the transaction or the account; wraps to 2 lines so
            'Transferred from X to Y' isn't cut off */}
        <div style={{ fontFamily: "var(--f-display)", fontSize: "var(--ld-type-body)", fontWeight: 500, lineHeight: 1.3, display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden" }}>{tx.title}</div>
        {/* category under the title; automation-powered rows stay highlighted */}
        <div style={{ fontFamily: "var(--f-display)", fontSize: "var(--ld-type-caption)", color: "var(--ink-3)", fontWeight: 400, marginTop: 2, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
          {sub}
        </div>
      </div>
      {/* value on top, date/time beneath */}
      <span style={{ textAlign: "right", whiteSpace: "nowrap" }}>
        {tx.net !== 0
          ? <Money value={tx.net} size="var(--ld-type-small)" sign color={tx.net > 0 ? "var(--accent-pos)" : "var(--ink)"} dim="var(--ink-3)" />
          : tx.amount ? <Money value={tx.amount} size="var(--ld-type-small)" color="var(--ink-2)" dim="var(--ink-3)" /> : <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(12), fontWeight: 500, color: "var(--ink-2)" }}>Done</span>}
        <div style={{ fontFamily: "var(--f-display)", fontSize: "var(--ld-type-caption)", color: "var(--ink-3)", marginTop: 2 }}>{stamp}</div>
      </span>
    </button>
  );
};

const ActivityTab = ({ nav, extra, coming = [], filter = "all", onFilter }) => {
  const [q, setQ] = useState("");
  const activeFilter = ACTIVITY_FILTERS.find((option) => option.id === filter) || ACTIVITY_FILTERS[0];
  const matchesActivityFilter = (item) => {
    const type = activityTypeOf(item);
    return !!type && (filter === "all" || type === filter);
  };
  const rows = sortTransactionsNewestFirst([...extra, ...TRANSACTIONS]);
  const shown = rows.filter((tx) => {
    if (!matchesActivityFilter(tx)) return false;
    const s = q.trim().toLowerCase();
    return !s || (tx.title + " " + (tx.detail || "") + " " + (tx.category || "")).toLowerCase().includes(s);
  });
  const inMotion = sortTransactionsNewestFirst(coming.filter((item) => item.kind === "once" && matchesActivityFilter(item)));
  const comingShown = coming.filter((item) => item.kind !== "once" && matchesActivityFilter(item));

  // one row shape for both sections — predicted events stay passive (plain
  // txn sheet); instructions open the in-flight detail with stepper + actions
  const queueRow = (item) => {
    // the core app labels transfers directionally (Transfer in / Transfer out)
    const iCat = directionalCategory(item.category, item.net);
    // automation rows: one glyph, one word — the rule and route live in the detail
    const isAuto = iCat === "Automation";
    const cSub = isAuto ? "Automation" : item.kind === "recurring" ? `Recurring ${iCat.toLowerCase()}` : iCat === "Trade" ? "One-time investment" : iCat;
    const passive = item.kind === "predicted";
    const live = item.kind === "once"; // in flight right now — breathes
    const fundingAvailable = live && item.instantAvailable > 0;
    const fundingSplit = live && item.instantAvailable > 0 && item.pendingAmount > 0;
    // In-motion items open the SAME transaction detail as any other txn, just
    // flagged pending (+ a cancel button when they can still be pulled).
    // Predicted → the same detail as scheduled. Recurring → the rule sheet.
    const canCancel = live && (item.state === "queued" || item.state === "working" || (item.state === "pending" && item.cancelable));
    const open = passive
      ? () => nav.sheet({ type: "txn", tx: { ...item, net: null, amount: Math.abs(item.net), tone: item.net < 0 ? "out" : "in", detail: item.acct } })
      : live
        ? () => nav.sheet({ type: "txn", tx: { ...item, detail: item.acct, pending: true }, item, cancelable: canCancel })
        : () => nav.sheet({ type: "txn", tx: { ...item, net: null, amount: Math.abs(item.net), tone: item.net < 0 ? "out" : "in", detail: [item.rail, item.acct].filter(Boolean).join(" · "), trigger: item.chip, cancelNote: item.skipNote, cancelLabel: "Skip this run" }, item, cancelable: true });
    return (
      <button key={item.id} className="press" onClick={open} style={{ width: "100%", textAlign: "left", display: "grid", gridTemplateColumns: "30px 1fr auto", gap: 12, alignItems: "center", padding: "11px 20px", background: item.fresh ? "color-mix(in srgb, var(--accent) 8%, transparent)" : "none", transition: "background 900ms ease", border: "none", borderBottom: "1px solid var(--rule)", cursor: "pointer", animation: item.fresh ? "count-up 420ms ease both" : "none" }}>
        <span style={{ width: 30, height: 30, borderRadius: 9, background: "var(--bg-2)", display: "grid", placeItems: "center", color: item.agent ? "var(--accent)" : "var(--ink-2)" }}>
          <Icon name={item.icon} size={15} stroke={1.5} />
        </span>
        <div style={{ minWidth: 0 }}>
          <div style={{ fontFamily: "var(--f-display)", fontSize: "var(--ld-type-body)", fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{item.title}</div>
          <div style={{ fontFamily: "var(--f-display)", fontSize: "var(--ld-type-caption)", color: "var(--ink-3)", fontWeight: 400, marginTop: 2, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
            {cSub}
          </div>
        </div>
        <span style={{ textAlign: "right", whiteSpace: "nowrap" }}>
          {item.net !== 0
            ? <Money value={item.net} size="var(--ld-type-small)" sign color={item.net > 0 ? "var(--accent-pos)" : "var(--ink)"} dim="var(--ink-3)" />
            : <Money value={item.amount || 0} size="var(--ld-type-small)" color="var(--ink-2)" dim="var(--ink-3)" />}
          {/* In motion rows read a uniform "Pending" —
              the state/timing lives in the detail sheet. Coming up keeps its
              informative chip (Runs Fri · market open, Monday Jun 15, …). */}
          <div style={{ display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 5, fontFamily: "var(--f-display)", fontSize: "var(--ld-type-caption)", fontWeight: passive ? 400 : 500, color: live ? "var(--ink-2)" : inflightChipColor(item), marginTop: 2 }}>
            {live ? (fundingSplit ? "Partially available now" : fundingAvailable ? "Available instantly" : "Pending") : (item.chip || item.when)}
          </div>
        </span>
      </button>
    );
  };
  return (
    <div style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }}>
      <div style={{ flex: "none", padding: "6px 20px 0" }}>
        <span style={{ fontFamily: "var(--f-display)", fontSize: "var(--ld-type-heading)", fontWeight: 500, letterSpacing: "normal" }}>Activity</span>
        <div style={{ marginTop: 12 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "9px 12px", background: "var(--bg-2)", border: "1px solid var(--rule-2)", borderRadius: 10 }}>
            <Icon name="search" size={15} color="var(--ink-3)" />
            <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search activity" style={{ flex: 1, minWidth: 0, background: "transparent", border: "none", outline: "none", color: "var(--ink)", fontFamily: "var(--f-display)", fontSize: "var(--ld-type-body)" }} />
            {q && <button className="press" onClick={() => setQ("")} style={{ background: "none", border: "none", display: "flex", color: "var(--ink-3)", padding: 0, cursor: "pointer" }}><Icon name="close" size={15} /></button>}
          </div>
          <div className="hcar" role="group" aria-label="Filter activity by type" style={{ display: "flex", alignItems: "center", gap: 8, overflowX: "auto", padding: "8px 0 5px" }}>
            {ACTIVITY_FILTERS.map((option) => {
              const active = filter === option.id;
              return (
                <button key={option.id} className="press" aria-pressed={active} onClick={() => onFilter(option.id)} style={{ position: "relative", flex: "none", minHeight: 40, padding: "10px 16px", borderRadius: 999, whiteSpace: "nowrap", fontFamily: "var(--f-display)", fontSize: "var(--ld-type-small)", fontWeight: 500, border: "none", cursor: "pointer", background: "none", color: active ? "var(--ink)" : "var(--ink-3)", transition: "color 140ms ease" }}>
                  {option.label}
                </button>
              );
            })}
          </div>
        </div>
      </div>
      <div className="scroll">
        {/* In motion — trades and transfers in flight right now. Tap for the
            stepper + cancel; on settlement the row drops into Recent. */}
        {!q && inMotion.length > 0 && (
          <>
            <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "12px 20px 6px" }}>
              <Eyebrow color="var(--accent)">In motion</Eyebrow>
            </div>
            {inMotion.map(queueRow)}
          </>
        )}
        {/* Coming up — scheduled automations, bills, payday (full view only) */}
        {!q && comingShown.length > 0 && (
          <>
            <div style={{ padding: "12px 20px 6px", borderTop: inMotion.length > 0 && !q ? "1px solid var(--rule)" : "none" }}><Eyebrow>Coming up</Eyebrow></div>
            {comingShown.map(queueRow)}
          </>
        )}
        {!q && (inMotion.length > 0 || comingShown.length > 0) && (
          <div style={{ padding: "12px 20px 6px", borderTop: "1px solid var(--rule)" }}><Eyebrow>Recent</Eyebrow></div>
        )}
        {shown.map((tx, i) => <ActivityTransactionRow key={tx.id || "x" + i} tx={tx} nav={nav} />)}
        {filter !== "all" && shown.length === 0 && inMotion.length === 0 && comingShown.length === 0 && (
          <div style={{ padding: "28px 20px", textAlign: "center", fontFamily: "var(--f-display)", fontSize: typeSize(12.5), color: "var(--ink-3)" }}>No {activeFilter.label.toLowerCase()} yet.</div>
        )}
        <div style={{ height: 20 }} />
      </div>
    </div>
  );
};

Object.assign(window, { HomeTab, YoshiTab, ActivityTab, AllocStrip, actorOf });
