/* global window */
// Sales Memory — the browser-side strategic intelligence engine.
//
// The forecast cockpit answers "which deals are at risk this quarter." This
// module answers the questions a level up — what the whole book is telling the
// org manager:
//   • What are we good at?            (win patterns by segment / rep / stage)
//   • Where do we spend marketing $?  (channel / persona / industry / angle ROI)
//   • Should we hire more AE / BDR?    (coverage + capacity math)
//   • Where are we leaking?           (loss reasons, forecast theatre, stalls)
//
// It ports the analytics that already live Node-side in
// slack-app/platform-data.js (cohortStats / repAnalysis / orgAnalysis /
// winLoss / pipelineSummary) so they run in the browser over the same window
// globals the web app renders — the numbers never diverge from the book.
//
// Two consumers:
//   buildSalesMemory()      → the curated board (4 themes of insight cards)
//   buildStrategyContext()  → the grounding snapshot POSTed to the Claude
//                             strategist (api/strategist-agent.js) so it can
//                             answer ANY open question, not a fixed menu
//   answerStrategy(q)       → deterministic offline fallback over the same data
//
// All data is synthetic seed data — this is the prototype's design source of
// truth, not a live system.

(function () {
  const FE = window.ForecastEngine;
  if (!FE) {
    // forecast-engine.jsx must load first (see index.html order). Fail soft.
    window.SalesMemory = { unavailable: true };
    return;
  }

  const REPS = window.REPS || [];
  const SEED_DEALS = window.SEED_DEALS || [];
  const OI = window.OPENINGS_INSIGHTS || null;
  const COHORT = FE.BACKTEST_COHORT || [];
  const MODEL = FE.defaultModel();
  const PERIOD = FE.computePeriod(MODEL.cadence.type, MODEL.cadence.fyStartMonth, MODEL.cadence.offset);

  // ---------- assumptions (the only non-derived inputs, clearly labelled) ----------
  const AE_CAP = 12;          // active open opps one AE can carry well
  const TARGET_COVERAGE = 3;  // healthy weighted-pipeline ÷ gap ratio

  // ---------- formatting (mirrors platform-data.js) ----------
  const money = (v) => {
    const n = Number(v) || 0;
    const s = Math.abs(n) >= 1e6 ? `$${(Math.abs(n) / 1e6).toFixed(2)}M` : `$${Math.round(Math.abs(n) / 1000)}K`;
    return n < 0 ? `-${s}` : s;
  };
  const pct = (v) => `${Math.round((Number(v) || 0) * 100)}%`;
  const isOpen = (d) => !/^closed/i.test(d.stage || "");
  const isWon = (d) => /closed won/i.test(d.stage || "");
  const isLost = (d) => /closed lost/i.test(d.stage || "");

  // ---------- segment tag (zero deal edits — keyword over the company name) ----------
  // Covers all 26 BACKTEST_COHORT companies for free; the override map pins the
  // handful of live SEED_DEALS whose names are ambiguous.
  const SEGMENT_RULES = [
    [/logistic|freight|tidewater|stellaris/i, "Logistics"],
    [/health|bio|medica|boreal|marrow/i, "Healthcare"],
    [/bank|capital|insur|trading|argent|sablefin|polaris|ironpeak|harbor/i, "Financial services"],
    [/media|lyric|cypress/i, "Media"],
    [/robot|manufactur|industr|onyx|castor|vandermark|apex/i, "Manufacturing"],
    [/energy|climate|esg|cobalt/i, "Energy / ESG"],
    [/retail|foods|cosmetic|kindred|ember|solene|solstice/i, "Retail / CPG"],
    [/educ|helix/i, "Education"],
    [/cloud|saas|apps|tools|veridian|pinecrest|mercia|halcyon|ridgeline/i, "Tech / SaaS"],
  ];
  const SEGMENT_OVERRIDES = {
    atlas: "Logistics",
    techstart: "Tech / SaaS",
    global: "Tech / SaaS",
    acme: "Manufacturing",
    meridian: "Financial services",
    harbor: "Financial services",
  };
  function segmentOf(nameOrDeal) {
    if (nameOrDeal && typeof nameOrDeal === "object") {
      if (nameOrDeal.id && SEGMENT_OVERRIDES[nameOrDeal.id]) return SEGMENT_OVERRIDES[nameOrDeal.id];
      nameOrDeal = nameOrDeal.company || "";
    }
    const s = String(nameOrDeal || "");
    for (const [re, seg] of SEGMENT_RULES) if (re.test(s)) return seg;
    return "Other";
  }

  // ---------- deal ownership (seed book isn't rep-tagged) ----------
  const OWNERSHIP = {
    priya: ["global", "startupco", "vector", "quantum", "ironbridge"],
    marco: ["horizon", "aurora", "bluefish", "pinnacle", "min-armees"],
    jen: ["northpoint", "pacific", "skyline", "verdant", "rooted", "vertex"],
    ridha: ["atlas", "techstart", "meridian", "cobalt", "harbor", "idf-citoyens"],
  };
  const ownerOf = (id) => {
    for (const rep of Object.keys(OWNERSHIP)) if (OWNERSHIP[rep].includes(id)) return rep;
    return "ridha";
  };
  function resolveRep(q) {
    if (!q) return null;
    const s = String(q).toLowerCase().trim();
    if (["me", "you", "my", "i", "myself"].includes(s)) return REPS.find((r) => r.id === "ridha") || null;
    return (
      REPS.find((r) => r.id === s) ||
      REPS.find((r) => r.name.toLowerCase().includes(s)) ||
      REPS.find((r) => r.name.toLowerCase().split(/\s+/)[0] === s) ||
      null
    );
  }

  // ---------- cohort win/loss (historic) ----------
  function cohortStats(cohortKey) {
    const rows = COHORT.filter((r) => r.aeName === cohortKey);
    const wins = rows.filter((r) => r.outcome === 1);
    const losses = rows.filter((r) => r.outcome === 0);
    const avg = (arr) => (arr.length ? Math.round(arr.reduce((s, x) => s + x.value, 0) / arr.length) : 0);
    const reasons = {};
    losses.forEach((l) => (l.evidenceMissing || []).forEach((e) => { reasons[e] = (reasons[e] || 0) + 1; }));
    const lossReasons = Object.entries(reasons).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k} (${n})`);
    const byStage = {};
    rows.forEach((r) => {
      byStage[r.stage] = byStage[r.stage] || { n: 0, won: 0 };
      byStage[r.stage].n++;
      if (r.outcome === 1) byStage[r.stage].won++;
    });
    return {
      sample: rows.length,
      winRate: rows.length ? wins.length / rows.length : 0,
      wins: wins.length,
      losses: losses.length,
      avgWonDeal: avg(wins),
      avgDealSize: avg(rows),
      lossReasons,
      byStage: Object.entries(byStage).map(([stage, s]) => ({ stage, winRate: s.won / s.n, n: s.n })),
    };
  }

  // ---------- per-segment win/loss (the "by segment" answer) ----------
  function segmentStats() {
    const bySeg = {};
    COHORT.forEach((r) => {
      const seg = segmentOf(r.company);
      bySeg[seg] = bySeg[seg] || { seg, rows: [], wins: 0 };
      bySeg[seg].rows.push(r);
      if (r.outcome === 1) bySeg[seg].wins++;
    });
    return Object.values(bySeg)
      .map((s) => {
        const wonRows = s.rows.filter((r) => r.outcome === 1);
        return {
          segment: s.seg,
          sample: s.rows.length,
          wins: s.wins,
          losses: s.rows.length - s.wins,
          winRate: s.rows.length ? s.wins / s.rows.length : 0,
          avgWonDeal: wonRows.length ? Math.round(wonRows.reduce((a, r) => a + r.value, 0) / wonRows.length) : 0,
        };
      })
      .sort((a, b) => b.winRate - a.winRate || b.sample - a.sample);
  }

  // ---------- per-rep pipeline + quota analysis ----------
  function repOpenDeals(repId) {
    return SEED_DEALS.filter((d) => isOpen(d) && ownerOf(d.id) === repId).map((d) => FE.calcDeal(d, MODEL));
  }
  function repAnalysisRaw(rep) {
    const co = cohortStats(rep.cohortKey);
    const open = repOpenDeals(rep.id);
    const weightedPipeline = open.reduce((s, r) => s + r.nudgeAdjustedAmount, 0);
    const rawPipeline = open.reduce((s, r) => s + r.declaredAmount, 0);
    const gap = Math.max(0, rep.quota - rep.attained);
    const avgWon = co.avgWonDeal || 180000;
    const winRate = co.winRate || 0.4;
    const dealsNeeded = gap > 0 ? Math.ceil(gap / avgWon) : 0;
    const dealsToWork = gap > 0 ? Math.ceil(dealsNeeded / winRate) : 0;
    const projected = rep.attained + weightedPipeline;
    const covers = projected >= rep.quota;
    const weeksLeft = Math.max(1, Math.ceil(PERIOD.daysRemaining / 7));
    return {
      rep, co, open, weightedPipeline, rawPipeline, gap, avgWon, winRate,
      dealsNeeded, dealsToWork, projected, covers, weeksLeft,
    };
  }
  function repAnalysis(query) {
    const rep = resolveRep(query);
    if (!rep) return { error: `No rep matching "${query}". Try: ${REPS.map((r) => r.name).join(", ")}.` };
    const a = repAnalysisRaw(rep);
    const verdict = rep.attained >= rep.quota ? "Quota hit"
      : a.covers ? "On track — weighted pipeline covers the gap"
      : `At risk — short by ${money(rep.quota - a.projected)} even after risk-adjusting pipeline`;
    return {
      rep: rep.name, role: rep.role, period: PERIOD.name, days_remaining: PERIOD.daysRemaining,
      quota: money(rep.quota), attained: money(rep.attained), attainment_pct: pct(rep.attained / rep.quota),
      gap_to_quota: money(a.gap),
      open_pipeline: money(a.rawPipeline), weighted_pipeline: money(a.weightedPipeline),
      projected_close: money(a.projected), verdict,
      historic_win_rate: pct(a.winRate), avg_won_deal: money(a.avgWon), cohort_sample: a.co.sample,
      deals_needed: a.dealsNeeded, deals_to_work: a.dealsToWork,
      pace_required: a.gap > 0
        ? `${a.dealsToWork} active opps over ${a.weeksLeft} weeks (~${(a.dealsToWork / a.weeksLeft).toFixed(1)}/wk) to land ${a.dealsNeeded} wins`
        : "Quota already covered",
      open_deal_count: a.open.length,
    };
  }

  function orgAnalysis() {
    const reps = REPS.map((r) => ({ id: r.id, ...repAnalysis(r.id) }));
    const totalQuota = REPS.reduce((s, r) => s + r.quota, 0);
    const totalAttained = REPS.reduce((s, r) => s + r.attained, 0);
    const totalWeighted = REPS.reduce((s, r) => s + repOpenDeals(r.id).reduce((a, x) => a + x.nudgeAdjustedAmount, 0), 0);
    const gap = Math.max(0, totalQuota - totalAttained);
    return {
      period: PERIOD.name, days_remaining: PERIOD.daysRemaining,
      org_quota: money(totalQuota), org_attained: money(totalAttained), org_attainment_pct: pct(totalAttained / totalQuota),
      org_gap: money(gap), org_weighted_pipeline: money(totalWeighted),
      org_projected: money(totalAttained + totalWeighted),
      org_verdict: totalAttained + totalWeighted >= totalQuota ? "Team on track" : `Team short by ${money(totalQuota - totalAttained - totalWeighted)}`,
      by_rep: reps.map((r) => ({ rep: r.rep, attainment: r.attainment_pct, gap: r.gap_to_quota, verdict: r.verdict })),
      // raw numbers for downstream math
      _raw: { totalQuota, totalAttained, totalWeighted, gap },
    };
  }

  function winLoss(query) {
    if (query && resolveRep(query)) {
      const rep = resolveRep(query);
      const c = cohortStats(rep.cohortKey);
      return { rep: rep.name, sample: c.sample, win_rate: pct(c.winRate), avg_won_deal: money(c.avgWonDeal), avg_deal_size: money(c.avgDealSize), top_loss_reasons: c.lossReasons };
    }
    const rows = COHORT;
    const wins = rows.filter((r) => r.outcome === 1);
    const reasons = {};
    rows.filter((r) => r.outcome === 0).forEach((l) => (l.evidenceMissing || []).forEach((e) => { reasons[e] = (reasons[e] || 0) + 1; }));
    return {
      scope: "team", sample: rows.length, win_rate: pct(wins.length / (rows.length || 1)),
      avg_won_deal: money(Math.round(wins.reduce((s, r) => s + r.value, 0) / (wins.length || 1))),
      top_loss_reasons: Object.entries(reasons).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k} (${n} losses)`),
      by_rep: REPS.map((r) => { const c = cohortStats(r.cohortKey); return { rep: r.name, win_rate: pct(c.winRate), avg_won_deal: money(c.avgWonDeal), sample: c.sample }; })
        .sort((a, b) => parseInt(b.win_rate) - parseInt(a.win_rate)),
    };
  }

  function pipelineSummary() {
    const sum = FE.summarize(SEED_DEALS, MODEL);
    const open = SEED_DEALS.filter(isOpen);
    const byStage = {};
    open.forEach((d) => { byStage[d.stage] = byStage[d.stage] || { n: 0, value: 0 }; byStage[d.stage].n++; byStage[d.stage].value += (d.value || 0); });
    const won = SEED_DEALS.filter(isWon), lost = SEED_DEALS.filter(isLost);
    return {
      period: PERIOD.name,
      open_deals: open.length, open_pipeline: money(open.reduce((s, d) => s + (d.value || 0), 0)),
      crm_weighted_forecast: money(sum.crmWeightedForecast),
      nudge_adjusted_forecast: money(sum.nudgeAdjustedForecast),
      forecast_theatre_gap: `${money(sum.forecastGap)} (${sum.forecastGapPercentage}% of declared looks inflated vs evidence)`,
      commit_at_risk: money(sum.commitAtRisk),
      by_stage: Object.entries(byStage).map(([stage, s]) => ({ stage, deals: s.n, value: money(s.value) })),
      closed_won: { count: won.length, value: money(won.reduce((s, d) => s + (d.value || 0), 0)) },
      closed_lost: { count: lost.length, value: money(lost.reduce((s, d) => s + (d.value || 0), 0)) },
      // raw for math / cards
      _raw: { commitAtRisk: sum.commitAtRisk, forecastGap: sum.forecastGap, forecastGapPercentage: sum.forecastGapPercentage },
      // riskiest at-risk deals (Commit that lost its Commit call) — for the leak CTA
      at_risk_deals: sum.rows
        .filter((r) => r.declaredCategory === "Commit" && r.recommendedCategory !== "Commit")
        .map((r) => ({ id: r.id, company: r.company })),
    };
  }

  // ---------- hiring recommendation ----------
  function hiringRecommendation() {
    const org = orgAnalysis()._raw;
    const orgGap = org.gap;
    const weightedPipeline = org.totalWeighted;
    const coverageRatio = orgGap > 0 ? weightedPipeline / orgGap : Infinity;
    const dealsToWorkTotal = REPS.reduce((s, r) => s + repAnalysisRaw(r).dealsToWork, 0);
    const capacityGap = dealsToWorkTotal - REPS.length * AE_CAP;
    const hireAE = capacityGap > 0 ? Math.ceil(capacityGap / AE_CAP) : 0;

    // BDR / sourcing side
    const funnel = OI && OI.funnel ? OI.funnel : [];
    const sourced = funnel.find((f) => f.key === "Sourced");
    const graduated = funnel.find((f) => f.key === "Graduated");
    const graduationRate = sourced && sourced.count ? (graduated ? graduated.count : 0) / sourced.count : 0;
    const sourcedRunRate = OI && OI.totals ? OI.totals.sourced : 0; // $ sourced this period
    const pipelineNeeded = Math.max(0, TARGET_COVERAGE * orgGap - weightedPipeline);
    const hireBDR = sourcedRunRate > 0 ? Math.min(2, Math.max(0, Math.ceil(pipelineNeeded / sourcedRunRate))) : 0;

    const verdict = hireAE > 0
      ? `Hire ${hireAE} AE — team is coverage-constrained (${coverageRatio.toFixed(1)}x vs ${TARGET_COVERAGE}x healthy) and carrying ${dealsToWorkTotal} deals-to-work against ~${REPS.length * AE_CAP} of capacity.`
      : hireBDR > 0
      ? `Hold on AE hiring, add ${hireBDR} BDR — closers have capacity, but pipeline needs ${money(pipelineNeeded)} more to hit ${TARGET_COVERAGE}x coverage.`
      : "No new hire needed this period — coverage and capacity are both healthy.";

    return {
      hireAE, hireBDR,
      coverageRatio: coverageRatio === Infinity ? null : Number(coverageRatio.toFixed(2)),
      orgGap, weightedPipeline, dealsToWorkTotal, aeCapacity: REPS.length * AE_CAP,
      sourcedRunRate, graduationRate: Number((graduationRate).toFixed(3)),
      pipelineNeeded, verdict,
      evidence: [
        { label: "Coverage", value: coverageRatio === Infinity ? "—" : `${coverageRatio.toFixed(1)}x (target ${TARGET_COVERAGE}x)` },
        { label: "Gap to quota", value: money(orgGap) },
        { label: "Deals to work", value: `${dealsToWorkTotal} vs ~${REPS.length * AE_CAP} capacity` },
        { label: "Opener sourced", value: `${money(sourcedRunRate)} · ${pct(graduationRate)} graduate` },
      ],
    };
  }

  // ---------- marketing channel ROI (best + cooling) ----------
  function rankDim(rows) {
    if (!rows || !rows.length) return { best: null, cooling: null };
    const best = [...rows].sort((a, b) => (b.meetingRate || 0) - (a.meetingRate || 0))[0];
    const cooling = [...rows].sort((a, b) => (a.delta || 0) - (b.delta || 0))[0];
    return { best, cooling: cooling && cooling.delta < 0 ? cooling : null };
  }

  // ---------- the board: 4 themes of insight cards ----------
  function buildSalesMemory() {
    const themes = [];

    // A — What we're good at
    const segs = segmentStats().filter((s) => s.sample >= 3);
    const topSeg = segs[0];
    const wl = winLoss();
    const repRanked = wl.by_rep;
    const goodCards = [];
    if (topSeg) {
      goodCards.push({
        theme: "good", kicker: "Strongest segment",
        focalValue: pct(topSeg.winRate), focalUnit: "win rate",
        headline: `We win ${topSeg.segment} deals`, tone: "good",
        why: [
          `${topSeg.wins}/${topSeg.sample} closed in ${topSeg.segment}, avg won ${money(topSeg.avgWonDeal)}.`,
          `Highest win rate of any segment with a real sample — this is our proven motion.`,
        ],
        evidence: [
          { label: "Segment", value: topSeg.segment },
          { label: "Win rate", value: `${pct(topSeg.winRate)} (${topSeg.wins}/${topSeg.sample})` },
          { label: "Avg won", value: money(topSeg.avgWonDeal) },
        ],
        cta: {
          label: "Prioritize this in the Opener brief", mode: "ai",
          preview: {
            kicker: "Sales memory → Opener brief",
            title: `Prioritize ${topSeg.segment} in outbound`,
            meta: [
              { label: "WHY", value: `${pct(topSeg.winRate)} win rate, our best-proven segment` },
              { label: "CHANGE", value: "Reweight ICP toward this segment" },
            ],
            body: { label: "Drafted brief update", text: `Reweight the Opener's ICP toward ${topSeg.segment}: it converts at ${pct(topSeg.winRate)} (${topSeg.wins}/${topSeg.sample} closed, avg ${money(topSeg.avgWonDeal)}) — our strongest motion. Bias sourcing and angle selection here first.` },
            evidence: [
              { label: "Win rate", value: `${pct(topSeg.winRate)} (${topSeg.wins}/${topSeg.sample})` },
              { label: "Avg won", value: money(topSeg.avgWonDeal) },
            ],
            primaryLabel: "Update the brief",
          },
          toast: `✓ Opener brief now prioritizes ${topSeg.segment}`,
        },
      });
    }
    if (repRanked && repRanked[0]) {
      const r = repRanked[0];
      goodCards.push({
        theme: "good", kicker: "Top closer",
        focalValue: r.win_rate, focalUnit: "win rate",
        headline: `${r.rep} closes best`, tone: "neutral",
        why: [`${r.rep} carries the team's highest historic win rate (${r.win_rate}, ${r.sample} closed), avg won ${r.avg_won_deal}.`, `Their deal pattern is the one to clone into coaching.`],
        evidence: [
          { label: "Rep", value: r.rep },
          { label: "Win rate", value: `${r.win_rate} (${r.sample} closed)` },
          { label: "Avg won", value: r.avg_won_deal },
        ],
        cta: {
          label: "Share the winning pattern", mode: "ai",
          preview: {
            kicker: "Sales memory → coaching",
            title: `Clone ${r.rep}'s deal pattern`,
            meta: [{ label: "WHY", value: `${r.win_rate} win rate — team best` }],
            body: { label: "Drafted coaching note", text: `${r.rep} closes at ${r.win_rate} (${r.sample} deals, avg ${r.avg_won_deal}). Their winning deals share verified buyer proof early. Package this as the team's reference motion and pair it with the reps carrying open loss patterns.` },
            evidence: [{ label: "Win rate", value: r.win_rate }, { label: "Sample", value: `${r.sample} closed` }],
            primaryLabel: "Send to the team",
          },
          toast: `✓ ${r.rep}'s pattern shared with the team`,
        },
      });
    }
    themes.push({ theme: "good", title: "What we're good at", cards: goodCards });

    // B — Where to spend marketing $
    const spendCards = [];
    if (OI) {
      const angle = rankDim(OI.byAngle);
      const industry = rankDim(OI.byIndustry);
      if (angle.best) {
        spendCards.push({
          theme: "spend", kicker: "Best-converting angle",
          focalValue: pct(angle.best.meetingRate), focalUnit: "meeting rate",
          headline: `Fund the "${angle.best.key}" angle`, tone: "good",
          why: [
            `${pct(angle.best.replyRate)} reply → ${pct(angle.best.meetingRate)} meeting rate, ${angle.best.delta >= 0 ? "+" : ""}${Math.round(angle.best.delta * 100)}pts WoW.`,
            `The clearest signal of where the first beat lands — put budget behind it.`,
          ],
          evidence: [
            { label: "Angle", value: angle.best.key },
            { label: "Meeting rate", value: pct(angle.best.meetingRate) },
            { label: "WoW", value: `${angle.best.delta >= 0 ? "+" : ""}${Math.round(angle.best.delta * 100)}pts` },
          ],
          cta: {
            label: "Shift the Opener toward this angle", mode: "ai",
            preview: {
              kicker: "Sales memory → Opener",
              title: `Weight outbound toward "${angle.best.key}"`,
              meta: [{ label: "WHY", value: `${pct(angle.best.meetingRate)} meeting rate, best on the board` }],
              body: { label: "Drafted change", text: `Increase send-share on the "${angle.best.key}" angle — it converts at ${pct(angle.best.meetingRate)} to meeting (${pct(angle.best.replyRate)} reply), trending ${angle.best.delta >= 0 ? "up" : "down"} ${Math.abs(Math.round(angle.best.delta * 100))}pts WoW. Reallocate from the cooling angles.` },
              evidence: [{ label: "Reply", value: pct(angle.best.replyRate) }, { label: "Meeting", value: pct(angle.best.meetingRate) }],
              primaryLabel: "Reweight outbound",
            },
            toast: `✓ Opener weighted toward "${angle.best.key}"`,
          },
        });
      }
      const cool = angle.cooling || industry.cooling;
      if (cool) {
        spendCards.push({
          theme: "spend", kicker: "Cooling spend",
          focalValue: `${Math.round(cool.delta * 100)}pts`, focalUnit: "WoW",
          headline: `Pull back on "${cool.key}"`, tone: "warn",
          why: [`Reply rate is dropping (${Math.round(cool.delta * 100)}pts WoW) at ${pct(cool.meetingRate)} meeting rate.`, `Reallocate this spend to the growing angle instead of defending a fading one.`],
          evidence: [
            { label: "Segment", value: cool.key },
            { label: "Meeting rate", value: pct(cool.meetingRate) },
            { label: "WoW", value: `${Math.round(cool.delta * 100)}pts` },
          ],
          cta: {
            label: "Draft the reallocation memo", mode: "ai",
            preview: {
              kicker: "Sales memory → budget",
              title: `Cut spend on "${cool.key}"`,
              meta: [{ label: "WHY", value: `${Math.round(cool.delta * 100)}pts WoW, ${pct(cool.meetingRate)} meeting rate` }],
              body: { label: "Drafted memo", text: `"${cool.key}" is cooling — reply rate down ${Math.abs(Math.round(cool.delta * 100))}pts WoW and only ${pct(cool.meetingRate)} converts to meetings. Recommend pausing net-new spend here and moving it to the best-performing angle.` },
              evidence: [{ label: "Meeting rate", value: pct(cool.meetingRate) }, { label: "Trend", value: `${Math.round(cool.delta * 100)}pts WoW` }],
              primaryLabel: "Save the memo",
            },
            toast: "✓ Reallocation memo saved",
          },
        });
      }
    }
    themes.push({ theme: "spend", title: "Where to spend marketing $", cards: spendCards });

    // C — Hire more AE / BDR?
    const hire = hiringRecommendation();
    const hireHeadline = hire.hireAE > 0 ? `Hire ${hire.hireAE} AE` : hire.hireBDR > 0 ? `Hire ${hire.hireBDR} BDR` : "Team is right-sized";
    themes.push({
      theme: "hire", title: "Hire more AE / BDR?", cards: [{
        theme: "hire", kicker: "Capacity read",
        focalValue: hire.coverageRatio == null ? "—" : `${hire.coverageRatio}x`, focalUnit: "coverage",
        headline: hireHeadline, tone: hire.hireAE > 0 ? "warn" : "neutral",
        why: [hire.verdict],
        evidence: hire.evidence,
        cta: {
          label: "Draft the hiring case", mode: "ai",
          preview: {
            kicker: "Sales memory → planning",
            title: hireHeadline === "Team is right-sized" ? "Capacity review" : `Hiring case: ${hireHeadline}`,
            meta: [{ label: "COVERAGE", value: hire.coverageRatio == null ? "—" : `${hire.coverageRatio}x (target ${TARGET_COVERAGE}x)` }, { label: "GAP", value: money(hire.orgGap) }],
            body: { label: "Drafted hiring case", text: `${hire.verdict} Coverage is ${hire.coverageRatio == null ? "healthy" : hire.coverageRatio + "x"} against a ${TARGET_COVERAGE}x target; the team is working ${hire.dealsToWorkTotal} deals against roughly ${hire.aeCapacity} of capacity. The Opener is sourcing ${money(hire.sourcedRunRate)} per period at a ${pct(hire.graduationRate)} graduation rate.` },
            evidence: hire.evidence,
            primaryLabel: "Save the hiring case",
          },
          toast: "✓ Hiring case saved",
        },
      }],
    });

    // D — Where we're leaking
    const ps = pipelineSummary();
    const teamLoss = wl.top_loss_reasons && wl.top_loss_reasons[0];
    const teamByStage = (() => {
      const agg = {};
      COHORT.forEach((r) => { agg[r.stage] = agg[r.stage] || { n: 0, won: 0 }; agg[r.stage].n++; if (r.outcome === 1) agg[r.stage].won++; });
      return Object.entries(agg).map(([stage, s]) => ({ stage, winRate: s.won / s.n, n: s.n })).sort((a, b) => a.winRate - b.winRate)[0];
    })();
    const leakCards = [];
    if (teamLoss) {
      leakCards.push({
        theme: "leak", kicker: "Top loss reason",
        focalValue: teamLoss.replace(/\D+/g, "") || "", focalUnit: "lost deals",
        headline: `Missing "${teamLoss.replace(/\s*\(.*/, "")}"`, tone: "warn",
        why: [`${teamLoss} — the single most common gap on deals we lose.`, `Every open deal without this proof is carrying the same risk.`],
        evidence: (wl.top_loss_reasons || []).slice(0, 3).map((x) => ({ label: "Loss pattern", value: x })),
        cta: {
          label: "Draft the team fix note", mode: "ai",
          preview: {
            kicker: "Sales memory → coaching",
            title: `Close the "${teamLoss.replace(/\s*\(.*/, "")}" gap`,
            meta: [{ label: "PATTERN", value: teamLoss }],
            body: { label: "Drafted team note", text: `Our #1 loss pattern is ${teamLoss}. Make it a required exit criterion: no deal advances past Proposal without this proof on file. This alone is the highest-leverage fix on the board.` },
            evidence: (wl.top_loss_reasons || []).slice(0, 3).map((x) => ({ label: "Pattern", value: x })),
            primaryLabel: "Send the note",
          },
          toast: "✓ Team fix note sent",
        },
      });
    }
    leakCards.push({
      theme: "leak", kicker: "Forecast theatre",
      focalValue: ps.commit_at_risk, focalUnit: "at risk in Commit",
      headline: "Commit without buyer proof", tone: "warn",
      why: [`${ps.commit_at_risk} sits in Commit but the evidence doesn't back the call.`, `Forecast gap: ${ps.forecast_theatre_gap}.`],
      evidence: [
        { label: "Commit at risk", value: ps.commit_at_risk },
        { label: "Theatre gap", value: ps.forecast_theatre_gap },
        teamByStage ? { label: "Weakest stage", value: `${teamByStage.stage} (${pct(teamByStage.winRate)} win)` } : null,
      ].filter(Boolean),
      cta: ps.at_risk_deals.length
        ? { label: "Open the at-risk deals", mode: "self", dealId: ps.at_risk_deals[0].id, dealIds: ps.at_risk_deals.map((d) => d.id) }
        : { label: "Draft the forecast note", mode: "ai", preview: { kicker: "Sales memory → forecast", title: "Defend the commit number", meta: [{ label: "AT RISK", value: ps.commit_at_risk }], body: { label: "Drafted note", text: `${ps.commit_at_risk} of the commit forecast lacks buyer proof (${ps.forecast_theatre_gap}). Pull these deals into a working session before the number is locked.` }, evidence: [{ label: "Commit at risk", value: ps.commit_at_risk }], primaryLabel: "Save the note" }, toast: "✓ Forecast note saved" },
    });
    themes.push({ theme: "leak", title: "Where we're leaking", cards: leakCards });

    return { period: PERIOD.name, generatedFor: "manager", themes };
  }

  // ---------- strategist grounding snapshot (POSTed to the Claude agent) ----------
  function buildStrategyContext() {
    return {
      period: { name: PERIOD.name, days_remaining: PERIOD.daysRemaining },
      org: orgAnalysis(),
      reps: REPS.map((r) => repAnalysis(r.id)),
      win_loss: winLoss(),
      segments: segmentStats().map((s) => ({
        segment: s.segment, sample: s.sample, win_rate: pct(s.winRate), avg_won_deal: money(s.avgWonDeal), wins: s.wins, losses: s.losses,
      })),
      pipeline: pipelineSummary(),
      marketing: OI ? {
        totals: OI.totals,
        funnel: OI.funnel,
        by_channel: OI.byChannel,
        by_persona: OI.byPersona,
        by_industry: OI.byIndustry,
        by_angle: OI.byAngle,
      } : null,
      hiring: hiringRecommendation(),
    };
  }

  // ---------- offline deterministic fallback (announced when no backend) ----------
  function answerStrategy(question) {
    const t = String(question || "").toLowerCase();
    const wl = winLoss();
    const segs = segmentStats().filter((s) => s.sample >= 3);
    if (/best|good at|strength|win.*most|which segment|strong/.test(t)) {
      const s = segs[0];
      return {
        answer: s
          ? `We're strongest in ${s.segment} — ${pct(s.winRate)} win rate over ${s.sample} closed (avg ${money(s.avgWonDeal)}). ${wl.by_rep[0] ? `${wl.by_rep[0].rep} is the top closer at ${wl.by_rep[0].win_rate}.` : ""}`
          : "Not enough closed history yet to call a strongest segment.",
        evidence: s ? [{ label: "Segment", value: s.segment }, { label: "Win rate", value: `${pct(s.winRate)} (${s.wins}/${s.sample})` }, { label: "Avg won", value: money(s.avgWonDeal) }] : [],
        deals: [],
      };
    }
    if (/spend|marketing|budget|channel|angle|source|invest/.test(t) && OI) {
      const angle = rankDim(OI.byAngle);
      return {
        answer: angle.best
          ? `Put the next marketing dollar behind the "${angle.best.key}" angle — ${pct(angle.best.meetingRate)} meeting rate, ${angle.best.delta >= 0 ? "+" : ""}${Math.round(angle.best.delta * 100)}pts WoW.${angle.cooling ? ` Pull it from "${angle.cooling.key}", which is cooling ${Math.round(angle.cooling.delta * 100)}pts.` : ""}`
          : "No outbound signal available.",
        evidence: angle.best ? [{ label: "Best angle", value: angle.best.key }, { label: "Meeting rate", value: pct(angle.best.meetingRate) }] : [],
        deals: [],
      };
    }
    if (/hire|headcount|capacity|bdr|\bae\b|rep\b/.test(t)) {
      const h = hiringRecommendation();
      return { answer: h.verdict, evidence: h.evidence, deals: [] };
    }
    if (/leak|losing|lose|risk|drop|theatre|inflat|why.*lose/.test(t)) {
      const ps = pipelineSummary();
      return {
        answer: `Biggest leak: ${wl.top_loss_reasons[0] || "no clear pattern"}. And ${ps.commit_at_risk} sits in Commit without buyer proof (${ps.forecast_theatre_gap}).`,
        evidence: [{ label: "Top loss", value: wl.top_loss_reasons[0] || "—" }, { label: "Commit at risk", value: ps.commit_at_risk }],
        deals: ps.at_risk_deals.map((d) => d.id),
      };
    }
    if (/quota|forecast|gap|cover|on track/.test(t)) {
      const o = orgAnalysis();
      return { answer: `${o.org_verdict}. Attained ${o.org_attained} of ${o.org_quota} (${o.org_attainment_pct}); weighted pipeline ${o.org_weighted_pipeline}, gap ${o.org_gap}.`, evidence: [{ label: "Attainment", value: o.org_attainment_pct }, { label: "Gap", value: o.org_gap }, { label: "Weighted pipeline", value: o.org_weighted_pipeline }], deals: [] };
    }
    const rep = resolveRep((t.match(/[a-z]+/g) || []).find((w) => resolveRep(w)));
    if (rep) {
      const a = repAnalysis(rep.id);
      return { answer: `${a.rep}: ${a.verdict}. Attained ${a.attained} of ${a.quota} (${a.attainment_pct}), gap ${a.gap_to_quota}. ${a.pace_required}.`, evidence: [{ label: "Attainment", value: a.attainment_pct }, { label: "Gap", value: a.gap_to_quota }, { label: "Win rate", value: a.historic_win_rate }], deals: [] };
    }
    const o = orgAnalysis();
    return { answer: `${o.org_verdict}. Ask me about what we're best at, where to spend marketing dollars, whether to hire, or where the book is leaking.`, evidence: [{ label: "Attainment", value: o.org_attainment_pct }], deals: [] };
  }

  function strategistStarters() {
    return [
      { label: "What are we best at?", text: "What are we best at right now?" },
      { label: "Where to spend?", text: "Where should we spend the next marketing dollar?" },
      { label: "Hire AE or BDR?", text: "Do we need to hire a BDR or an AE?" },
      { label: "Where are we leaking?", text: "Where is the book leaking the most?" },
      { label: "On track to quota?", text: "Is the team on track to quota?" },
      { label: "Who needs help?", text: "Which rep needs help closing the gap?" },
    ];
  }

  window.SalesMemory = {
    cohortStats, segmentStats, repAnalysis, orgAnalysis, winLoss, pipelineSummary,
    segmentOf, hiringRecommendation,
    buildSalesMemory, buildStrategyContext, answerStrategy, strategistStarters,
  };
})();
