/* Research vertical: Studies dashboard + study wizard + fieldwork + results */
const ResearchReact = React;
const {
  useEffect: rUseEffect,
  useMemo: rUseMemo,
  useState: rUseState,
} = React;

const RESEARCH_TEMPLATES = [
  {
    id: "customer_satisfaction",
    project_type: "survey",
    emoji: "📊",
    name: "Customer satisfaction survey",
    desc: "Mikaka calls your customers and runs a short structured survey — satisfaction scores, drivers, and one open question.",
    objectiveHint: "How satisfied recent customers are with our service and what we should improve",
  },
  {
    id: "market_research",
    project_type: "interview",
    emoji: "🎙️",
    name: "Market research interview",
    desc: "Deeper structured interviews with screeners and open-ended probing, recorded as clean verbatims.",
    objectiveHint: "Understand how the target market chooses providers and what they pay today",
  },
  {
    id: "public_feedback",
    project_type: "census_feedback",
    emoji: "🏛️",
    name: "Census / public feedback",
    desc: "Public-interest data collection: outbound waves plus a toll-free line people can call to respond.",
    objectiveHint: "Collect structured public feedback for the programme",
  },
  {
    id: "inform_notify",
    project_type: "inform",
    emoji: "📣",
    name: "Inform & notify",
    desc: "Mikaka delivers a study update or public notice, confirms understanding, and answers questions from your brief.",
    objectiveHint: "Inform participants about the upcoming exercise and confirm they understand",
  },
];

const RESEARCH_WIZARD_STEPS = [
  { id: 1, label: "Study setup" },
  { id: 2, label: "Questionnaire" },
  { id: 3, label: "Audience & channel" },
  { id: 4, label: "Review & launch" },
];

const RESEARCH_QUESTION_TYPES = [
  { id: "yes_no", label: "Yes / No" },
  { id: "scale_1_5", label: "Scale 1–5" },
  { id: "single_choice", label: "Single choice" },
  { id: "multi_choice", label: "Multiple choice" },
  { id: "numeric", label: "Number" },
  { id: "open", label: "Open-ended" },
];

const researchEstimateSeconds = (questions) =>
  75 + (questions || []).reduce((sum, q) => sum + (q.type === "open" ? 30 : 15), 0);

const researchCsvDownload = (filename, columns, rows) => {
  const esc = (v) => {
    const s = v == null ? "" : String(v);
    return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
  };
  const head = columns.map(esc).join(",");
  const body = (rows || []).map((row) => columns.map((c) => esc(row[c])).join(",")).join("\n");
  const blob = new Blob([head + "\n" + body], { type: "text/csv" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
};

const researchStatusStyle = (status) => {
  const s = String(status || "draft");
  if (s === "active") return { bg: "color-mix(in srgb, var(--mk-success) 14%, transparent)", fg: "var(--mk-success)" };
  if (s === "completed") return { bg: "color-mix(in srgb, var(--mk-orange) 14%, transparent)", fg: "var(--mk-orange)" };
  if (s === "paused") return { bg: "color-mix(in srgb, var(--mk-warning, #b8860b) 14%, transparent)", fg: "var(--mk-warning, #b8860b)" };
  return { bg: "var(--bg-subtle)", fg: "var(--fg-muted)" };
};

const ResearchBar = ({ label, count, total, accent }) => {
  const pctWidth = total > 0 ? Math.round((count / total) * 100) : 0;
  return (
    <div style={{ marginBottom: 8 }}>
      <div className="row between" style={{ fontSize: 12.5, marginBottom: 3 }}>
        <span style={{ color: "var(--fg)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{label}</span>
        <span className="mono" style={{ color: "var(--fg-muted)", flexShrink: 0, marginLeft: 8 }}>{count} · {pctWidth}%</span>
      </div>
      <div style={{ height: 7, borderRadius: 4, background: "var(--bg-subtle)", overflow: "hidden" }}>
        <div style={{ height: "100%", width: `${pctWidth}%`, background: accent || "var(--mk-orange)", borderRadius: 4, transition: "width .3s" }} />
      </div>
    </div>
  );
};

const ResearchQuestionEditor = ({ q, index, onChange, onRemove, isScreener }) => {
  const set = (patch) => onChange({ ...q, ...patch });
  const optionsText = (q.options || []).map((o) => o.label).join("\n");
  const needsOptions = q.type === "single_choice" || q.type === "multi_choice";
  return (
    <div className="card card-pad" style={{ marginBottom: 12 }}>
      <div className="row between" style={{ marginBottom: 10, gap: 8 }}>
        <div className="row gap-2" style={{ alignItems: "center" }}>
          <span className="mono" style={{ fontSize: 12, color: "var(--fg-muted)" }}>{q.id}</span>
          <select className="input" style={{ width: "auto", padding: "5px 8px", fontSize: 12.5 }} value={q.type}
            onChange={(e) => set({ type: e.target.value })}>
            {RESEARCH_QUESTION_TYPES.map((t) => <option key={t.id} value={t.id}>{t.label}</option>)}
          </select>
          <label className="row gap-1" style={{ fontSize: 12, color: "var(--fg-muted)", alignItems: "center", cursor: "pointer" }}>
            <input type="checkbox" checked={!!q.required} onChange={(e) => set({ required: e.target.checked })} /> required
          </label>
        </div>
        <button className="btn btn-ghost" style={{ padding: "4px 8px", fontSize: 12 }} onClick={onRemove}>Remove</button>
      </div>
      <textarea className="input" rows={2} placeholder={isScreener ? "Screener question, e.g. Are you over 18?" : "Question text as the interviewer should ask it"}
        value={q.text || ""} onChange={(e) => set({ text: e.target.value })} style={{ marginBottom: 8, resize: "vertical" }} />
      {needsOptions ? (
        <textarea className="input" rows={3} placeholder={"One option per line (max 4), e.g.\nPrice\nSpeed\nReliability"}
          value={optionsText}
          onChange={(e) => {
            const options = e.target.value.split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 4)
              .map((label) => ({ key: label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "") || "opt", label }));
            set({ options });
          }}
          style={{ marginBottom: 8, resize: "vertical" }} />
      ) : null}
      <div className="row gap-2" style={{ flexWrap: "wrap" }}>
        <input className="input" style={{ flex: 1, minWidth: 160, fontSize: 12.5 }} placeholder="Demographic tag (optional), e.g. gender, county, age_band"
          value={q.attribute || ""} onChange={(e) => set({ attribute: e.target.value.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_") || undefined })} />
        {isScreener ? (
          <input className="input" style={{ flex: 1, minWidth: 160, fontSize: 12.5 }} placeholder="Disqualify on answers (comma keys), e.g. no, under_18"
            value={(q.disqualify_if && (q.disqualify_if.in || [q.disqualify_if.equals]).join(", ")) || ""}
            onChange={(e) => {
              const keys = e.target.value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
              set({ disqualify_if: keys.length ? { in: keys } : undefined });
            }} />
        ) : null}
      </div>
    </div>
  );
};

/* Pre-launch test: rehearse the interview on the web, or dial a real number.
   Works against any study (incl. drafts) — the study id rides on the test
   session / call metadata so the agent runs this study's interviewer flow. */
const ResearchTestPanel = ({ project, tenantNumbers }) => {
  const [mode, setMode] = rUseState("web"); // web | phone
  const [callState, setCallState] = rUseState("idle"); // idle | connecting | live | dialing | ringing | ended
  const [transcript, setTranscript] = rUseState([]);
  const [input, setInput] = rUseState("");
  const [sending, setSending] = rUseState(false);
  const [err, setErr] = rUseState("");
  const [backendStatus, setBackendStatus] = rUseState("idle"); // idle | connecting | live | error
  const [phone, setPhone] = rUseState("");
  const [sampleName, setSampleName] = rUseState(""); // simulated respondent — greeted by name
  // Any tenant number can place the test call; prefer outbound-enabled ones first.
  const outboundNumbers = (Array.isArray(tenantNumbers) ? tenantNumbers : [])
    .slice()
    .sort((a, b) => (b.outbound_enabled || b.capabilities?.outbound ? 1 : 0) - (a.outbound_enabled || a.capabilities?.outbound ? 1 : 0));
  const [sourceNumberId, setSourceNumberId] = rUseState(outboundNumbers[0]?.id ? String(outboundNumbers[0].id) : "");

  const sessionRef = React.useRef(null);
  const scrollRef = React.useRef(null);
  const pollRef = React.useRef(null);
  const phoneCallRef = React.useRef(null);
  const phoneStartRef = React.useRef(0);
  const live = callState !== "idle" && callState !== "ended";

  const stopPoll = () => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } };
  React.useEffect(() => () => {
    stopPoll();
    if (sessionRef.current) api.research.endTestSession(sessionRef.current).catch(() => {});
  }, []);
  rUseEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, [transcript]);

  const replyText = (res) =>
    res?.assistant_text || res?.response || res?.agent_response || res?.message || "";

  const startWeb = async () => {
    setErr("");
    setTranscript([]);
    setCallState("connecting");
    setBackendStatus("connecting");
    let callId = "";
    try {
      const session = await api.research.startInterviewTest(project.id, {
        sampleRecipient: sampleName.trim() ? { name: sampleName.trim() } : null,
      });
      callId = session?.call_id || session?.session_id || session?.id || "";
      if (!callId) throw new Error("Session created but no id returned.");
      sessionRef.current = callId;
    } catch (e) {
      setErr(String(e?.message || e));
      setBackendStatus("error");
      setCallState("ended");
      return;
    }
    setBackendStatus("live");
    setCallState("live");
    try {
      const open = await api.research.sendTestMessage(callId, "(interview connected)");
      const opening = replyText(open);
      if (opening) setTranscript([{ who: "agent", text: opening }]);
    } catch (e) {
      setTranscript([{ who: "system", text: `Backend error: ${String(e?.message || e)}` }]);
    }
  };

  const sendWeb = async () => {
    const text = input.trim();
    if (!text || sending || !sessionRef.current) return;
    setInput("");
    setSending(true);
    setTranscript((prev) => [...prev, { who: "respondent", text }]);
    try {
      const res = await api.research.sendTestMessage(sessionRef.current, text);
      const reply = replyText(res);
      if (reply) setTranscript((prev) => [...prev, { who: "agent", text: reply }]);
    } catch (e) {
      setTranscript((prev) => [...prev, { who: "system", text: `Backend error: ${String(e?.message || e)}` }]);
    } finally {
      setSending(false);
    }
  };

  const endWeb = async () => {
    if (sessionRef.current) {
      await api.research.endTestSession(sessionRef.current).catch(() => {});
      sessionRef.current = null;
    }
    setCallState("ended");
  };

  const normPhone = (raw) =>
    (typeof normalizeKenyanPhone === "function" ? normalizeKenyanPhone(raw) : String(raw || "").trim());

  const startPhone = async () => {
    setErr("");
    const target = normPhone(phone);
    if (!target) { setErr("Enter a valid phone number, e.g. 0712 345 678."); return; }
    const src = sourceNumberId || outboundNumbers[0]?.id;
    if (!src) { setErr("No business number to call from — add one first."); return; }
    setTranscript([{ who: "system", text: `Dialing ${target} from your business line — answer your phone.` }]);
    setCallState("dialing");
    setBackendStatus("connecting");
    let created;
    try {
      created = await api.research.createTestCall({
        to_e164: target,
        source_number_id: String(src),
        objective: project.objective || undefined,
        reason_summary: project.objective || "a short research interview",
        recipient_name: sampleName.trim() || "Test respondent",
        metadata: {
          is_test: true,
          source: "research_test",
          research_project_id: String(project.id),
        },
        idempotency_key: `restest-${project.id}-${Date.now()}`,
      });
    } catch (e) {
      setErr(`Could not place the call: ${String(e?.message || e)}`);
      setBackendStatus("error");
      setCallState("ended");
      return;
    }
    const callId = created?.call?.id || created?.call_id || "";
    if (!callId) {
      setErr("Call was queued but no call id came back — check Fieldwork for the attempt.");
      setBackendStatus("error");
      setCallState("ended");
      return;
    }
    phoneCallRef.current = String(callId);
    phoneStartRef.current = Date.now();
    setBackendStatus("live");
    pollRef.current = setInterval(async () => {
      try {
        const data = await api.calls.get(phoneCallRef.current);
        const st = String(data?.status || data?.state || "").toLowerCase();
        const ended = ["completed", "ended", "failed", "no_answer", "busy", "cancelled", "voicemail"].includes(st);
        const ringing = ["answered", "answering", "active", "in_progress", "live"].includes(st);
        if (!ended) setCallState(ringing ? "live" : "ringing");
        const turns = (typeof normalizeTranscriptMessages === "function" ? normalizeTranscriptMessages(data) : [])
          .map((m) => ({ who: m.who === "agent" ? "agent" : "respondent", text: m.text }));
        if (turns.length) setTranscript((prev) => [...prev.filter((t) => t.who === "system"), ...turns]);
        const timedOut = Date.now() - phoneStartRef.current > 240000;
        if (ended || timedOut) {
          stopPoll();
          setCallState("ended");
          setTranscript((prev) => [...prev, {
            who: "system",
            text: timedOut && !ended ? "Stopped watching after 4 minutes — check Fieldwork for the result." : `Call ${st || "ended"}.`,
          }]);
        }
      } catch (_) {
        if (Date.now() - phoneStartRef.current > 240000) { stopPoll(); setCallState("ended"); }
      }
    }, 3500);
  };

  const reset = () => {
    stopPoll();
    sessionRef.current = null;
    phoneCallRef.current = null;
    setTranscript([]);
    setInput("");
    setErr("");
    setBackendStatus("idle");
    setCallState("idle");
  };

  const bubbleStyle = (who) => {
    if (who === "system") return { alignSelf: "center", background: "var(--bg-sunken)", color: "var(--fg-muted)", fontStyle: "italic", fontSize: 11.5, padding: "5px 12px", borderRadius: 999 };
    if (who === "agent") return { alignSelf: "flex-start", background: "color-mix(in srgb, var(--mk-orange) 12%, var(--bg))", color: "var(--fg)", padding: "9px 13px", borderRadius: 14, borderBottomLeftRadius: 4 };
    return { alignSelf: "flex-end", background: "var(--fg)", color: "var(--bg)", padding: "9px 13px", borderRadius: 14, borderBottomRightRadius: 4 };
  };

  const statePill = () => {
    if (backendStatus === "connecting") return <span className="mono" style={{ fontSize: 11, color: "var(--mk-info)" }}>connecting…</span>;
    if (backendStatus === "live" && live) return <span className="mono" style={{ fontSize: 11, color: "var(--mk-success)" }}>● live</span>;
    if (backendStatus === "error") return <span className="mono" style={{ fontSize: 11, color: "var(--mk-danger)" }}>error</span>;
    if (callState === "ended") return <span className="mono" style={{ fontSize: 11, color: "var(--fg-muted)" }}>ended</span>;
    return null;
  };

  return (
    <div className="stack" style={{ gap: 14, maxWidth: 760 }}>
      <div className="card card-pad">
        <div style={{ fontWeight: 600, fontSize: 13.5, marginBottom: 4 }}>Test before launching</div>
        <div style={{ fontSize: 12.5, color: "var(--fg-muted)", marginBottom: 12 }}>
          Hear the interview the way a respondent will — chat with the interviewer here, or have Mikaka call your own phone and run the real questionnaire. Test runs never count toward your results.
        </div>
        <div className="row gap-2" style={{ flexWrap: "wrap" }}>
          {[["web", "💬 Web test", "Chat here — free, instant."], ["phone", "📱 Call my phone", "Mikaka dials a number and runs the interview."]].map(([id, label, desc]) => (
            <button key={id} className={mode === id ? "btn btn-accent" : "btn btn-outline"} disabled={live}
              onClick={() => { setMode(id); reset(); }} title={desc}>{label}</button>
          ))}
        </div>
      </div>

      {mode === "phone" ? (
        <div className="card card-pad">
          <label style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>Call from</label>
          <select className="input" style={{ margin: "6px 0 12px" }} value={sourceNumberId} disabled={live}
            onChange={(e) => setSourceNumberId(e.target.value)}>
            <option value="">Pick a number…</option>
            {outboundNumbers.map((n) => <option key={n.id} value={n.id}>{n.did_e164 || n.number || n.id}</option>)}
          </select>
          <label style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>Phone to call (answer it and play the respondent)</label>
          <div className="row gap-2" style={{ marginTop: 6 }}>
            <input className="input mono" style={{ flex: 1 }} type="tel" placeholder="0712 345 678" value={phone} disabled={live}
              onChange={(e) => setPhone(e.target.value)} />
            {callState === "idle" || callState === "ended" ? (
              <button className="btn btn-accent" disabled={!phone.trim()} onClick={startPhone}>{callState === "ended" ? "Call again" : "Call my phone"}</button>
            ) : (
              <button className="btn btn-outline" onClick={() => { stopPoll(); setCallState("ended"); }}>Stop watching</button>
            )}
          </div>
        </div>
      ) : (
        <div className="card card-pad">
          {callState === "idle" ? (
            <div style={{ padding: "4px 0" }}>
              <label style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>Simulated respondent's name (optional — greeted by name)</label>
              <input className="input" style={{ margin: "6px 0 12px" }} placeholder="e.g. Mary Wanjiku"
                value={sampleName} onChange={(e) => setSampleName(e.target.value)} />
              <div style={{ textAlign: "center" }}>
                <button className="btn btn-accent" onClick={startWeb}>Start web test</button>
                <div style={{ fontSize: 12, color: "var(--fg-faint)", marginTop: 8 }}>You play the respondent — the interviewer asks for consent first, then works through this study's questionnaire. No call is placed.</div>
              </div>
            </div>
          ) : (
            <button className="btn btn-ghost" style={{ fontSize: 12 }} onClick={() => { endWeb(); }} disabled={callState === "ended"}>
              {callState === "ended" ? "Interview ended" : "End interview"}
            </button>
          )}
        </div>
      )}

      {callState !== "idle" ? (
        <div className="card" style={{ overflow: "hidden" }}>
          <div className="row between" style={{ padding: "10px 14px", borderBottom: "1px solid var(--border)" }}>
            <div style={{ fontSize: 12.5, fontWeight: 600 }}>{mode === "phone" ? `📱 ${phone || "—"}` : "💬 Web interview"}</div>
            {statePill()}
          </div>
          <div ref={scrollRef} style={{ display: "flex", flexDirection: "column", gap: 8, padding: 14, maxHeight: 320, overflowY: "auto" }}>
            {transcript.length ? transcript.map((t, i) => (
              <div key={i} style={{ maxWidth: "85%", fontSize: 13, lineHeight: 1.5, ...bubbleStyle(t.who) }}>{t.text}</div>
            )) : (
              <div style={{ fontSize: 12.5, color: "var(--fg-faint)", textAlign: "center", padding: 18 }}>
                {mode === "phone" ? "Waiting for the call to connect…" : "Starting the interview…"}
              </div>
            )}
          </div>
          {mode === "web" && callState === "live" ? (
            <div className="row gap-2" style={{ padding: 12, borderTop: "1px solid var(--border)" }}>
              <input className="input" style={{ flex: 1 }} placeholder="Reply as the respondent…" value={input}
                onChange={(e) => setInput(e.target.value)}
                onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendWeb(); } }} />
              <button className="btn btn-accent" disabled={!input.trim() || sending} onClick={sendWeb}>{sending ? "…" : "Send"}</button>
            </div>
          ) : null}
        </div>
      ) : null}

      {err ? <div style={{ color: "var(--mk-danger)", fontSize: 13 }}>{err}</div> : null}
    </div>
  );
};

const ResearchPage = ({ onGo }) => {
  const store = MK_USE();
  const toast = useToast();
  const tenantNumbers = Array.isArray(store.tenantNumbers) ? store.tenantNumbers : [];

  const [mode, setMode] = rUseState("dashboard"); // dashboard | wizard | detail
  const [projects, setProjects] = rUseState([]);
  const [loading, setLoading] = rUseState(false);
  const [busy, setBusy] = rUseState(false);
  const [error, setError] = rUseState("");

  // Wizard state
  const [step, setStep] = rUseState(1);
  const [templateId, setTemplateId] = rUseState("customer_satisfaction");
  const [draft, setDraft] = rUseState({
    name: "",
    objective: "",
    consent: "",
    targetResponses: "",
    screener: [],
    questions: [],
    channel: "outbound", // outbound | inbound | both
    sourceNumberId: "",
    inboundNumberId: "",
    inboundMode: "dedicated_line",
    contactListId: "",
    csvName: "",
    csvContacts: [],
    quotaCells: [],
    informPoints: "",
  });
  const [contactLists, setContactLists] = rUseState([]);

  // Questionnaire import (PDF / DOCX / pasted text -> AI-extracted questions)
  const [importBusy, setImportBusy] = rUseState(false);
  const [importNote, setImportNote] = rUseState("");
  const [importWarnings, setImportWarnings] = rUseState([]);
  const [importReplace, setImportReplace] = rUseState(true);
  const [pasteOpen, setPasteOpen] = rUseState(false);
  const [pasteText, setPasteText] = rUseState("");

  // Detail state
  const [activeProject, setActiveProject] = rUseState(null);
  const [detailTab, setDetailTab] = rUseState("fieldwork"); // fieldwork | results | data
  const [fieldwork, setFieldwork] = rUseState(null);
  const [results, setResults] = rUseState(null);
  const [resultsLoading, setResultsLoading] = rUseState(false);
  const [crosstabQ, setCrosstabQ] = rUseState("");
  const [crosstabAttr, setCrosstabAttr] = rUseState("");

  const template = RESEARCH_TEMPLATES.find((t) => t.id === templateId) || RESEARCH_TEMPLATES[0];
  const isInform = template.project_type === "inform";

  const loadProjects = async () => {
    setLoading(true);
    try {
      const rows = await api.research.listProjects();
      setProjects(Array.isArray(rows) ? rows : []);
      setError("");
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setLoading(false);
    }
  };

  rUseEffect(() => { loadProjects(); }, []);
  rUseEffect(() => {
    if (mode === "wizard" && step === 3) {
      api.outbound.listContactLists().then((rows) => setContactLists(Array.isArray(rows) ? rows : [])).catch(() => {});
    }
  }, [mode, step]);

  const allQuestions = [...draft.screener, ...draft.questions];
  const estimate = researchEstimateSeconds(allQuestions);
  const estimateTone = estimate > 480 ? "var(--mk-danger)" : estimate > 360 ? "var(--mk-warning, #b8860b)" : "var(--mk-success)";

  const nextQuestionId = (prefix, list) => {
    let n = list.length + 1;
    const taken = new Set(allQuestions.map((q) => q.id));
    while (taken.has(`${prefix}${n}`)) n += 1;
    return `${prefix}${n}`;
  };

  const addQuestion = (isScreener) => {
    if (isScreener) {
      setDraft((d) => ({ ...d, screener: [...d.screener, { id: nextQuestionId("s", d.screener), type: "yes_no", text: "", required: true }] }));
    } else {
      setDraft((d) => ({ ...d, questions: [...d.questions, { id: nextQuestionId("q", d.questions), type: "scale_1_5", text: "", required: true }] }));
    }
  };

  const importQuestionnaire = async ({ file, text }) => {
    setImportBusy(true);
    setImportNote("");
    setImportWarnings([]);
    setError("");
    try {
      const data = await api.research.extractQuestionnaire({ file, text, projectType: template.project_type });
      const inScreener = Array.isArray(data?.screener) ? data.screener : [];
      const inQuestions = Array.isArray(data?.questions) ? data.questions : [];
      if (!inScreener.length && !inQuestions.length) {
        setImportNote("No questions found in that document.");
        return;
      }
      setDraft((d) => {
        const baseScreener = importReplace ? [] : d.screener;
        const baseQuestions = importReplace ? [] : d.questions;
        const taken = new Set([...baseScreener, ...baseQuestions].map((q) => q.id));
        const claim = (prefix) => {
          let n = 1;
          while (taken.has(`${prefix}${n}`)) n += 1;
          const id = `${prefix}${n}`;
          taken.add(id);
          return id;
        };
        return {
          ...d,
          screener: [...baseScreener, ...inScreener.map((q) => ({ ...q, id: claim("s") }))],
          questions: [...baseQuestions, ...inQuestions.map((q) => ({ ...q, id: claim("q") }))],
        };
      });
      setImportWarnings(Array.isArray(data?.warnings) ? data.warnings : []);
      setImportNote(`Imported ${inScreener.length ? `${inScreener.length} screener + ` : ""}${inQuestions.length} question${inQuestions.length === 1 ? "" : "s"} from ${data?.source || "the document"} — review and edit before continuing.`);
      if (text) {
        setPasteText("");
        setPasteOpen(false);
      }
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setImportBusy(false);
    }
  };

  // Everything launch would reject, surfaced while editing instead of failing at step 4.
  const questionnaireIssues = [];
  if (step === 2) {
    if (!isInform && !draft.questions.length) questionnaireIssues.push("Add at least one question.");
    if (isInform && draft.questions.length > 3) questionnaireIssues.push("Inform studies support at most 3 confirmation questions.");
    if (draft.questions.length > 20) questionnaireIssues.push("Max 20 questions per voice study.");
    const blank = allQuestions.filter((q) => !String(q.text || "").trim()).length;
    if (blank) questionnaireIssues.push(`${blank} question${blank > 1 ? "s have" : " has"} no text.`);
    const overlong = allQuestions.filter((q) => String(q.text || "").length > 200).length;
    if (overlong) questionnaireIssues.push(`${overlong} question${overlong > 1 ? "s are" : " is"} over 200 characters — too long to ask by voice.`);
    const fewOptions = allQuestions.filter((q) => (q.type === "single_choice" || q.type === "multi_choice") && (q.options || []).length < 2).length;
    if (fewOptions) questionnaireIssues.push(`${fewOptions} choice question${fewOptions > 1 ? "s need" : " needs"} at least 2 options.`);
    const manyOptions = allQuestions.filter((q) => (q.options || []).length > 4).length;
    if (manyOptions) questionnaireIssues.push(`${manyOptions} question${manyOptions > 1 ? "s have" : " has"} more than 4 options — too many to read aloud.`);
    const longLabels = allQuestions.filter((q) => (q.options || []).some((o) => String(o.label || "").split(/\s+/).length > 8)).length;
    if (longLabels) questionnaireIssues.push(`${longLabels} question${longLabels > 1 ? "s have" : " has"} option labels over 8 words.`);
    const dupKeys = allQuestions.filter((q) => {
      const keys = (q.options || []).map((o) => o.key);
      return new Set(keys).size !== keys.length;
    }).length;
    if (dupKeys) questionnaireIssues.push(`${dupKeys} question${dupKeys > 1 ? "s have" : " has"} duplicate options.`);
    if (estimate > 480) questionnaireIssues.push("Estimated interview is over the 8-minute voice limit — trim questions.");
  }

  const launchStudy = async (activate) => {
    setBusy(true);
    setError("");
    try {
      let contactListId = draft.contactListId;
      if ((draft.channel === "outbound" || draft.channel === "both") && !contactListId && draft.csvContacts.length) {
        const list = await api.outbound.createContactList({ name: `${draft.name || template.name} respondents` });
        await api.outbound.importContacts(list.id, draft.csvContacts);
        contactListId = list.id;
      }
      const payload = {
        name: draft.name || template.name,
        objective: draft.objective || template.objectiveHint,
        project_type: template.project_type,
        consent_script: draft.consent || null,
        target_responses: draft.targetResponses ? Number(draft.targetResponses) : null,
        quota_cells: draft.quotaCells,
        questionnaire: { version: 1, screener: draft.screener, questions: draft.questions },
        inform_brief: isInform
          ? { key_points: draft.informPoints.split("\n").map((s) => s.trim()).filter(Boolean) }
          : {},
      };
      if (draft.channel === "outbound" || draft.channel === "both") {
        if (!draft.sourceNumberId) throw new Error("Pick the number Mikaka should call from.");
        payload.outbound = {
          source_number_id: draft.sourceNumberId,
          contact_list_id: contactListId || null,
          schedule: {},
          retry_policy: {},
          max_concurrent_calls: 1,
        };
      }
      if (draft.channel === "inbound" || draft.channel === "both") {
        if (!draft.inboundNumberId) throw new Error("Pick the respondent line callers will use.");
        payload.inbound_number_id = draft.inboundNumberId;
        payload.inbound_mode = draft.inboundMode;
      }
      const project = await api.research.createProject(payload);
      if (activate) {
        await api.research.projectAction(project.id, "activate");
        toast({ title: "Study launched", body: "Your study is now live and collecting responses." });
        await loadProjects();
        setMode("dashboard");
        setStep(1);
      } else {
        // Land on the saved draft's Test tab so they can rehearse before launching.
        await loadProjects();
        setStep(1);
        await openDetail(project);
        toast({ title: "Saved as draft", body: "Test the interview here, then launch when it sounds right." });
      }
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setBusy(false);
    }
  };

  const openDetail = async (project) => {
    setActiveProject(project);
    setDetailTab(project.status === "draft" ? "test" : "fieldwork");
    setMode("detail");
    setFieldwork(null);
    setResults(null);
    try {
      setFieldwork(await api.research.getFieldwork(project.id));
    } catch (e) {
      setError(String(e?.message || e));
    }
  };

  const loadResults = async (opts = {}) => {
    if (!activeProject) return;
    setResultsLoading(true);
    try {
      setResults(await api.research.getResults(activeProject.id, opts));
      setError("");
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setResultsLoading(false);
    }
  };

  rUseEffect(() => {
    if (mode === "detail" && detailTab === "results" && activeProject && !results) loadResults();
  }, [mode, detailTab, activeProject]);

  const exportData = async () => {
    if (!activeProject) return;
    setBusy(true);
    try {
      const data = await api.research.listResponses(activeProject.id, { format: "wide" });
      const rows = data?.rows || [];
      const columns = data?.columns && data.columns.length
        ? [...data.columns, ...Object.keys(rows[0] || {}).filter((k) => k.startsWith("attr_"))]
        : Object.keys(rows[0] || {});
      researchCsvDownload(`mikaka-study-${activeProject.id.slice(0, 8)}.csv`, [...new Set(columns)], rows);
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setBusy(false);
    }
  };

  const projectAction = async (project, action) => {
    setBusy(true);
    try {
      await api.research.projectAction(project.id, action);
      await loadProjects();
      if (activeProject?.id === project.id) {
        const fresh = await api.research.getProject(project.id);
        setActiveProject(fresh);
      }
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setBusy(false);
    }
  };

  /* ---------------- Dashboard ---------------- */
  if (mode === "dashboard") {
    const active = projects.filter((p) => p.status === "active").length;
    const completes = projects.reduce((sum, p) => sum + Number(p.completed_sessions || 0), 0);
    return (
      <div className="page-pad">
        <div className="row between" style={{ marginBottom: 18, gap: 10, flexWrap: "wrap" }}>
          <div>
            <h1 style={{ margin: 0, fontSize: 22 }}>Studies</h1>
            <div style={{ color: "var(--fg-muted)", fontSize: 13.5, marginTop: 4 }}>
              Mikaka interviews respondents for you — outbound survey waves and toll-free respondent lines — and turns the answers into results.
            </div>
          </div>
          <button className="btn btn-accent" onClick={() => { setMode("wizard"); setStep(1); }}>New study</button>
        </div>

        <div className="row gap-3" style={{ marginBottom: 20, flexWrap: "wrap" }}>
          <div className="card stat" style={{ flex: 1, minWidth: 150 }}>
            <div className="k">Active studies</div>
            <div className="v">{active}</div>
            <div className="d">{projects.length} total</div>
          </div>
          <div className="card stat" style={{ flex: 1, minWidth: 150 }}>
            <div className="k">Completed interviews</div>
            <div className="v">{completes}</div>
            <div className="d">across all studies</div>
          </div>
        </div>

        {loading ? <div style={{ color: "var(--fg-muted)" }}>Loading studies…</div> : null}
        {!loading && !projects.length ? (
          <div className="card card-pad" style={{ textAlign: "center", padding: 42 }}>
            <div style={{ fontSize: 32 }}>📊</div>
            <div style={{ fontWeight: 600, marginTop: 10 }}>No studies yet</div>
            <div style={{ color: "var(--fg-muted)", fontSize: 13.5, marginTop: 6 }}>
              Create your first study: pick a template, build the questionnaire, choose who Mikaka should interview.
            </div>
            <button className="btn btn-accent" style={{ marginTop: 16 }} onClick={() => setMode("wizard")}>Create a study</button>
          </div>
        ) : null}

        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(290px, 1fr))", gap: 14 }}>
          {projects.map((p) => {
            const st = researchStatusStyle(p.status);
            const target = Number(p.target_responses || 0);
            const done = Number(p.completed_sessions || 0);
            const progress = target ? Math.min(100, Math.round((done / target) * 100)) : null;
            return (
              <button key={p.id} className="campaign-card" onClick={() => openDetail(p)}>
                <div className="row between" style={{ marginBottom: 10, gap: 8 }}>
                  <div style={{ textAlign: "left", minWidth: 0 }}>
                    <div style={{ fontWeight: 600, fontSize: 14.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.name}</div>
                    <div style={{ fontSize: 12, color: "var(--fg-muted)", marginTop: 3 }}>
                      {String(p.project_type || "survey").replace("_", " ")}
                      {p.inbound_number_id ? " · inbound line" : ""}
                      {p.outbound_campaign_id ? " · outbound" : ""}
                    </div>
                  </div>
                  <div className="campaign-status" style={{ background: st.bg, color: st.fg }}>{p.status}</div>
                </div>
                <div className="campaign-progress">
                  <div className="campaign-progress-bar">
                    <div className="campaign-progress-fill" style={{ width: `${progress ?? (done ? 100 : 0)}%`, background: "var(--mk-orange)" }} />
                  </div>
                  <div className="row between" style={{ fontSize: 11.5, color: "var(--fg-muted)", marginTop: 6 }}>
                    <span>{done} complete{target ? ` of ${target}` : ""}</span>
                    <span className="mono">{progress != null ? `${progress}%` : ""}</span>
                  </div>
                </div>
              </button>
            );
          })}
        </div>
        {error ? <div style={{ color: "var(--mk-danger)", marginTop: 14, fontSize: 13 }}>{error}</div> : null}
      </div>
    );
  }

  /* ---------------- Wizard ---------------- */
  if (mode === "wizard") {
    const numbersOutbound = tenantNumbers;
    return (
      <div className="page-pad" style={{ maxWidth: 860 }}>
        <div className="row between" style={{ marginBottom: 16 }}>
          <h1 style={{ margin: 0, fontSize: 20 }}>New study</h1>
          <button className="btn btn-ghost" onClick={() => { setMode("dashboard"); setStep(1); }}>Cancel</button>
        </div>
        <div className="row gap-2" style={{ marginBottom: 20, flexWrap: "wrap" }}>
          {RESEARCH_WIZARD_STEPS.map((s) => (
            <div key={s.id} className="row gap-1" style={{ alignItems: "center", fontSize: 12.5, color: step === s.id ? "var(--mk-orange)" : step > s.id ? "var(--mk-success)" : "var(--fg-faint)" }}>
              <span className="mono" style={{ border: "1px solid currentColor", borderRadius: 999, width: 20, height: 20, display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: 11 }}>{step > s.id ? "✓" : s.id}</span>
              <span>{s.label}</span>
              {s.id < 4 ? <span style={{ color: "var(--fg-faint)", margin: "0 6px" }}>—</span> : null}
            </div>
          ))}
        </div>

        {step === 1 ? (
          <>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))", gap: 12, marginBottom: 18 }}>
              {RESEARCH_TEMPLATES.map((t) => (
                <button key={t.id} className={templateId === t.id ? "template-pick on" : "template-card"} style={{ textAlign: "left" }} onClick={() => setTemplateId(t.id)}>
                  <div style={{ fontSize: 22 }}>{t.emoji}</div>
                  <div style={{ marginTop: 8, fontWeight: 600 }}>{t.name}</div>
                  <div style={{ marginTop: 5, fontSize: 12.5, color: "var(--fg-muted)", lineHeight: 1.45 }}>{t.desc}</div>
                </button>
              ))}
            </div>
            <div className="card card-pad">
              <label style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>Study name</label>
              <input className="input" style={{ margin: "6px 0 12px" }} placeholder={template.name}
                value={draft.name} onChange={(e) => setDraft((d) => ({ ...d, name: e.target.value }))} />
              <label style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>What do you want to learn?</label>
              <textarea className="input" rows={2} style={{ margin: "6px 0 12px", resize: "vertical" }} placeholder={template.objectiveHint}
                value={draft.objective} onChange={(e) => setDraft((d) => ({ ...d, objective: e.target.value }))} />
              <label style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>Consent line (read before any question; leave blank for the standard one)</label>
              <textarea className="input" rows={2} style={{ margin: "6px 0 0", resize: "vertical" }}
                placeholder="This call is part of a research study; participation is voluntary and answers are confidential. Is it okay to continue?"
                value={draft.consent} onChange={(e) => setDraft((d) => ({ ...d, consent: e.target.value }))} />
            </div>
          </>
        ) : null}

        {step === 2 ? (
          <>
            <div className="row between" style={{ marginBottom: 12, flexWrap: "wrap", gap: 8 }}>
              <div style={{ fontSize: 13, color: "var(--fg-muted)" }}>
                Voice rules: short questions, max 4 options, 1–5 scales. Tag demographic questions (gender, county…) to unlock crosstabs and quotas.
              </div>
              <div className="mono" style={{ fontSize: 12.5, color: estimateTone }}>
                ≈ {Math.floor(estimate / 60)}m {estimate % 60}s {estimate > 480 ? "— too long, trim it" : estimate > 360 ? "— getting long" : ""}
              </div>
            </div>
            <div className="card card-pad" style={{ marginBottom: 14 }}>
              <div style={{ fontWeight: 600, fontSize: 13.5 }}>Already have a questionnaire?</div>
              <div style={{ fontSize: 12.5, color: "var(--fg-muted)", marginTop: 4 }}>
                Upload it as a PDF, Word, or text file — or paste the text — and Mikaka converts it into voice-ready questions you can edit below.
              </div>
              <div className="row gap-2" style={{ marginTop: 10, flexWrap: "wrap", alignItems: "center" }}>
                <label className="btn btn-outline" style={{ display: "inline-block", cursor: importBusy ? "wait" : "pointer", opacity: importBusy ? 0.6 : 1 }}>
                  {importBusy ? "Converting…" : "Upload PDF / DOCX / TXT"}
                  <input type="file" accept=".pdf,.docx,.txt,.csv,.md,application/pdf" style={{ display: "none" }} disabled={importBusy}
                    onChange={(e) => {
                      const f = e.target.files && e.target.files[0];
                      e.target.value = "";
                      if (f) importQuestionnaire({ file: f });
                    }} />
                </label>
                <button className="btn btn-ghost" disabled={importBusy} onClick={() => setPasteOpen((v) => !v)}>
                  {pasteOpen ? "Hide paste box" : "Paste text instead"}
                </button>
                {allQuestions.length ? (
                  <label className="row gap-1" style={{ fontSize: 12, color: "var(--fg-muted)", alignItems: "center", cursor: "pointer" }}>
                    <input type="checkbox" checked={importReplace} onChange={(e) => setImportReplace(e.target.checked)} /> replace current questions
                  </label>
                ) : null}
              </div>
              {pasteOpen ? (
                <>
                  <textarea className="input" rows={5} style={{ marginTop: 10, resize: "vertical" }}
                    placeholder={"Paste the questionnaire here, e.g.\n1. How satisfied are you with our service? (1–10)\n2. What should we improve?"}
                    value={pasteText} onChange={(e) => setPasteText(e.target.value)} />
                  <button className="btn btn-outline" style={{ marginTop: 8 }} disabled={importBusy || !pasteText.trim()}
                    onClick={() => importQuestionnaire({ text: pasteText })}>
                    {importBusy ? "Converting…" : "Convert to questions"}
                  </button>
                </>
              ) : null}
              {importNote ? <div style={{ fontSize: 12.5, color: "var(--mk-success)", marginTop: 10 }}>{importNote}</div> : null}
              {importWarnings.map((w, i) => (
                <div key={i} style={{ fontSize: 12, color: "var(--mk-warning, #b8860b)", marginTop: 4 }}>⚠ {w}</div>
              ))}
            </div>

            {isInform ? (
              <div className="card card-pad" style={{ marginBottom: 14 }}>
                <label style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>Key points to deliver (one per line)</label>
                <textarea className="input" rows={4} style={{ marginTop: 6, resize: "vertical" }}
                  placeholder={"The exercise starts on 1 July\nEnumerators carry official IDs\nParticipation takes about 10 minutes"}
                  value={draft.informPoints} onChange={(e) => setDraft((d) => ({ ...d, informPoints: e.target.value }))} />
              </div>
            ) : null}
            <div style={{ fontWeight: 600, fontSize: 13.5, margin: "8px 0" }}>Screener (optional — qualifies respondents first)</div>
            {draft.screener.map((q, i) => (
              <ResearchQuestionEditor key={q.id} q={q} index={i} isScreener
                onChange={(nq) => setDraft((d) => ({ ...d, screener: d.screener.map((x, xi) => (xi === i ? nq : x)) }))}
                onRemove={() => setDraft((d) => ({ ...d, screener: d.screener.filter((_, xi) => xi !== i) }))} />
            ))}
            <button className="btn btn-outline" style={{ marginBottom: 18 }} onClick={() => addQuestion(true)}>+ Add screener question</button>

            <div style={{ fontWeight: 600, fontSize: 13.5, margin: "8px 0" }}>{isInform ? "Confirmation questions (optional, max 3)" : "Questionnaire"}</div>
            {draft.questions.map((q, i) => (
              <ResearchQuestionEditor key={q.id} q={q} index={i}
                onChange={(nq) => setDraft((d) => ({ ...d, questions: d.questions.map((x, xi) => (xi === i ? nq : x)) }))}
                onRemove={() => setDraft((d) => ({ ...d, questions: d.questions.filter((_, xi) => xi !== i) }))} />
            ))}
            <button className="btn btn-outline" onClick={() => addQuestion(false)}>+ Add question</button>
          </>
        ) : null}

        {step === 3 ? (
          <div className="stack" style={{ gap: 14 }}>
            <div className="card card-pad">
              <div style={{ fontWeight: 600, fontSize: 13.5, marginBottom: 10 }}>How do respondents take part?</div>
              <div className="row gap-2" style={{ flexWrap: "wrap" }}>
                {[["outbound", "Mikaka calls them"], ["inbound", "They call a line"], ["both", "Both"]].map(([id, label]) => (
                  <button key={id} className={draft.channel === id ? "btn btn-accent" : "btn btn-outline"} onClick={() => setDraft((d) => ({ ...d, channel: id }))}>{label}</button>
                ))}
              </div>
            </div>

            {draft.channel !== "inbound" ? (
              <div className="card card-pad">
                <div style={{ fontWeight: 600, fontSize: 13.5, marginBottom: 10 }}>Outbound wave</div>
                <label style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>Call from</label>
                <select className="input" style={{ margin: "6px 0 12px" }} value={draft.sourceNumberId}
                  onChange={(e) => setDraft((d) => ({ ...d, sourceNumberId: e.target.value }))}>
                  <option value="">Pick a number…</option>
                  {tenantNumbers.map((n) => <option key={n.id} value={n.id}>{n.did_e164 || n.number || n.id}</option>)}
                </select>
                <label style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>Respondent sample</label>
                <select className="input" style={{ margin: "6px 0 10px" }} value={draft.contactListId}
                  onChange={(e) => setDraft((d) => ({ ...d, contactListId: e.target.value }))}>
                  <option value="">Upload a CSV instead…</option>
                  {contactLists.map((l) => <option key={l.id} value={l.id}>{l.name}</option>)}
                </select>
                {!draft.contactListId ? (
                  <label className="btn btn-outline" style={{ display: "inline-block", cursor: "pointer" }}>
                    {draft.csvName ? `${draft.csvName} · ${draft.csvContacts.length} contacts` : "Upload CSV (phone, name, gender, county…)"}
                    <input type="file" accept=".csv,text/csv" style={{ display: "none" }}
                      onChange={(e) => {
                        const file = e.target.files && e.target.files[0];
                        if (!file) return;
                        file.text().then((text) => {
                          const parsed = typeof contactsFromCsv === "function" ? contactsFromCsv(text) : [];
                          setDraft((d) => ({ ...d, csvName: file.name, csvContacts: parsed }));
                        });
                      }} />
                  </label>
                ) : null}
                {!draft.contactListId && typeof downloadCsvText === "function" ? (
                  <button className="btn btn-ghost" style={{ marginLeft: 8, fontSize: 12.5 }}
                    onClick={() => downloadCsvText("mikaka-sample-audience.csv", SAMPLE_AUDIENCE_CSV)}>
                    ↓ Sample CSV
                  </button>
                ) : null}
                <div style={{ fontSize: 12, color: "var(--fg-faint)", marginTop: 8 }}>
                  Extra CSV columns (gender, county, age_band…) ride along as demographics for quotas and crosstabs.
                </div>
              </div>
            ) : null}

            {draft.channel !== "outbound" ? (
              <div className="card card-pad">
                <div style={{ fontWeight: 600, fontSize: 13.5, marginBottom: 10 }}>Respondent line (toll / toll-free)</div>
                <label style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>Number callers will dial</label>
                <select className="input" style={{ margin: "6px 0 12px" }} value={draft.inboundNumberId}
                  onChange={(e) => setDraft((d) => ({ ...d, inboundNumberId: e.target.value }))}>
                  <option value="">Pick a number…</option>
                  {tenantNumbers.map((n) => <option key={n.id} value={n.id}>{n.did_e164 || n.number || n.id}{n.number_class === "tollfree" ? " (toll-free)" : ""}</option>)}
                </select>
                <div className="row gap-2" style={{ flexWrap: "wrap" }}>
                  <button className={draft.inboundMode === "dedicated_line" ? "btn btn-accent" : "btn btn-outline"} onClick={() => setDraft((d) => ({ ...d, inboundMode: "dedicated_line" }))}>Dedicated study line</button>
                  <button className={draft.inboundMode === "menu_offer" ? "btn btn-accent" : "btn btn-outline"} onClick={() => setDraft((d) => ({ ...d, inboundMode: "menu_offer" }))}>Offer after handling the call</button>
                </div>
              </div>
            ) : null}

            <div className="card card-pad">
              <div style={{ fontWeight: 600, fontSize: 13.5, marginBottom: 10 }}>Quota</div>
              <label style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>Stop after this many completed interviews (blank = interview the whole sample)</label>
              <input className="input" type="number" min="1" style={{ margin: "6px 0 0", maxWidth: 220 }} placeholder="e.g. 400"
                value={draft.targetResponses} onChange={(e) => setDraft((d) => ({ ...d, targetResponses: e.target.value }))} />
            </div>
          </div>
        ) : null}

        {step === 4 ? (
          <div className="card card-pad">
            <div style={{ fontWeight: 600, fontSize: 15, marginBottom: 12 }}>{draft.name || template.name}</div>
            <div className="stack" style={{ gap: 8, fontSize: 13.5 }}>
              <div><span style={{ color: "var(--fg-muted)" }}>Type:</span> {template.name}</div>
              <div><span style={{ color: "var(--fg-muted)" }}>Objective:</span> {draft.objective || template.objectiveHint}</div>
              <div><span style={{ color: "var(--fg-muted)" }}>Questions:</span> {draft.screener.length ? `${draft.screener.length} screener + ` : ""}{draft.questions.length} questions (≈ {Math.floor(estimate / 60)}m {estimate % 60}s per interview)</div>
              <div><span style={{ color: "var(--fg-muted)" }}>Channel:</span> {draft.channel === "both" ? "Outbound wave + respondent line" : draft.channel === "inbound" ? "Respondent line" : "Outbound wave"}</div>
              {draft.channel !== "inbound" ? <div><span style={{ color: "var(--fg-muted)" }}>Sample:</span> {draft.contactListId ? (contactLists.find((l) => l.id === draft.contactListId)?.name || "Existing list") : `${draft.csvContacts.length} uploaded contacts`}</div> : null}
              <div><span style={{ color: "var(--fg-muted)" }}>Quota:</span> {draft.targetResponses ? `${draft.targetResponses} completed interviews` : "Whole sample"}</div>
              <div><span style={{ color: "var(--fg-muted)" }}>Consent:</span> asked before every interview; refusals and "never call me" honoured automatically.</div>
            </div>
          </div>
        ) : null}

        {step === 2 && questionnaireIssues.length ? (
          <div style={{ fontSize: 12.5, color: "var(--mk-warning, #b8860b)", marginTop: 16, textAlign: "right" }}>
            {questionnaireIssues.map((msg, i) => <div key={i}>{msg}</div>)}
          </div>
        ) : null}
        <div className="row between" style={{ marginTop: questionnaireIssues.length ? 8 : 20 }}>
          <button className="btn btn-outline" disabled={step === 1 || busy} onClick={() => setStep((s) => Math.max(1, s - 1))}>Back</button>
          {step < 4 ? (
            <button className="btn btn-accent" disabled={busy || (step === 2 && questionnaireIssues.length > 0)}
              onClick={() => setStep((s) => Math.min(4, s + 1))}>Continue</button>
          ) : (
            <div className="row gap-2">
              <button className="btn btn-outline" disabled={busy} onClick={() => launchStudy(false)}>Save as draft</button>
              <button className="btn btn-accent" disabled={busy} onClick={() => launchStudy(true)}>{busy ? "Launching…" : "Launch study"}</button>
            </div>
          )}
        </div>
        {error ? <div style={{ color: "var(--mk-danger)", marginTop: 12, fontSize: 13 }}>{error}</div> : null}
      </div>
    );
  }

  /* ---------------- Detail (fieldwork / results / data) ---------------- */
  const p = activeProject || {};
  const quota = fieldwork?.quota || {};
  return (
    <div className="page-pad">
      <div className="row between" style={{ marginBottom: 16, gap: 10, flexWrap: "wrap" }}>
        <div className="row gap-2" style={{ alignItems: "center", minWidth: 0 }}>
          <button className="btn btn-ghost" onClick={() => { setMode("dashboard"); loadProjects(); }}>← Studies</button>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontWeight: 600, fontSize: 17, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.name}</div>
            <div style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>{p.objective}</div>
          </div>
        </div>
        <div className="row gap-2">
          {p.status === "active" ? (
            <button className="btn btn-outline" disabled={busy} onClick={() => projectAction(p, "pause")}>Pause</button>
          ) : p.status === "draft" || p.status === "paused" ? (
            <button className="btn btn-accent" disabled={busy} onClick={() => projectAction(p, "activate")}>{p.status === "paused" ? "Resume" : "Launch"}</button>
          ) : null}
          <button className="btn btn-outline" disabled={busy} onClick={exportData}>Export CSV</button>
        </div>
      </div>

      <div className="row gap-2" style={{ marginBottom: 18 }}>
        {[["test", "Test"], ["fieldwork", "Fieldwork"], ["results", "Results"]].map(([id, label]) => (
          <button key={id} className={detailTab === id ? "btn btn-accent" : "btn btn-ghost"} onClick={() => setDetailTab(id)}>{label}</button>
        ))}
      </div>

      {detailTab === "test" ? (
        <ResearchTestPanel project={p} tenantNumbers={tenantNumbers} />
      ) : null}

      {detailTab === "fieldwork" ? (
        !fieldwork ? <div style={{ color: "var(--fg-muted)" }}>Loading fieldwork…</div> : (
          <>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))", gap: 12, marginBottom: 20 }}>
              {[
                ["Dials", fieldwork.dials ?? "—"],
                ["Interviews", fieldwork.sessions_total],
                ["Completes", fieldwork.completes],
                ["Partials", fieldwork.partials],
                ["Refusals", fieldwork.refusals],
                ["Ineligible", fieldwork.ineligible],
                ["Consent rate", fieldwork.consent_rate != null ? `${Math.round(fieldwork.consent_rate * 100)}%` : "—"],
                ["Inbound", fieldwork.inbound_sessions],
              ].map(([k, v]) => (
                <div key={k} className="card stat"><div className="k">{k}</div><div className="v">{v ?? 0}</div><div className="d" /></div>
              ))}
            </div>
            <div className="card card-pad" style={{ maxWidth: 560 }}>
              <div style={{ fontWeight: 600, fontSize: 13.5, marginBottom: 10 }}>Quota fill</div>
              <ResearchBar label="Total completes" count={quota.completed || 0} total={Math.max(quota.target_responses || quota.completed || 1, 1)} accent="var(--mk-success)" />
              {(quota.cells || []).map((cell) => (
                <ResearchBar key={cell.key} label={cell.key} count={cell.completed || 0} total={Math.max(Number(cell.target || cell.completed || 1), 1)} />
              ))}
              {!quota.target_responses && !(quota.cells || []).length ? (
                <div style={{ fontSize: 12.5, color: "var(--fg-faint)" }}>No quota set — the study runs through the whole sample.</div>
              ) : null}
            </div>
          </>
        )
      ) : null}

      {detailTab === "results" ? (
        resultsLoading && !results ? <div style={{ color: "var(--fg-muted)" }}>Computing results…</div> : !results ? null : (
          <>
            <div className="row gap-3" style={{ marginBottom: 16, flexWrap: "wrap", fontSize: 13, color: "var(--fg-muted)" }}>
              <span>n = <b style={{ color: "var(--fg)" }}>{results.n_completed}</b> completed</span>
              <span>{results.n_partial} partial</span>
              <span>{results.n_refused} refused</span>
              <span>{results.n_ineligible} ineligible</span>
              <button className="btn btn-ghost" style={{ fontSize: 12 }} disabled={resultsLoading} onClick={() => loadResults({ refresh: true })}>Re-analyse</button>
            </div>

            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(330px, 1fr))", gap: 14 }}>
              {(results.questions || []).map((q) => {
                const dist = q.distribution || {};
                const total = Object.values(dist).reduce((a, b) => a + Number(b || 0), 0);
                const labels = q.option_labels || {};
                return (
                  <div key={q.question_id} className="card card-pad">
                    <div style={{ fontSize: 13.5, fontWeight: 600, marginBottom: 2 }}>{q.question_id.toUpperCase()} · {q.text}</div>
                    <div style={{ fontSize: 11.5, color: "var(--fg-faint)", marginBottom: 10 }}>{q.type.replace("_", " ")} · {q.n} answers{q.mean != null ? ` · mean ${q.mean}` : ""}</div>
                    {q.type === "open" ? (
                      <div className="stack" style={{ gap: 6, maxHeight: 180, overflowY: "auto" }}>
                        {(q.verbatims || []).slice(0, 8).map((v, i) => (
                          <div key={i} style={{ fontSize: 12.5, color: "var(--fg-muted)", borderLeft: "2px solid var(--border)", paddingLeft: 8 }}>“{v}”</div>
                        ))}
                        {!(q.verbatims || []).length ? <div style={{ fontSize: 12.5, color: "var(--fg-faint)" }}>No answers yet.</div> : null}
                      </div>
                    ) : q.type === "numeric" ? (
                      <div style={{ fontSize: 13 }}>
                        mean <b>{q.mean ?? "—"}</b> · median <b>{q.median ?? "—"}</b> · range <b>{q.min ?? "—"}–{q.max ?? "—"}</b>
                      </div>
                    ) : (
                      Object.entries(dist).map(([key, count]) => (
                        <ResearchBar key={key} label={labels[key] || key} count={Number(count || 0)} total={Math.max(total, 1)} />
                      ))
                    )}
                  </div>
                );
              })}
            </div>

            {(results.themes || []).length ? (
              <div className="card card-pad" style={{ marginTop: 16 }}>
                <div style={{ fontWeight: 600, fontSize: 13.5, marginBottom: 4 }}>Themes in open answers <span style={{ fontWeight: 400, fontSize: 11.5, color: "var(--fg-faint)" }}>AI-coded from verbatims</span></div>
                <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))", gap: 12, marginTop: 10 }}>
                  {results.themes.map((t, i) => (
                    <div key={i} style={{ border: "1px solid var(--border)", borderRadius: "var(--r-md)", padding: 12 }}>
                      <div className="row between"><b style={{ fontSize: 13 }}>{t.label}</b><span className="mono" style={{ fontSize: 12, color: "var(--fg-muted)" }}>{t.count}</span></div>
                      <div style={{ fontSize: 12.5, color: "var(--fg-muted)", marginTop: 4 }}>{t.description}</div>
                      {(t.example_quotes || []).slice(0, 1).map((qt, qi) => (
                        <div key={qi} style={{ fontSize: 12, color: "var(--fg-faint)", marginTop: 6, fontStyle: "italic" }}>“{qt}”</div>
                      ))}
                    </div>
                  ))}
                </div>
              </div>
            ) : null}

            {(results.crosstab_attributes || []).length ? (
              <div className="card card-pad" style={{ marginTop: 16 }}>
                <div style={{ fontWeight: 600, fontSize: 13.5, marginBottom: 10 }}>Crosstab</div>
                <div className="row gap-2" style={{ flexWrap: "wrap", marginBottom: 12 }}>
                  <select className="input" style={{ width: "auto" }} value={crosstabQ} onChange={(e) => setCrosstabQ(e.target.value)}>
                    <option value="">Question…</option>
                    {(results.questions || []).filter((q) => q.type !== "open").map((q) => <option key={q.question_id} value={q.question_id}>{q.question_id.toUpperCase()} · {String(q.text || "").slice(0, 50)}</option>)}
                  </select>
                  <select className="input" style={{ width: "auto" }} value={crosstabAttr} onChange={(e) => setCrosstabAttr(e.target.value)}>
                    <option value="">By attribute…</option>
                    {results.crosstab_attributes.map((a) => <option key={a} value={a}>{a}</option>)}
                  </select>
                  <button className="btn btn-outline" disabled={!crosstabQ || !crosstabAttr || resultsLoading}
                    onClick={() => loadResults({ crosstabQuestion: crosstabQ, crosstabAttribute: crosstabAttr })}>Run</button>
                </div>
                {results.crosstab && results.crosstab.cells ? (
                  <div style={{ overflowX: "auto" }}>
                    <table className="table" style={{ fontSize: 12.5 }}>
                      <thead>
                        <tr>
                          <th style={{ textAlign: "left", padding: 6 }}>{results.crosstab.attribute}</th>
                          {[...new Set(Object.values(results.crosstab.cells).flatMap((col) => Object.keys(col)))].map((ans) => (
                            <th key={ans} style={{ textAlign: "right", padding: 6 }}>{ans}</th>
                          ))}
                        </tr>
                      </thead>
                      <tbody>
                        {Object.entries(results.crosstab.cells).map(([attrVal, col]) => {
                          const answerKeys = [...new Set(Object.values(results.crosstab.cells).flatMap((c) => Object.keys(c)))];
                          return (
                            <tr key={attrVal} style={{ borderTop: "1px solid var(--border)" }}>
                              <td style={{ padding: 6, fontWeight: 600 }}>{attrVal}</td>
                              {answerKeys.map((ans) => <td key={ans} className="mono" style={{ textAlign: "right", padding: 6 }}>{col[ans] || 0}</td>)}
                            </tr>
                          );
                        })}
                      </tbody>
                    </table>
                  </div>
                ) : <div style={{ fontSize: 12.5, color: "var(--fg-faint)" }}>Pick a question and an attribute, then Run.</div>}
              </div>
            ) : null}
          </>
        )
      ) : null}

      {error ? <div style={{ color: "var(--mk-danger)", marginTop: 14, fontSize: 13 }}>{error}</div> : null}
    </div>
  );
};

Object.assign(window, { ResearchPage });
