// Fetch procurement data — the Sourcing page's data-pull experience.
// Pure render (FetchDataDialog) + a module-level run store so progress survives
// closing the dialog or navigating away. No real backend: the ticker is a stand-in
// for server-pushed workflow events.

const FD_SOURCES = [
  { id: "gmail", name: "Gmail", icon: "mail", connected: true, account: "priya.nair@la28.org", fetchedThrough: null, lastSync: null },
  { id: "outlook", name: "Outlook", icon: "mail", connected: true, account: "procurement@la28.org", fetchedThrough: null, lastSync: null },
  { id: "salesforce", name: "Salesforce", icon: "globe", connected: false, account: null, fetchedThrough: null, lastSync: null },
  { id: "hubspot", name: "HubSpot", icon: "exchange", connected: true, account: "LA28 Partnerships hub", fetchedThrough: "Sep 15, 2026", lastSync: "Sep 15, 2026 at 7:40 AM" },
];
const FD_TODAY = "Sep 15, 2026";
// Mailbox sources can be narrowed to the places procurement work actually lives.
// Nothing picked = everything, stated plainly rather than implied by an empty list.
const FD_SCOPES = {
  gmail: { label: "Labels", all: "All mail", one: "label", many: "labels",
    options: ["Inbox", "Sent", "Procurement", "Suppliers", "RFPs", "Contracts", "Vendor invoices", "Partnerships"] },
  outlook: { label: "Folders", all: "All folders", one: "folder", many: "folders",
    options: ["Inbox", "Sent Items", "Procurement", "Suppliers", "Tenders", "Contracts", "Archive"] },
};
// What each source has already fetched, keyed by id — written when a run finishes.
const FD_FETCHED = {};
// When each source was last synced (the run's finish time), keyed by id.
const FD_SYNCED = {};
function fdSynced(s) { return FD_SYNCED[s.id] || s.lastSync || null; }

// ── dates: one 12-month window, and the incremental slice for a source with history
const fdParse = (s) => new Date(s + " 12:00:00");
const fdFmt = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
const fdDay = (d, n) => new Date(d.getTime() + n * 86400000);
const FD_END = fdParse(FD_TODAY);
const FD_START = fdDay(new Date(FD_END.getFullYear() - 1, FD_END.getMonth(), FD_END.getDate()), 1);
const FD_FULL_RANGE = fdFmt(FD_START) + " – " + FD_TODAY;
function fdThrough(s) { return FD_FETCHED[s.id] || s.fetchedThrough || null; }
// The span a source's existing data actually covers: the 12 months ending at its
// last fetch. Stating both ends stops "through Aug 14, 2026" reading as a start date.
function fdCovered(s) {
  const through = fdThrough(s);
  if (!through) return null;
  const t = fdParse(through);
  const start = fdDay(new Date(t.getFullYear() - 1, t.getMonth(), t.getDate()), 1);
  return fdFmt(start) + " – " + through;
}
// A source with history is only ever read forward from its last fetch — history is
// never re-fetched, so there is no re-fetch option to get wrong.
function fdPlan(s) {
  const through = fdThrough(s);
  if (!through) return { mode: "full", start: fdFmt(FD_START), end: FD_TODAY, range: FD_FULL_RANGE };
  const t = fdParse(through);
  if (t >= FD_END) return { mode: "uptodate", through, range: null };
  const start = fdFmt(fdDay(t, 1));
  return { mode: "new", through, start, end: FD_TODAY, range: start + " – " + FD_TODAY };
}
const FD_STAGES = ["Waiting", "Connecting", "Fetching data", "Finding procurement information",
  "Matching companies and people", "Saving relationships", "Enriching company profiles", "Complete"];

// ── state copy ────────────────────────────────────────────────────────────
const FD_STATUS = {
  waiting:   { tone: "idle", chip: "Waiting to start", t: "Waiting to start", d: "The fetch is queued. Nothing has been read yet." },
  running:   { tone: "live", chip: "In progress",      t: "Fetch in progress", d: "You can leave this page — the fetch keeps running and the record updates as it goes." },
  paused:    { tone: "warn", chip: "Paused",           t: "Paused by you", d: "Nothing is being read right now. Resuming continues from the last completed step." },
  retrying:  { tone: "warn", chip: "Retrying",         t: "Retrying one source", d: "One source stopped responding. Stimulus retries from its last completed step — completed work is kept." },
  expired:   { tone: "bad",  chip: "Connection expired", t: "A connection expired", d: "Reconnect the source to continue. Everything fetched before the expiry is saved." },
  review:    { tone: "attn", chip: "Checks pending", t: "A few things need a quick check", d: "The fetch finished. Stimulus wasn’t sure it read a few records right, so it’s asking you before they go into the record." },
  partial:   { tone: "warn", chip: "Partly complete",  t: "Partly complete", d: "One source finished, one did not. What was fetched is saved — the rest can resume from its last completed step." },
  failed:    { tone: "bad",  chip: "Failed — retryable", t: "The fetch stopped", d: "Stimulus will resume from the last completed step and will not start over." },
  cancelled: { tone: "idle", chip: "Cancelled",        t: "Fetch cancelled", d: "Stopped at your request. Records saved before you cancelled are kept." },
  complete:  { tone: "ok",   chip: "Complete",         t: "Data fetch complete", d: "Everything below is in the record, with its source attached." },
  nothingnew: { tone: "idle", chip: "Nothing to fetch", t: "Nothing new to fetch", d: "Every source you picked is already up to date, so nothing was read. Pick a source with newer data to fetch." },
};
// Work states that map 1:1 onto the design system's WorkStateChip use it verbatim;
// the fetch-only states (retrying, paused, expired, failed, cancelled) extend it in the
// same idiom with semantic status tokens — never a reserved consent hue.
const DSWorkStateChip = (props) => {
  const C = (window.StimulusDesignSystem_bb8fa3 || {}).WorkStateChip;
  return C ? React.createElement(C, props) : null;
};
const FD_SRC_STATE = {
  waiting:  { ds: "queued" },
  running:  { ds: "running" },
  done:     { ds: "done" },
  review:   { tone: "warn", chip: "Checks pending" },
  retrying: { tone: "warn", chip: "Retrying" },
  paused:   { tone: "warn", chip: "Paused" },
  expired:  { tone: "bad",  chip: "Connection expired" },
  failed:   { tone: "bad",  chip: "Failed" },
  skipped:  { tone: "idle", chip: "Not started" },
  cancelled: { tone: "idle", chip: "Cancelled" },
  uptodate: { tone: "idle", chip: "Already up to date" },
};

const fdNum = (n) => n.toLocaleString("en-US");
// Days in an inclusive date range, and the days a row has finished reading.
const fdDays = (from, to) => Math.max(1, Math.round((fdParse(to) - fdParse(from)) / 86400000) + 1);
function fdRowDays(s) {
  const from = (s.plan && s.plan.start) || fdFmt(FD_START), to = (s.plan && s.plan.end) || FD_TODAY;
  const total = fdDays(from, to);
  return { from, to, total, done: s.state === "done" ? total : Math.min(total, s.daysDone || 0) };
}
const fdNow = () => FD_TODAY + " at " + new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
// What a finished run looks like in the history: its sources, their counts, and what
// each one was scoped to. Nothing derived, nothing rounded.
function fdSnapshot(run) {
  return { id: run.id, status: run.status, startedAt: run.startedAt, finishedAt: run.finishedAt || fdNow(), ts: run.ts || Date.now(),
    sources: run.sources.map((s) => ({ id: s.id, name: (fdSrc(s.id) || {}).name || s.id, state: s.state,
      stage: s.stage, reviewed: s.reviewed, procurement: s.procurement, companies: s.companies,
      relationships: s.relationships, hold: s.hold || 0, skippedCount: s.skippedCount || 0,
      range: (s.plan && s.plan.range) || FD_FULL_RANGE, scope: (s.scope || []).slice(),
      from: (s.plan && s.plan.start) || fdFmt(FD_START),
      to: (s.plan && s.plan.end) || FD_TODAY,
      // How far through the requested dates this source actually got.
      daysDone: fdRowDays(s).done,
      through: s.state === "done" ? ((s.plan && s.plan.end) || FD_TODAY) : null })) };
}
const fdSrc = (id) => FD_SOURCES.find((s) => s.id === id);
function fdRow(id, state, stage, c, opts) {
  return Object.assign({ id, state, stage, reviewed: c[0] || 0, procurement: c[1] || 0, companies: c[2] || 0,
    relationships: c[3] || 0, daysDone: 0, updated: "a few seconds ago", note: null }, opts || {});
}

// ── review questions ──────────────────────────────────────────────────────
// Records Stimulus read but could not place with confidence. Each one becomes a
// plain yes/no question about the thing it was unsure of, with the evidence it
// was working from. Answers are the correction — the record follows the answer.
const FD_REVIEW_BANK = [
  { subject: "Acme Corp", q: "Acme Corp is still working with you.",
    why: "Last email was Mar 2, 2026. Nothing since, and no open contract." },
  { subject: "Meridian Logistics", q: "Meridian Logistics and Meridian Freight Services are the same company.",
    why: "Same billing address, two different names on invoices." },
  { subject: "Northgate Supply", q: "The $1.2M order from Northgate Supply is a procurement record.",
    why: "It arrived in a thread about a sponsorship renewal, so it could be either." },
  { subject: "Sunline Hospitality", q: "Dana Whitfield is your contact for catering at Sunline Hospitality.",
    why: "Two people signed recent emails and neither is listed as the account owner." },
  { subject: "Halvorsen Group", q: "Halvorsen Group supplies temporary power.",
    why: "Their quotes mention both power and lighting, and no category is set." },
  { subject: "Vector Field Media", q: "Vector Field Media is a supplier, not a partner.",
    why: "They appear on a purchase order and in a partnership pipeline." },
  { subject: "Coastline Rentals", q: "The Coastline Rentals contract ended in July 2026.",
    why: "A renewal was drafted but never signed in any source read." },
  { subject: "Pacific Steel Works", q: "Pacific Steel Works is based in Long Beach, CA.",
    why: "Two addresses appear across signatures — Long Beach and Carson." },
];
function fdReviewItems(srcId, n) {
  const off = { gmail: 0, salesforce: 2, hubspot: 4, outlook: 6 }[srcId] || 0;
  return Array.from({ length: Math.max(0, n) }, (_, i) => {
    const b = FD_REVIEW_BANK[(off + i) % FD_REVIEW_BANK.length];
    return { ...b, key: srcId + "-" + i };
  });
}

// ── run store — survives the dialog closing ───────────────────────────────
const FDStore = (() => {
  let run = null, step = "select", selected = ["gmail"], subs = [], timer = null;
  // Per-source folder/label picks. [] means the whole mailbox.
  let scopes = { gmail: [], outlook: [] };
  // Finished runs, newest first. Kept across reset so the history survives a new fetch.
  let history = [], runSeq = 0;
  const emit = () => subs.forEach((f) => f());
  const set = (patch) => { if ("run" in patch) run = patch.run;
    if ("step" in patch) step = patch.step; if ("selected" in patch) selected = patch.selected; emit(); };

  // Sources that will actually be read: connected, selected, and with something new
  // to fetch (an up-to-date source is skipped rather than re-read).
  const plans = () => FD_SOURCES.filter((s) => s.connected && selected.includes(s.id))
    .map((s) => ({ s, plan: fdPlan(s) }));

  const stop = () => { if (timer) { clearInterval(timer); timer = null; } };
  const start = () => {
    const all = plans();
    // Every existing row is carried over — live ones keep their progress, finished ones
    // keep their record and any outstanding holds. Only reset() clears a run.
    const existing = run ? run.sources : [];
    const already = existing.map((r) => r.id);
    const runnable = all.filter((p) => p.plan.mode !== "uptodate" && !already.includes(p.s.id));
    const upToDate = all.filter((p) => p.plan.mode === "uptodate").map((p) => p.s.name);
    // Nothing new to fetch: never mint a finished run — and leave any existing run alone.
    if (!runnable.length) {
      if (run) { run = { ...run, nothingNew: upToDate }; selected = []; emit(); return; }
      run = { status: "nothingnew", tick: 0, skippedUpToDate: upToDate, sources: [] };
      selected = []; emit(); return;
    }
    const rows = runnable.map((p) => fdRow(p.s.id, "waiting", 0, [], { plan: p.plan, scope: (scopes[p.s.id] || []).slice() }));
    const sources = [...existing, ...rows];
    run = { tick: 0, id: (run && run.id) || ++runSeq, ts: (run && run.ts) || Date.now(), startedAt: (run && run.startedAt) || fdNow(), ...(run || {}), status: "waiting", sources,
      skippedUpToDate: [...new Set([...((run && run.skippedUpToDate) || []), ...upToDate])],
      review: sources.reduce((a, s) => a + (s.hold || 0), 0),
      skipped: sources.reduce((a, s) => a + (s.skippedCount || 0), 0) };
    // The run owns those sources now — clear the selection so nothing sits pre-checked.
    selected = [];
    emit();
    if (!timer) timer = setInterval(tick, 950);
  };
  // Terminal bookkeeping for a run whose live sources have all finished: holds land on
  // the source holding them, and only rows that actually completed get stamped.
  function finalize(sources, t) {
    stop();
    const done = sources.filter((s) => s.state === "done" && !s.hold && !s.skippedCount);
    const now = new Date();
    const marked = sources.map((s) => {
      if (s.state !== "done" || s.hold || s.skippedCount) return s;
      const i = done.indexOf(s), hold = i === 0 ? 4 : 2, skippedCount = i === 0 ? 3 : 0;
      return { ...s, hold, skippedCount,
        note: "Stimulus wasn’t sure about " + hold + " records"
          + (skippedCount ? ", and " + skippedCount + " could not be read" : "") + "." };
    });
    marked.forEach((s) => {
      if (s.state !== "done" || FD_FETCHED[s.id] === ((s.plan && s.plan.end) || FD_TODAY)) return;
      FD_FETCHED[s.id] = (s.plan && s.plan.end) || FD_TODAY;
      FD_SYNCED[s.id] = FD_TODAY + " at " + now.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
    });
    const allDone = marked.filter((s) => s.state === "done");
    const held = marked.reduce((a, s) => a + (s.hold || 0), 0);
    const status = !allDone.length ? "cancelled" : allDone.length < marked.length ? "partial" : held ? "review" : "complete";
    run = { ...run, tick: t, sources: marked, status, review: held,
      skipped: marked.reduce((a, s) => a + (s.skippedCount || 0), 0), finishedAt: fdNow() };
    // The finished run is recorded once; a later change to the same run replaces its entry.
    history = [fdSnapshot(run), ...history.filter((h) => h.id !== run.id)];
    emit();
  }

  function tick() {
    if (!run || !run.sources.length || !["waiting", "running", "retrying"].includes(run.status)) return;
    const t = run.tick + 1;
    const sources = run.sources.map((s) => {
      if (!["waiting", "running", "retrying"].includes(s.state)) return s;
      const n = { ...s, updated: "a few seconds ago" };
      // scripted interruption: Salesforce stalls once, then resumes from its last step
      if (s.id === "salesforce" && s.state === "running" && t === 5) {
        return { ...n, state: "retrying",
          note: "Salesforce temporarily stopped responding. Stimulus will retry from the last completed step." };
      }
      if (s.state === "retrying" && t >= 7) { n.state = "running"; n.note = "Resumed at “" + FD_STAGES[s.stage] + "” — earlier steps were kept."; }
      else if (s.state === "waiting") { n.state = "running"; n.stage = 1; }
      else if (s.state === "running") n.stage = Math.min(7, s.stage + 1);
      if (n.state === "running" || n.state === "retrying") {
        const fast = s.id === "gmail";
        n.reviewed = s.reviewed + (fast ? 380 + ((t * 47) % 90) : 120 + ((t * 31) % 60));
        if (n.stage >= 3) n.procurement = s.procurement + (fast ? 21 + (t % 9) : 9 + (t % 5));
        if (n.stage >= 4) n.companies = s.companies + (fast ? 6 + (t % 3) : 3);
        if (n.stage >= 5) n.relationships = s.relationships + (fast ? 9 + (t % 4) : 4);
        // Days are read in order, so the day count only ever moves forward.
        const d = fdRowDays(s);
        if (n.stage >= 2) n.daysDone = Math.min(d.total, (s.daysDone || 0) + Math.ceil(d.total / 6));
      }
      if (n.stage === 7) { n.state = "done"; n.note = null; n.daysDone = fdRowDays(n).total; }
      return n;
    });
    if (!sources.some((s) => ["waiting", "running", "retrying"].includes(s.state))) return finalize(sources, t);
    run = { ...run, tick: t, sources, status: sources.some((s) => s.state === "retrying") ? "retrying" : "running" };
    emit();
  }

  return {
    get: () => ({ run, step, selected, scopes, history, plans: plans() }),
    subscribe(f) { subs.push(f); return () => { subs = subs.filter((x) => x !== f); }; },
    open() { set({ step: run ? "run" : "select" }); },
    toggleScope(id, name) {
      const cur = scopes[id] || [];
      scopes = { ...scopes, [id]: cur.includes(name) ? cur.filter((x) => x !== name) : [...cur, name] };
      // Picking a folder or label is a statement of intent — the source joins the fetch.
      if (scopes[id].length && !selected.includes(id)) selected = [...selected, id];
      emit();
    },
    clearScope(id) { scopes = { ...scopes, [id]: [] }; emit(); },
    // Connecting a mailbox is what makes its folder picker reachable.
    connect(id) {
      const s = fdSrc(id);
      if (!s || s.connected) return;
      s.connected = true;
      s.account = { outlook: "procurement@la28.org", salesforce: "LA28 Procurement (prod)" }[id] || "Connected account";
      emit();
    },
    toggle(id) { set({ selected: selected.includes(id) ? selected.filter((x) => x !== id) : [...selected, id] }); },
    goto(s) { set({ step: s }); },
    confirm() { set({ step: "run" }); start(); },    pause() { stop(); run = { ...run, status: "paused", sources: run.sources.map((s) => s.state === "done" ? s : { ...s, state: "paused" }) }; emit(); },
    resume() { run = { ...run, status: "running", sources: run.sources.map((s) => s.state === "paused" ? { ...s, state: "running" } : s) }; emit(); if (!timer) timer = setInterval(tick, 950); },
    cancel() { stop(); run = { ...run, status: "cancelled", sources: run.sources.map((s) => s.state === "done" ? s
      : { ...s, state: s.stage > 0 || s.reviewed > 0 ? "cancelled" : "skipped",
          note: s.stage > 0 || s.reviewed > 0
            ? "Stopped at “" + FD_STAGES[s.stage] + "”. The " + fdNum(s.reviewed) + " records already reviewed are kept."
            : "Cancelled before this source started. Nothing was read." }) }; emit(); },
    finish() { run = { ...run, status: "complete" }; emit(); },
    // Answered review items leave the hold. Nothing else about the run changes.
    // A run that changes after it finished (answers saved) keeps its history entry current.
    syncHistory() { if (run && run.finishedAt) history = [fdSnapshot(run), ...history.filter((h) => h.id !== run.id)]; },
    resolveHold(id, answered) {
      if (!run || !answered) return;
      const sources = run.sources.map((s) => s.id !== id ? s
        : { ...s, hold: Math.max(0, (s.hold || 0) - answered),
            note: Math.max(0, (s.hold || 0) - answered) > 0
              ? Math.max(0, (s.hold || 0) - answered) + " records are still waiting on you."
              : s.skippedCount ? s.skippedCount + " records could not be read." : null });
      const held = sources.reduce((a, s) => a + (s.hold || 0), 0);
      run = { ...run, sources, review: held,
        status: run.status === "review" && !held ? "complete" : run.status };
      history = [fdSnapshot(run), ...history.filter((h) => h.id !== run.id)];
      emit();
    },
    // Take one source out of a live fetch. The row leaves run.sources entirely — the
    // ticker can never touch it again, and the card returns to being selectable. What
    // it read before being removed is recorded in run.removed.
    removeSource(id) {
      if (!run || !run.sources.length) return;
      const gone = run.sources.find((s) => s.id === id);
      if (!gone || gone.state === "done") return;
      const sources = run.sources.filter((s) => s.id !== id);
      const removed = [...(run.removed || []), { id, name: fdSrc(id).name, stage: gone.stage, reviewed: gone.reviewed }];
      if (!sources.length) { stop(); run = null; step = "select"; emit(); return; }
      run = { ...run, sources, removed };
      if (!sources.some((s) => ["waiting", "running", "retrying"].includes(s.state))) finalize(sources, run.tick);
      else emit();
    },    reset() { stop(); run = null; set({ step: "select" }); },
  };
})();

function useFetchRun() {
  const [, force] = React.useReducer((x) => x + 1, 0);
  React.useEffect(() => FDStore.subscribe(force), []);
  return FDStore.get();
}
// ── pieces ────────────────────────────────────────────────────────────────
function fdSourceStatus(run) {
  return FD_SOURCES.map((s) => {
    const r = run && run.sources.find((x) => x.id === s.id);
    const through = fdThrough(s);
    if (!s.connected) return { s, tone: "idle", label: "Not connected", sub: "Connect it to include it in a fetch." };
    if (r && ["running", "waiting", "retrying"].includes(r.state))
      return { s, tone: "live", label: "Fetching now", sub: FD_STAGES[r.stage] + " · step " + (r.stage + 1) + " of 8" };
    if (r && r.state === "paused") return { s, tone: "warn", label: "Paused", sub: "Resumes from “" + FD_STAGES[r.stage] + "”." };
    if (r && ["expired", "failed"].includes(r.state))
      return { s, tone: "bad", label: r.state === "expired" ? "Connection expired" : "Stopped",
        sub: through ? "Data fetched through " + through : "Nothing fetched yet." };
    if (r && r.state === "review") return { s, tone: "attn", label: "Checks pending", sub: "A few records are waiting on a quick check from you." };
    if (through) return { s, tone: "ok", label: "Fetched", sub: "Historical data through " + through + "." };
    return { s, tone: "idle", label: "Not fetched", sub: "No data pulled from this source yet." };
  });
}

function FdChip({ tone, children, live }) {
  return <span className={"fdChip is-" + tone}>{(tone === "live" || live) && <span className="fdDot" />}{children}</span>;
}

// One chip for a source's work state: the DS primitive where it has a member,
// the local extension where it does not.
function FdSrcStateChip({ state, progress }) {
  const st = FD_SRC_STATE[state] || FD_SRC_STATE.waiting;
  if (st.ds) return <DSWorkStateChip state={st.ds} progress={progress} />;
  return <FdChip tone={st.tone}>{st.chip}</FdChip>;
}
const fdTone = (state) => {
  const st = FD_SRC_STATE[state] || FD_SRC_STATE.waiting;
  return st.ds ? ({ running: "live", done: "ok", "needs-you": "attn", queued: "idle" })[st.ds] : st.tone;
};

// Multi-select folder/label picker for a mailbox source.
function FdScopePicker({ s, picked, onToggle, onClear, locked }) {
  const cfg = FD_SCOPES[s.id];
  // The list comes from the mailbox, so it is fetched, not known. Honest wait:
  // a skeleton that mirrors the chips, then the real folders.
  const [loading, setLoading] = React.useState(true);
  React.useEffect(() => {
    setLoading(true);
    const t = setTimeout(() => setLoading(false), 1000);
    return () => clearTimeout(t);
  }, [s.id]);
  if (!cfg) return null;
  const n = picked.length;
  return (
    <div className="fdScope">
      <div className="fdScopeTop">
        <span className="fdScopeT">{cfg.label} to read</span>
        <span className="fdScopeState">{loading
          ? "Loading " + cfg.many + " from " + s.name + "…"
          : n ? n + " " + (n === 1 ? cfg.one : cfg.many) + " picked" : cfg.all}</span>
        {n && !loading && !locked ? <button type="button" className="fdScopeClear" onClick={onClear}>Clear</button> : null}
      </div>
      <div className="fdScopeList">
        {loading ? [62, 74, 96, 80, 68, 88].map((w, i) => <span className="fdScopeGhost" key={i} style={{ width: w }} />)
          : cfg.options.map((o) => {
          const on = picked.includes(o);
          return (
            <button type="button" key={o} className={"fdScopeChip" + (on ? " is-on" : "")} aria-pressed={on} disabled={locked}
              onClick={() => !locked && onToggle(o)}>
              <span className="fdScopeBox">{on ? <Icon name="check" size={9} /> : null}</span>{o}
            </button>
          );
        })}
      </div>
      <div className="fdScopeNote">{loading ? "Reading the " + cfg.many.toLowerCase() + " list. Nothing is fetched yet."
        : locked ? (n ? "Reading " + (n === 1 ? "this " + cfg.one : "these " + cfg.many) + " in this fetch. Changes apply to the next one."
                      : "Reading the whole mailbox in this fetch. Changes apply to the next one.") : n
        ? (n === 1 ? "Only this " + cfg.one : "Only these " + cfg.many) + " will be read. Everything else in the mailbox is left alone."
        : "Nothing picked, so the whole mailbox is read. Pick " + cfg.many + " to narrow it."}</div>
    </div>
  );
}

function FdSourceCard({ s, on, onToggle, onConnect, busy, removed, scope, onScope, onScopeClear, onHistory }) {
  const cls = "fdSrc" + (s.connected ? " isConn" : " isNotConn") + (!s.connected || busy ? " isOff" : on ? " isOn" : "");
  const through = fdThrough(s);
  const plan = fdPlan(s);
  const inner = (
    <React.Fragment>
      <span className="fdBox"><Icon name="check" size={11} /></span>
      <span className="fdSrcIcon"><Icon name={s.icon} size={15} /></span>
      <span className="fdSrcMain">
        <span className="fdSrcTop">
          <span className="fdSrcName">{s.name}</span>
          {!s.connected ? <FdChip tone="idle">Not connected</FdChip>
            : busy ? <FdSrcStateChip state="running" />
            : <FdChip tone="conn"><span className="fdDot" />Connected</FdChip>}
        </span>
        <span className="fdSrcMeta">
          {!s.connected ? "No account connected yet. Connect it to include it in a fetch."
            : busy ? "A fetch is already running for this source — it cannot be started again until that finishes."
            : through ? s.account + " · data fetched for " + fdCovered(s)
            : s.account + " · nothing fetched from this source yet"}
        </span>
        {s.connected && (
          <span className="fdSrcSync">{removed
            ? "Removed from the current fetch · " + fdNum(removed.reviewed) + " records read before that are kept"
            : busy ? (fdSynced(s) ? "Syncing now · last synced " + fdSynced(s) : "Syncing now · never synced before")
            : fdSynced(s) ? "Last synced " + fdSynced(s) : "Never synced"}</span>
        )}
      </span>
      {!s.connected && (
        <span className="fdSrcRight">
          <button type="button" className="fdConnect" onClick={onConnect}>Connect</button>
        </span>
      )}
    </React.Fragment>
  );
  if (!s.connected) return <div className={cls} aria-disabled="true">{inner}</div>;
  if (busy) return <div className={cls} aria-disabled="true">{inner}</div>;
  return (
    <div className={cls + " fdSrcWrap"}>
      <button type="button" className="fdSrcHit" role="checkbox" aria-checked={on} onClick={() => onToggle(s.id)}>{inner}</button>
      {FD_SCOPES[s.id] && (
        <FdScopePicker s={s} picked={scope || []} onToggle={(o) => onScope && onScope(s.id, o)}
          onClear={() => onScopeClear && onScopeClear(s.id)} />
      )}
      {s.connected && (
        <div className="fdSrcAct">
          <span className="fdSrcActT">{through ? "Last fetch read data through " + through + "." : "This source has not been fetched yet."}</span>
          <button type="button" className="fdSmallBtn" onClick={() => onHistory && onHistory(s.id)}>Run history</button>
        </div>
      )}
      {on && through && (
        <div className="fdSrcPlan">
          {plan.mode === "uptodate" ? (
            <span className="fdSrcPlanT">Already up to date through {through} — nothing new to fetch, so this source will be skipped.</span>
          ) : (
            <span className="fdSrcPlanT">{fdCovered(s)} is already fetched and stays as it is. Only new data will be fetched:
              <span className="mono fdRangeDates">{plan.range}</span></span>
          )}
        </div>
      )}
    </div>
  );
}

function FdRangeBlock() {
  return (
    <div className="fdRange">
      <span className="fdRangeIcon"><Icon name="calendar" size={14} /></span>
      <span>
        <div className="fdRangeT">Data range: Last 12 months <span className="mono fdRangeDates">{FD_FULL_RANGE}</span></div>
        <div className="fdRangeD">Stimulus will only read data within this period. A source with history is read from
          the day after its last fetch instead.</div>
      </span>
    </div>
  );
}
function FdPrivacyBlock() {
  return (
    <div className="fdPrivacy">
      <span className="fdRangeIcon"><Icon name="shield" size={14} /></span>
      <span className="fdPrivacyT">Stimulus will extract relevant business information and relationship context.
        Complete email bodies will not be stored by default.</span>
    </div>
  );
}

function FdRunCard({ r, onAction }) {
  const src = fdSrc(r.id), tone = fdTone(r.state);
  const loading = r.state === "waiting";
  const acts = {
    expired: [["Reconnect", "reconnect"], ["Check items", "review"]],
    failed: [["Retry", "retry"]],
    paused: [["Resume", "resume"]],
    review: [["Check items", "review"]],
  }[r.state] || [];
  return (
    <div className={"fdRunCard" + (tone === "live" ? " is-live" : tone === "bad" ? " is-bad" : tone === "warn" ? " is-warn" : tone === "attn" ? " is-attn" : "")}>
      <div className="fdRunTop">
        <span className="fdSrcIcon"><Icon name={src.icon} size={14} /></span>
        <span>
          <div className="fdRunName">{src.name}</div>
          <div className="fdRunAcct">{src.account}</div>
        </span>
        <span style={{ marginLeft: "auto" }}><FdSrcStateChip state={r.state} /></span>
      </div>
      <div className="fdRunStage">{FD_STAGES[r.stage]} <span className="fdStageMut">· step {r.stage + 1} of 8</span></div>
      {r.plan && <div className="fdRunRange mono">{r.plan.mode === "new" ? "New data only · " : ""}{r.plan.range}</div>}
      <div className="fdSteps">
        {FD_STAGES.map((_, i) => (
          <span key={i} className={"fdStep" + (i < r.stage ? " isDone" : i === r.stage
            ? (tone === "bad" ? " isBad" : r.state === "done" ? " isDone" : " isNow") : "")} />
        ))}
      </div>
      {loading ? (
        <div style={{ marginTop: 11 }}>
          <div className="fdSkelRow w40" /><div className="fdSkelRow w70" /><div className="fdSkelRow w55" />
        </div>
      ) : (
        <div className="fdCounts">
          <div><div className="fdCountV">{fdNum(r.reviewed)}</div><div className="fdCountL">records reviewed</div></div>
          <div><div className="fdCountV">{fdNum(r.procurement)}</div><div className="fdCountL">procurement records found</div></div>
          <div><div className="fdCountV">{fdNum(r.companies)}</div><div className="fdCountL">companies found</div></div>
          <div><div className="fdCountV">{fdNum(r.relationships)}</div><div className="fdCountL">relationships found</div></div>
        </div>
      )}
      {r.note && <div className="fdRunNote">{r.note}</div>}
      <div className="fdRunFoot">
        <span className="fdRunTime">Last updated {r.updated}{r.state === "done" ? " · data fetched through " + ((r.plan && r.plan.end) || FD_TODAY) : ""}</span>
        {acts.map(([label, key]) => (
          <button key={key} type="button" className="fdSmallBtn" onClick={() => onAction && onAction(key, r.id)}>{label}</button>
        ))}
      </div>
    </div>
  );
}

function FdRunGuts({ r, tone, onAction }) {
  const loading = r.state === "waiting";
  const acts = {
    expired: [["Reconnect", "reconnect"], ["Check items", "review"]],
    failed: [["Retry", "retry"]],
    paused: [["Resume", "resume"]],
    review: [["Check items", "review"]],
  }[r.state] || [];
  return (
    <React.Fragment>
      <div className="fdRunStage">{FD_STAGES[r.stage]} <span className="fdStageMut">· step {r.stage + 1} of 8</span></div>
      {r.plan && <div className="fdRunRange mono">{r.plan.mode === "new" ? "New data only · " : ""}{r.plan.range}</div>}
      <div className="fdSteps">
        {FD_STAGES.map((_, i) => (
          <span key={i} className={"fdStep" + (i < r.stage ? " isDone" : i === r.stage
            ? (tone === "bad" ? " isBad" : r.state === "done" ? " isDone" : " isNow") : "")} />
        ))}
      </div>
      {loading ? (
        <div style={{ marginTop: 11 }}>
          <div className="fdSkelRow w40" /><div className="fdSkelRow w70" /><div className="fdSkelRow w55" />
        </div>
      ) : (
        <div className="fdCounts">
          <div><div className="fdCountV">{fdNum(r.reviewed)}</div><div className="fdCountL">records reviewed</div></div>
          <div><div className="fdCountV">{fdNum(r.procurement)}</div><div className="fdCountL">procurement records found</div></div>
          <div><div className="fdCountV">{fdNum(r.companies)}</div><div className="fdCountL">companies found</div></div>
          <div><div className="fdCountV">{fdNum(r.relationships)}</div><div className="fdCountL">relationships found</div></div>
        </div>
      )}
      {r.note && <div className="fdRunNote">{r.note}</div>}
      <div className="fdRunFoot">
        <span className="fdRunTime">Last updated {r.updated}{r.state === "done" ? " · data fetched through " + ((r.plan && r.plan.end) || FD_TODAY) : ""}</span>
        {acts.map(([label, key]) => (
          <button key={key} type="button" className="fdSmallBtn" onClick={() => onAction && onAction(key, r.id)}>{label}</button>
        ))}
      </div>
    </React.Fragment>
  );
}

// The run view keeps the default source list: the same cards, each carrying its own
// status and expanding in place for its individual detail.
function FdSourceRunCard({ s, r, run, open, onOpen, onAction, scope, onScope, onScopeClear }) {
  const tone = r ? fdTone(r.state) : "idle";
  const live = r && ["waiting", "running", "retrying"].includes(r.state);
  const through = fdThrough(s);
  const skipped = (run.skippedUpToDate || []).includes(s.name);
  const line = r ? (r.state === "cancelled" ? "Stopped at “" + FD_STAGES[r.stage] + "” · " + fdNum(r.reviewed) + " records kept"
      : r.state === "skipped" ? "Never started — nothing was read"
      : r.state === "review" ? "Finished · Stimulus wasn’t sure about " + (r.hold || 0) + " records"
      : r.state === "done" ? "Data fetched for " + (r.plan && r.plan.mode === "new"
          ? r.plan.start + " – " + r.plan.end + " (added to what was already there)" : fdCovered(s) || FD_FULL_RANGE)
      : FD_STAGES[r.stage] + " · step " + (r.stage + 1) + " of 8")
    : skipped ? "Already up to date through " + through
    : !s.connected ? "No account connected yet. Connect it to include it in a fetch."
    : through ? s.account + " · data fetched for " + fdCovered(s)
    : s.account + " · nothing fetched from this source yet";
  // Only a source actually being read may claim to be syncing; every other state
  // shows the prior sync stamp, or says plainly that there has never been one.
  const prior = fdSynced(s);
  const syncLine = r && r.state === "done" ? "Last synced " + (FD_SYNCED[s.id] || FD_TODAY + " at 9:41 AM")
    : r && ["waiting", "running", "retrying"].includes(r.state)
      ? (prior ? "Syncing now · last synced " + prior : "Syncing now · never synced before")
      : prior ? "Last synced " + prior : "Never synced";
  return (
    <div className={"fdSrc fdSrcWrap" + (live ? " isLiveSrc" : "")}>
      <button type="button" className="fdSrcHit" aria-expanded={!!open} disabled={!r || !live} onClick={() => r && live && onOpen(open ? null : s.id)}>
        <span className="fdSrcIcon"><Icon name={s.icon} size={15} /></span>
        <span className="fdSrcMain">
          <span className="fdSrcTop">
            <span className="fdSrcName">{s.name}</span>
            {r ? (r.state === "done" ? null : <FdSrcStateChip state={r.state} />)
              : skipped ? <FdChip tone="idle">Already up to date</FdChip>
              : !s.connected ? <FdChip tone="idle">Not connected</FdChip> : null}
          </span>
          <span className="fdSrcMeta">{line}</span>
          {s.connected && (
            <span className="fdSrcSync">{syncLine}</span>
          )}
        </span>
        {r && live ? <span className="fdSrcChev"><Icon name={open ? "chevD" : "chevR"} size={13} /></span> : null}
      </button>
      {r && (live || r.state === "done" || r.daysDone) ? (
        <div className="fdSrcBar"><FdDayBar r={{ ...r, ...fdRowDays(r), daysDone: fdRowDays(r).done }} /></div>
      ) : null}
      {FD_SCOPES[s.id] && s.connected && (
        <FdScopePicker s={s} picked={live ? (r && r.scope) || [] : scope || []} locked={!!live}
          onToggle={(o) => onScope && onScope(s.id, o)} onClear={() => onScopeClear && onScopeClear(s.id)} />
      )}
      {live ? (
        <div className="fdSrcAct">
          <span className="fdSrcActT">In this fetch. Removing it stops only this source — the others keep going.</span>
          <button type="button" className="fdSmallBtn" onClick={() => onAction && onAction("history", s.id)}>Run history</button>
          <button type="button" className="fdSmallBtn" onClick={() => onAction && onAction("remove", s.id)}>Remove from fetch</button>
        </div>
      ) : null}
      {r && !live ? (
        <div className="fdSrcAct">
          <span className="fdSrcActT">{r.state === "done" ? "Read " + fdNum(r.reviewed) + " records in this fetch." : "This source did not finish."}</span>
          <button type="button" className="fdSmallBtn" onClick={() => onAction && onAction("history", s.id)}>Run history</button>
        </div>
      ) : null}
      {r && (r.hold || r.skippedCount) ? (
        <div className="fdSrcAct">
          <span className="fdSrcActT">{r.hold ? "Stimulus wasn’t sure it read " + r.hold + " records right" : ""}
            {r.hold && r.skippedCount ? " · " : ""}{r.skippedCount ? r.skippedCount + " could not be read" : ""}.</span>
          {r.hold ? <button type="button" className="fdSmallBtn" onClick={() => onAction && onAction("review", s.id)}>Check {r.hold} items</button> : null}
        </div>
      ) : r && ["expired", "failed", "paused"].includes(r.state) ? (
        <div className="fdSrcAct">
          <span className="fdSrcActT">Continues from “{FD_STAGES[r.stage]}” — nothing already read is re-read.</span>
          <button type="button" className="fdSmallBtn" onClick={() => onAction && onAction(r.state === "expired" ? "reconnect" : r.state === "failed" ? "retry" : "resume", s.id)}>
            {r.state === "expired" ? "Reconnect" : r.state === "failed" ? "Retry" : "Resume"}</button>
        </div>
      ) : null}
      {r && open && live && <div className="fdSrcDetail"><FdRunGuts r={r} tone={tone} onAction={onAction} /></div>}
    </div>
  );
}

function FdComplete({ run }) {
  const tot = (k) => run.sources.reduce((a, s) => a + s[k], 0);
  const skipped = run.skippedUpToDate || [];
  const hold = run.sources.reduce((a, s) => a + (s.hold || 0), 0);
  const skippedRecs = run.sources.reduce((a, s) => a + (s.skippedCount || 0), 0);
  const cells = [
    [run.sources.length + " of " + (run.sources.length + skipped.length), "sources processed"],    [fdNum(tot("reviewed")), "records reviewed"],
    [fdNum(tot("procurement")), "procurement records found"],
    [fdNum(tot("companies")), "companies added or updated"],
    [fdNum(Math.round(tot("companies") * 1.8)), "people identified"],
    [fdNum(Math.round(tot("procurement") * 0.4)), "products and services found"],
    [fdNum(tot("relationships")), "relationships created or updated"],
    [String(hold || run.review || 0), "items to check", hold ? "warn" : null],
    [String(skippedRecs || run.skipped || 0), "items skipped or failed", skippedRecs ? "warn" : null],
  ];
  return (
    <React.Fragment>
      <div className="fdDone">
        {cells.map(([v, l, tone], i) => (
          <div key={i} className={"fdDoneCell" + (tone ? " is-" + tone : "")}>
            <div className="fdDoneV">{v}</div><div className="fdDoneL">{l}</div>
          </div>
        ))}
      </div>
      {skipped.length > 0 && (
        <div className="fdRunNote" style={{ marginTop: 10 }}>Skipped: {skipped.join(", ")} — already up to date through {FD_TODAY}, so nothing was re-read.</div>
      )}
    </React.Fragment>
  );
}

// ── the dialog ────────────────────────────────────────────────────────────
function FetchDataDialog({ run, step, selected, scopes, history, onToggle, onScope, onScopeClear, onGoto, onConfirm, onClose, onAction, embedded }) {
  const hist = history || [];
  const sc = scopes || {};
  const [openSrc, setOpenSrc] = React.useState(null);
  const busyIds = run && ["waiting", "running", "retrying", "paused"].includes(run.status)
    ? run.sources.filter((r) => !["done", "skipped", "cancelled"].includes(r.state)).map((r) => r.id) : [];
  const lockedNames = FD_SOURCES.filter((s) => busyIds.includes(s.id)).map((s) => s.name);
  const chosen = FD_SOURCES.filter((s) => selected.includes(s.id) && s.connected && !busyIds.includes(s.id))
    .map((s) => ({ s, plan: fdPlan(s) }));
  const runnable = chosen.filter((c) => c.plan.mode !== "uptodate");
  const upToDate = chosen.filter((c) => c.plan.mode === "uptodate");
  const anyConnected = FD_SOURCES.some((s) => s.connected);
  const status = run ? (FD_STATUS[run.status] || FD_STATUS.running) : null;
  const finished = run && ["review", "complete", "partial", "cancelled", "nothingnew"].includes(run.status);

  let body, foot, sub = null, title = "Fetch procurement data";
  if (step === "select") {
    sub = "Select one or more connected sources. Stimulus will review data from the last 12 months to find companies, people, products, services, opportunities, and other procurement-related information.";
    body = (
      <React.Fragment>
        <div className="fdLabel">Sources</div>
        <div className="fdSources">
          {FD_SOURCES.map((s) => (
            <FdSourceCard key={s.id} s={s} on={selected.includes(s.id)} onToggle={onToggle} busy={busyIds.includes(s.id)}
              scope={sc[s.id]} onScope={onScope} onScopeClear={onScopeClear}
              onHistory={(x) => onAction && onAction("history", x)}
              onConnect={() => onAction && onAction("connect", s.id)} />
          ))}
        </div>
        <div className="fdSummary">
          {!anyConnected
            ? <span className="fdSummaryMut">No source is connected yet. Connect one to fetch anything.</span>
            : chosen.length
            ? <React.Fragment><Icon name="check" size={13} /><span>{chosen.length} source{chosen.length > 1 ? "s" : ""} selected <span className="fdSummaryMut">· {chosen.map((c) => c.s.name).join(" · ")}</span></span></React.Fragment>
            : lockedNames.length
            ? <span className="fdSummaryMut">{lockedNames.join(" and ")} {lockedNames.length > 1 ? "are" : "is"} already fetching — pick a different source to start another fetch.</span>
            : <span className="fdSummaryMut">No sources selected yet — pick at least one to continue.</span>}
        </div>
        {chosen.length > 0 && !runnable.length && (
          <div className="fdRunNote" style={{ marginTop: 10 }}>Everything selected is already up to date through {upToDate[0].plan.through} — there is nothing new to fetch.
            Pick another source to continue.</div>
        )}
        <FdRangeBlock />
      </React.Fragment>
    );
    foot = (
      <React.Fragment>
        <span className="fdFootNote">Read-only. Nothing is written back to your sources.</span>
        <div className="fdFootBtns">
          <button type="button" className="fdBtn fdBtnGhost" onClick={onClose}>Cancel</button>
          <button type="button" className="fdBtn fdBtnPrimary" disabled={!runnable.length} onClick={() => onGoto("confirm")}>Continue</button>
        </div>
      </React.Fragment>
    );
  } else if (step === "confirm") {
    const fresh = runnable.filter((c) => c.plan.mode === "full");
    const incr = runnable.filter((c) => c.plan.mode === "new");
    body = (
      <React.Fragment>
        <div className="fdConfirmQ">Ready to fetch data?</div>
        <div className="fdConfirmB">Stimulus will review procurement-related data from the selected sources for the
          dates below. This may take some time. You can leave this page while the process continues.</div>
        <div className="fdLabel" style={{ marginTop: 14 }}>What will happen — can you confirm?</div>
        <div className="fdRecap">
          {runnable.map((c) => (
            <div key={c.s.id} className="fdRecapRow">
              <span className="fdSrcIcon" style={{ width: 24, height: 24 }}><Icon name={c.s.icon} size={12} /></span>
              <span>
                {c.plan.mode === "full"
                  ? <React.Fragment><b>{c.s.name}</b> has no history yet — fetching the full 12 months.</React.Fragment>
                  : <React.Fragment><b>{c.s.name}</b> already has data for {fdCovered(c.s)} — only newer data will be fetched. Nothing already fetched is re-read.</React.Fragment>}
                <span className="fdRecapRange mono">{c.plan.range}</span>
                {FD_SCOPES[c.s.id] && (
                  <span className="fdRecapScope">{(sc[c.s.id] || []).length
                    ? FD_SCOPES[c.s.id].label + ": " + sc[c.s.id].join(", ")
                    : FD_SCOPES[c.s.id].all + " will be read"}</span>
                )}
              </span>
            </div>
          ))}
          {upToDate.map((c) => (
            <div key={c.s.id} className="fdRecapRow">
              <span className="fdSrcIcon" style={{ width: 24, height: 24 }}><Icon name={c.s.icon} size={12} /></span>
              <span><b>{c.s.name}</b> is already up to date through {c.plan.through} — it will be skipped.</span>
            </div>
          ))}
          <div className="fdRecapRow"><span className="fdRecapK">Access</span><span>Read-only · revocable at any time</span></div>
        </div>
        <FdPrivacyBlock />
      </React.Fragment>
    );
    foot = (
      <React.Fragment>
        <span className="fdFootNote">{runnable.length} source{runnable.length > 1 ? "s" : ""} will be read
          {incr.length ? " · " + incr.length + " incremental" : ""}{fresh.length ? " · " + fresh.length + " full" : ""}
          {upToDate.length ? " · " + upToDate.length + " skipped" : ""}</span>
        <div className="fdFootBtns">
          <button type="button" className="fdBtn fdBtnGhost" onClick={() => onGoto("select")}>Cancel</button>
          <button type="button" className="fdBtn fdBtnPrimary" onClick={onConfirm}>Confirm and fetch</button>
        </div>
      </React.Fragment>
    );
  } else {
    const live = ["running", "waiting", "retrying"].includes(run.status);
    const nothingNew = run.status === "nothingnew";
    const holdTotal = run.sources.reduce((a, s) => a + (s.hold || 0), 0);
    title = run.status === "complete" ? "Data fetch complete" : "Fetch procurement data";
    body = (
      <React.Fragment>
        <div className={"fdOverall is-" + status.tone}>
          <span className="fdRangeIcon" style={{ color: "inherit" }}>
            <Icon name={status.tone === "ok" ? "check" : status.tone === "bad" || status.tone === "warn" || status.tone === "attn" ? "alert" : "clock"} size={14} />
          </span>
          <span style={{ flex: 1 }}>
            <div className="fdOverallT">{status.t}</div>
            <div className="fdOverallD">{status.d}</div>
          </span>
          <span className="fdOverallRight">
            <FdChip tone={status.tone}>{status.chip}</FdChip>
            {holdTotal > 0 && (
              <button type="button" className="fdSmallBtn" onClick={() => onAction && onAction("review")}>Check {holdTotal} items</button>
            )}
          </span>
        </div>
        <React.Fragment>
          {(run.status === "complete" || run.status === "review") && <FdComplete run={run} />}
          <div className="fdLabel" style={{ marginTop: (run.status === "complete" || run.status === "review") ? 16 : 0 }}>Sources</div>          {FD_SOURCES.map((s) => {
            const r = run.sources.find((x) => x.id === s.id);
            // A source in this run reports its status; anything else stays a normal,
            // unselected card you can pick to start another fetch.
            return r
              ? <FdSourceRunCard key={s.id} s={s} run={run} r={r} open={openSrc === s.id} onOpen={setOpenSrc} onAction={onAction}
                  scope={sc[s.id]} onScope={onScope} onScopeClear={onScopeClear} />
              : <FdSourceCard key={s.id} s={s} on={selected.includes(s.id)} onToggle={onToggle}
                  scope={sc[s.id]} onScope={onScope} onScopeClear={onScopeClear}
                  onHistory={(x) => onAction && onAction("history", x)}
                  removed={(run.removed || []).find((x) => x.id === s.id)}
                  onConnect={() => onAction && onAction("connect", s.id)} />;
          })}
          {run.nothingNew && run.nothingNew.length > 0 && (
            <div className="fdRunNote" style={{ margin: "10px 0" }}>{run.nothingNew.join(", ")} had nothing new to fetch, so this run is unchanged.</div>
          )}
          {run.skippedUpToDate && run.skippedUpToDate.length > 0 && (
            <div className="fdRunNote" style={{ margin: "10px 0" }}>Skipped: {run.skippedUpToDate.join(", ")} — already up to date through {FD_TODAY}.</div>
          )}
          <FdRangeBlock />
        </React.Fragment>
      </React.Fragment>
    );
    const recover = { expired: ["Reconnect", "reconnect"], failed: ["Retry", "retry"],
      paused: ["Resume", "resume"], partial: ["Reconnect and resume", "reconnect"] }[run.status];
    // Sources picked since this run started — they can be fetched without waiting for it.
    const alsoRunnable = chosen.filter((c) => c.plan.mode !== "uptodate" && !run.sources.some((r) => r.id === c.s.id));
    foot = (
      <React.Fragment>
        <span className="fdFootNote">{alsoRunnable.length
          ? alsoRunnable.map((c) => c.s.name).join(" and ") + " " + (alsoRunnable.length > 1 ? "are" : "is") + " selected — continue to fetch " + (alsoRunnable.length > 1 ? "them" : "it") + " alongside this run."
          : nothingNew ? "No data was read. Nothing in the record changed."
          : live ? "Safe to close — the fetch keeps running."
          : run.status === "complete" ? "Every record carries its source and fetch date. Anything needing a look is marked on its source above."
          : run.status === "review" ? "The fetch finished. The records Stimulus wasn’t sure about stay on their source until you check them — nothing was written to the record."
          : run.status === "cancelled" ? "You stopped this fetch. Records read before you cancelled are kept; the rest was never read."
          : recover ? "Progress is kept — " + recover[0].toLowerCase() + " continues from the last completed step."
          : "Progress is kept. Nothing restarts from the beginning."}</span>
        <div className="fdFootBtns">
          {live && !alsoRunnable.length && <button type="button" className="fdBtn fdBtnGhost" onClick={() => onAction && onAction("pause")}>Pause</button>}
          {!finished && !alsoRunnable.length && <button type="button" className="fdBtn fdBtnGhost" onClick={() => onAction && onAction("cancelRun")}>Cancel fetch</button>}
          {run.status === "complete" && !alsoRunnable.length && <button type="button" className="fdBtn fdBtnGhost" onClick={() => onAction && onAction("companies")}>View companies</button>}
          {!alsoRunnable.length && (run.status === "complete" || run.status === "review" || run.status === "cancelled" || nothingNew) &&
            <button type="button" className="fdBtn fdBtnGhost" onClick={() => onAction && onAction("again")}>Fetch data</button>}
          <button type="button" className={"fdBtn " + (recover || alsoRunnable.length ? "fdBtnGhost" : "fdBtnPrimary")} onClick={onClose}>Close</button>
          {alsoRunnable.length
            ? <button type="button" className="fdBtn fdBtnPrimary" onClick={() => onGoto("confirm")}>Continue</button>
            : recover ? <button type="button" className="fdBtn fdBtnPrimary" onClick={() => onAction && onAction(recover[1])}>{recover[0]}</button> : null}
        </div>
      </React.Fragment>
    );
  }

  const dialog = (
    <div className="fdDialog" role="dialog" aria-modal={embedded ? undefined : "true"} aria-label={title}>
      <div className="fdGrab" />
      <div className="fdHead">
        <span className="fdHeadIcon"><Icon name="download" size={14} /></span>
        <span style={{ flex: 1, minWidth: 0 }}>
          <div className="fdTitle">{title}</div>
          {sub && <div className="fdSub">{sub}</div>}
          {step === "run" && ["waiting", "running", "retrying"].includes(run.status) && <div className="fdSub">Live — updates arrive on their own. No refresh needed.</div>}
        </span>
        <button type="button" className="fdClose" aria-label="Close" onClick={onClose}><Icon name="x" size={14} /></button>
      </div>
      <div className="fdBody">{body}</div>
      <div className="fdFoot">{foot}</div>
    </div>
  );
  if (embedded) return dialog;
  return <div className="fdScrim" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>{dialog}</div>;
}

// ── review dialog ───────────────────────────────────────────────────────
function FdReviewDialog({ src, count, onClose, onSave }) {
  const items = fdReviewItems(src.id, count);
  const [ans, setAns] = React.useState({});
  const set = (k, patch) => setAns((a) => ({ ...a, [k]: { ...(a[k] || {}), ...patch } }));
  const answered = items.filter((i) => ans[i.key] && ans[i.key].a).length;
  React.useEffect(() => {
    const esc = (e) => { if (e.key === "Escape") { e.stopPropagation(); onClose(); } };
    window.addEventListener("keydown", esc, true);
    return () => window.removeEventListener("keydown", esc, true);
  }, [onClose]);
  return (
    <div className="fdScrim fdScrimTop" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="fdDialog" role="dialog" aria-modal="true" aria-label="Quick check">
        <div className="fdGrab" />
        <div className="fdHead">
          <span className="fdHeadIcon"><Icon name="alert" size={14} /></span>
          <span style={{ flex: 1, minWidth: 0 }}>
            <div className="fdTitle">A quick check on {count} {count === 1 ? "record" : "records"}</div>
            <div className="fdSub">Stimulus read these from {src.name} but wasn’t sure it got them right, so it left
              them out of the record until you say. Answer what you know — you can skip anything you don’t.</div>
          </span>
          <button type="button" className="fdClose" aria-label="Close" onClick={onClose}><Icon name="x" size={14} /></button>
        </div>
        <div className="fdBody">
          {items.map((it, n) => {
            const a = ans[it.key] || {};
            return (
              <div className={"fdRevItem" + (a.a ? " is-answered" : "")} key={it.key}>
                <div className="fdRevN">{n + 1} of {items.length}</div>
                <div className="fdRevQ">{it.q}</div>
                <div className="fdRevWhy">{it.why}</div>
                <div className="fdRevBtns">
                  <button type="button" className={"fdRevBtn" + (a.a === "yes" ? " is-yes" : "")}
                    onClick={() => set(it.key, { a: a.a === "yes" ? null : "yes" })}>That’s right</button>
                  <button type="button" className={"fdRevBtn" + (a.a === "no" ? " is-no" : "")}
                    onClick={() => set(it.key, { a: a.a === "no" ? null : "no" })}>Not right</button>
                  {a.a && <span className="fdRevMark">{a.a === "yes"
                    ? "You said yes — goes into the record when you save"
                    : "You said no — stays out of the record when you save"}</span>}
                </div>
                <textarea className="fdRevNote" rows={a.a === "no" ? 2 : 1} value={a.c || ""}
                  onChange={(e) => set(it.key, { c: e.target.value })}
                  placeholder={a.a === "no" ? "What’s the correct answer? (optional)" : "Add a comment (optional)"} />
              </div>
            );
          })}
        </div>
        <div className="fdFoot">
          <span className="fdFootNote">{answered
            ? answered + " of " + items.length + " answered. Anything you leave blank stays here for later."
            : "Nothing is written to the record until you save."}</span>
          <div className="fdFootBtns">
            <button type="button" className="fdBtn fdBtnGhost" onClick={onClose}>Not now</button>
            <button type="button" className="fdBtn fdBtnPrimary" disabled={!answered}
              onClick={() => onSave(answered)}>Save {answered || ""} {answered === 1 ? "answer" : "answers"}</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ── run history ───────────────────────────────────────────────────────────
// Per-run detail moved out of the source cards: what each run read, source by
// source, current run first. Every number is the one the run actually recorded.
// How much of the requested date range a run actually completed, in days.
// Days are the honest unit here — a run either finished a day or it did not.
function FdDayBar({ r }) {
  const total = fdDays(r.from, r.to);
  const done = Math.min(total, r.daysDone || 0);
  const pct = Math.min(100, Math.round((done / total) * 100));
  return (
    <div className="fdDayBar">
      <div className="fdDayBarTrack"><span className={"fdDayBarFill" + (pct === 100 ? " is-full" : "")} style={{ width: pct + "%" }} /></div>
      <div className="fdDayBarL tabular">{done
        ? fdNum(done) + " of " + fdNum(total) + " days read · " + pct + "%"
        : "No days read yet · 0%"}</div>
    </div>
  );
}

const FD_HIST_WINDOWS = [["Last 7 days", 7], ["Last 30 days", 30], ["All time", 0]];
function FdHistoryDialog({ entries, onClose, focus }) {
  // Default view is the last 7 days — the window an operator is actually asking about.
  const [days, setDays] = React.useState(7);
  const cutoff = days ? Date.now() - days * 86400000 : 0;
  const shown = entries.filter((e) => !cutoff || (e.ts || 0) >= cutoff);
  const hidden = entries.length - shown.length;
  // Source-first: each source lists its own runs, newest first.
  const groups = [];
  shown.forEach((e) => {
    e.sources.forEach((s) => {
      let g = groups.find((x) => x.id === s.id);
      if (!g) { g = { id: s.id, name: s.name, icon: (fdSrc(s.id) || {}).icon || "mail", runs: [] }; groups.push(g); }
      g.runs.push({ ...s, runId: e.id, current: !!e.current, status: e.status,
        when: e.current && !e.finishedAt ? "Started " + e.startedAt : "Finished " + e.finishedAt });
    });
  });
  const [open, setOpen] = React.useState(focus || (groups[0] && groups[0].id) || null);
  React.useEffect(() => {
    const esc = (e) => { if (e.key === "Escape") { e.stopPropagation(); onClose(); } };
    window.addEventListener("keydown", esc, true);
    return () => window.removeEventListener("keydown", esc, true);
  }, [onClose]);
  return (
    <div className="fdScrim fdScrimTop" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="fdDialog" role="dialog" aria-modal="true" aria-label="Run history">
        <div className="fdGrab" />
        <div className="fdHead">
          <span className="fdHeadIcon"><Icon name="clock" size={14} /></span>
          <span style={{ flex: 1, minWidth: 0 }}>
            <div className="fdTitle">Run history</div>
            <div className="fdSub">What each run did, source by source, newest first.</div>
          </span>
          <button type="button" className="fdClose" aria-label="Close" onClick={onClose}><Icon name="x" size={14} /></button>
        </div>
        <div className="fdBody">
          <div className="fdHistWin">
            {FD_HIST_WINDOWS.map(([label, d]) => (
              <button type="button" key={d} className={"fdHistWinBtn" + (days === d ? " is-on" : "")}
                aria-pressed={days === d} onClick={() => setDays(d)}>{label}</button>
            ))}
            {hidden > 0 && <span className="fdHistWinNote">{hidden} older run{hidden > 1 ? "s" : ""} outside this window</span>}
          </div>
          {!groups.length ? (
            <div className="fdRunNote">{entries.length
              ? "No fetches in the last " + days + " days. " + hidden + " older run" + (hidden > 1 ? "s are" : " is") + " kept — widen the window to see " + (hidden > 1 ? "them" : "it") + "."
              : "No fetches yet. Once a source runs, every fetch it makes is recorded here with its counts and dates."}</div>
          ) : groups.map((g) => {
            const isOpen = open === g.id;
            const last = g.runs[0];
            return (
              <div className={"fdHist" + (isOpen ? " is-open" : "")} key={g.id}>
                <button type="button" className="fdHistHead" aria-expanded={isOpen} onClick={() => setOpen(isOpen ? null : g.id)}>
                  <span className="fdSrcIcon" style={{ width: 26, height: 26 }}><Icon name={g.icon} size={13} /></span>
                  <span className="fdHistMain">
                    <span className="fdHistT">{g.name}
                      <FdChip tone="idle">{g.runs.length} run{g.runs.length > 1 ? "s" : ""}</FdChip></span>
                    <span className="fdHistWhen">{last.current && !last.when.startsWith("Finished") ? "Running now" : last.when}
                      {" · "}{fdNum(g.runs.reduce((a, r) => a + r.reviewed, 0))} records reviewed in total</span>
                  </span>
                  <span className="fdSrcChev"><Icon name={isOpen ? "chevD" : "chevR"} size={13} /></span>
                </button>
                {isOpen && (
                  <div className="fdHistBody">
                    {g.runs.map((r) => {
                      const st = FD_STATUS[r.status] || FD_STATUS.running;
                      return (
                        <div className="fdHistSrc" key={r.runId}>
                          <div className="fdHistSrcTop">
                            <span className="fdHistSrcN">{r.current ? "Current run" : "Run " + r.runId}</span>
                            <FdChip tone={st.tone}>{st.chip}</FdChip>
                          </div>
                          <div className="fdHistSrcMeta">{r.when}</div>
                          <div className="fdHistSrcMeta mono">{r.range}</div>
                          <FdDayBar r={r} />
                          {FD_SCOPES[r.id] && (
                            <div className="fdHistSrcMeta">{r.scope && r.scope.length
                              ? FD_SCOPES[r.id].label + ": " + r.scope.join(", ")
                              : FD_SCOPES[r.id].all}</div>
                          )}
                          <div className="fdCounts">
                            <div><div className="fdCountV">{fdNum(r.reviewed)}</div><div className="fdCountL">records reviewed</div></div>
                            <div><div className="fdCountV">{fdNum(r.procurement)}</div><div className="fdCountL">procurement records found</div></div>
                            <div><div className="fdCountV">{fdNum(r.companies)}</div><div className="fdCountL">companies found</div></div>
                            <div><div className="fdCountV">{fdNum(r.relationships)}</div><div className="fdCountL">relationships found</div></div>
                          </div>
                          {(r.hold || r.skippedCount) ? (
                            <div className="fdRunNote">{r.hold ? "Stimulus wasn’t sure it read " + r.hold + " records right" : ""}
                              {r.hold && r.skippedCount ? " · " : ""}{r.skippedCount ? r.skippedCount + " could not be read" : ""}.</div>
                          ) : null}
                        </div>
                      );
                    })}
                  </div>
                )}
              </div>
            );
          })}
        </div>
        <div className="fdFoot">
          <span className="fdFootNote">Kept for audit — every record in the graph carries the source and run it came from.</span>
          <div className="fdFootBtns">
            <button type="button" className="fdBtn fdBtnPrimary" onClick={onClose}>Close</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ── connect hand-off ──────────────────────────────────────────────────────
// Connections are made in one place — Settings → Data sources. This says so
// before it moves you, so the fetch you were setting up is not lost silently.
function FdConnectDialog({ src, onClose, onGo }) {
  React.useEffect(() => {
    const esc = (e) => { if (e.key === "Escape") { e.stopPropagation(); onClose(); } };
    window.addEventListener("keydown", esc, true);
    return () => window.removeEventListener("keydown", esc, true);
  }, [onClose]);
  return (
    <div className="fdScrim fdScrimTop" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="fdDialog fdDialogSm" role="dialog" aria-modal="true" aria-label={"Connect " + src.name}>
        <div className="fdGrab" />
        <div className="fdHead">
          <span className="fdHeadIcon"><Icon name={src.icon} size={14} /></span>
          <span style={{ flex: 1, minWidth: 0 }}>
            <div className="fdTitle">Connect {src.name} in Settings</div>
            <div className="fdSub">Create the {src.name} connection in Settings → Data sources, then come back here.
              {src.name} will be ready to pick.</div>
          </span>
          <button type="button" className="fdClose" aria-label="Close" onClick={onClose}><Icon name="x" size={14} /></button>
        </div>
        <div className="fdBody">
          <div className="fdRecap">
            <div className="fdRecapRow"><span className="fdRecapK">Where</span><span>Settings → Data sources</span></div>
            <div className="fdRecapRow"><span className="fdRecapK">Access</span><span>Read-only · revocable at any time</span></div>
            <div className="fdRecapRow"><span className="fdRecapK">This fetch</span><span>Your selected sources and {FD_SCOPES[src.id] ? "picks" : "dates"} are kept while you are away.</span></div>
          </div>
        </div>
        <div className="fdFoot">
          <span className="fdFootNote">Nothing is connected until you finish sign-in there.</span>
          <div className="fdFootBtns">
            <button type="button" className="fdBtn fdBtnGhost" onClick={onClose}>Not now</button>
            <button type="button" className="fdBtn fdBtnPrimary" onClick={onGo}>Go to data sources</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ── connected wrapper used by the Sourcing page ───────────────────────────
function FetchDataFlow({ open, onClose }) {
  const { run, step, selected, scopes, history } = useFetchRun();
  const [review, setReview] = React.useState(null);
  const [connect, setConnect] = React.useState(null);
  const [showHist, setShowHist] = React.useState(false);
  React.useEffect(() => {
    if (!open) return;
    const esc = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", esc);
    return () => window.removeEventListener("keydown", esc);
  }, [open, onClose]);
  if (!open) return null;
  const onAction = (key, id) => {
    if (key === "pause") FDStore.pause();
    else if (key === "resume" || key === "retry" || key === "reconnect") FDStore.resume();
    else if (key === "cancelRun") FDStore.cancel();
    else if (key === "review") {
      // Run-level with no id: review the first source still holding records.
      const row = id ? run.sources.find((s) => s.id === id)
        : run.sources.find((s) => (s.hold || 0) > 0);
      if (row) setReview({ id: row.id, count: row.hold || 0 });
    }
    else if (key === "again") FDStore.reset();
    else if (key === "remove" && id) FDStore.removeSource(id);
    else if (key === "connect" && id) setConnect(id);
    else if (key === "history") setShowHist(id || true);
  };
  return (
    <React.Fragment>
      <FetchDataDialog run={run} step={step} selected={selected} scopes={scopes} history={history} onToggle={FDStore.toggle}
        onScope={FDStore.toggleScope} onScopeClear={FDStore.clearScope} onGoto={FDStore.goto} onConfirm={FDStore.confirm} onClose={onClose} onAction={onAction} />
      {showHist && (
        <FdHistoryDialog onClose={() => setShowHist(false)} focus={typeof showHist === "string" ? showHist : null} entries={[
          ...(run && run.sources.length ? [{ ...fdSnapshot(run), current: true, finishedAt: run.finishedAt || null }] : []),
          ...history.filter((h) => !run || h.id !== run.id),
        ]} />
      )}
      {connect && (
        <FdConnectDialog src={fdSrc(connect)} onClose={() => setConnect(null)}
          onGo={() => { FDStore.connect(connect); setConnect(null); }} />
      )}
      {review && (
        <FdReviewDialog src={fdSrc(review.id)} count={review.count} onClose={() => setReview(null)}
          onSave={(n) => { FDStore.resolveHold(review.id, n); setReview(null); }} />
      )}
    </React.Fragment>
  );
}

// One control in the page header: the Fetch data button, carrying its own status.
// Idle → synced-through date. Running → share of workflow steps completed (real steps,
// not a fabricated percentage). Anything needing attention → a count badge.
// Clicking it opens the dialog, where the per-source detail lives.
function FetchDataButton({ onOpen }) {
  const { run } = useFetchRun();
  const rows = fdSourceStatus(run);
  const fetching = rows.filter((r) => r.tone === "live").length;
  const attn = rows.filter((r) => r.tone === "bad" || r.tone === "attn").length;
  const done = rows.filter((r) => r.tone === "ok").length;
  const through = rows.map((r) => FD_FETCHED[r.s.id] || r.s.fetchedThrough).filter(Boolean);
  const pct = run ? Math.round(run.sources.reduce((a, s) => a + Math.min(s.stage, 7), 0) / (run.sources.length * 7) * 100) : 0;

  let status = null;
  if (fetching) status = <React.Fragment><span className="fdDot" />{pct}% of steps done · {fetching} source{fetching > 1 ? "s" : ""}</React.Fragment>;
  else if (attn) status = attn + " source" + (attn > 1 ? "s" : "") + " need" + (attn > 1 ? "" : "s") + " you";

  return (
    <button type="button" className={"owBtn owBtnGhost fdMainBtn" + (fetching ? " is-live" : attn ? " is-attn" : "")} onClick={onOpen}>
      <Icon name="download" size={12} />
      <span className="fdMainMain">
        <span className="fdMainT">Fetch data</span>
        {status && <span className="fdMainS">{status}</span>}
      </span>
      {fetching || attn ? <span className="fdBadge">{fetching || attn}</span> : null}
    </button>
  );
}

Object.assign(window, { FD_SOURCES, FD_STAGES, FD_STATUS, FD_SRC_STATE, FDStore, useFetchRun,
  FetchDataDialog, FetchDataFlow, FetchDataButton, FdSrcStateChip, fdSourceStatus, fdRow,
  FdReviewDialog, FD_REVIEW_BANK, fdReviewItems, FD_SCOPES, FdScopePicker, FdConnectDialog,
  FdHistoryDialog, fdSnapshot });
