/* global React, window */
// =========================================================
// Calculator — the sticky checkout for a study, as a third column.
//
// It rides the right edge of the workspace: the receipt is pinned to the top of
// the rail no matter how far the brief scrolls, so cost and time are never
// something you scroll back up to find. Audience sets the envelope; timeline
// stays the user's lever; cost and panel mix answer to it live. Open, the rail
// carries the whole arithmetic, including what this would cost with real people
// only. Launch lives in the study header row, not here.
// =========================================================
const { useState: useStateCC, useEffect: useEffectCC, useRef: useRefCC } = React;

// The three depths, re-expressed as the variable the user actually controls.
const STOPS = [
  { id: "directional", label: "Today", when: "~2 hours", days: 0.1 },
  { id: "decision", label: "~48 hours", when: "~48 hours", days: 2 },
  { id: "defensible", label: "Two weeks", when: "8–12 days", days: 11 }
];
const stopOf = (tierId) => STOPS.find(s => s.id === tierId) || STOPS[1];
// Respondents per segment at each stop. Sample is a consequence of how many
// groups you want to read apart, never a number someone types.
const SEG_N = { directional: 120, decision: 200, defensible: 320 };
const RECRUIT_DAYS = 4;   // a group we've never asked has to be found first
const READ_FLOOR = 60;    // below this a group isn't worth reading on its own

function quote(segments, tierId, opts) {
  const o = opts || {};
  const stop = stopOf(tierId);
  const spec = window.specOf(tierId);
  // Dropping an audience is a trade you can take back, so it filters the quote
  // rather than deleting the segment — toggling it off restores the row.
  const dropped = o.dropSegs || [];
  // Panel make-up: Digital Twins, a hybrid mix, or a recruited human panel. Left
  // on "auto" the mix is derived from how well we know each group; the other two
  // pin it, and confidential assets always route to Twins.
  const panel = o.panel || "auto";
  const list = (segments || []).filter(s => !dropped.includes(s.id)).map((s, i) => {
    const cov = window.segCoverage(s);
    const derived = window.FW.shareFor(tierId, cov.strength);
    const share = o.confidential || panel === "twin" ? 100 : panel === "human" ? 0 : derived;
    // Merging adjacent markets halves the second market cell rather than
    // dropping the read entirely — that's the real-world move, not a deletion.
    const weight = (o.mergeMarkets && s.group === "market" && i > 0) ? 0.5 : 1;
    const n = Math.round(SEG_N[tierId] * (s.bundle || 1) * weight);
    const twinN = Math.round(n * share / 100);
    let humanN = n - twinN;
    // Relaxing the income quota doesn't change who answers, it changes how hard
    // they are to find — so it lands on human cost and field time.
    const credits = Math.round(humanN * (o.dropIncome ? 0.86 : 1) + twinN * 0.1);
    const recruiting = cov.needsRecruiting && humanN > 0;
    return { seg: s, cov, share, n, twinN, humanN, credits, recruiting,
      organicCredits: n, readable: n >= READ_FLOOR };
  });
  const credits = list.reduce((a, r) => a + r.credits, 0) + spec.base;
  const n = list.reduce((a, r) => a + r.n, 0);
  const twinN = list.reduce((a, r) => a + r.twinN, 0);
  const twin = n ? Math.round(100 * twinN / n) : 0;
  const recruiting = list.some(r => r.recruiting);
  let days = stop.days + (recruiting ? RECRUIT_DAYS : 0) - (o.dropIncome ? 2 : 0);
  if (panel === "human" && !recruiting) days += RECRUIT_DAYS;
  if (panel === "twin" || o.confidential) days = Math.min(days, 0.4);
  days = Math.max(0.1, days);
  // The comparison that makes the case: the same design, real people only.
  const organicCredits = list.reduce((a, r) => a + r.organicCredits, 0) + spec.base;
  const organicDays = Math.max(8, days + 7);
  return { list, credits, n, twin, twinN, humanN: n - twinN, panel, days, recruiting, spec, stop,
    mode: twin >= 85 ? "Digital Twin panel" : twin >= 50 ? "Twin-led hybrid panel" : twin >= 15 ? "Human-led hybrid panel" : "Human respondent panel",
    organicCredits, organicDays,
    savedCredits: Math.max(0, organicCredits - credits),
    savedDays: Math.max(0, Math.round(organicDays - days)),
    thin: list.filter(r => !r.readable).length };
}
const fmtDays = (d) => d < 1 ? "Today" : d < 2.5 ? "~48 hours" : Math.round(d) + " days";

// Substitutions are the manual juggling a CSM does by hand today — surfaced as
// offers with their price attached. Each one is a toggle: the list never changes
// shape, so a trade can be taken back to compare against.
function substitutions(q, state, api) {
  const out = [];
  const dropped = (state.dropSegs || []);
  // The two levers that actually move the clock lead the list: which questions
  // need real people, and which audience is hardest to field. Both are named.
  // Questions are grounded in what this study asks — the same tier-seeded set the
  // Questions cards render — not the whole twin-gap catalog.
  const pool = [...(window.AUDIENCE_ATTRS || []), ...(window.MORE_ATTRS || [])];
  const roleOf = (a) => a.role || "filter";
  const readTier = state.tier === "directional" ? (state.tierBefore || "decision") : state.tier;
  const spec = window.specOf ? window.specOf(readTier) : null;
  const asked = spec ? [
    ...pool.filter(a => roleOf(a) === "filter").slice(0, spec.filters),
    ...pool.filter(a => roleOf(a) === "product").slice(0, Math.max(3, spec.filters - 1))
  ] : [];
  const humanQs = asked.filter(a => window.FW && window.FW.needsHuman(a.id, readTier));
  if (humanQs.length) {
    const listed = humanQs.slice(0, 3).map(a => a.name.toLowerCase());
    out.push({
      id: "cuthuman", on: state.tier === "directional",
      label: "Cut the " + humanQs.length + " question" + (humanQs.length === 1 ? "" : "s") + " that need real people",
      detail: listed.join(", ") + (humanQs.length > listed.length ? " and " + (humanQs.length - listed.length) + " more" : "") + " can only be answered live — they're the reason the study waits on panel. Cut them and the twins answer the rest today.",
      win: "−" + RECRUIT_DAYS + " days",
      apply: () => {
        if (state.tier === "directional") api.setTier(state.tierBefore || "decision");
        else { api.set({ tierBefore: state.tier }); api.setTier("directional"); }
      }
    });
  }
  // The audience lever: the group we'd have to recruit if there is one, otherwise
  // the one the twins cover least — the audience that costs the most panel. Ranked
  // at readTier so cutting the human questions (which sends every group to 100%
  // twins) doesn't make the audience trade vanish mid-comparison.
  const all = quote(state.segments, readTier, { ...state, dropSegs: [] }).list;
  const rec = all.find(r => window.segCoverage(r.seg).needsRecruiting);
  const hardest = rec || all.slice().sort((a, b) => (a.cov.strength - b.cov.strength) || (b.humanN - a.humanN))[0];
  if (rec) out.push({
    id: "recruit", on: dropped.includes(rec.seg.id),
    label: "Drop " + rec.seg.name + " — the audience we'd have to recruit",
    detail: "We've never asked this group, so they have to be found and screened from scratch — that's where the " + RECRUIT_DAYS + " days and most of the cost sit. Every other audience in this study is already on hand. Keep them if the finding is why you're losing people.",
    win: "−" + RECRUIT_DAYS + " days",
    apply: () => api.toggleSeg(rec.seg.id)
  });
  else if (hardest && all.length > 1) out.push({
    id: "hardaud", on: dropped.includes(hardest.seg.id),
    label: "Drop " + hardest.seg.name + " — the audience hardest to field",
    detail: "We know this group least well, so " + hardest.humanN.toLocaleString() + " of their " + hardest.n.toLocaleString() + " responses have to come from real people — more than any other audience here. Dropping them shortens the field and leaves the rest on twins.",
    win: "−" + hardest.humanN.toLocaleString() + " panel responses",
    apply: () => api.toggleSeg(hardest.seg.id)
  });
  const jp = all.find(r => /japan/i.test(r.seg.name));
  if (jp) out.push({
    id: "income", on: !!state.dropIncome,
    label: "Relax the income quota on " + jp.seg.name,
    detail: "Hardest quota to fill in this market. Dropping it fields faster and cheaper — you keep the market, you lose the affluence screen.",
    win: "−2 days",
    apply: () => api.set({ dropIncome: !state.dropIncome })
  });
  const markets = all.filter(r => r.seg.group === "market");
  if (markets.length >= 2) out.push({
    id: "markets", on: !!state.mergeMarkets,
    label: "Read " + markets[0].seg.name + " and " + markets[1].seg.name + " together",
    detail: "You lose the market-by-market split but keep the overall call. Worth it unless the difference between them is the decision.",
    win: "−" + Math.abs(quote(state.segments, state.tier, { ...state, mergeMarkets: false }).n - quote(state.segments, state.tier, { ...state, mergeMarkets: true }).n).toLocaleString() + " respondents",
    apply: () => api.set({ mergeMarkets: !state.mergeMarkets })
  });
  return out.slice(0, 4);
}

// Narrow column, so a line item stacks instead of tabulating: who, then what
// they cost, then how they're made up.
function Row({ r, ready }) {
  // r is null while the audience itself is still being drafted: the row shows its
  // shape so the block has a length, but names nothing it doesn't know.
  if (!r) return (
    <div className="cc-row">
      <div className="cc-row__hd">
        <span className="cc-row__mark cc-row__mark--wait" />
        <b className="ix-skb ix-skb--nm" />
      </div>
      <em className="cc-row__mx"><span className="ix-skb ix-skb--mx" /></em>
    </div>
  );
  return (
    <div className="cc-row">
      <div className="cc-row__hd">
        <span className="cc-row__mark" style={{ background: r.seg.color }}>{r.seg.mark}</span>
        <b>{r.seg.name}</b>
      </div>
      {/* The row carries its own make-up: how many of its responses are modeled
          and how many come from recruited people. */}
      {ready ? (
        <span className="cc-mixbar"><i style={{ width: r.share + "%" }} /></span>
      ) : (
        <span className="cc-mixbar is-wait" />
      )}
    </div>
  );
}

function Calculator({ segments, tier, onTier, opts, onOpts, onRemoveSegment, onOpenShelf, onGo, goLabel, goDisabled, published, busy, phase, step, noProductAssets, onPreview }) {
  const state = { segments, tier, ...(opts || {}) };
  const q = quote(segments, tier, state);
  // Cost follows from the questions, so the panel doesn't exist until they do: the
  // rail holds its column empty while the brief drafts, then the card appears and
  // works its numbers out in front of you. The clock is latched on the draft
  // FINISHING (and on a later audience arriving), never on the brief still being
  // mid-draft — the drafted groups and the "ready" phase land in one React commit,
  // so that condition is already false the first render there is anything to price.
  const ph = phase || "ready";
  const drafting = ph !== "ready";
  const sig = q.list.map(r => r.seg.id).join(",");
  const [rev, setRev] = useStateCC(drafting ? 0 : 99);
  const prevSig = useRefCC(sig);
  const wasDrafting = useRefCC(drafting);
  useEffectCC(() => {
    const finished = !drafting && wasDrafting.current;
    const arrived = !drafting && sig && !prevSig.current;
    prevSig.current = sig; wasDrafting.current = drafting;
    if (finished || arrived) setRev(0);
  }, [sig, drafting]);
  useEffectCC(() => {
    if (drafting || rev >= 5) return;
    const t = setTimeout(() => setRev(r => r + 1), 300);
    return () => clearTimeout(t);
  }, [drafting, sig, rev]);
  const st = rev;
  const pending = false;
  const rowReady = (i) => !pending && st >= i + 1;
  const sampleReady = !pending && st >= 3;
  const timeReady = !pending && st >= 4;
  const costReady = !pending && st >= 5;
  const rows = (pending || (drafting && !q.list.length)) ? [null, null, null] : q.list;
  const api = {
    set: (patch) => onOpts && onOpts({ ...(opts || {}), ...patch }),
    setTier: (t) => onTier && onTier(t),
    toggleSeg: (id) => {
      const cur = (opts && opts.dropSegs) || [];
      const next = cur.includes(id) ? cur.filter(x => x !== id) : [...cur, id];
      onOpts && onOpts({ ...(opts || {}), dropSegs: next });
    },
    removeSegment: onRemoveSegment
  };
  const subs = substitutions(q, state, api);
  const [trades, setTrades] = useStateCC(false);
  return (
    <aside className="cc-rail ix-font" aria-label="Time and cost">
      <div className="cc-card">
      {/* The commit row leads the card: what you're about to do sits above the
          audience, timeline and mix it commits to. */}
      <div className="cc-foot">
        {published ? (
          <span className="cc-live cc-live--wide"><i />Live</span>
        ) : (
          <div className="cc-footrow">
            <button className="cc-preview" type="button" onClick={onPreview}>Review / Edit</button>
            <span className={"cc-go-wrap" + (noProductAssets ? " has-tip" : "")} data-tip={noProductAssets ? "No asset uploaded yet" : null}>
              <button className={"cc-go" + (noProductAssets ? " cc-go--wait" : "")} onClick={onGo} disabled={goDisabled || !costReady}>{goLabel || "Publish"}
                <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" /></svg>
              </button>
            </span>
          </div>
        )}
      </div>
      <div className="cc-top">
        <div className="cc-top__ey">TIMELINE &amp; DECISION TYPE</div>
        <div className="cc-figs">
          {/* Time is the lever, so it sits on the figure it changes — and the trades
              that buy time or spend it sit with it, not in a section of their own. */}
          {/* The milestone IS the timeline: what the decision has to survive sets
              both how deep the read goes and how long it takes. Same three calls
              as the composer, so the rail and the brief speak one language. */}
          <div className={"cc-mile" + (timeReady ? "" : " is-wait")} role="tablist">
            {STOPS.map(s => {
              const spec = window.specOf(s.id);
              const on = s.id === tier;
              return (
                <button key={s.id} role="tab" aria-selected={on} disabled={!timeReady} title={spec.use}
                  className={"cc-mile__b" + (on ? " is-on" : "")} onClick={() => timeReady && onTier(s.id)}>
                  <span className="cc-mile__dots" aria-hidden="true">
                    {[0, 1, 2].map(i => <i key={i} className={i < window.TIER_LEVEL[s.id] ? "is-on" : ""} />)}
                  </span>
                  <span className="cc-mile__txt">
                    <span className="cc-mile__lab">{spec.label}</span>
                  </span>
                  <span className="cc-mile__when">{spec.delivery}</span>
                </button>
              );
            })}
          </div>
          {timeReady && q.recruiting && <div className="cc-mile__note">Includes ~{RECRUIT_DAYS} days to recruit an audience we haven't asked before.</div>}
        </div>
        <div className="cc-aud-sec cc-aud-sec--in">
          <div className="cc-top__hd">
            <div className="cc-top__ey cc-top__ey--sub">Audience</div>
            <div className="cc-aud__tot"><span>Total respondents</span>{sampleReady ? <b className="cc-land">{q.n.toLocaleString()}</b> : <b className="ix-skb ix-skb--fig" />}</div>
          </div>
          {sampleReady ? (
            <div className="cc-aud__split">
              <span className="cc-aud__split-i cc-aud__split-i--twin" /> {q.twinN.toLocaleString()} Digital Twins
              <span className="cc-aud__split-i cc-aud__split-i--human" /> {q.humanN.toLocaleString()} people
            </div>
          ) : (
            <div className="cc-aud__split"><span className="ix-skb ix-skb--mx" /></div>
          )}
          <div className="cc-rows">
            {rows.map((r, i) => <Row key={r ? r.seg.id : "wait" + i} r={r} ready={rowReady(i)} />)}
          </div>
        </div>
        <div className="cc-figs">
          {costReady && subs.length > 0 && (
            <div className="cc-trades">
              <button className="cc-fold" onClick={() => setTrades(v => !v)} aria-expanded={trades}>
                <span>How to get the results faster</span>
                <i>({subs.length})</i>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><polyline points={trades ? "18 15 12 9 6 15" : "6 9 12 15 18 9"} /></svg>
              </button>
              {trades && (
                <>
                  <div className="cc-subs">
                    {subs.map(s => (
                      <div key={s.id} className={"cc-sub" + (s.on ? " is-on" : "")}>
                        <div className="cc-sub__t">{s.label}</div>
                        <div className="cc-sub__d">{s.detail}</div>
                        <div className="cc-sub__f">
                          <span className="cc-sub__w">{s.win}</span>
                          <button className={"cc-sub__a" + (s.on ? " is-on" : "")} onClick={s.apply} aria-pressed={!!s.on}>
                            <span className="cc-sub__sw" aria-hidden="true"><i /></span>
                            {s.on ? "Applied" : "Apply"}
                          </button>
                        </div>
                      </div>
                    ))}
                  </div>
                  {(opts && (opts.dropIncome || opts.mergeMarkets || (opts.dropSegs || []).length)) && (
                    <button className="cc-reset" onClick={() => onOpts({})}>Undo the trades I've applied</button>
                  )}
                </>
              )}
            </div>
          )}

        </div>
        {published && <span className="cc-live"><i />Live</span>}
      </div>
      </div>
    </aside>
  );
}

Object.assign(window, { Calculator, quoteStudy: quote, CALC_STOPS: STOPS });
