// screens-research.jsx
// Tier 2 Phase 1: Research Tab (v2 — Plans architecture)
//
// Module-scoped page. Plans render side-by-side as columns. Inside each Plan,
// 5 sections stack: Weekly Goals / Research Goals / Approval Lists Sent /
// Mailings Planned / Marketing Deck Budget. Plans persist across weeks.
const { useState, useEffect, useMemo, useCallback, useRef } = React;

// ---------- Outreach Playbook cycles ----------
// Outreach Playbook — full 5 cycles from /mnt/user-data/uploads/Onboarding-Dashboard
// "initialEmailCode" marks the step where the first email of the cycle goes out;
// bounce-tracking inputs render on that step's forecast row.
const OUTREACH_CYCLES = {
  M1: {
    label: "Initial Mailing",
    initialEmailCode: "E1",
    steps: [
      { day: 1,  code: "M1", type: "Mailing", owner: "MC" },
      { day: 5,  code: "E1", type: "Email",   owner: "MC" },
      { day: 6,  code: "C1", type: "Call",    owner: "Analyst" },
      { day: 8,  code: "E2", type: "Email",   owner: "MC" },
      { day: 11, code: "E3", type: "Email",   owner: "MC" },
      { day: 12, code: "C2", type: "Call",    owner: "Analyst" },
      { day: 14, code: "E4", type: "Email",   owner: "MC" },
      { day: 15, code: "C3", type: "Call",    owner: "Analyst" },
      { day: 18, code: "E9", type: "Email",   owner: "MC" },
    ],
  },
  eM1: {
    label: "Initial Email",
    initialEmailCode: "eM1",
    steps: [
      { day: 1,  code: "eM1", type: "Email", owner: "MC" },
      { day: 3,  code: "E2",  type: "Email", owner: "MC" },
      { day: 4,  code: "C1",  type: "Call",  owner: "Analyst" },
      { day: 6,  code: "E3",  type: "Email", owner: "MC" },
      { day: 9,  code: "E4",  type: "Email", owner: "MC" },
      { day: 10, code: "C2",  type: "Call",  owner: "Analyst" },
      { day: 13, code: "E9",  type: "Email", owner: "MC" },
    ],
  },
  M2: {
    label: "Re-Approach Mailing",
    initialEmailCode: "E5",
    steps: [
      { day: 1,  code: "M2", type: "Mailing", owner: "MC" },
      { day: 5,  code: "E5", type: "Email",   owner: "MC" },
      { day: 6,  code: "C1", type: "Call",    owner: "Analyst" },
      { day: 8,  code: "E6", type: "Email",   owner: "MC" },
      { day: 9,  code: "C2", type: "Call",    owner: "Analyst" },
      { day: 11, code: "E7", type: "Email",   owner: "MC" },
      { day: 13, code: "E8", type: "Email",   owner: "MC" },
      { day: 14, code: "C3", type: "Call",    owner: "Analyst" },
      { day: 17, code: "E9", type: "Email",   owner: "MC" },
    ],
  },
  eM2: {
    label: "Re-Approach Email",
    initialEmailCode: "eM2",
    steps: [
      { day: 1,  code: "eM2", type: "Email", owner: "MC" },
      { day: 3,  code: "E6",  type: "Email", owner: "MC" },
      { day: 3,  code: "C1",  type: "Call",  owner: "Analyst" },
      { day: 4,  code: "C2",  type: "Call",  owner: "Analyst" },
      { day: 6,  code: "E7",  type: "Email", owner: "MC" },
      { day: 8,  code: "E8",  type: "Email", owner: "MC" },
      { day: 9,  code: "C3",  type: "Call",  owner: "Analyst" },
      { day: 12, code: "E9",  type: "Email", owner: "MC" },
    ],
  },
  CR: {
    label: "Custom / Tradeshow",
    initialEmailCode: "eCM1",
    steps: [
      { day: 1, code: "eCM1", type: "Email", owner: "MC" },
      { day: 3, code: "C1",   type: "Call",  owner: "Analyst" },
      { day: 3, code: "E2b",  type: "Email", owner: "MC" },
      { day: 4, code: "C2",   type: "Call",  owner: "Analyst" },
      { day: 6, code: "E3b",  type: "Email", owner: "MC" },
      { day: 9, code: "E9b",  type: "Email", owner: "MC" },
    ],
  },
};

// Default cycle when a mailings row has no cycle set yet
const DEFAULT_CYCLE = "M1";

// ---------- Date helpers ----------
function mondayOf(date) {
  const d = (date instanceof Date) ? new Date(date) : new Date(date + "T00:00:00Z");
  const dow = d.getUTCDay() || 7;
  if (dow !== 1) d.setUTCDate(d.getUTCDate() - (dow - 1));
  return d.toISOString().slice(0, 10);
}
function shiftWeek(weekStartStr, deltaWeeks) {
  const d = new Date(weekStartStr + "T00:00:00Z");
  d.setUTCDate(d.getUTCDate() + (deltaWeeks * 7));
  return d.toISOString().slice(0, 10);
}
function formatWeekRange(weekStartStr) {
  const start = new Date(weekStartStr + "T00:00:00Z");
  const end = new Date(start);
  end.setUTCDate(start.getUTCDate() + 6);
  const fmt = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
  return `${fmt(start)} – ${fmt(end)}`;
}
function addBusinessDays(startDateStr, businessDays) {
  const d = new Date(startDateStr + "T00:00:00Z");
  let added = 0;
  while (added < businessDays - 1) {
    d.setUTCDate(d.getUTCDate() + 1);
    const dow = d.getUTCDay();
    if (dow !== 0 && dow !== 6) added++;
  }
  return d.toISOString().slice(0, 10);
}

// ---------- Section configuration ----------
const SECTIONS = [
  { key: "goals",          label: "Weekly Goals",          description: "List goals per week" },
  { key: "research",       label: "Research Goals",        description: "Targets to profile per project" },
  { key: "approval",       label: "Approval Lists Sent",   description: "Lists sent to clients this week" },
  { key: "mailings",       label: "Mailings Planned",      description: "Mailings to send this week (A/P)" },
  { key: "marketing_deck", label: "Marketing Deck Budget", description: "Deck production status per project" },
];

// Plan color palette
const PLAN_COLORS = [
  { accent: "#3b82f6", soft: "rgba(59, 130, 246, 0.07)",  line: "rgba(59, 130, 246, 0.30)"  },
  { accent: "#d4a017", soft: "rgba(212, 160, 23, 0.08)",  line: "rgba(212, 160, 23, 0.35)"  },
  { accent: "#16a34a", soft: "rgba(22, 163, 74, 0.07)",   line: "rgba(22, 163, 74, 0.30)"   },
  { accent: "#8b5cf6", soft: "rgba(139, 92, 246, 0.07)",  line: "rgba(139, 92, 246, 0.30)"  },
  { accent: "#0ea5e9", soft: "rgba(14, 165, 233, 0.07)",  line: "rgba(14, 165, 233, 0.30)"  },
];

// Stable color slot per plan. Uses the saved color_index from Supabase so:
//  - Same plan keeps its color across reorders, refreshes, sessions.
//  - No two plans collide (assigned at create time from first free slot).
// Falls back to UUID hash for legacy plans that pre-date the color_index column.
function planColorIndex(plan) {
  if (plan.colorIndex != null) return plan.colorIndex % PLAN_COLORS.length;
  const id = plan.id || "";
  let hash = 0;
  for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) >>> 0;
  return hash % PLAN_COLORS.length;
}

const MARKETING_DECK_ACTIONS = [
  "Create Internal Deck Draft",
  "Marketing Team in Process",
  "Waiting Client Feedback",
  "Applying Client Redlines",
  "Waiting for Final Approval",
];

function deriveLabelFromOwners(ownerIds, peopleById) {
  if (!ownerIds || ownerIds.length === 0) return "Unassigned Plan";
  const surnames = ownerIds.map(id => {
    const p = peopleById[id];
    if (!p || !p.name) return id;
    const parts = p.name.split(" ");
    return parts[parts.length - 1];
  });
  return surnames.join("/");
}

// ---------- Main screen ----------
function ResearchScreen({ scope, setScope, user, hideForecast }) {
  const F = window.VAULT_FIRM;
  const PEOPLE_BY_ID = F.PEOPLE_BY_ID || {};

  // Resolve current module
  const moduleHandle = useMemo(() => {
    if (scope.subteam && scope.subteam.length >= 1) return scope.subteam[0].replace(/^st-/, "");
    if (scope.subSubteam && scope.subSubteam.length >= 1) {
      const sst = (F.SUBSUBTEAMS || []).find(s => s.id === scope.subSubteam[0]);
      if (sst && sst.parentSubteam) return sst.parentSubteam.replace(/^st-/, "");
    }
    if (scope.individuals && scope.individuals.length >= 1) {
      const p = PEOPLE_BY_ID[scope.individuals[0]];
      if (p) return p.subteam ? p.subteam.replace(/^st-/, "") : p.team;
    }
    const me = user && PEOPLE_BY_ID[user.id];
    if (me) return me.subteam ? me.subteam.replace(/^st-/, "") : (me.team || "bscott");
    return "bscott";
  }, [JSON.stringify(scope.subteam), JSON.stringify(scope.subSubteam), JSON.stringify(scope.individuals), user]);

  const moduleLabel = useMemo(() => {
    const st = F.SUBTEAMS.find(s => s.id === "st-" + moduleHandle);
    return st ? st.label.replace(/^.*— /, "") + " Module" : moduleHandle + " Module";
  }, [moduleHandle]);

  // Week navigation
  const [weekStart, setWeekStart] = useState(() => mondayOf(new Date()));
  const prevWeek = useCallback(() => setWeekStart(w => shiftWeek(w, -1)), []);
  const nextWeek = useCallback(() => setWeekStart(w => shiftWeek(w, 1)), []);

  // Data
  const [plans, setPlans] = useState([]);
  const [goals, setGoals] = useState([]);
  const [lastWeekSummary, setLastWeekSummary] = useState([]);
  const [projects, setProjects] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [refreshKey, setRefreshKey] = useState(0);
  const reload = useCallback(() => setRefreshKey(k => k + 1), []);

  const hydratedRef = React.useRef(false);
  useEffect(() => {
    let cancelled = false;
    if (!hydratedRef.current) setLoading(true);
    setError(null);
    const lastWeek = shiftWeek(weekStart, -1);
    Promise.all([
      window.VaultAPI.listResearchPlans(moduleHandle),
      window.VaultAPI.listResearchGoals(moduleHandle, weekStart),
      window.VaultAPI.getResearchWeekSummary(moduleHandle, lastWeek),
    ])
      .then(([planRows, goalRows, summary]) => {
        if (cancelled) return;
        setPlans(planRows || []);
        setGoals(goalRows || []);
        setLastWeekSummary(summary || []);
        hydratedRef.current = true;
        setLoading(false);
      })
      .catch(err => {
        if (cancelled) return;
        setError(err.message || String(err));
        setLoading(false);
      });
    return () => { cancelled = true; };
  }, [moduleHandle, weekStart, refreshKey]);

  // CRM stats — module-level totals split A/P
  const [crmStats, setCrmStats] = useState({ addon: null, platform: null, loading: false });
  useEffect(() => {
    let cancelled = false;
    setCrmStats(s => ({ ...s, loading: true }));
    Promise.all([
      window.VaultAPI.getOverviewTotals([moduleHandle], null, null, weekStart, weekStart, "Add-on").catch(() => null),
      window.VaultAPI.getOverviewTotals([moduleHandle], null, null, weekStart, weekStart, "Platform").catch(() => null),
    ]).then(([addon, platform]) => {
      if (cancelled) return;
      setCrmStats({ addon, platform, loading: false });
    });
    return () => { cancelled = true; };
  }, [moduleHandle, weekStart, refreshKey]);

  // Per-individual stats for the week — used to compute per-Plan actuals
  const [individuals, setIndividuals] = useState([]);
  useEffect(() => {
    let cancelled = false;
    window.VaultAPI.getModuleIndividuals(moduleHandle, weekStart, weekStart)
      .then(rows => { if (!cancelled) setIndividuals(rows || []); })
      .catch(() => { if (!cancelled) setIndividuals([]); });
    return () => { cancelled = true; };
  }, [moduleHandle, weekStart, refreshKey]);

  // Project picker source — Harvey-native project list (was the ~20-row curated HARVEY_CLIENTS roster).
  // listBookProjects().module is the director_id, same namespace as moduleHandle ("st-"+leadId stripped).
  useEffect(() => {
    let cancelled = false;
    const apply = (all) => {
      if (cancelled) return;
      const mine = (all || []).filter(p => p.module === moduleHandle);
      setProjects(mine.map(p => {
        const buyer = p.buyer_name || p.project_name || ("Project " + p.project_id);
        const label = (p.buyer_name && p.project_name && p.project_name !== p.buyer_name) ? p.project_name : null;
        return { id: p.project_id, name: buyer, type: label, lead: p.module, peg: null };
      }));
    };
    if (window.__VAULT_BOOK_PROJECTS) { apply(window.__VAULT_BOOK_PROJECTS); return () => { cancelled = true; }; }
    const p = (window.VaultAPI && window.VaultAPI.listBookProjects) ? window.VaultAPI.listBookProjects() : Promise.resolve([]);
    p.then(rows => { window.__VAULT_BOOK_PROJECTS = rows || []; apply(window.__VAULT_BOOK_PROJECTS); }).catch(() => apply([]));
    return () => { cancelled = true; };
  }, [moduleHandle]);

  const modulePeople = useMemo(() => {
    return F.PEOPLE.filter(p => {
      const team = p.team || (p.subteam ? p.subteam.replace(/^st-/, "") : null);
      return team === moduleHandle;
    }).sort((a, b) => (a.name || "").localeCompare(b.name || ""));
  }, [moduleHandle]);

  const analystCount = modulePeople.length;

  // ---------- Plan CRUD ----------
  const addPlan = async () => {
    if (plans.length >= 5) { window.VaultUI.toast("error", "Maximum of 5 Plans per module."); return; }
    // Pick the first color slot not already in use by an existing plan
    const used = new Set(plans.map(p => p.colorIndex).filter(i => i != null));
    let freeSlot = 0;
    while (used.has(freeSlot) && freeSlot < PLAN_COLORS.length) freeSlot++;
    if (freeSlot >= PLAN_COLORS.length) freeSlot = plans.length; // all 5 used — wrap
    try {
      await window.VaultAPI.createResearchPlan({
        module: moduleHandle,
        ownerIds: [],
        label: `Plan ${plans.length + 1}`,
        displayOrder: plans.length,
        colorIndex: freeSlot,
      });
      reload();
    } catch (err) { window.VaultUI.toast("error", "Failed to create plan: " + (err.message || err)); }
  };

  const patchPlan = async (id, patch) => {
    if ("ownerIds" in patch) {
      patch.label = deriveLabelFromOwners(patch.ownerIds, PEOPLE_BY_ID);
    }
    try {
      await window.VaultAPI.updateResearchPlan(id, patch);
      setPlans(prev => prev.map(p => p.id === id ? { ...p, ...patch } : p));
    } catch (err) { window.VaultUI.toast("error", "Failed to update plan: " + (err.message || err)); reload(); }
  };

  const deletePlan = async (id) => {
    if (!(await window.VaultUI.confirm({ message: "Delete this plan and ALL its goals across every week? This cannot be undone.", title: "Delete plan?", danger: true }))) return;
    try {
      await window.VaultAPI.deleteResearchPlan(id);
      reload();
    } catch (err) { window.VaultUI.toast("error", "Failed to delete plan: " + (err.message || err)); }
  };

  // Reorder plans via drag-drop. fromIdx = source position, toIdx = destination position.
  // Updates display_order on every plan in one batch.
  const reorderPlans = async (fromIdx, toIdx) => {
    if (fromIdx === toIdx) return;
    const next = [...plans];
    const [moved] = next.splice(fromIdx, 1);
    next.splice(toIdx, 0, moved);
    // Optimistic local update so the UI feels instant
    setPlans(next.map((p, i) => ({ ...p, displayOrder: i })));
    try {
      await Promise.all(next.map((p, i) =>
        window.VaultAPI.updateResearchPlan(p.id, { displayOrder: i })
      ));
    } catch (err) {
      window.VaultUI.toast("error", "Failed to reorder plans: " + (err.message || err));
      reload();
    }
  };

  // Clear all rows in a plan for the current week only (keeps the plan + its history)
  const clearPlanWeek = async (planId) => {
    const planGoals = goals.filter(g => g.planId === planId);
    if (planGoals.length === 0) { window.VaultUI.toast("error", "This plan has no rows for this week."); return; }
    if (!(await window.VaultUI.confirm({ message: `Clear all ${planGoals.length} rows from this plan for the current week? This cannot be undone.`, title: "Clear plan?", danger: true }))) return;
    try {
      await Promise.all(planGoals.map(g => window.VaultAPI.deleteResearchGoal(g.id)));
      reload();
    } catch (err) { window.VaultUI.toast("error", "Failed to clear plan: " + (err.message || err)); }
  };

  // ---------- Goal CRUD ----------
  const addGoal = async (planId, section) => {
    const planGoals = goals.filter(g => g.planId === planId && g.section === section);
    const nextRank = planGoals.length + 1;
    try {
      await window.VaultAPI.createResearchGoal({
        module: moduleHandle,
        weekStart,
        section,
        rank: nextRank,
        planId,
        subteam: null,
        ownerId: null,
        projectName: "",
        targetCount: null, // repurposed as nominal List # (manual); no longer a target count
        type: (section === "mailings" || section === "approval") ? "A" : null,
        action: section === "marketing_deck" ? MARKETING_DECK_ACTIONS[0] : null,
        cycle: section === "mailings" ? DEFAULT_CYCLE : null,
        done: false,
      });
      reload();
    } catch (err) { window.VaultUI.toast("error", "Failed to add row: " + (err.message || err)); }
  };

  const patchGoal = async (id, patch) => {
    // Auto-stamp done_date when marking done (and clear it when un-marking)
    if ("done" in patch && !("doneDate" in patch)) {
      patch = { ...patch };
      if (patch.done) {
        patch.doneDate = new Date().toISOString().slice(0, 10);
      } else {
        patch.doneDate = null;
      }
    }
    try {
      await window.VaultAPI.updateResearchGoal(id, patch);
      setGoals(prev => prev.map(g => g.id === id ? { ...g, ...patch } : g));
    } catch (err) { window.VaultUI.toast("error", "Failed to update row: " + (err.message || err)); reload(); }
  };

  const deleteGoal = async (id) => {
    if (!(await window.VaultUI.confirm({ message: "Delete this row?", danger: true }))) return;
    try {
      await window.VaultAPI.deleteResearchGoal(id);
      reload();
    } catch (err) { window.VaultUI.toast("error", "Failed to delete row: " + (err.message || err)); }
  };

  // Aggregate goal/actual stats
  const aggregateGoal = (section, field) => {
    const rows = goals.filter(g => g.section === section);
    if (field === "targetSum") return rows.reduce((s, r) => s + (r.targetCount || 0), 0);
    if (field === "doneCount") return rows.filter(r => r.done).length;
    return rows.length;
  };

  // Past mailings for Cadence Forecast
  const [pastMailings, setPastMailings] = useState([]);
  useEffect(() => {
    let cancelled = false;
    const fetchPast = async () => {
      const fetches = [];
      for (let i = 0; i < 5; i++) fetches.push(window.VaultAPI.listResearchGoals(moduleHandle, shiftWeek(weekStart, -i)));
      const results = await Promise.all(fetches);
      if (cancelled) return;
      const flat = [];
      results.forEach((rows, idx) => {
        const wk = shiftWeek(weekStart, -idx);
        rows.forEach(r => { if (r.section === "mailings" && r.done) flat.push({ ...r, sentWeek: wk }); });
      });
      setPastMailings(flat);
    };
    fetchPast();
    return () => { cancelled = true; };
  }, [moduleHandle, weekStart, refreshKey]);

  const cadenceForecast = useMemo(() => {
    // Include ALL steps for every past mailing (past + future), regardless of today\'s date.
    // The CadenceForecast component handles the split into "weekly process" vs "beyond".
    const out = [];
    pastMailings.forEach(m => {
      const startDate = m.doneDate || m.sentWeek;
      const cycleKey = m.cycle && OUTREACH_CYCLES[m.cycle] ? m.cycle : DEFAULT_CYCLE;
      const cycleDef = OUTREACH_CYCLES[cycleKey];
      cycleDef.steps.forEach(step => {
        const dueDate = addBusinessDays(startDate, step.day);
        out.push({
          mailing: m,
          step,
          dueDate,
          startDate,
          cycleKey,
          isInitialEmail: step.code === cycleDef.initialEmailCode,
        });
      });
    });
    return out.sort((a, b) => a.dueDate.localeCompare(b.dueDate));
  }, [pastMailings]);

  return (
    <div style={{ padding: "20px 28px 60px" }}>
      <window.PageToolbar title={"Research — " + moduleLabel} subtitle="Weekly planning, target tracking, outreach cadence"/>

      <div className="card" style={{ padding: 12, marginBottom: 18, display: "flex", gap: 12, alignItems: "center", justifyContent: "space-between" }}>
        <div className="row" style={{ gap: 8, alignItems: "center" }}>
          <button className="btn ghost" onClick={prevWeek} title="Previous week">←</button>
          <div style={{ display: "flex", flexDirection: "column" }}>
            <strong style={{ fontSize: 14 }}>Week of {formatWeekRange(weekStart)}</strong>
            <span className="muted small">Monday {weekStart}</span>
          </div>
          <button className="btn ghost" onClick={nextWeek} title="Next week">→</button>
          <button className="btn ghost small" onClick={() => setWeekStart(mondayOf(new Date()))} style={{ marginLeft: 8 }}>Today</button>
        </div>
        <div className="row" style={{ gap: 8, alignItems: "center" }}>
          <span className="muted small">Jump to:</span>
          <input type="date" value={weekStart}
            onChange={e => e.target.value && setWeekStart(mondayOf(e.target.value))}
            style={{ padding: "5px 8px", border: "1px solid var(--line-2)", borderRadius: 4, fontFamily: "inherit", fontSize: 12 }}/>
        </div>
      </div>

      {loading && <div className="card card-pad muted" style={{ textAlign: "center", padding: 40 }}>Loading…</div>}
      {error && <div className="card card-pad" style={{ textAlign: "center", padding: 40, color: "var(--risk)" }}>Error: {error}</div>}

      {!loading && !error && (
        <>
          {lastWeekSummary.length > 0 && (
            <div className="card" style={{ padding: 14, marginBottom: 18, background: "var(--surface-2)" }}>
              <div className="row" style={{ justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
                <strong style={{ fontSize: 13 }}>Last Week's Hit Rate</strong>
                <span className="muted small">Week of {formatWeekRange(shiftWeek(weekStart, -1))}</span>
              </div>
              <div className="row" style={{ gap: 24, flexWrap: "wrap" }}>
                {lastWeekSummary.map(s => (
                  <div key={s.section}>
                    <div className="muted small" style={{ textTransform: "uppercase", letterSpacing: "0.05em", fontSize: 10 }}>
                      {SECTIONS.find(sec => sec.key === s.section)?.label || s.section}
                    </div>
                    <div style={{ fontSize: 17, fontWeight: 600, marginTop: 3 }}>
                      {s.done_count}/{s.total_count}
                      <span className="muted small" style={{ marginLeft: 6, fontWeight: 400 }}>
                        ({s.total_count > 0 ? Math.round((s.done_count / s.total_count) * 100) : 0}%)
                      </span>
                    </div>
                    {s.target_sum > 0 && <div className="muted small" style={{ marginTop: 2 }}>{s.done_target_sum} / {s.target_sum} targets</div>}
                  </div>
                ))}
              </div>
            </div>
          )}

          <div className="row" style={{ justifyContent: "space-between", alignItems: "center", marginBottom: 12 }}>
            <h3 className="display" style={{ margin: 0, fontSize: 17, fontWeight: 400 }}>This Week's Plans</h3>
            <button className="btn primary small" onClick={addPlan} disabled={plans.length >= 5}>
              + Add Plan {plans.length >= 5 && "(max 5)"}
            </button>
          </div>

          {plans.length === 0 ? (
            <div className="card card-pad" style={{ textAlign: "center", padding: 40, color: "var(--muted)" }}>
              No plans yet. Click "+ Add Plan" to create your first one.
            </div>
          ) : (
            <div style={{ display: "grid", gridTemplateColumns: `repeat(${plans.length}, minmax(360px, 1fr))`, gap: 14, overflowX: "auto" }}>
              {plans.map((plan, idx) => (
                <PlanColumn key={plan.id} plan={plan} colorIdx={planColorIndex(plan)} planIdx={idx}
                  goals={goals.filter(g => g.planId === plan.id)}
                  modulePeople={modulePeople}
                  projects={projects}
                  individuals={individuals}
                  onPlanPatch={patchPlan}
                  onPlanDelete={deletePlan}
                  onPlanClear={clearPlanWeek}
                  onPlanReorder={reorderPlans}
                  onGoalAdd={addGoal} onGoalPatch={patchGoal} onGoalDelete={deleteGoal}
                />
              ))}
            </div>
          )}

          {/* Module-wide stats (smaller, reference). Per-plan stats live inside each Plan column. */}
          <div className="card" style={{ padding: 12, marginBottom: 18, background: "var(--surface-2)" }}>
            <div className="row" style={{ justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
              <strong style={{ fontSize: 12 }}>Module-wide totals this week</strong>
              {crmStats.loading && <span className="muted small">Loading…</span>}
            </div>
            <div className="row" style={{ gap: 24, flexWrap: "wrap", fontSize: 12 }}>
              <ModuleStat label="NTPs" addon={crmStats.addon?.ntps || 0} platform={crmStats.platform?.ntps || 0}/>
              <ModuleStat label="Mailings" addon={crmStats.addon?.mailings || 0} platform={crmStats.platform?.mailings || 0}/>
              <ModuleStat label="Lists Sent" addon={aggregateGoal("approval", "doneCount")} platform={null} vaultOnly/>
              <ModuleStat label="Bounced Emails Fixed"
                addon={goals.filter(g => g.section === "mailings").reduce((s, g) => s + (g.bouncesFixed || 0), 0)}
                platform={null} vaultOnly/>
            </div>
          </div>

          {!hideForecast && (
            <>
              <h3 className="display" style={{ margin: "32px 0 12px", fontSize: 17, fontWeight: 400 }}>
                Outreach Cadence Forecast
              </h3>
              <CadenceForecast forecast={cadenceForecast} onPatchMailing={patchGoal} weekStart={weekStart}/>
            </>
          )}
        </>
      )}
    </div>
  );
}

// ---------- Plan column ----------
function PlanColumn({ plan, colorIdx, planIdx, goals, modulePeople, projects, individuals, onPlanPatch, onPlanDelete, onPlanClear, onPlanReorder, onGoalAdd, onGoalPatch, onGoalDelete }) {
  const colors = PLAN_COLORS[colorIdx % PLAN_COLORS.length];
  const [editingOwners, setEditingOwners] = useState(false);
  const [isDragOver, setIsDragOver] = useState(false);
  const G = window.VAULT_GOALS;
  const [, setGoalTick] = useState(0);
  React.useEffect(() => (G ? G.onChange(() => setGoalTick(t => t + 1)) : undefined), []);

  // Compute per-Plan stats by summing individuals' stats for this Plan's owners
  const ownerIds = plan.ownerIds || [];
  const planIndividuals = individuals.filter(i => ownerIds.includes(i.personId));
  const planActualNTPs     = planIndividuals.reduce((s, i) => s + (i.ntps || 0), 0);
  const planActualMailings = planIndividuals.reduce((s, i) => s + (i.mailings || 0), 0);
  const planActualCalls    = planIndividuals.reduce((s, i) => s + (i.calls || 0), 0);

  // Compute per-Plan goals (sum target_count within this Plan)
  // NTP / mailings GOAL sourced from the Company & Team Goals page: each owner's
  // per-person weekly target (with sub-team overrides), summed across this plan's
  // owners for the week. List # is manual and no longer drives goals. Falls back to
  // null (→ "goal on Goals page") when unset, so it never shows a fake 0 target.
  const goalCfg = G ? G.load() : null;
  const planPeople = ownerIds.map(id => ({ id }));
  const rawGoalNTPs     = (G && goalCfg && ownerIds.length) ? G.periodExpected(planPeople, "lists", 1, goalCfg) : 0;
  // Mailings are team-level goals (set on the Goals page's sub-team strip),
  // so add each distinct owner sub-team's weekly mailings target on top of any
  // per-person values.
  const ownerSubteamLeads = Array.from(new Set(ownerIds
    .map(id => window.VAULT_FIRM?.PEOPLE_BY_ID?.[id]?.subSubteam)
    .filter(Boolean)
    .map(sst => sst.replace(/^sst-/, "").replace(/-direct$/, ""))));
  const teamWeekly = (m) => ownerSubteamLeads.reduce(
    (t, lead) => t + (Number(goalCfg?.subteam?.[lead]?.[m]) || 0), 0);
  const rawGoalMailings = (G && goalCfg && ownerIds.length)
    ? G.periodExpected(planPeople, "mailings", 1, goalCfg) + teamWeekly("mailings")
    : 0;
  const planGoalNTPs     = rawGoalNTPs > 0 ? rawGoalNTPs : null;
  const planGoalMailings = rawGoalMailings > 0 ? rawGoalMailings : null;
  const planGoalListsSent = goals.filter(g => g.section === "approval").length;
  const planDoneListsSent = goals.filter(g => g.section === "approval" && g.done).length;

  return (
    <div className="card"
      onDragOver={e => { e.preventDefault(); setIsDragOver(true); }}
      onDragLeave={() => setIsDragOver(false)}
      onDrop={e => {
        e.preventDefault();
        setIsDragOver(false);
        const fromIdx = parseInt(e.dataTransfer.getData("text/plain"), 10);
        if (!isNaN(fromIdx) && fromIdx !== planIdx) onPlanReorder(fromIdx, planIdx);
      }}
      style={{
        display: "flex",
        flexDirection: "column",
        borderTop: `4px solid ${colors.accent}`,
        minWidth: 0,
        overflow: "visible",
        outline: isDragOver ? `2px dashed ${colors.accent}` : "none",
        outlineOffset: isDragOver ? 4 : 0,
        transition: "outline 120ms",
      }}>
      <div draggable
        onDragStart={e => { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(planIdx)); }}
        style={{ padding: "12px 14px", borderBottom: `1px solid ${colors.line}`, background: colors.soft, cursor: "grab", userSelect: "none" }}
        title="Drag to reorder Plans">
        <div className="row" style={{ justifyContent: "space-between", alignItems: "flex-start", marginBottom: 4 }}>
          <strong style={{ fontSize: 14, color: colors.accent }}>{plan.label || "Untitled Plan"}</strong>
          <div className="row" style={{ gap: 4 }}>
            <button onClick={() => setEditingOwners(!editingOwners)} className="btn ghost small" title="Edit owners"
              style={{ padding: "2px 6px", fontSize: 11 }}>
              {editingOwners ? "Done" : "Edit"}
            </button>
            <button onClick={() => onPlanClear(plan.id)} className="btn ghost small" title="Clear all rows for this week"
              style={{ padding: "2px 6px", fontSize: 11, color: "var(--muted)" }}>Clear</button>
            <button onClick={() => onPlanDelete(plan.id)} className="btn ghost small" title="Delete plan (all weeks)"
              style={{ color: "var(--risk)", padding: "2px 6px", fontSize: 11 }}>×</button>
          </div>
        </div>
        {editingOwners ? (
          <OwnerMultiSelect
            value={plan.ownerIds || []}
            people={modulePeople}
            onChange={(ids) => onPlanPatch(plan.id, { ownerIds: ids })}
          />
        ) : (
          <div className="muted small">
            {plan.ownerIds && plan.ownerIds.length > 0
              ? plan.ownerIds.map(id => modulePeople.find(p => p.id === id)?.name || id).join(", ")
              : <em>No owners assigned — click Edit</em>}
          </div>
        )}
      </div>

      <div style={{ padding: 10 }}>
        {SECTIONS.map(section => (
          <PlanSection key={section.key}
            section={section}
            rows={goals.filter(g => g.section === section.key).sort((a, b) => (a.rank || 0) - (b.rank || 0))}
            modulePeople={modulePeople}
            projects={projects}
            colors={colors}
            onAdd={() => onGoalAdd(plan.id, section.key)}
            onPatch={onGoalPatch}
            onDelete={onGoalDelete}
          />
        ))}

      </div>

      {/* Goal vs Actual — separated from sections with breathing room */}
      {ownerIds.length > 0 && (
        <div style={{
          margin: "16px 10px 10px",
          padding: "10px 12px",
          background: colors.soft,
          border: `1px solid ${colors.line}`,
          borderRadius: 6,
        }}>
          <div className="muted small" style={{ textTransform: "uppercase", letterSpacing: "0.05em", fontSize: 9, marginBottom: 6, color: colors.accent, fontWeight: 600 }}>
            Goal vs Actual (this Plan)
          </div>
          <MiniStatRow label="NTPs"        goal={planGoalNTPs}     actual={planActualNTPs}     color={colors.accent}/>
          <MiniStatRow label="Mailings"    goal={planGoalMailings} actual={planActualMailings} color={colors.accent}/>
          <MiniStatRow label="Lists Sent"  goal={planGoalListsSent} actual={planDoneListsSent} color={colors.accent} vaultOnly/>
        </div>
      )}

      {/* Spacer so the parent <div> below closes correctly */}
      <div style={{ display: "none" }}>
      </div>
    </div>
  );
}

// ---------- Mini stat row inside a Plan ----------
function MiniStatRow({ label, goal, actual, color, vaultOnly }) {
  const pct = goal > 0 ? Math.min(100, Math.round((actual / goal) * 100)) : 0;
  return (
    <div style={{ marginBottom: 6 }}>
      <div className="row" style={{ justifyContent: "space-between", marginBottom: 2 }}>
        <span style={{ fontSize: 11, color: "var(--ink-2)" }}>{label}</span>
        <span style={{ fontSize: 11 }}>
          <strong>{actual}</strong>
          {goal == null
            ? <span className="muted small" style={{ marginLeft: 4, fontSize: 9 }}>· goal on Goals page</span>
            : <span className="muted" style={{ marginLeft: 4 }}>/ {goal}</span>}
          {vaultOnly && <span className="muted small" style={{ marginLeft: 4, fontSize: 9 }}>(Vault)</span>}
        </span>
      </div>
      <div style={{ background: "var(--surface-2)", borderRadius: 3, height: 4, overflow: "hidden" }}>
        <div style={{ background: color, width: pct + "%", height: "100%", transition: "width 0.3s" }}/>
      </div>
    </div>
  );
}

// ---------- Module-wide stat helper ----------
function ModuleStat({ label, addon, platform, vaultOnly }) {
  const total = (addon || 0) + (platform || 0);
  return (
    <div style={{ display: "flex", flexDirection: "column" }}>
      <span className="muted small" style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.05em" }}>{label}</span>
      <span style={{ fontSize: 16, fontWeight: 600, marginTop: 2 }}>{total}</span>
      {!vaultOnly && (
        <span className="muted small" style={{ fontSize: 10, marginTop: 1 }}>{addon}A · {platform}P</span>
      )}
    </div>
  );
}

// Persisted collapse state for goal sections — survives page navigation and reload,
// and broadcasts so the side-by-side plan columns stay in sync.
const RESEARCH_COLLAPSE_KEY = "vault.research.collapsed";
function loadCollapseMap() {
  try { return JSON.parse(localStorage.getItem(RESEARCH_COLLAPSE_KEY) || "{}") || {}; }
  catch (e) { return {}; }
}
function setSectionCollapsed(key, val) {
  const m = loadCollapseMap(); m[key] = val;
  try { localStorage.setItem(RESEARCH_COLLAPSE_KEY, JSON.stringify(m)); } catch (e) {}
  window.dispatchEvent(new CustomEvent("vault:research-collapse"));
}

// ---------- Section inside a Plan ----------
function PlanSection({ section, rows, modulePeople, projects, colors, onAdd, onPatch, onDelete }) {
  const showCount = section.key === "research";
  const showType = (section.key === "mailings" || section.key === "approval");
  const showAction = section.key === "marketing_deck";
  const [collapsed, setCollapsed] = React.useState(() => !!loadCollapseMap()[section.key]);
  React.useEffect(() => {
    const h = () => setCollapsed(!!loadCollapseMap()[section.key]);
    window.addEventListener("vault:research-collapse", h);
    return () => window.removeEventListener("vault:research-collapse", h);
  }, [section.key]);

  return (
    <div style={{ marginBottom: 16, border: `1px solid ${colors.line}`, borderRadius: 10, overflow: "visible", background: "var(--surface)" }}>
      <div className="row" style={{ padding: "11px 14px", background: colors.soft, justifyContent: "space-between", alignItems: "center", borderRadius: collapsed ? "10px" : "10px 10px 0 0" }}>
        <div onClick={() => setSectionCollapsed(section.key, !collapsed)} style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer", flex: 1, minWidth: 0 }}>
          <span style={{ fontSize: 10, color: colors.accent, transform: collapsed ? "rotate(-90deg)" : "none", transition: "transform .15s", lineHeight: 1 }}>▾</span>
          <div style={{ display: "flex", flexDirection: "column", gap: 1 }}>
            <strong style={{ fontSize: 12.5, letterSpacing: "-.1px" }}>{section.label}{collapsed && rows.length > 0 ? ` (${rows.length})` : ""}</strong>
            <span className="muted" style={{ fontSize: 10.5 }}>{section.description}</span>
          </div>
        </div>
        <button onClick={onAdd} className="btn ghost small"
          style={{ padding: "4px 10px", fontSize: 11.5, fontWeight: 600, color: colors.accent }}>+ Add</button>
      </div>

      {!collapsed && (rows.length === 0 ? (
        <div className="muted small" style={{ padding: "14px 10px", textAlign: "center", fontSize: 11.5 }}>
          No rows yet. Click + Add.
        </div>
      ) : (
        <div>
          {rows.map((r, idx) => (
            <CompactRow key={r.id} row={r} idx={idx}
              section={section}
              modulePeople={modulePeople}
              projects={projects}
              showCount={showCount}
              showType={showType}
              showAction={showAction}
              showSentDate={section.key === "mailings"}
              showCycle={section.key === "mailings"}
              showMailingCount={section.key === "mailings"}
              onPatch={onPatch}
              onDelete={onDelete}
            />
          ))}
        </div>
      ))}
    </div>
  );
}

const selectStyleSmall = {
  padding: "3px 6px",
  border: "1px solid var(--line-2)",
  borderRadius: 4,
  fontFamily: "inherit",
  fontSize: 11,
  background: "var(--surface)",
  color: "var(--ink)",
  minWidth: 0,
};

// Secondary-tier control style — the calm strip of settings under each project name.
const stripCtl = {
  padding: "4px 8px",
  border: "1px solid var(--line-2)",
  borderRadius: 6,
  fontFamily: "inherit",
  fontSize: 12,
  background: "var(--surface)",
  color: "var(--ink)",
  minWidth: 0,
  height: 28,
};

function initialsOf(name) {
  if (!name) return "?";
  const p = String(name).trim().split(/\s+/);
  return ((p[0] ? p[0][0] : "") + (p[1] ? p[1][0] : "")).toUpperCase() || "?";
}

function OwnerAvatar({ person }) {
  if (!person) return (
    <span style={{ width: 22, height: 22, borderRadius: "50%", border: "1px dashed var(--line-2)", display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: 11, color: "var(--muted)" }}>+</span>
  );
  return (
    <span style={{ width: 22, height: 22, borderRadius: "50%", background: "#E6F1FB", color: "#0C447C", display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: 10, fontWeight: 600 }}>{initialsOf(person.name)}</span>
  );
}

function fmtShortDate(d) {
  if (!d) return "\u2014";
  const dt = new Date(String(d).slice(0, 10) + "T00:00:00");
  if (isNaN(dt)) return "\u2014";
  return dt.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}

// A quiet, aligned cell that becomes an editable control on click (Linear/Airtable pattern).
function EditCell({ width, title, children, editor }) {
  const [editing, setEditing] = React.useState(false);
  if (editing) {
    return <span style={{ flex: "0 0 auto", display: "inline-flex", alignItems: "center" }}>{editor(() => setEditing(false))}</span>;
  }
  return (
    <span onClick={() => setEditing(true)} title={title}
      style={{ flex: `0 0 ${width}px`, display: "inline-flex", justifyContent: "flex-end", alignItems: "center", cursor: "pointer", borderRadius: 6 }}>
      {children}
    </span>
  );
}

function CompactRow({ row, idx, section, modulePeople, projects, showCount, showType, showAction, showSentDate, showCycle, showMailingCount, onPatch, onDelete }) {
  const sectionKey = section ? section.key : null;
  const isMailing = sectionKey === "mailings";
  const [hover, setHover] = React.useState(false);
  const owner = modulePeople.find(p => p.id === row.ownerId)
    || (window.VAULT_FIRM && window.VAULT_FIRM.PEOPLE_BY_ID ? window.VAULT_FIRM.PEOPLE_BY_ID[row.ownerId] : null)
    || null;
  const isPlatform = row.type === "P";
  const typePill = { fontSize: 11, padding: "2px 8px", borderRadius: 10, whiteSpace: "nowrap",
    color: isPlatform ? "#72243E" : "#0C447C", background: isPlatform ? "#FBEAF0" : "#E6F1FB" };

  return (
    <div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{ display: "flex", alignItems: "center", gap: 8, padding: "9px 12px", borderTop: idx === 0 ? "none" : "1px solid var(--line-2)" }}>
      <input type="checkbox" checked={!!row.done}
        onChange={e => onPatch(row.id, { done: e.target.checked })}
        title={isMailing ? "Sent?" : "Done?"}
        style={{ cursor: "pointer", width: 15, height: 15, flexShrink: 0, accentColor: "var(--accent)" }}/>

      <div style={{ flex: 1, minWidth: 0 }}>
        <ProjectPicker value={row.projectName || ""} projects={projects}
          onChange={(name) => onPatch(row.id, { projectName: name })} compact/>
      </div>

      <EditCell width={24} title="Owner"
        editor={(done) => (
          <select autoFocus defaultValue={row.ownerId || ""} onBlur={done}
            onChange={e => { onPatch(row.id, { ownerId: e.target.value || null }); done(); }}
            style={{ ...stripCtl, minWidth: 130 }}>
            <option value="">Owner…</option>
            {modulePeople.map(p => (<option key={p.id} value={p.id}>{p.name}</option>))}
          </select>
        )}>
        <OwnerAvatar person={owner}/>
      </EditCell>

      {showType && (
        <EditCell width={56} title="Engagement type"
          editor={(done) => (
            <select autoFocus defaultValue={row.type || "A"} onBlur={done}
              onChange={e => { onPatch(row.id, { type: e.target.value }); done(); }} style={{ ...stripCtl }}>
              <option value="A">Add-on</option>
              <option value="P">Platform</option>
            </select>
          )}>
          <span style={typePill}>{isPlatform ? "Platform" : "Add-on"}</span>
        </EditCell>
      )}

      {showCycle && (
        <EditCell width={30} title="Outreach cycle"
          editor={(done) => (
            <select autoFocus defaultValue={row.cycle || DEFAULT_CYCLE} onBlur={done}
              onChange={e => { onPatch(row.id, { cycle: e.target.value }); done(); }} style={{ ...stripCtl }}>
              {Object.entries(OUTREACH_CYCLES).map(([k, v]) => (<option key={k} value={k} title={v.label}>{k}</option>))}
            </select>
          )}>
          <span style={{ fontSize: 11, color: "var(--muted)", fontFamily: "var(--f-mono, monospace)" }}>{row.cycle || DEFAULT_CYCLE}</span>
        </EditCell>
      )}

      {(showCount || showMailingCount || sectionKey === "approval") && (
        <EditCell width={28} title="List number (manual)"
          editor={(done) => (
            <input autoFocus type="number" min={0} defaultValue={row.targetCount != null ? row.targetCount : ""}
              placeholder="List #"
              onBlur={e => { onPatch(row.id, { targetCount: e.target.value === "" ? null : parseInt(e.target.value) }); done(); }}
              style={{ ...stripCtl, width: 64 }}/>
          )}>
          <span style={{ fontSize: 11, color: "var(--muted)" }}>{row.targetCount != null ? ("L" + row.targetCount) : "L\u2014"}</span>
        </EditCell>
      )}

      {showSentDate && (
        <EditCell width={48} title="Sent date (drives cadence forecast)"
          editor={(done) => (
            <input autoFocus type="date" defaultValue={row.doneDate || ""}
              onBlur={e => { onPatch(row.id, { doneDate: e.target.value || null }); done(); }}
              style={{ ...stripCtl }}/>
          )}>
          <span style={{ fontSize: 11, color: "var(--muted)", whiteSpace: "nowrap" }}>{fmtShortDate(row.doneDate)}</span>
        </EditCell>
      )}

      {showAction && (
        <EditCell width={140} title="Marketing deck status"
          editor={(done) => (
            <select autoFocus defaultValue={row.action || MARKETING_DECK_ACTIONS[0]} onBlur={done}
              onChange={e => { onPatch(row.id, { action: e.target.value }); done(); }} style={{ ...stripCtl, width: 180 }}>
              {MARKETING_DECK_ACTIONS.map(a => <option key={a} value={a}>{a}</option>)}
            </select>
          )}>
          <span style={{ fontSize: 11, color: "var(--muted)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{row.action || MARKETING_DECK_ACTIONS[0]}</span>
        </EditCell>
      )}

      <button onClick={() => onDelete(row.id)} className="btn ghost small"
        style={{ color: "var(--risk)", padding: "2px 6px", fontSize: 13, flexShrink: 0, opacity: hover ? 1 : 0.3, transition: "opacity .12s" }}
        title="Delete row">×</button>
    </div>
  );
}

// ---------- Owner multi-select ----------
function OwnerMultiSelect({ value, people, onChange }) {
  const [open, setOpen] = useState(false);
  const wrapRef = useRef(null);
  useEffect(() => {
    if (!open) return;
    const onClick = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onClick);
    return () => document.removeEventListener("mousedown", onClick);
  }, [open]);

  const selected = new Set(value || []);
  const toggle = (id) => {
    const next = new Set(selected);
    if (next.has(id)) next.delete(id);
    else next.add(id);
    onChange([...next]);
  };

  return (
    <div ref={wrapRef} style={{ position: "relative" }}>
      <button onClick={() => setOpen(!open)} className="btn ghost small"
        style={{ fontSize: 11, padding: "3px 8px" }}>
        {selected.size === 0 ? "Pick owners…" : `${selected.size} selected`}
      </button>
      {open && (
        <div style={{
          position: "absolute", top: "100%", left: 0, marginTop: 4,
          background: "var(--surface)", border: "1px solid var(--line-2)", borderRadius: 4,
          boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
          maxHeight: 260, overflowY: "auto", zIndex: 100, minWidth: 200, fontSize: 12,
        }}>
          {people.map(p => (
            <label key={p.id} style={{
              display: "flex", alignItems: "center", gap: 6,
              padding: "5px 10px", cursor: "pointer",
              borderBottom: "1px solid var(--line-2)",
            }} onMouseEnter={e => e.currentTarget.style.background = "var(--surface-2)"}
               onMouseLeave={e => e.currentTarget.style.background = "var(--surface)"}>
              <input type="checkbox" checked={selected.has(p.id)} onChange={() => toggle(p.id)}/>
              <span>{p.name}</span>
            </label>
          ))}
        </div>
      )}
    </div>
  );
}

// ---------- ProjectPicker (portal-based to escape clipping) ----------
function ProjectPicker({ value, projects, onChange, compact }) {
  const [open, setOpen] = useState(false);
  const [text, setText] = useState(value || "");
  const inputRef = useRef(null);
  const [pos, setPos] = useState(null);

  useEffect(() => { setText(value || ""); }, [value]);

  const recalcPos = useCallback(() => {
    if (!inputRef.current) return;
    const r = inputRef.current.getBoundingClientRect();
    setPos({ top: r.bottom + 4, left: r.left, width: Math.max(r.width, 240) });
  }, []);

  useEffect(() => {
    if (!open) return;
    recalcPos();
    const onScroll = () => recalcPos();
    window.addEventListener("scroll", onScroll, true);
    window.addEventListener("resize", recalcPos);
    return () => {
      window.removeEventListener("scroll", onScroll, true);
      window.removeEventListener("resize", recalcPos);
    };
  }, [open, recalcPos]);

  useEffect(() => {
    if (!open) return;
    const onClick = (e) => {
      if (inputRef.current && inputRef.current.contains(e.target)) return;
      if (e.target.closest && e.target.closest("[data-projectpicker-dropdown]")) return;
      setOpen(false);
    };
    document.addEventListener("mousedown", onClick);
    return () => document.removeEventListener("mousedown", onClick);
  }, [open]);

  const filtered = useMemo(() => {
    const q = (text || "").trim().toLowerCase();
    if (!q) return projects.slice(0, 15);
    return projects.filter(p => (p.name || "").toLowerCase().includes(q)).slice(0, 15);
  }, [text, projects]);

  const commit = (val) => {
    setText(val);
    setOpen(false);
    if (val !== value) onChange(val);
  };

  return (
    <>
      <input
        ref={inputRef}
        type="text"
        value={text}
        placeholder="Project…"
        onFocus={() => setOpen(true)}
        onChange={e => { setText(e.target.value); setOpen(true); }}
        onKeyDown={e => {
          if (e.key === "Enter") { e.preventDefault(); commit((text || "").trim()); }
          if (e.key === "Escape") { setText(value || ""); setOpen(false); }
        }}
        style={{
          width: "100%",
          padding: compact ? "3px 6px" : "4px 8px",
          border: "1px solid var(--line-2)",
          borderRadius: 4,
          fontFamily: "inherit",
          fontSize: compact ? 11.5 : 12,
          background: "var(--surface)",
          color: "var(--ink)",
          outline: "none",
        }}
      />
      {open && pos && ReactDOM.createPortal(
        <div data-projectpicker-dropdown style={{
          position: "fixed",
          top: pos.top, left: pos.left, width: pos.width,
          background: "var(--surface)",
          border: "1px solid var(--line-2)",
          borderRadius: 4,
          boxShadow: "0 6px 18px rgba(0,0,0,0.12)",
          maxHeight: 240, overflowY: "auto",
          zIndex: 9999, fontSize: 12,
          color: "var(--ink)",
        }}>
          {filtered.length === 0 ? (
            <div className="muted small" style={{ padding: "8px 10px" }}>
              {text && text.trim() ? `Press Enter to use "${text.trim()}"` : "No projects in this module"}
            </div>
          ) : filtered.map(p => (
            <div key={p.id}
              onMouseDown={e => { e.preventDefault(); commit(p.name); }}
              style={{
                padding: "6px 10px",
                cursor: "pointer",
                borderBottom: "1px solid var(--line-2)",
                background: "var(--surface)",
                color: "var(--ink)",
              }}
              onMouseEnter={e => e.currentTarget.style.background = "var(--surface-2)"}
              onMouseLeave={e => e.currentTarget.style.background = "var(--surface)"}
            >
              <div style={{ fontWeight: 500 }}>{p.name}</div>
              {(p.type || p.peg) && (
                <div className="muted small" style={{ fontSize: 10.5, marginTop: 1 }}>
                  {[p.type, p.peg].filter(Boolean).join(" · ")}
                </div>
              )}
            </div>
          ))}
        </div>,
        document.body
      )}
    </>
  );
}

// ---------- Goal vs Actual ----------
function GoalVsActualCard({ label, goal, actualA, actualP, loading, analystCount, accentColor, vaultBased }) {
  const actualTotal = (actualA || 0) + (actualP || 0);
  const progressPct = goal > 0 ? Math.min(100, Math.round((actualTotal / goal) * 100)) : 0;
  const avgPerAnalyst = analystCount > 0 ? Math.round(actualTotal / analystCount) : 0;
  return (
    <div className="card" style={{ flex: "1 1 280px", padding: 16, minWidth: 260, borderTop: `3px solid ${accentColor}` }}>
      <div className="row" style={{ justifyContent: "space-between", alignItems: "flex-start" }}>
        <div className="muted small" style={{ textTransform: "uppercase", letterSpacing: "0.05em", fontSize: 10 }}>{label}</div>
        {loading && <span className="muted small">…</span>}
      </div>
      <div className="row" style={{ alignItems: "baseline", gap: 6, marginTop: 8 }}>
        <span style={{ fontSize: 26, fontWeight: 600 }}>{actualTotal}</span>
        <span className="muted" style={{ fontSize: 13 }}>actual</span>
        <span className="muted" style={{ fontSize: 13, marginLeft: "auto" }}>goal: {goal}</span>
      </div>
      <div style={{ background: "var(--surface-2)", borderRadius: 4, height: 6, marginTop: 8, overflow: "hidden" }}>
        <div style={{ background: accentColor, width: progressPct + "%", height: "100%", transition: "width 0.3s" }}/>
      </div>
      <div className="row" style={{ justifyContent: "space-between", marginTop: 4 }}>
        <span className="muted small">{progressPct}% of goal</span>
        {vaultBased ? <span className="muted small" title="Tracked via Vault planning rows">Vault-tracked</span> : <span className="muted small">{actualA || 0}A · {actualP || 0}P</span>}
      </div>
      <div style={{ borderTop: "1px solid var(--line-2)", marginTop: 10, paddingTop: 8 }}>
        <span className="muted small">Avg/analyst: <strong style={{ color: "var(--ink)" }}>{avgPerAnalyst}</strong> across {analystCount} people</span>
      </div>
    </div>
  );
}

// ---------- Cadence Forecast ----------
function CadenceForecast({ forecast, onPatchMailing, weekStart }) {
  const F = window.VAULT_FIRM;
  const [ownerFilter, setOwnerFilter] = useState("");

  // Hooks first, before any early return.
  const ownerOptions = useMemo(() => {
    const ids = new Set();
    forecast.forEach(f => { if (f.mailing.ownerId) ids.add(f.mailing.ownerId); });
    return [...ids].map(id => ({ id, name: F.PEOPLE_BY_ID[id]?.name || id }))
      .sort((a, b) => a.name.localeCompare(b.name));
  }, [forecast]);

  // Column widths (resizable, persisted per user in localStorage), shared across both tables
  const CADENCE_COL_DEFAULTS = { due: 110, code: 60, project: 240, owner: 120, type: 70, bounced: 110, fixed: 100 };
  const { widths: cadenceWidths, setWidth: setCadenceWidth } =
    window.useColumnWidths("cadence", CADENCE_COL_DEFAULTS);

  if (forecast.length === 0) {
    return (
      <div className="card card-pad" style={{ padding: 20, color: "var(--muted)" }}>
        No upcoming actions forecasted. Forecast is built from completed mailings in the past 5 weeks
        (Initial Mailing M1 cycle by default). Mark mailings as Done to populate this.
      </div>
    );
  }

  // Week bounds based on the VIEWED week (weekStart is Monday of that week).
  // Sunday-end = Monday + 6 days. "Weekly Process Actions" = everything that falls
  // Monday..Sunday of the viewed week, regardless of whether it\'s past or future.
  // "Beyond this week" = anything after the viewed Sunday.
  // Anything before the viewed Monday is intentionally hidden (deep history).
  const weekEnd = (() => {
    const d = new Date(weekStart + "T00:00:00Z");
    d.setUTCDate(d.getUTCDate() + 6);
    return d.toISOString().slice(0, 10);
  })();
  const thisWeek = forecast.filter(f => f.dueDate >= weekStart && f.dueDate <= weekEnd);
  const later = forecast.filter(f => f.dueDate > weekEnd);
  const visibleThisWeek = ownerFilter
    ? thisWeek.filter(f => f.mailing.ownerId === ownerFilter)
    : thisWeek;
  const visibleLater = ownerFilter
    ? later.filter(f => f.mailing.ownerId === ownerFilter)
    : later;

  return (
    <>
      <div className="row" style={{ justifyContent: "space-between", alignItems: "center", marginBottom: 10, flexWrap: "wrap", gap: 8 }}>
        <div className="muted small" style={{ fontSize: 11, flex: "1 1 400px" }}>
          Note: <em>Bounced Emails</em> and <em>Bounces Fixed</em> fields appear on each cycle's first email step
          (E1 for M1, eM1 for eM1, E5 for M2, eM2 for eM2, eCM1 for Custom). Bounces Fixed totals
          roll up into the Module-wide "Bounced Emails Fixed" stat.
        </div>
        <div className="row" style={{ gap: 8, alignItems: "center" }}>
          <span className="muted small">Filter:</span>
          <select value={ownerFilter}
            onChange={e => setOwnerFilter(e.target.value)}
            style={{ padding: "5px 8px", border: "1px solid var(--line-2)", borderRadius: 4, fontFamily: "inherit", fontSize: 12, background: "var(--surface)", color: "var(--ink)" }}>
            <option value="">All owners</option>
            {ownerOptions.map(o => <option key={o.id} value={o.id}>{o.name}</option>)}
          </select>
        </div>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14, alignItems: "start" }}>
        <div>
          {visibleThisWeek.length > 0 ? (
            <ForecastTable title="Weekly Process Actions" rows={visibleThisWeek}
              onPatchMailing={onPatchMailing}
              widths={cadenceWidths} setWidth={setCadenceWidth}/>
          ) : (
            <div className="card card-pad" style={{ padding: 20, color: "var(--muted)", textAlign: "center" }}>
              <strong style={{ fontSize: 13, color: "var(--ink-2)" }}>Weekly Process Actions</strong>
              <div className="muted small" style={{ marginTop: 8 }}>No actions in this week{ownerFilter && " for selected owner"}.</div>
            </div>
          )}
        </div>
        <div>
          {visibleLater.length > 0 ? (
            <ForecastTable title="Beyond this week" rows={visibleLater}
              onPatchMailing={onPatchMailing}
              widths={cadenceWidths} setWidth={setCadenceWidth}/>
          ) : (
            <div className="card card-pad" style={{ padding: 20, color: "var(--muted)", textAlign: "center" }}>
              <strong style={{ fontSize: 13, color: "var(--ink-2)" }}>Beyond this week</strong>
              <div className="muted small" style={{ marginTop: 8 }}>No actions in this window{ownerFilter && " for selected owner"}.</div>
            </div>
          )}
        </div>
      </div>
    </>
  );
}

function ForecastTable({ title, rows, onPatchMailing, widths, setWidth }) {
  const F = window.VAULT_FIRM;
  const [localEdits, setLocalEdits] = useState({});
  const saveTimers = useRef({});

  // Helper: renders a resizable <th>
  const RTh = ({ col, label, title }) => {
    const w = widths ? widths[col] : null;
    return (
      <th title={title}
        style={w ? { width: w, minWidth: w, maxWidth: w, position: "relative" } : { position: "relative" }}>
        {label}
        {setWidth && <window.ResizeHandle col={col} currentWidth={w || 100} onResize={setWidth}/>}
      </th>
    );
  };

  const onBounceChange = (mailingId, field, value) => {
    setLocalEdits(prev => ({ ...prev, [mailingId + ":" + field]: value }));
    // Debounce save by 600ms
    const key = mailingId + ":" + field;
    if (saveTimers.current[key]) clearTimeout(saveTimers.current[key]);
    saveTimers.current[key] = setTimeout(() => {
      const numeric = value === "" ? 0 : parseInt(value) || 0;
      onPatchMailing(mailingId, { [field]: numeric });
    }, 600);
  };

  const getValue = (mailingId, field, fallback) => {
    const key = mailingId + ":" + field;
    return key in localEdits ? localEdits[key] : (fallback ?? "");
  };

  return (
    <div className="card" style={{ overflow: "hidden", marginBottom: 16 }}>
      <div className="row" style={{ padding: "10px 16px", borderBottom: "1px solid var(--line-2)" }}>
        <strong style={{ fontSize: 13 }}>{title}</strong>
        <span className="muted small" style={{ marginLeft: 10 }}>{rows.length} action{rows.length === 1 ? "" : "s"}</span>
      </div>
      <table className="dt" style={{ fontSize: 12.5, width: "100%" }}>
        <thead><tr>
          <RTh col="due" label="Due"/>
          <RTh col="code" label="Code"/>
          <RTh col="project" label="Project"/>
          <RTh col="owner" label="Owner"/>
          <RTh col="type" label="Type"/>
          <RTh col="bounced" label="Bounced Emails" title="Visible on each cycle\'s initial email step"/>
          <RTh col="fixed" label="Bounces Fixed" title="Visible on each cycle\'s initial email step"/>
        </tr></thead>
        <tbody>
          {rows.map((f, i) => {
            const ownerLabel = f.step.owner === "MC" ? "Marketing" : (f.mailing.ownerId ? (F.PEOPLE_BY_ID[f.mailing.ownerId]?.name || f.mailing.ownerId) : "Unassigned");
            const dueDate = new Date(f.dueDate + "T00:00:00Z").toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", timeZone: "UTC" });
            const isInitialEmail = f.isInitialEmail;
            return (
              <tr key={i}>
                <td><span style={{ fontFamily: "var(--f-mono)", fontSize: 11 }}>{dueDate}</span></td>
                <td>
                  <span style={{ fontWeight: 600, fontSize: 11, padding: "2px 6px", borderRadius: 4,
                    background: f.step.type === "Call" ? "var(--accent-soft)" : "var(--surface-2)",
                    color: f.step.type === "Call" ? "var(--accent)" : "var(--ink-2)" }}>{f.step.code}</span>
                </td>
                <td>{f.mailing.projectName || <span className="muted">—</span>}</td>
                <td className="small">{ownerLabel}</td>
                <td><span className="muted small">{f.step.type}</span></td>
                <td>
                  {isInitialEmail ? (
                    <input
                      type="number"
                      min={0}
                      placeholder="0"
                      title="Bounced Emails"
                      value={getValue(f.mailing.id, "bouncedEmails", f.mailing.bouncedEmails)}
                      onChange={e => onBounceChange(f.mailing.id, "bouncedEmails", e.target.value)}
                      style={{ width: 80, padding: "3px 6px", border: "1px solid var(--line-2)", borderRadius: 4, fontSize: 11, fontFamily: "inherit" }}
                    />
                  ) : <span className="muted small">—</span>}
                </td>
                <td>
                  {isInitialEmail ? (
                    <input
                      type="number"
                      min={0}
                      placeholder="0"
                      title="Bounces Fixed"
                      value={getValue(f.mailing.id, "bouncesFixed", f.mailing.bouncesFixed)}
                      onChange={e => onBounceChange(f.mailing.id, "bouncesFixed", e.target.value)}
                      style={{ width: 70, padding: "3px 6px", border: "1px solid var(--line-2)", borderRadius: 4, fontSize: 11, fontFamily: "inherit" }}
                    />
                  ) : <span className="muted small">—</span>}
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

window.ResearchScreen = ResearchScreen;