/* =============================================================
   pm-extras.jsx — Advanced PM surfaces (PM-13 to PM-23)
   ─────────────────────────────────────────────────────────────
   Exports onto window:
     NotificationBell, NotificationInbox       (PM-14)
     MentionTextarea                            (PM-14)
     PmTeam                                     (PM-13)
     PmPlanner                                  (PM-15..17)
     PlanMigrationModal                         (PM-17/18)
     PmReports, ReportPreview                   (PM-20..23)
   ============================================================= */
const { useEffect, useMemo, useRef, useState, useCallback } = React;

/* ---------- shared bits ---------- */
const TYPE_COLORS = {
  "underlay-link":             "var(--info)",
  "overlay-site":              "var(--accent)",
  "combined-underlay-overlay": "var(--info)",
  "isp-termination":           "var(--warn)",
  "other":                     "var(--ink-400)",
};
const TYPE_LABEL = {
  "underlay-link":             "Underlay link",
  "overlay-site":              "Overlay site",
  "combined-underlay-overlay": "Underlay + Overlay",
  "isp-termination":           "ISP termination",
  "other":                     "Other",
};
const STATUS_COLORS = {
  planned:      "var(--ink-400)",
  scheduled:    "var(--info)",
  in_progress:  "var(--accent)",
  blocked:      "var(--risk)",
  done:         "var(--ok)",
  cancelled:    "var(--ink-300)",
};
const STATUS_LABEL = {
  planned: "Planned", scheduled: "Scheduled", in_progress: "In progress",
  blocked: "Blocked", done: "Done", cancelled: "Cancelled",
};

function _esc(s) {
  return String(s == null ? "" : s)
    .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
function safeMarkdown(text) {
  if (!text) return null;
  let html = _esc(text);
  html = html.replace(/(^|\s)@([a-zA-Z0-9._-]+)/g, '$1<span class="pm-mention">@$2</span>');
  html = html.replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>");
  html = html.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
  html = html.replace(/`([^`\n]+)`/g, '<code class="pm-code">$1</code>');
  html = html.replace(/(https?:\/\/[^\s<]+)/g, (u) => `<a href="${u}" target="_blank" rel="noreferrer">${u}</a>`);
  html = html.replace(/\n/g, "<br/>");
  return <span dangerouslySetInnerHTML={{ __html: html }} />;
}

function isoWeekId(date) {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  const day = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() + 4 - day);
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  const weekNo = Math.ceil((((d - yearStart) / (24 * 3600 * 1000)) + 1) / 7);
  return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
}
function startOfWeek(date) {
  const d = new Date(date);
  const day = d.getDay() || 7;
  d.setHours(0, 0, 0, 0);
  d.setDate(d.getDate() - (day - 1));
  return d;
}
function addDays(d, n) { return new Date(d.getTime() + n * 86400 * 1000); }
function dOnly(d) {
  if (!d) return "";
  if (typeof d === "string" && /^\d{4}-\d{2}-\d{2}/.test(d)) return d.slice(0, 10);
  const dd = new Date(d);
  if (isNaN(dd)) return "";
  return `${dd.getFullYear()}-${String(dd.getMonth()+1).padStart(2,"0")}-${String(dd.getDate()).padStart(2,"0")}`;
}
function pmFindSite(model, siteLike) {
  if (!model || !siteLike) return null;
  const code = String(siteLike.siteCode || "").trim().toUpperCase();
  const name = String(siteLike.siteName || siteLike.shortLabel || "").trim().toLowerCase();
  const refs = new Set([
    ...((siteLike.canonicalLinkRefs || siteLike.linkRefs || [])),
    ...((siteLike.links || []).map((l) => l && l.linkRef)),
    ...((siteLike.newUnderlayRows || []).map((n) => n && n.linkRef)),
  ].filter(Boolean).map((r) => String(r).trim().toUpperCase()));
  return ((model && model.sites) || []).find((site) => {
    if (code && String(site.siteCode || "").trim().toUpperCase() === code) return true;
    if (name && String(site.siteName || site.shortLabel || "").trim().toLowerCase() === name) return true;
    return ((site.canonicalLinkRefs || site.linkRefs || [])).some((ref) => refs.has(String(ref || "").trim().toUpperCase()));
  }) || null;
}
function pmReadinessSummaryForSite(siteLike, model) {
  const site = pmFindSite(model, siteLike) || siteLike || null;
  const stats = site && PRData.siteNewUnderlayReadiness
    ? PRData.siteNewUnderlayReadiness(site, model)
    : { rowCount: 0, readinessPercent: 0, phases: {}, blockedReadinessCount: 0, blockedBeforeChange: 0, readyForMigration: false, linksInMigration: 0, postMigrationComplete: false };
  const phase = (key) => stats.phases && stats.phases[key]
    ? stats.phases[key]
    : { key, label: key, percent: 0, blocked: 0, ready: 0, total: 0, complete: false };
  const blockedPhase = ["preChangeReadiness", "changeManagement", "linkMigration", "postMigration"]
    .map(phase)
    .find((p) => (p.blocked || 0) > 0);
  const hints = [];
  if ((stats.blockedBeforeChange || 0) > 0) hints.push({ key: "blocked-before-change", label: "Blocked before change", tone: "risk" });
  if (stats.readyForMigration) hints.push({ key: "ready-cr", label: "Ready for change request", tone: "ok" });
  if (stats.readyForMigration && (stats.linkMigrationCompleteLinks || 0) < (stats.rowCount || 0)) hints.push({ key: "ready-migration", label: "Ready for migration", tone: "ok" });
  if ((stats.linksInMigration || 0) > 0) hints.push({ key: "in-migration", label: "Link migration active", tone: "info" });
  if ((stats.postMigrationCompleteLinks || 0) < (stats.rowCount || 0) && (stats.linkMigrationCompleteLinks || 0) > 0) hints.push({ key: "post-pending", label: "Post-migration pending", tone: "warn" });
  if (!hints.length && stats.rowCount > 0) hints.push({ key: "readiness-open", label: "Readiness in progress", tone: "neutral" });
  if (!stats.rowCount) hints.push({ key: "no-new-underlay", label: "No New Underlay row", tone: "muted" });
  return {
    ...stats,
    site,
    blockedPhase,
    preChange: phase("preChangeReadiness"),
    changeManagement: phase("changeManagement"),
    migration: phase("linkMigration"),
    postMigration: phase("postMigration"),
    hints,
  };
}
function pmReadinessContextLines(summary) {
  const s = summary || {};
  const lines = [
    `New Underlay readiness: ${s.readinessPercent || 0}% across ${s.rowCount || 0} link row(s)`,
    `Pre-change readiness: ${(s.preChange && s.preChange.percent) || 0}%`,
    `Change management: ${(s.changeManagement && s.changeManagement.percent) || 0}%`,
    `Link migration: ${(s.migration && s.migration.percent) || 0}%`,
    `Post migration: ${(s.postMigration && s.postMigration.percent) || 0}%`,
  ];
  if (s.blockedPhase) lines.push(`Blocked phase: ${s.blockedPhase.label || s.blockedPhase.key}`);
  if (s.readyForMigration) lines.push("Eligibility: ready for change request and migration planning");
  if ((s.blockedBeforeChange || 0) > 0) lines.push(`Eligibility: ${s.blockedBeforeChange} blocked readiness item(s) before change`);
  if ((s.postMigrationCompleteLinks || 0) < (s.rowCount || 0) && (s.linkMigrationCompleteLinks || 0) > 0) lines.push("Eligibility: post-migration closure pending");
  return lines;
}
function _relTime(iso) {
  if (!iso) return "";
  const d = new Date(iso);
  const diff = (Date.now() - d.getTime()) / 1000;
  if (diff < 60) return `${Math.floor(diff)}s ago`;
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
  return `${Math.floor(diff / 86400)}d ago`;
}

/* =============================================================
   NotificationBell + NotificationInbox (PM-14)
   ============================================================= */
function NotificationBell({ user, onOpenEntity }) {
  const [items, setItems] = useState([]);
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  const email = (user && user.email) || "";

  const reload = useCallback(async () => {
    if (!email) { setItems([]); return; }
    try {
      const list = await PMClient.listNotifications({ subjectEmail: email });
      setItems(list);
    } catch (_) { setItems([]); }
  }, [email]);

  useEffect(() => {
    reload();
    const unsub = PMClient.subscribe(reload);
    return () => unsub && unsub();
  }, [reload]);

  useEffect(() => {
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    if (open) document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, [open]);

  const unread = items.filter((n) => !n.readAt).length;

  const onClickItem = async (n) => {
    if (!n.readAt) await PMClient.markNotificationRead(n.id);
    setOpen(false);
    if (onOpenEntity) onOpenEntity(n);
  };

  return (
    <div className="pm-bell-wrap" ref={ref}>
      <button className={"pm-bell" + (unread > 0 ? " has-unread" : "")} onClick={() => setOpen((v) => !v)} title="Notifications">
        <Glyph name="info" size={14}/>
        {unread > 0 && <span className="pm-bell-badge">{unread > 99 ? "99+" : unread}</span>}
      </button>
      {open && (
        <div className="pm-bell-pop">
          <div className="pm-bell-h">
            <div className="title">Notifications</div>
            <div className="flex gap-sm">
              {items.some((n) => !n.readAt) && (
                <button className="btn ghost" onClick={async () => { await PMClient.markAllNotificationsRead(email); await reload(); }}>
                  Mark all read
                </button>
              )}
            </div>
          </div>
          <div className="pm-bell-list">
            {items.length === 0 && <Empty title="No notifications" sub="Mentions, assignments, and plan updates land here."/>}
            {items.slice(0, 50).map((n) => (
              <div key={n.id} className={"pm-bell-item" + (n.readAt ? "" : " unread")} onClick={() => onClickItem(n)}>
                <div className="pm-bell-actor">
                  <span className="pm-avatar" style={{width:24, height:24, borderRadius:12, fontSize:9, background: `hsl(${(n.actor || "").length * 13 % 360}deg 45% 45%)`}}>
                    {(n.actorName || n.actor || "?").slice(0,2).toUpperCase()}
                  </span>
                </div>
                <div className="pm-bell-body">
                  <div className="pm-bell-kind">
                    <span className="strong">{n.actorName || n.actor || "someone"}</span>{" "}
                    {n.kind === "mention" && <>mentioned you</>}
                    {n.kind === "plan-assigned" && <>assigned a plan to you</>}
                    {n.kind === "plan-co-assigned" && <>added you to a plan</>}
                    {!["mention","plan-assigned","plan-co-assigned"].includes(n.kind) && <>{n.kind}</>}
                  </div>
                  {n.body && <div className="pm-bell-snippet">{n.body}</div>}
                  <div className="pm-bell-meta">
                    {n.sourceEntity && <span className="mono">{n.sourceEntity}{n.sourceIdRef ? ` · ${n.sourceIdRef}` : ""}</span>}
                    <span style={{flex:1}}/>
                    <span>{_relTime(n.createdAt)}</span>
                  </div>
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

/* =============================================================
   MentionTextarea (PM-14)
   ─────────────────────────────────────────────────────────────
   Drop-in <textarea> replacement with @mention autocomplete.
   ============================================================= */
function MentionTextarea({ value, onChange, placeholder, onKeyDown, rows = 4, className }) {
  const [users, setUsers] = useState([]);
  const [popOpen, setPopOpen] = useState(false);
  const [matches, setMatches] = useState([]);
  const [selectedIdx, setSelectedIdx] = useState(0);
  const [mentionStart, setMentionStart] = useState(-1);
  const mentionQueryRef = useRef("");
  const ref = useRef(null);

  useEffect(() => {
    let alive = true;
    const loadCandidates = () => (PMClient.resolveMentions ? PMClient.resolveMentions("") : PMClient.listUsers());
    loadCandidates().then((u) => { if (alive) setUsers(u || []); }).catch(() => {});
    const unsub = PMClient.subscribe(async () => {
      const u = await loadCandidates().catch(() => []);
      if (alive) setUsers(u || []);
    });
    return () => { alive = false; unsub && unsub(); };
  }, []);

  const handleChange = (e) => {
    const v = e.target.value;
    onChange(v);
    detectMention(v, e.target.selectionStart);
  };

  function detectMention(v, caret) {
    const before = v.slice(0, caret);
    const m = before.match(/(?:^|\s)@([a-zA-Z0-9._-]*)$/);
    if (!m) { setPopOpen(false); setMatches([]); return; }
    const start = caret - m[1].length - 1; // position of '@'
    setMentionStart(start);
    const q = m[1].toLowerCase();
    mentionQueryRef.current = q;
    const applyCandidates = (candidates) => {
      const filtered = (candidates || []).filter((u) => u.isActive !== false).filter((u) => {
      if (!q) return true;
      const h = (u.email || "").toLowerCase();
      const n = (u.displayName || "").toLowerCase();
      const handle = (u.handle || "").toLowerCase();
      const role = (u.role || "").toLowerCase();
      return h.startsWith(q) || h.includes(q) || n.includes(q) || handle.includes(q) || role.includes(q);
    }).slice(0, 6);
      setMatches(filtered);
      setSelectedIdx(0);
      setPopOpen(filtered.length > 0);
    };
    applyCandidates(users);
    if (PMClient.resolveMentions) {
      PMClient.resolveMentions(q).then((u) => {
        if (mentionQueryRef.current === q) applyCandidates(u || []);
      }).catch(() => {});
    }
  }

  const insertMention = (u) => {
    if (mentionStart < 0 || !ref.current) return;
    const handle = (u.email || "").split("@")[0] || u.displayName || "user";
    const v = value || "";
    const before = v.slice(0, mentionStart);
    const after = v.slice(ref.current.selectionStart);
    const insert = "@" + handle + " ";
    const next = before + insert + after;
    onChange(next);
    setPopOpen(false);
    // restore caret
    setTimeout(() => {
      if (!ref.current) return;
      const pos = before.length + insert.length;
      ref.current.focus();
      ref.current.setSelectionRange(pos, pos);
    }, 0);
  };

  const handleKey = (e) => {
    if (popOpen && matches.length > 0) {
      if (e.key === "ArrowDown") { e.preventDefault(); setSelectedIdx((i) => (i + 1) % matches.length); return; }
      if (e.key === "ArrowUp")   { e.preventDefault(); setSelectedIdx((i) => (i - 1 + matches.length) % matches.length); return; }
      if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); insertMention(matches[selectedIdx]); return; }
      if (e.key === "Escape") { e.preventDefault(); setPopOpen(false); return; }
    }
    if (onKeyDown) onKeyDown(e);
  };

  return (
    <div className="pm-mention-host">
      <textarea
        ref={ref}
        className={className || "pm-input pm-textarea"}
        rows={rows}
        placeholder={placeholder}
        value={value || ""}
        onChange={handleChange}
        onKeyDown={handleKey}
        onBlur={() => setTimeout(() => setPopOpen(false), 120)}
      />
      {popOpen && (
        <div className="pm-mention-pop">
          {matches.map((u, i) => (
            <div key={u.email} className={"pm-mention-item" + (i === selectedIdx ? " sel" : "")}
                 onMouseDown={(e) => { e.preventDefault(); insertMention(u); }}>
              <span className="pm-avatar" style={{width:22, height:22, borderRadius:11, fontSize:9, background:u.avatarColor || "#888"}}>
                {(u.displayName || u.email || "?").slice(0, 2).toUpperCase()}
              </span>
              <span className="pm-mention-name">{u.displayName || u.email}</span>
              <span className="pm-mention-handle">@{(u.email || "").split("@")[0]}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* =============================================================
   PmTeam — admin-managed user registry (PM-13)
   ============================================================= */
function PmTeam({ user }) {
  const [users, setUsers] = useState([]);
  const [edit, setEdit] = useState(null);
  const [busy, setBusy] = useState(false);
  const isAdmin = user && user.role === "admin";

  const reload = useCallback(async () => {
    const list = await PMClient.listUsers().catch(() => []);
    setUsers(list);
  }, []);
  useEffect(() => { reload(); const u = PMClient.subscribe(reload); return () => u && u(); }, [reload]);

  const save = async (next) => {
    setBusy(true);
    try { await PMClient.upsertUser(next); setEdit(null); await reload(); }
    finally { setBusy(false); }
  };
  const deactivate = async (email) => {
    if (!confirm(`Deactivate ${email}?`)) return;
    await PMClient.deactivateUser(email); await reload();
  };

  return (
    <div className="pm-team">
      <div className="pm-section-h" style={{marginTop:6}}>
        <div className="title">Team — {users.length} {users.length === 1 ? "person" : "people"}</div>
        {isAdmin && (
          <button className="btn primary" onClick={() => setEdit({ email: "", displayName: "", role: "engineer", regionScope: [], isActive: true })}>
            <Glyph name="add" size={13}/> Add user
          </button>
        )}
      </div>
      {!isAdmin && <div className="pm-readonly-pill" style={{marginBottom:10}}>Read-only · only Admin can change roles.</div>}
      <div className="pm-team-grid">
        {users.length === 0 && <Empty title="No users yet" sub="The local user registers automatically. Add teammates here."/>}
        {users.map((u) => (
          <div key={u.email} className={"pm-team-card" + (u.isActive === false ? " inactive" : "")}>
            <div className="pm-team-top">
              <span className="pm-avatar" style={{width:38, height:38, borderRadius:19, fontSize:12, background:u.avatarColor || "#888"}}>
                {(u.displayName || u.email || "?").slice(0, 2).toUpperCase()}
              </span>
              <div className="pm-team-id">
                <div className="pm-team-name">{u.displayName || u.email}</div>
                <div className="pm-team-email mono">{u.email}</div>
              </div>
              <span className={"pm-team-role pm-role-" + (u.role || "engineer")}>{u.role || "engineer"}</span>
            </div>
            <div className="pm-team-meta">
              {u.regionScope && u.regionScope.length > 0 && (
                <span>Scope: <span className="mono">{u.regionScope.join(", ")}</span></span>
              )}
              {u.lastSeenAt && <span>Last seen {_relTime(u.lastSeenAt)}</span>}
              {u.isActive === false && <span className="pm-tag-inactive">Inactive</span>}
            </div>
            {isAdmin && (
              <div className="pm-team-actions">
                <button className="btn" onClick={() => setEdit(u)}><Glyph name="edit" size={11}/> Edit</button>
                {u.isActive !== false && <button className="btn danger" onClick={() => deactivate(u.email)}>Deactivate</button>}
                {u.isActive === false && <button className="btn" onClick={() => save({ ...u, isActive: true })}>Reactivate</button>}
              </div>
            )}
          </div>
        ))}
      </div>
      {edit && <UserEditor user={edit} onClose={() => setEdit(null)} onSave={save} busy={busy}/>}
    </div>
  );
}

function UserEditor({ user, onClose, onSave, busy }) {
  const [draft, setDraft] = useState(user || {});
  const scopeStr = (draft.regionScope || []).join(", ");
  return (
    <div className="pm-modal-backdrop" onClick={onClose}>
      <div className="pm-modal" onClick={(e) => e.stopPropagation()}>
        <div className="pm-modal-h"><div className="title">{user && user.email ? "Edit user" : "Add user"}</div>
          <button className="btn ghost" onClick={onClose}><Glyph name="close" size={14}/></button></div>
        <div className="pm-modal-body">
          <div className="pm-field"><label className="pm-field-label">Email</label>
            <input className="pm-input" value={draft.email || ""} onChange={(e) => setDraft({ ...draft, email: e.target.value })} disabled={user && user.email} autoFocus/></div>
          <div className="pm-field"><label className="pm-field-label">Display name</label>
            <input className="pm-input" value={draft.displayName || ""} onChange={(e) => setDraft({ ...draft, displayName: e.target.value })}/></div>
          <div className="pm-field"><label className="pm-field-label">Role</label>
            <div className="pm-role-radio">
              {["management","engineer","po","admin"].map((r) => (
                <button key={r}
                        className={"pm-role-btn pm-role-" + r + (draft.role === r ? " active" : "")}
                        onClick={() => setDraft({ ...draft, role: r })}>{r}</button>
              ))}
            </div></div>
          <div className="pm-field"><label className="pm-field-label">Region scope (comma-separated)</label>
            <input className="pm-input" value={scopeStr}
                   onChange={(e) => setDraft({ ...draft, regionScope: e.target.value.split(",").map((s) => s.trim()).filter(Boolean) })}
                   placeholder="EMEA, APAC"/></div>
        </div>
        <div className="pm-modal-actions">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" disabled={busy || !draft.email || !draft.email.includes("@")} onClick={() => onSave(draft)}>{busy ? "Saving…" : "Save"}</button>
        </div>
      </div>
    </div>
  );
}

/* =============================================================
   PmPlanner (PM-15, PM-16, PM-17) — calendar + site search rail
   ============================================================= */
function PmPlanner({ model, role, plans, onPlanChange, setFocus }) {
  const [view, setView] = useState("month");       // month | week | list | board
  const [anchor, setAnchor] = useState(() => startOfWeek(new Date()));
  const [searchOpen, setSearchOpen] = useState(true);
  const [openModal, setOpenModal] = useState(null); // {site, weekStart}
  const [editPlan, setEditPlan] = useState(null);
  const [typeFilter, setTypeFilter] = useState("");
  const [statusFilter, setStatusFilter] = useState("");
  const canEdit = role !== "management";

  // Resolve weeks shown: 12 weeks (3 months) starting from anchor
  const weeks = useMemo(() => {
    const out = [];
    for (let i = 0; i < 12; i++) out.push(addDays(anchor, i * 7));
    return out;
  }, [anchor]);

  const filteredPlans = useMemo(() => {
    return plans.filter((p) => {
      if (typeFilter && p.type !== typeFilter) return false;
      if (statusFilter && p.status !== statusFilter) return false;
      return true;
    });
  }, [plans, typeFilter, statusFilter]);

  const onDropSiteOnWeek = (site, weekStart) => {
    setOpenModal({ site, weekStart, model });
  };
  const onSavePlan = async (input) => {
    await PMClient.upsertPlan(input);
    setOpenModal(null);
    setEditPlan(null);
    onPlanChange && onPlanChange();
  };

  return (
    <div className="pm-planner">
      <div className="pm-planner-toolbar">
        <div className="pm-window-toggle">
          {[
            { id: "month", label: "Month" },
            { id: "week", label: "Week" },
            { id: "list", label: "List" },
            { id: "board", label: "Board" },
          ].map((m) => (
            <button key={m.id} className={view === m.id ? "active" : ""} onClick={() => setView(m.id)}>{m.label}</button>
          ))}
        </div>
        <div className="flex gap-sm">
          <button className="btn" onClick={() => setAnchor(addDays(anchor, -7))}><Glyph name="chevron" size={11} style={{transform:"rotate(180deg)"}}/> Prev</button>
          <button className="btn" onClick={() => setAnchor(startOfWeek(new Date()))}>Today</button>
          <button className="btn" onClick={() => setAnchor(addDays(anchor, 7))}>Next <Glyph name="chevron" size={11}/></button>
        </div>
        <FilterSelect label="Type" value={typeFilter} onChange={setTypeFilter} options={[
          {value:"underlay-link", label:"Underlay link"},
          {value:"overlay-site", label:"Overlay site"},
          {value:"combined-underlay-overlay", label:"Underlay + Overlay"},
          {value:"isp-termination", label:"ISP termination"},
          {value:"other", label:"Other"},
        ]}/>
        <FilterSelect label="Status" value={statusFilter} onChange={setStatusFilter} options={[
          {value:"planned", label:"Planned"},
          {value:"scheduled", label:"Scheduled"},
          {value:"in_progress", label:"In progress"},
          {value:"blocked", label:"Blocked"},
          {value:"done", label:"Done"},
        ]}/>
        <span style={{flex:1}}/>
        {canEdit && (
          <button className="btn primary" onClick={() => setOpenModal({ weekStart: anchor, model })}>
            <Glyph name="add" size={12}/> New plan
          </button>
        )}
        <button className="btn" onClick={() => setSearchOpen((v) => !v)}>
          {searchOpen ? "Hide" : "Show"} site selector
        </button>
      </div>

      <div className={"pm-planner-body" + (searchOpen ? "" : " no-rail")}>
        <div className="pm-planner-main">
          {view === "month" && <PlannerMonthGrid weeks={weeks} plans={filteredPlans} onDropSite={onDropSiteOnWeek} onOpenPlan={setEditPlan} canEdit={canEdit}/>}
          {view === "week" && <PlannerWeekStrip weeks={weeks} plans={filteredPlans} onDropSite={onDropSiteOnWeek} onOpenPlan={setEditPlan} canEdit={canEdit}/>}
          {view === "list" && <PlannerListView weeks={weeks} plans={filteredPlans} onOpenPlan={setEditPlan}/>}
          {view === "board" && <PlannerBoard plans={filteredPlans} canEdit={canEdit} onOpenPlan={setEditPlan} onStatusChange={async (id, status) => { await PMClient.upsertPlan({ id, status }); onPlanChange && onPlanChange(); }}/>}
        </div>
      </div>

      {searchOpen && (
        <PmSiteSearchRail model={model} plans={plans}
                          canDrag={canEdit}
                          onDragStartSite={() => {}}
                          onPickSite={(site) => canEdit && setOpenModal({ site, weekStart: anchor, model })}
                          setFocus={setFocus}/>
      )}

      {openModal && (
        <PlanMigrationModal
          initial={openModal}
          model={model}
          onClose={() => setOpenModal(null)}
          onSave={onSavePlan}/>
      )}
      {editPlan && (
        <PlanMigrationModal
          initial={{ existing: editPlan, model }}
          model={model}
          onClose={() => setEditPlan(null)}
          onSave={onSavePlan}
          onClose_={() => setEditPlan(null)}
          onDelete={async () => { await PMClient.deletePlan(editPlan.id); setEditPlan(null); onPlanChange && onPlanChange(); }}
          onCloseAsDone={async (options) => { await PMClient.closePlan(editPlan.id, options); setEditPlan(null); onPlanChange && onPlanChange(); }}
        />
      )}
    </div>
  );
}

function PlannerWeekHeader({ start }) {
  const w = isoWeekId(start);
  const end = addDays(start, 6);
  const label = `${start.toLocaleDateString(undefined, { day:"2-digit", month:"short" })} – ${end.toLocaleDateString(undefined, { day:"2-digit", month:"short" })}`;
  return (
    <div className="pm-plan-week-h">
      <span className="pm-plan-week-id mono">{w}</span>
      <span className="pm-plan-week-range">{label}</span>
    </div>
  );
}

function PlannerWeekSlot({ weekStart, plans, onDropSite, onOpenPlan, canEdit, compact }) {
  const [hovering, setHovering] = useState(false);
  const ws = dOnly(weekStart);
  const we = dOnly(addDays(weekStart, 6));
  const inWeek = plans.filter((p) => {
    if (!p.targetDate) return p.targetWeek === isoWeekId(weekStart);
    return p.targetDate >= ws && p.targetDate <= we;
  });
  return (
    <div className={"pm-plan-slot" + (hovering ? " hovering" : "") + (compact ? " compact" : "")}
         onDragOver={(e) => { if (canEdit && e.dataTransfer.types.includes("application/x-pm-site")) { e.preventDefault(); setHovering(true); } }}
         onDragLeave={() => setHovering(false)}
         onDrop={(e) => {
           setHovering(false);
           const raw = e.dataTransfer.getData("application/x-pm-site");
           if (!raw || !canEdit) return;
           try { const site = JSON.parse(raw); onDropSite(site, weekStart); } catch (_) {}
         }}>
      <PlannerWeekHeader start={weekStart}/>
      <div className="pm-plan-slot-cap">
        <span className={"pm-plan-cap-pill" + (inWeek.length >= 5 ? " hot" : "")}>{inWeek.length} plan{inWeek.length === 1 ? "" : "s"}</span>
      </div>
      <div className="pm-plan-cards">
        {inWeek.length === 0 && <div className="pm-plan-empty">{canEdit ? "Drop a site here" : "—"}</div>}
        {inWeek.map((p) => <PlanCard key={p.id} plan={p} onOpen={() => onOpenPlan(p)}/>)}
      </div>
    </div>
  );
}

function PlanCard({ plan, onOpen }) {
  return (
    <div className="pm-plan-card" style={{ borderLeftColor: TYPE_COLORS[plan.type] || "var(--ink-400)" }} onClick={onOpen}>
      <div className="pm-plan-card-h">
        <span className="pm-plan-card-type" style={{ background: TYPE_COLORS[plan.type] || "var(--ink-400)" }}>{TYPE_LABEL[plan.type] || plan.type}</span>
        <span className={"pm-plan-card-status pm-status-" + plan.status}>{STATUS_LABEL[plan.status] || plan.status}</span>
      </div>
      <div className="pm-plan-card-title">{plan.siteCode || plan.siteName || "—"}</div>
      <div className="pm-plan-card-meta">
        <span>{plan.country || "—"}</span>
        {plan.targetDate && <span className="mono">{plan.targetDate}</span>}
        {plan.ownerEmail && <span className="mono">{(plan.ownerEmail || "").split("@")[0]}</span>}
      </div>
    </div>
  );
}

function PlannerMonthGrid({ weeks, plans, onDropSite, onOpenPlan, canEdit }) {
  // 12 weeks rendered as 3 rows × 4 weeks
  const rows = [weeks.slice(0,4), weeks.slice(4,8), weeks.slice(8,12)];
  return (
    <div className="pm-plan-month">
      {rows.map((row, i) => (
        <div key={i} className="pm-plan-month-row">
          {row.map((w) => (
            <PlannerWeekSlot key={dOnly(w)} weekStart={w} plans={plans} onDropSite={onDropSite} onOpenPlan={onOpenPlan} canEdit={canEdit}/>
          ))}
        </div>
      ))}
    </div>
  );
}

function PlannerWeekStrip({ weeks, plans, onDropSite, onOpenPlan, canEdit }) {
  return (
    <div className="pm-plan-strip">
      {weeks.map((w) => (
        <PlannerWeekSlot key={dOnly(w)} weekStart={w} plans={plans} onDropSite={onDropSite} onOpenPlan={onOpenPlan} canEdit={canEdit} compact/>
      ))}
    </div>
  );
}

function PlannerListView({ weeks, plans, onOpenPlan }) {
  const start = dOnly(weeks[0]);
  const end = dOnly(addDays(weeks[weeks.length - 1], 6));
  const visible = plans.filter((p) => (p.targetDate || "") >= start && (p.targetDate || "") <= end).sort((a,b) => (a.targetDate || "").localeCompare(b.targetDate || ""));
  return (
    <div className="pm-plan-list">
      {visible.length === 0 && <Empty title="Nothing planned" sub="Drag a site from the search rail onto a week."/>}
      {visible.map((p) => (
        <div key={p.id} className="pm-plan-list-row" onClick={() => onOpenPlan(p)}>
          <span className="mono pm-plan-list-date">{p.targetDate || "—"}</span>
          <span className="pm-plan-card-type" style={{background: TYPE_COLORS[p.type]}}>{TYPE_LABEL[p.type]}</span>
          <span className="strong">{p.siteCode || p.siteName || "—"}</span>
          <span className="muted">{p.country}</span>
          <span style={{flex:1}}/>
          <span className={"pm-plan-card-status pm-status-" + p.status}>{STATUS_LABEL[p.status] || p.status}</span>
          {p.ownerEmail && <span className="mono">{p.ownerEmail.split("@")[0]}</span>}
        </div>
      ))}
    </div>
  );
}

function PlannerBoard({ plans, canEdit, onOpenPlan, onStatusChange }) {
  const cols = [
    { id: "planned",     label: "Planned",      color: STATUS_COLORS.planned },
    { id: "scheduled",   label: "Scheduled",    color: STATUS_COLORS.scheduled },
    { id: "in_progress", label: "In progress",  color: STATUS_COLORS.in_progress },
    { id: "blocked",     label: "Blocked",      color: STATUS_COLORS.blocked },
    { id: "done",        label: "Done",         color: STATUS_COLORS.done },
  ];
  const [hovering, setHovering] = useState(null);
  return (
    <div className="pm-plan-board">
      {cols.map((c) => {
        const items = plans.filter((p) => p.status === c.id);
        return (
          <div key={c.id} className={"pm-kcol" + (hovering === c.id ? " hovering" : "")}
               onDragOver={(e) => { if (canEdit && e.dataTransfer.types.includes("application/x-pm-plan")) { e.preventDefault(); setHovering(c.id); } }}
               onDragLeave={() => setHovering(null)}
               onDrop={(e) => {
                 setHovering(null);
                 const id = e.dataTransfer.getData("application/x-pm-plan");
                 if (id && canEdit) onStatusChange(id, c.id);
               }}>
            <div className="pm-kcol-h">
              <span className="pm-kcol-dot" style={{background: c.color}}/>
              <span>{c.label}</span>
              <span className="mono pm-kcol-count">{items.length}</span>
            </div>
            <div className="pm-kcol-body">
              {items.length === 0 && <div className="pm-kcol-empty">—</div>}
              {items.map((p) => (
                <div key={p.id}
                     draggable={canEdit}
                     onDragStart={(e) => e.dataTransfer.setData("application/x-pm-plan", p.id)}
                     onClick={() => onOpenPlan(p)}>
                  <PlanCard plan={p} onOpen={() => onOpenPlan(p)}/>
                </div>
              ))}
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* =============================================================
   PmSiteSearchRail (PM-16)
   ============================================================= */
function PmSiteSearchRail({ model, plans, onPickSite, canDrag, setFocus }) {
  const [q, setQ] = useState("");
  const [region, setRegion] = useState("");
  const [country, setCountry] = useState("");
  const [batch, setBatch] = useState("");
  const [classification, setClassification] = useState("");
  const [criticality, setCriticality] = useState("");
  const [overlayStatus, setOverlayStatus] = useState("");
  const [hwStatus, setHwStatus] = useState("");
  const [mode, setMode] = useState("all");
  const [pinned, setPinned] = useState([]);

  useEffect(() => {
    let alive = true;
    const load = async () => {
      const list = await PMClient.listPinnedSites().catch(() => []);
      if (alive) setPinned(list || []);
    };
    load();
    const u = PMClient.subscribe(load);
    return () => { alive = false; u && u(); };
  }, []);

  // Build a single index of site cards from the canonical joined model.
  const activePlanMap = useMemo(() => {
    const map = new Map();
    plans
      .filter((p) => p && p.siteCode && p.status !== "done" && p.status !== "cancelled")
      .forEach((p) => {
        const key = String(p.siteCode || "").trim().toUpperCase();
        if (!key) return;
        if (!map.has(key)) map.set(key, []);
        map.get(key).push(p);
      });
    return map;
  }, [plans]);

  const cards = useMemo(() => {
    return PRData.siteCards(model).map((card) => {
      const sourceSite = pmFindSite(model, card);
      const nu = pmReadinessSummaryForSite(sourceSite || card, model);
      const cardKey = String(card.siteCode || "").trim().toUpperCase();
      const activePlans = activePlanMap.get(cardKey) || [];
      const linkScope = Number(card.underlayLinks || 0);
      const activated = Number(card.activatedLinks || 0);
      const readinessPct = linkScope > 0 ? Math.min(100, Math.round((activated / linkScope) * 100)) : 0;
      const hwDelivery = String(card.hwDelivery || "");
      const overlayStatus = String(card.overlayStatus || "");
      return {
        ...card,
        activePlanCount: activePlans.length,
        activePlanStatus: activePlans[0] ? activePlans[0].status : "",
        hasActivePlan: activePlans.length > 0,
        readinessPct,
        newUnderlayReadiness: nu,
        newUnderlayReadinessPct: nu.readinessPercent || 0,
        newUnderlayRows: nu.rowCount || 0,
        newUnderlayBlockedPhase: nu.blockedPhase ? (nu.blockedPhase.label || nu.blockedPhase.key) : "",
        newUnderlayChangeManagement: (nu.changeManagement && nu.changeManagement.percent) || 0,
        newUnderlayMigration: (nu.migration && nu.migration.percent) || 0,
        newUnderlayHints: nu.hints || [],
        isBlocked: (card.blockers || []).length > 0,
        hasReadinessBlocker: (nu.blockedReadinessCount || 0) > 0,
        isHwDelivered: /deliver/i.test(hwDelivery),
        isMigrated: /migrated/i.test(overlayStatus),
      };
    });
  }, [model, activePlanMap]);

  // Normalize once per card so all filter compares are case/whitespace tolerant.
  // Without this, a single stray trailing space in the workbook silently
  // breaks the "country === 'India'" filter even though the dropdown shows it.
  const norm = (v) => String(v == null ? "" : v).trim().toLowerCase();
  const indexedCards = useMemo(() => cards.map((c) => ({
    card: c,
    _region: norm(c.region),
    _country: norm(c.country),
    _batch: norm(c.batch),
    _class: norm(c.classification),
    _crit: norm(c.criticality),
    _overlay: norm(c.overlayStatus),
    _hw: norm(c.hwDelivery),
    _hay: [
      c.siteCode, c.siteName, c.shortLabel,
      c.country, c.region, c.classification, c.criticality, c.batch,
      c.overlayStatus, c.hwDelivery, c.activePlanStatus,
      c.underlayLinks, c.activatedLinks,
      c.newUnderlayReadinessPct, c.newUnderlayBlockedPhase, c.newUnderlayChangeManagement, c.newUnderlayMigration,
      ...((c.newUnderlayHints || []).map((h) => h.label)),
      ...(c.blockers || []),
    ].filter((v) => v != null && v !== "").join(" ").toLowerCase(),
  })), [cards]);

  const matchesScope = (ic, scope) => {
    // scope tells us which filter to *exclude* (so the option list for that
    // filter can still show every value that's reachable given the OTHER
    // active filters — cascading filter behaviour the user expects).
    if (scope !== "region" && region && ic._region !== norm(region)) return false;
    if (scope !== "country" && country && ic._country !== norm(country)) return false;
    if (scope !== "batch" && batch && ic._batch !== norm(batch)) return false;
    if (scope !== "classification" && classification && ic._class !== norm(classification)) return false;
    if (scope !== "criticality" && criticality && ic._crit !== norm(criticality)) return false;
    if (scope !== "overlayStatus" && overlayStatus && ic._overlay !== norm(overlayStatus)) return false;
    if (scope !== "hwStatus" && hwStatus && ic._hw !== norm(hwStatus)) return false;
    return true;
  };

  // Multi-token search: "india gold" matches sites in India that are Gold.
  const tokens = useMemo(() =>
    q.trim().toLowerCase().split(/\s+/).filter(Boolean),
  [q]);

  const filtered = useMemo(() => {
    return indexedCards.filter((ic) => {
      if (!matchesScope(ic, null)) return false;
      const c = ic.card;
      if (mode === "planned" && !c.hasActivePlan) return false;
      if (mode === "unplanned" && c.hasActivePlan) return false;
      if (mode === "risk" && !(c.atRisk || c.isBlocked || c.hasReadinessBlocker)) return false;
      if (mode === "migrated" && !c.isMigrated) return false;
      if (mode === "hardware" && !c.isHwDelivered) return false;
      if (mode === "ready-cr" && !(c.newUnderlayHints || []).some((h) => h.key === "ready-cr")) return false;
      if (tokens.length > 0) {
        for (const t of tokens) if (!ic._hay.includes(t)) return false;
      }
      return true;
    }).map((ic) => ic.card);
  }, [indexedCards, tokens, region, country, batch, classification, criticality, overlayStatus, hwStatus, mode]);

  // Cascading options: each dropdown reflects only values reachable under the
  // OTHER active filters. Picking Region=APAC narrows Country to APAC countries.
  const reachableFor = (field, scope) => {
    const seen = new Map();
    for (const ic of indexedCards) {
      if (!matchesScope(ic, scope)) continue;
      const v = (ic.card[field] || "").toString().trim();
      if (!v) continue;
      const k = v.toLowerCase();
      if (!seen.has(k)) seen.set(k, v);
    }
    return [...seen.values()].sort((a, b) => a.localeCompare(b));
  };

  const opts = useMemo(() => ({
    region: reachableFor("region", "region"),
    country: reachableFor("country", "country"),
    batch: reachableFor("batch", "batch"),
    classification: reachableFor("classification", "classification"),
    criticality: reachableFor("criticality", "criticality"),
    overlayStatus: reachableFor("overlayStatus", "overlayStatus"),
    hwStatus: reachableFor("hwDelivery", "hwStatus"),
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }), [indexedCards, region, country, batch, classification, criticality, overlayStatus, hwStatus]);

  // If the active filter value isn't in the (possibly narrowed) options anymore,
  // drop it so the UI doesn't silently filter to zero with a stale chip.
  useEffect(() => {
    const drop = (val, options, setter) => {
      if (val && !options.some((o) => norm(o) === norm(val))) setter("");
    };
    drop(region, opts.region, setRegion);
    drop(country, opts.country, setCountry);
    drop(batch, opts.batch, setBatch);
    drop(classification, opts.classification, setClassification);
    drop(criticality, opts.criticality, setCriticality);
    drop(overlayStatus, opts.overlayStatus, setOverlayStatus);
    drop(hwStatus, opts.hwStatus, setHwStatus);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [opts]);

  const stats = useMemo(() => ({
    total: cards.length,
    filtered: filtered.length,
    planned: cards.filter((c) => c.hasActivePlan).length,
    risk: cards.filter((c) => c.atRisk || c.isBlocked || c.hasReadinessBlocker).length,
    unplanned: cards.filter((c) => !c.hasActivePlan).length,
    migrated: cards.filter((c) => c.isMigrated).length,
    hardware: cards.filter((c) => c.isHwDelivered).length,
    readyCr: cards.filter((c) => (c.newUnderlayHints || []).some((h) => h.key === "ready-cr")).length,
  }), [cards, filtered]);

  const modeOptions = [
    { id: "all", label: "All", count: stats.total },
    { id: "planned", label: "Planned", count: stats.planned },
    { id: "unplanned", label: "Unplanned", count: stats.unplanned },
    { id: "risk", label: "At risk", count: stats.risk },
    { id: "migrated", label: "Migrated", count: stats.migrated },
    { id: "hardware", label: "HW ready", count: stats.hardware },
    { id: "ready-cr", label: "CR ready", count: stats.readyCr },
  ];

  const hasFilters = q || region || country || batch || classification || criticality || overlayStatus || hwStatus || mode !== "all";
  const resetFilters = () => {
    setQ("");
    setRegion("");
    setCountry("");
    setBatch("");
    setClassification("");
    setCriticality("");
    setOverlayStatus("");
    setHwStatus("");
    setMode("all");
  };

  const pinnedSet = new Set(pinned);
  const togglePin = async (siteCode) => {
    const next = await PMClient.togglePinnedSite(siteCode);
    setPinned(next || []);
  };

  const pinnedCards = filtered.filter((c) => pinnedSet.has(c.siteCode));
  const otherCards = filtered.filter((c) => !pinnedSet.has(c.siteCode));

  return (
    <aside className="pm-site-rail pm-site-selector" aria-label="Site selector">
      <div className="pm-site-selector-head">
        <div>
          <div className="pm-site-selector-eyebrow">Horizon site selector</div>
          <div className="pm-site-selector-title">Workbook site floor</div>
          <div className="pm-site-selector-sub">Search, filter, click to plan, or drag any site onto a week above.</div>
        </div>
        <div className="pm-site-selector-count">
          <span className="mono">{stats.filtered}</span>
          <small>of {stats.total}</small>
        </div>
      </div>

      <div className="pm-site-selector-metrics">
        <div className="pm-site-metric">
          <span className="mono">{stats.planned}</span>
          <small>planned</small>
        </div>
        <div className="pm-site-metric warn">
          <span className="mono">{stats.risk}</span>
          <small>at risk</small>
        </div>
        <div className="pm-site-metric">
          <span className="mono">{stats.unplanned}</span>
          <small>unplanned</small>
        </div>
      </div>

      <div className="pm-site-mode-tabs" role="group" aria-label="Deployment mode">
        {modeOptions.map((item) => (
          <button
            key={item.id}
            type="button"
            className={"pm-site-mode-tab" + (mode === item.id ? " active" : "")}
            onClick={() => setMode(item.id)}
          >
            <span>{item.label}</span>
            <small className="mono">{item.count}</small>
          </button>
        ))}
      </div>

      <div className="pm-site-search-shell">
        <SearchInput value={q} onChange={setQ} placeholder="Search code, name, country, region, batch, status, hardware, blocker"/>
        {hasFilters && (
          <button className="btn ghost pm-site-reset" onClick={resetFilters}>Reset filters</button>
        )}
      </div>

      <div className="pm-site-rail-filters">
        <FilterSelect label="Region" value={region} onChange={setRegion} options={opts.region}/>
        <FilterSelect label="Country" value={country} onChange={setCountry} options={opts.country}/>
        {opts.batch.length > 0 && <FilterSelect label="Batch" value={batch} onChange={setBatch} options={opts.batch}/>}
        {opts.classification.length > 0 && <FilterSelect label="Class" value={classification} onChange={setClassification} options={opts.classification}/>}
        {opts.criticality.length > 0 && <FilterSelect label="Tier" value={criticality} onChange={setCriticality} options={opts.criticality}/>}
        {opts.overlayStatus.length > 0 && <FilterSelect label="Overlay" value={overlayStatus} onChange={setOverlayStatus} options={opts.overlayStatus}/>}
        {opts.hwStatus.length > 0 && <FilterSelect label="Hardware" value={hwStatus} onChange={setHwStatus} options={opts.hwStatus}/>}
      </div>

      <div className="pm-site-rail-list">
        {pinnedCards.length > 0 && (
          <>
            <div className="pm-site-rail-subh">Pinned sites</div>
            {pinnedCards.map((c, i) => <SiteResultCard key={(c.siteCode || c.siteName || "site") + "::pin::" + i} card={c} pinned={true} canDrag={canDrag} onPick={onPickSite} onTogglePin={togglePin} setFocus={setFocus}/>)}
            <div className="pm-divider"/>
          </>
        )}
        {otherCards.map((c, i) => (
          <SiteResultCard key={(c.siteCode || c.siteName || "site") + "::" + i} card={c} pinned={false} canDrag={canDrag} onPick={onPickSite} onTogglePin={togglePin} setFocus={setFocus}/>
        ))}
        {filtered.length === 0 && <Empty title="No sites match" sub="Try clearing filters"/>}
      </div>
    </aside>
  );
}

function SiteResultCard({ card, pinned, canDrag, onPick, onTogglePin, setFocus }) {
  const risk = card.atRisk || card.isBlocked || card.hasReadinessBlocker;
  const tone = risk ? "risk" : (card.hasActivePlan ? "planned" : (card.isMigrated ? "migrated" : "neutral"));
  const statusLabel = risk ? "At risk" : (card.hasActivePlan ? `${card.activePlanCount} plan${card.activePlanCount > 1 ? "s" : ""}` : (card.isMigrated ? "Migrated" : "Unplanned"));
  const dragTitle = canDrag ? "Drag to a week or click to plan" : "Open planning details";
  const nu = card.newUnderlayReadiness || {};

  return (
    <div
      className={"pm-site-card pm-site-selector-card tone-" + tone}
      draggable={!!canDrag}
      onDragStart={(e) => e.dataTransfer.setData("application/x-pm-site", JSON.stringify(card))}
      onClick={() => onPick && onPick(card)}
      title={dragTitle}
    >
      <div className="pm-site-card-accent" aria-hidden="true"/>
      <div className="pm-site-card-main">
        <div className="pm-site-card-h">
          <div className="pm-site-card-code">
            <span className="mono strong">{card.siteCode}</span>
            <span className={"pm-site-state state-" + tone}>{statusLabel}</span>
          </div>
          <div className="pm-site-card-actions">
            {setFocus && (
              <button
                type="button"
                className="btn ghost pm-site-focus"
                onClick={(e) => {
                  e.stopPropagation();
                  setFocus({ entity: "overlay", idRef: card.siteCode, kind: "sites" });
                }}
              >
                Open
              </button>
            )}
            <button
              type="button"
              className={"btn ghost pm-pin" + (pinned ? " active" : "")}
              onClick={(e) => { e.stopPropagation(); onTogglePin(card.siteCode); }}
              title={pinned ? "Unpin" : "Pin"}
            >
              {pinned ? "Pinned" : "Pin"}
            </button>
          </div>
        </div>

        <div className="pm-site-card-name">{card.shortLabel || card.siteName || "Unnamed site"}</div>

        <div className="pm-site-card-meta">
          <span>{card.country || "Unknown country"}</span>
          <span>{card.region || "No region"}</span>
          {card.batch && <span>{card.batch}</span>}
        </div>

        <div className="pm-site-progress">
          <div className="pm-site-progress-top">
            <span>Activation</span>
            <span className="mono">{card.activatedLinks}/{card.underlayLinks || 0}</span>
          </div>
          <div className="pm-site-progress-track">
            <span style={{width: `${card.readinessPct || 0}%`}}/>
          </div>
        </div>

        <div className="pm-site-readiness" data-planner-readiness="true">
          <div className="pm-site-progress-top">
            <span>NU readiness</span>
            <span className="mono">{card.newUnderlayReadinessPct || 0}%</span>
          </div>
          <div className="pm-site-progress-track nu">
            <span style={{width: `${card.newUnderlayReadinessPct || 0}%`}}/>
          </div>
          <div className="pm-site-readiness-meta">
            <span>Change mgmt <strong className="mono">{card.newUnderlayChangeManagement || 0}%</strong></span>
            <span>Migration <strong className="mono">{card.newUnderlayMigration || 0}%</strong></span>
            {card.newUnderlayBlockedPhase && <span className="risk">Blocked phase: {card.newUnderlayBlockedPhase}</span>}
          </div>
        </div>

        <div className="pm-site-card-tags">
          {card.classification && <span className="pm-tag">{card.classification}</span>}
          {card.criticality && <span className={"pm-tag pm-crit-" + (card.criticality || "").toLowerCase()}>{card.criticality}</span>}
          {card.isHwDelivered && <span className="pm-tag ok">HW delivered</span>}
          {card.isMigrated && <span className="pm-tag ok">migrated</span>}
          {(card.newUnderlayHints || []).slice(0, 3).map((hint) => (
            <span key={hint.key} className={"pm-tag " + (hint.tone || "neutral")}>{hint.label}</span>
          ))}
          {risk && <span className="pm-tag risk">risk</span>}
        </div>

        {(card.hwDelivery || card.activePlanStatus) && (
          <div className="pm-site-card-stats">
            {card.hwDelivery && <span>Hardware: {card.hwDelivery}</span>}
            {card.activePlanStatus && <span>Plan: {card.activePlanStatus}</span>}
            {nu.rowCount > 0 && <span>New Underlay rows: {nu.rowCount}</span>}
          </div>
        )}

        {(card.blockers || []).length > 0 && (
          <div className="pm-site-card-blockers">
            {(card.blockers || []).map((b, i) => <div key={i} className="pm-site-card-blocker">Risk: {b}</div>)}
          </div>
        )}
      </div>
    </div>
  );
}

/* =============================================================
   PlanMigrationModal (PM-17 / PM-18)
   ============================================================= */
function PlanMigrationModal({ initial, model, onClose, onSave, onDelete, onCloseAsDone }) {
  const existing = (initial && initial.existing) || null;
  const site = (initial && initial.site) || null;
  const weekStart = (initial && initial.weekStart) || null;

  const [tab, setTab] = useState("fields");
  const [draft, setDraft] = useState(() => {
    if (existing) return { ...existing };
    const overlay = site ? (model.overlay || []).find((o) => o.siteCode === site.siteCode) : null;
    return {
      id: undefined,
      type: "overlay-site",
      status: "planned",
      siteCode: (site && site.siteCode) || (overlay && overlay.siteCode) || "",
      siteName: (site && site.siteName) || (overlay && overlay.siteName) || "",
      country: (site && site.country) || (overlay && overlay.country) || "",
      region: (site && site.region) || (overlay && overlay.region) || "",
      linkRefs: [],
      targetDate: weekStart ? dOnly(addDays(new Date(weekStart), 2)) : "",
      targetWeek: weekStart ? isoWeekId(new Date(weekStart)) : "",
      ownerEmail: "",
      secondaryEngineers: [],
      migrationWindow: "",
      blockers: [],
      notes: "",
      tags: [],
      source: "manual",
      writeBackOnClose: true,
    };
  });
  const [busy, setBusy] = useState(false);
  const [closeOpts, setCloseOpts] = useState(null);

  const sourceSite = useMemo(() => pmFindSite(model, draft), [model, draft.siteCode, draft.siteName]);
  const readinessSummary = useMemo(() => pmReadinessSummaryForSite(sourceSite || draft, model), [sourceSite, model, draft.siteCode, draft.siteName]);

  // Available links for the chosen site
  const availableLinks = useMemo(() => {
    return sourceSite ? (sourceSite.links || []) : [];
  }, [sourceSite]);

  // PROP-08: overlay eligibility based on the underlying site lifecycle
  const eligibility = useMemo(() => {
    if (!PRData.overlayEligibility) return { eligible: true, blockers: [] };
    if (!sourceSite) return { eligible: true, blockers: [] };
    return PRData.overlayEligibility(sourceSite, model, {});
  }, [model, sourceSite]);
  const needsEligibility = draft.type === "overlay-site" || draft.type === "combined-underlay-overlay";

  // ISP terminations available
  const availableIsp = useMemo(() => {
    if (!sourceSite && !draft.siteName) return [];
    return PRData.enrichIspRows(model).filter((i) =>
      (draft.siteCode && i.siteCode === draft.siteCode) ||
      (i.siteName && i.siteName.toLowerCase() === draft.siteName.toLowerCase())
    );
  }, [model, draft.siteCode, draft.siteName, sourceSite]);

  const onChangeType = (type) => {
    let { linkRefs } = draft;
    if (type === "overlay-site") linkRefs = [];
    if (type === "underlay-link" && linkRefs.length === 0 && availableLinks.length > 0) linkRefs = [availableLinks[0].linkRef];
    if (type === "combined-underlay-overlay" && linkRefs.length === 0 && availableLinks.length > 0) linkRefs = [availableLinks[0].linkRef];
    if (type === "isp-termination" && linkRefs.length === 0 && availableIsp.length > 0) linkRefs = [availableIsp[0].linkRef];
    setDraft({ ...draft, type, linkRefs });
  };

  const toggleLink = (ref) => {
    const has = draft.linkRefs.includes(ref);
    setDraft({ ...draft, linkRefs: has ? draft.linkRefs.filter((r) => r !== ref) : [...draft.linkRefs, ref] });
  };

  const onTargetDateChange = (val) => {
    const week = val ? isoWeekId(new Date(val)) : "";
    setDraft({ ...draft, targetDate: val, targetWeek: week });
  };

  const addBlocker = () => setDraft({ ...draft, blockers: [...(draft.blockers || []), { description: "", owner: draft.ownerEmail || "", severity: "medium", openedAt: new Date().toISOString() }] });
  const updateBlocker = (i, patch) => setDraft({ ...draft, blockers: draft.blockers.map((b, idx) => idx === i ? { ...b, ...patch } : b) });
  const removeBlocker = (i) => setDraft({ ...draft, blockers: draft.blockers.filter((_, idx) => idx !== i) });

  const save = async () => {
    if (!draft.siteCode && !draft.siteName) { alert("Pick a site first."); return; }
    setBusy(true);
    try { await onSave(draft); }
    finally { setBusy(false); }
  };

  // G1 — non-blocking warnings shown before closing an overlay-site or combined plan
  const [closeWarnings, setCloseWarnings] = useState(null); // null | { warnings: [...] }
  const closeAsDone = async (force) => {
    // First pass: ask the client for warnings (underlay-not-activated etc.) when relevant.
    if (!force && existing && existing.id && PMClient && typeof PMClient.planCloseWarnings === "function") {
      try {
        const { warnings } = await PMClient.planCloseWarnings(existing.id);
        const blocking = (warnings || []).filter((w) => w.level === "warning");
        if (blocking.length > 0) { setCloseWarnings({ warnings }); return; }
      } catch (e) {
        console.warn("planCloseWarnings failed; proceeding to close", e);
      }
    }
    setBusy(true);
    try {
      await onCloseAsDone({
        completionDate: (closeOpts && closeOpts.completionDate) || draft.targetDate || dOnly(new Date()),
        writeBack: closeOpts ? closeOpts.writeBack : draft.writeBackOnClose,
        newUnderlayReadinessPatch: (closeOpts && closeOpts.newUnderlayReadinessPatch) || {},
        newUnderlayReadinessRefs: (draft.linkRefs && draft.linkRefs.length)
          ? draft.linkRefs
          : ((sourceSite && sourceSite.newUnderlayRows) || []).map((row) => row.linkRef).filter(Boolean),
      });
    } finally { setBusy(false); setCloseOpts(null); setCloseWarnings(null); }
  };

  // PM-19 write-back preview
  const writeBackPreview = useMemo(() => {
    if (!draft.writeBackOnClose) return [];
    const completion = (closeOpts && closeOpts.completionDate) || draft.targetDate || dOnly(new Date());
    if (draft.type === "overlay-site") {
      return [
        { entity: "overlay", id: draft.siteCode, field: "migrationStatus", value: "Migrated" },
        { entity: "overlay", id: draft.siteCode, field: "migrationSchedule", value: "Migrated" },
        { entity: "overlay", id: draft.siteCode, field: "confirmedMigration", value: completion },
      ];
    }
    if (draft.type === "underlay-link") {
      return (draft.linkRefs || []).flatMap((r) => [
        { entity: "underlay", id: r, field: "circuitStatus", value: "Activated" },
        { entity: "underlay", id: r, field: "migrationDone", value: "Yes" },
        { entity: "underlay", id: r, field: "migrationDate", value: completion },
      ]);
    }
    if (draft.type === "isp-termination") {
      return (draft.linkRefs || []).flatMap((r) => [
        { entity: "isp", id: r, field: "terminated", value: "Yes" },
        { entity: "isp", id: r, field: "terminationDate", value: completion },
      ]);
    }
    if (draft.type === "combined-underlay-overlay") {
      const underlay = (draft.linkRefs || []).flatMap((r) => [
        { entity: "underlay", id: r, field: "circuitStatus", value: "Activated" },
        { entity: "underlay", id: r, field: "migrationDone", value: "Yes" },
        { entity: "underlay", id: r, field: "migrationDate", value: completion },
      ]);
      const overlay = [
        { entity: "overlay", id: draft.siteCode, field: "migrationStatus", value: "Migrated" },
        { entity: "overlay", id: draft.siteCode, field: "migrationSchedule", value: "Migrated" },
        { entity: "overlay", id: draft.siteCode, field: "confirmedMigration", value: completion },
      ];
      return [...underlay, ...overlay];
    }
    return [];
  }, [draft, closeOpts]);

  const readinessCloseChoices = [
    { field: "readiness.gateway", label: "Gateway Connectivity", value: "Done" },
    { field: "readiness.internet", label: "Internet Connectivity", value: "Done" },
    { field: "readiness.userTests", label: "User Tests OK", value: "Done" },
    { field: "readiness.migration", label: "Migration Successful", value: "Done" },
    { field: "readiness.servicenow", label: "ServiceNOW Update", value: "Done" },
  ];
  const canCloseReadiness = closeOpts && closeOpts.writeBack &&
    (draft.type === "underlay-link" || draft.type === "combined-underlay-overlay") &&
    readinessSummary && readinessSummary.rowCount > 0;
  const toggleCloseReadiness = (field, value, checked) => {
    const current = (closeOpts && closeOpts.newUnderlayReadinessPatch) || {};
    const next = { ...current };
    if (checked) next[field] = value;
    else delete next[field];
    setCloseOpts({ ...closeOpts, newUnderlayReadinessPatch: next });
  };

  return (
    <div className="pm-modal-backdrop" onClick={onClose}>
      <div className="pm-modal large" onClick={(e) => e.stopPropagation()}>
        <div className="pm-modal-h">
          <div className="title">
            {existing ? "Edit plan" : "Plan migration"}
            {draft.siteCode && <span className="mono" style={{marginLeft:8, color:"var(--ink-500)"}}>· {draft.siteCode}</span>}
          </div>
          <button className="btn ghost" onClick={onClose}><Glyph name="close" size={14}/></button>
        </div>

        <div className="pm-edit-tabs">
          <button className={tab === "fields" ? "active" : ""} onClick={() => setTab("fields")}>Plan</button>
          <button className={tab === "blockers" ? "active" : ""} onClick={() => setTab("blockers")}>Blockers ({(draft.blockers || []).length})</button>
          {existing && <button className={tab === "comments" ? "active" : ""} onClick={() => setTab("comments")}>Discussion</button>}
        </div>

        <div className="pm-modal-body">
          {tab === "fields" && (
            <>
              <div className="pm-field">
                <label className="pm-field-label">Migration type</label>
                <div className="pm-toggle pm-type-toggle">
                  {["underlay-link","overlay-site","combined-underlay-overlay","isp-termination","other"].map((t) => (
                    <button key={t} className={draft.type === t ? "active" : ""} onClick={() => onChangeType(t)}>
                      <span className="pm-type-dot" style={{background: TYPE_COLORS[t]}}/> {TYPE_LABEL[t]}
                    </button>
                  ))}
                </div>
              </div>

              {needsEligibility && eligibility && !eligibility.eligible && (
                <div className="pm-readonly-pill" style={{ marginBottom: 10, background: "var(--risk-soft)", color: "var(--risk)" }}>
                  <strong>Overlay not eligible yet</strong>
                  <ul style={{ margin: "4px 0 0 16px", padding: 0, fontSize: 12 }}>
                    {(eligibility.blockers || []).map((b, i) => (
                      <li key={i}>{b.message || b.code} {b.ref ? <span className="mono">· {b.ref}</span> : null}</li>
                    ))}
                  </ul>
                  <div className="muted" style={{ marginTop: 4, fontSize: 11 }}>
                    Closing this plan will trigger a warning. Use "Underlay link" first, or "Underlay + Overlay" to activate both in the same window.
                  </div>
                </div>
              )}
              {needsEligibility && eligibility && eligibility.eligible && (
                <div className="pm-readonly-pill" style={{ marginBottom: 10, background: "var(--ok-soft)", color: "var(--ok)" }}>
                  <strong>Overlay eligible</strong> - underlay and New Underlay gates are clear.
                </div>
              )}
              {readinessSummary && readinessSummary.rowCount > 0 && (
                <div className="pm-plan-readiness-context" data-plan-readiness-context="true">
                  <div className="pm-plan-readiness-head">
                    <strong>New Underlay readiness</strong>
                    <span className="mono">{readinessSummary.readinessPercent || 0}%</span>
                  </div>
                  <div className="pm-plan-readiness-grid">
                    <span>Pre-change <strong className="mono">{(readinessSummary.preChange && readinessSummary.preChange.percent) || 0}%</strong></span>
                    <span>Change management <strong className="mono">{(readinessSummary.changeManagement && readinessSummary.changeManagement.percent) || 0}%</strong></span>
                    <span>Link migration <strong className="mono">{(readinessSummary.migration && readinessSummary.migration.percent) || 0}%</strong></span>
                    <span>Post migration <strong className="mono">{(readinessSummary.postMigration && readinessSummary.postMigration.percent) || 0}%</strong></span>
                  </div>
                  {readinessSummary.blockedPhase && (
                    <div className="pm-plan-readiness-blocked" data-plan-blocked-phase="true">
                      Blocked phase: <strong>{readinessSummary.blockedPhase.label || readinessSummary.blockedPhase.key}</strong>
                    </div>
                  )}
                  <div className="pm-plan-readiness-hints">
                    {(readinessSummary.hints || []).map((hint) => <span key={hint.key} className={"pm-tag " + (hint.tone || "neutral")}>{hint.label}</span>)}
                  </div>
                </div>
              )}
              <div className="grid g-2" style={{gap:12}}>
                <div className="pm-field"><label className="pm-field-label">Site code</label>
                  <input className="pm-input" value={draft.siteCode || ""} onChange={(e) => setDraft({ ...draft, siteCode: e.target.value })}/></div>
                <div className="pm-field"><label className="pm-field-label">Site name</label>
                  <input className="pm-input" value={draft.siteName || ""} onChange={(e) => setDraft({ ...draft, siteName: e.target.value })}/></div>
                <div className="pm-field"><label className="pm-field-label">Country</label>
                  <input className="pm-input" value={draft.country || ""} onChange={(e) => setDraft({ ...draft, country: e.target.value })}/></div>
                <div className="pm-field"><label className="pm-field-label">Region</label>
                  <input className="pm-input" value={draft.region || ""} onChange={(e) => setDraft({ ...draft, region: e.target.value })}/></div>
                <div className="pm-field"><label className="pm-field-label">Target date</label>
                  <input className="pm-input" type="date" value={draft.targetDate || ""} onChange={(e) => onTargetDateChange(e.target.value)}/></div>
                <div className="pm-field"><label className="pm-field-label">Target week</label>
                  <input className="pm-input" value={draft.targetWeek || ""} readOnly/></div>
                <div className="pm-field"><label className="pm-field-label">Migration window</label>
                  <input className="pm-input" placeholder="e.g. Sat 22 → Sun 23 · 02:00 UTC" value={draft.migrationWindow || ""} onChange={(e) => setDraft({ ...draft, migrationWindow: e.target.value })}/></div>
                <div className="pm-field"><label className="pm-field-label">Status</label>
                  <select className="pm-input" value={draft.status} onChange={(e) => setDraft({ ...draft, status: e.target.value })}>
                    {["planned","scheduled","in_progress","blocked","done","cancelled"].map((s) => <option key={s} value={s}>{STATUS_LABEL[s]}</option>)}
                  </select></div>
                <div className="pm-field"><label className="pm-field-label">Owner (the engineer "window")</label>
                  <OwnerPicker value={draft.ownerEmail} onChange={(v) => setDraft({ ...draft, ownerEmail: v })}/></div>
                <div className="pm-field"><label className="pm-field-label">Secondary engineers</label>
                  <EngineersPicker values={draft.secondaryEngineers} onChange={(v) => setDraft({ ...draft, secondaryEngineers: v })}/></div>
              </div>

              {(draft.type === "underlay-link" || draft.type === "combined-underlay-overlay") && (
                <div className="pm-field"><label className="pm-field-label">Link references{draft.type === "combined-underlay-overlay" && <span className="muted"> · activated in same window as overlay</span>}</label>
                  {availableLinks.length === 0 ? <div className="muted" style={{fontSize:12}}>No underlay links found for this site.</div> :
                  <div className="pm-link-grid">
                    {availableLinks.map((l) => (
                      <label key={l.linkRef} className={"pm-link-pick" + (draft.linkRefs.includes(l.linkRef) ? " selected" : "")}>
                        <input type="checkbox" checked={draft.linkRefs.includes(l.linkRef)} onChange={() => toggleLink(l.linkRef)}/>
                        <span className="mono strong">{l.linkRef}</span>
                        <span className="muted">· {l.vendor || "—"}</span>
                        <span className={"pm-tag " + (PRData.isActivatedLink(l) ? "ok" : "")}>{l.circuitStatus || "—"}</span>
                      </label>
                    ))}
                  </div>}
                </div>
              )}
              {draft.type === "isp-termination" && (
                <div className="pm-field"><label className="pm-field-label">ISP links to terminate</label>
                  {availableIsp.length === 0 ? <div className="muted" style={{fontSize:12}}>No ISP rows for this site.</div> :
                  <div className="pm-link-grid">
                    {availableIsp.map((l) => (
                      <label key={l.linkRef} className={"pm-link-pick" + (draft.linkRefs.includes(l.linkRef) ? " selected" : "")}>
                        <input type="checkbox" checked={draft.linkRefs.includes(l.linkRef)} onChange={() => toggleLink(l.linkRef)}/>
                        <span className="mono strong">{l.linkRef}</span>
                        <span className="muted">· {l.isp || "—"}</span>
                        <span className="pm-tag">{l.terminated || "—"}</span>
                      </label>
                    ))}
                  </div>}
                </div>
              )}

              <div className="pm-field">
                <label className="pm-field-label">Notes</label>
                <MentionTextarea value={draft.notes} onChange={(v) => setDraft({ ...draft, notes: v })} placeholder="Plan details · @mention engineers" rows={4}/>
              </div>

              <div className="pm-field">
                <label className="pm-checkbox-row">
                  <input type="checkbox" checked={!!draft.writeBackOnClose} onChange={(e) => setDraft({ ...draft, writeBackOnClose: e.target.checked })}/>
                  Write back to Excel when closed as done
                </label>
                {draft.writeBackOnClose && writeBackPreview.length > 0 && (
                  <div className="pm-writeback-preview">
                    <div className="pm-writeback-h">On close, these fields will update:</div>
                    {writeBackPreview.map((w, i) => (
                      <div key={i} className="pm-writeback-row mono">
                        <span>{w.entity}</span> · <span>{w.id || "—"}</span> · <span>{w.field}</span> → <span className="strong">{String(w.value)}</span>
                      </div>
                    ))}
                  </div>
                )}
              </div>
            </>
          )}
          {tab === "blockers" && (
            <div className="pm-blockers-edit">
              <button className="btn" onClick={addBlocker}><Glyph name="add" size={11}/> Add blocker</button>
              {(draft.blockers || []).length === 0 && <Empty title="No blockers" sub="Add one when the plan hits an obstacle."/>}
              {(draft.blockers || []).map((b, i) => (
                <div key={i} className="pm-blocker-row">
                  <div className="grid g-2" style={{gap:8}}>
                    <div className="pm-field"><label className="pm-field-label">Description</label>
                      <input className="pm-input" value={b.description || ""} onChange={(e) => updateBlocker(i, { description: e.target.value })}/></div>
                    <div className="pm-field"><label className="pm-field-label">Owner</label>
                      <OwnerPicker value={b.owner} onChange={(v) => updateBlocker(i, { owner: v })}/></div>
                    <div className="pm-field"><label className="pm-field-label">Severity</label>
                      <select className="pm-input" value={b.severity || "medium"} onChange={(e) => updateBlocker(i, { severity: e.target.value })}>
                        <option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option><option value="critical">Critical</option>
                      </select></div>
                    <div className="pm-field"><label className="pm-field-label">Resolved at</label>
                      <input className="pm-input" type="date" value={b.resolvedAt ? dOnly(b.resolvedAt) : ""} onChange={(e) => updateBlocker(i, { resolvedAt: e.target.value ? new Date(e.target.value).toISOString() : null })}/></div>
                  </div>
                  <button className="btn ghost danger" onClick={() => removeBlocker(i)}>Remove</button>
                </div>
              ))}
            </div>
          )}
          {tab === "comments" && existing && (
            <PmCommentsRail entity="plan" idRef={existing.id} compact/>
          )}
        </div>

        <div className="pm-modal-actions">
          {existing && onDelete && <button className="btn danger" onClick={onDelete}>Delete</button>}
          {existing && onCloseAsDone && draft.status !== "done" && (
            <button className="btn" onClick={() => setCloseOpts({ completionDate: dOnly(new Date()), writeBack: !!draft.writeBackOnClose })}>
              Close as done
            </button>
          )}
          <span style={{flex:1}}/>
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" disabled={busy} onClick={save}>{busy ? "Saving…" : "Save plan"}</button>
        </div>

        {closeOpts && (
          <div className="pm-modal-backdrop" onClick={() => setCloseOpts(null)}>
            <div className="pm-modal" onClick={(e) => e.stopPropagation()} style={{maxWidth:520}}>
              <div className="pm-modal-h"><div className="title">Confirm: close as done</div></div>
              <div className="pm-modal-body">
                <div className="pm-field"><label className="pm-field-label">Completion date</label>
                  <input className="pm-input" type="date" value={closeOpts.completionDate || ""} onChange={(e) => setCloseOpts({ ...closeOpts, completionDate: e.target.value })}/></div>
                <label className="pm-checkbox-row">
                  <input type="checkbox" checked={!!closeOpts.writeBack} onChange={(e) => setCloseOpts({ ...closeOpts, writeBack: e.target.checked })}/>
                  Apply write-back to Excel-shaped overrides
                </label>
                {canCloseReadiness && (
                  <div className="pm-plan-readiness-context" data-close-readiness-confirm="true">
                    <div className="pm-plan-readiness-head">
                      <strong>Optional New Underlay readiness updates</strong>
                      <span className="muted">explicit confirmation required</span>
                    </div>
                    <div className="muted" style={{ fontSize: 11.5, marginBottom: 8 }}>
                      These fields are engineer-owned New Underlay workflow gates. Nothing here is changed unless selected.
                    </div>
                    <div className="pm-readiness-check-grid">
                      {readinessCloseChoices.map((choice) => {
                        const selected = !!(closeOpts.newUnderlayReadinessPatch && closeOpts.newUnderlayReadinessPatch[choice.field]);
                        return (
                          <label key={choice.field} className={"pm-readiness-check" + (selected ? " active" : "")}>
                            <input type="checkbox" checked={selected} onChange={(e) => toggleCloseReadiness(choice.field, choice.value, e.target.checked)}/>
                            <span>{choice.label}</span>
                            <strong>{choice.value}</strong>
                          </label>
                        );
                      })}
                    </div>
                  </div>
                )}
                {closeOpts.writeBack && writeBackPreview.length > 0 && (
                  <div className="pm-writeback-preview">
                    <div className="pm-writeback-h">Will write:</div>
                    {writeBackPreview.map((w, i) => (
                      <div key={i} className="pm-writeback-row mono">
                        <span>{w.entity}</span> · <span>{w.id || "—"}</span> · <span>{w.field}</span> → <span className="strong">{String(w.value)}</span>
                      </div>
                    ))}
                    {Object.entries(closeOpts.newUnderlayReadinessPatch || {}).map(([field, value]) => (
                      <div key={field} className="pm-writeback-row mono">
                        <span>newUnderlay</span> · <span>{(draft.linkRefs || []).join(", ") || "site links"}</span> · <span>{field}</span> -> <span className="strong">{String(value)}</span>
                      </div>
                    ))}
                  </div>
                )}
              </div>
              <div className="pm-modal-actions">
                <button className="btn" onClick={() => setCloseOpts(null)}>Cancel</button>
                <button className="btn primary" disabled={busy} onClick={() => closeAsDone(false)}>{busy ? "Closing…" : "Confirm close"}</button>
              </div>
            </div>
          </div>
        )}

        {/* G1 — close warning dialog. Shows when planCloseWarnings reports underlay-not-activated etc. */}
        {closeWarnings && (
          <div className="pm-modal-backdrop" onClick={() => setCloseWarnings(null)}>
            <div className="pm-modal" onClick={(e) => e.stopPropagation()} style={{maxWidth: 560}}>
              <div className="pm-modal-h">
                <div className="title" style={{color: "var(--warn)"}}>
                  <Glyph name="risk" size={14}/> Close anyway?
                </div>
              </div>
              <div className="pm-modal-body">
                <div className="pm-readonly-pill" style={{ marginBottom: 12, background: "var(--warn-soft)", color: "var(--warn)" }}>
                  This plan has warnings that may leave inconsistent state if you close it now.
                </div>
                <ul style={{ margin: "0 0 10px 18px", padding: 0, fontSize: 13 }}>
                  {(closeWarnings.warnings || []).map((w, i) => (
                    <li key={i} style={{ marginBottom: 6 }}>
                      <strong>{w.code}</strong> — {w.message}
                      {w.refs && w.refs.length > 0 && (
                        <div className="mono" style={{ fontSize: 11, color: "var(--ink-500)", marginTop: 2 }}>
                          refs: {w.refs.slice(0, 8).join(", ")}{w.refs.length > 8 ? "…" : ""}
                        </div>
                      )}
                    </li>
                  ))}
                </ul>
                <div className="muted" style={{ fontSize: 12 }}>
                  Closing now will still write the migration-complete fields. Activate the underlay first if you want the lifecycle to roll up cleanly.
                </div>
              </div>
              <div className="pm-modal-actions">
                <button className="btn" onClick={() => setCloseWarnings(null)}>Go back</button>
                <button className="btn danger" disabled={busy} onClick={() => closeAsDone(true)}>{busy ? "Closing…" : "Close anyway"}</button>
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function OwnerPicker({ value, onChange }) {
  const [users, setUsers] = useState([]);
  const [focus, setFocus] = useState(false);
  useEffect(() => {
    let alive = true;
    PMClient.listUsers().then((u) => { if (alive) setUsers(u || []); }).catch(() => {});
  }, []);
  const matches = users.filter((u) => u.isActive !== false).filter((u) => {
    if (!value) return true;
    const q = value.toLowerCase();
    return (u.email || "").toLowerCase().includes(q) || (u.displayName || "").toLowerCase().includes(q);
  }).slice(0, 5);
  return (
    <div className="pm-owner-pick">
      <input className="pm-input" placeholder="email" value={value || ""}
             onChange={(e) => onChange(e.target.value)}
             onFocus={() => setFocus(true)}
             onBlur={() => setTimeout(() => setFocus(false), 150)}/>
      {focus && matches.length > 0 && (
        <div className="pm-mention-pop">
          {matches.map((u) => (
            <div key={u.email} className="pm-mention-item" onMouseDown={(e) => { e.preventDefault(); onChange(u.email); }}>
              <span className="pm-avatar" style={{width:20, height:20, borderRadius:10, fontSize:9, background:u.avatarColor || "#888"}}>{(u.displayName || u.email).slice(0,2).toUpperCase()}</span>
              <span className="pm-mention-name">{u.displayName || u.email}</span>
              <span className="pm-mention-handle">{u.email}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function EngineersPicker({ values, onChange }) {
  const [users, setUsers] = useState([]);
  const [draft, setDraft] = useState("");
  useEffect(() => {
    PMClient.listUsers().then((u) => setUsers(u || [])).catch(() => {});
  }, []);
  const add = () => {
    const v = draft.trim();
    if (!v) return;
    if ((values || []).includes(v)) { setDraft(""); return; }
    onChange([...(values || []), v]);
    setDraft("");
  };
  return (
    <div>
      <div className="pm-tag-row">
        {(values || []).map((v) => (
          <span key={v} className="pm-task-tag">{v}
            <button className="btn ghost" onClick={() => onChange(values.filter((x) => x !== v))}>×</button>
          </span>
        ))}
        <input className="pm-input" placeholder="add engineer email"
               value={draft} onChange={(e) => setDraft(e.target.value)}
               onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); add(); } }}/>
      </div>
    </div>
  );
}

/* =============================================================
   PmReports (PM-22) + ReportPreview (PM-21) + Saved (PM-23)
   ============================================================= */
const REPORT_TEMPLATES_UI = [
  { id: "site-report",     name: "Site report",          desc: "Deep-dive single-site history + lookahead", needs: ["siteCode"] },
  { id: "country-report",  name: "Country report",       desc: "All sites in a country, with last/next period", needs: ["country"] },
  { id: "weekly-digest",   name: "Weekly digest",        desc: "What happened in the last 7 days", needs: [] },
  { id: "monthly-summary", name: "Monthly summary",      desc: "What happened in the last 30 days", needs: [] },
  { id: "lookahead-4w",    name: "4-week lookahead",     desc: "What is planned for the next 4 weeks", needs: [] },
  { id: "blockers-risks",  name: "Blockers & risks",     desc: "Active blockers and at-risk underlay", needs: [] },
];

function PmReports({ model, plans, changeRequests, comments = [], activity = [], role, user, onReportChange }) {
  const [chosen, setChosen] = useState("weekly-digest");
  const [filters, setFilters] = useState({});
  const [report, setReport] = useState(null);
  const [savedTemplates, setSavedTemplates] = useState([]);
  const [recentRuns, setRecentRuns] = useState([]);
  const [savingTemplate, setSavingTemplate] = useState(false);
  const [templateName, setTemplateName] = useState("");

  const reloadSaved = useCallback(async () => {
    const [tpl, runs] = await Promise.all([
      PMClient.listReportTemplates().catch(() => []),
      PMClient.listReportRuns({ limit: 20 }).catch(() => []),
    ]);
    setSavedTemplates(tpl);
    setRecentRuns(runs);
  }, []);
  useEffect(() => { reloadSaved(); const u = PMClient.subscribe(reloadSaved); return () => u && u(); }, [reloadSaved]);

  const opts = useMemo(() => {
    const scope = PRData.scopeOptions(model);
    const sites = ((model && model.sites) || [])
      .filter((s) => s.siteCode)
      .map((s) => ({ siteCode: s.siteCode, siteName: s.siteName || s.id, country: s.country, region: s.region }));
    return { countries: scope.countries, regions: scope.regions, sites };
  }, [model]);

  const tmpl = REPORT_TEMPLATES_UI.find((t) => t.id === chosen);
  const filterFields = useMemo(() => {
    const needs = (tmpl && tmpl.needs) || [];
    return needs;
  }, [tmpl]);

  const generate = useCallback(() => {
    try {
      const r = PMReport.generate(chosen, {
        model, plans, changeRequests, comments, activity, user,
        filters: { ...filters },
      });
      setReport(r);
    } catch (e) {
      alert("Report failed: " + e.message);
    }
  }, [chosen, filters, model, plans, changeRequests, comments, activity, user]);

  // Auto-regenerate on chooser change
  useEffect(() => {
    setReport(null);
    // Auto-fill smart defaults
    if (tmpl) {
      const need = tmpl.needs;
      if (need.includes("siteCode") && !filters.siteCode && opts.sites.length) setFilters((f) => ({ ...f, siteCode: opts.sites[0].siteCode }));
      if (need.includes("country") && !filters.country && opts.countries.length) setFilters((f) => ({ ...f, country: opts.countries[0] }));
    }
    // Generate after a tick so filters apply
    setTimeout(() => {
      try {
        const r = PMReport.generate(chosen, {
          model, plans, changeRequests, comments, activity, user,
          filters: { ...filters, ...(tmpl && tmpl.needs.includes("siteCode") && !filters.siteCode && opts.sites.length ? { siteCode: opts.sites[0].siteCode } : {}), ...(tmpl && tmpl.needs.includes("country") && !filters.country && opts.countries.length ? { country: opts.countries[0] } : {}) },
        });
        setReport(r);
      } catch (_) {}
    }, 0);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [chosen]);

  // Re-generate when filters change
  useEffect(() => { if (chosen) generate(); /* eslint-disable-next-line */ }, [filters, model, plans, changeRequests, comments, activity]);

  const saveTemplate = async () => {
    if (!templateName.trim()) return;
    await PMClient.upsertReportTemplate({ name: templateName.trim(), kind: chosen, filters });
    setSavingTemplate(false);
    setTemplateName("");
    await reloadSaved();
  };
  const runAndArchive = async () => {
    if (!report) return;
    await PMClient.saveReportRun({ kind: chosen, name: report.title, filters, report });
    await reloadSaved();
  };
  const openTemplate = (t) => {
    setChosen(t.kind);
    setFilters(t.filters || {});
  };

  return (
    <div className="pm-reports">
      <div className="pm-section-h" style={{marginTop:6}}>
        <div className="title">Reports — generate, preview, export PDF</div>
      </div>
      <div className="pm-reports-grid">
        <aside className="pm-reports-rail">
          <div className="pm-reports-rail-h">Templates</div>
          <div className="pm-reports-templates">
            {REPORT_TEMPLATES_UI.map((t) => (
              <div key={t.id} className={"pm-report-tpl" + (chosen === t.id ? " active" : "")} onClick={() => setChosen(t.id)}>
                <div className="pm-report-tpl-name">{t.name}</div>
                <div className="pm-report-tpl-desc">{t.desc}</div>
              </div>
            ))}
          </div>
          {savedTemplates.length > 0 && (
            <>
              <div className="pm-reports-rail-h" style={{marginTop:14}}>Saved</div>
              <div className="pm-reports-saved">
                {savedTemplates.map((t) => (
                  <div key={t.id} className="pm-report-saved" onClick={() => openTemplate(t)}>
                    <div className="strong">{t.name}</div>
                    <div className="muted" style={{fontSize:11}}>{REPORT_TEMPLATES_UI.find((x) => x.id === t.kind)?.name || t.kind}</div>
                  </div>
                ))}
              </div>
            </>
          )}
          {recentRuns.length > 0 && (
            <>
              <div className="pm-reports-rail-h" style={{marginTop:14}}>Recent runs</div>
              <div className="pm-reports-saved">
                {recentRuns.slice(0, 8).map((r) => (
                  <div key={r.id} className="pm-report-saved" onClick={() => { setChosen(r.kind); setFilters(r.filters || {}); setReport(r.report); }}>
                    <div className="strong">{r.name}</div>
                    <div className="muted" style={{fontSize:11}}>{_relTime(r.createdAt)}</div>
                  </div>
                ))}
              </div>
            </>
          )}
        </aside>

        <main className="pm-reports-main">
          <div className="pm-report-config card">
            <div className="card-h"><div className="title">{tmpl ? tmpl.name : "—"}</div></div>
            <div className="pm-report-filters">
              {filterFields.includes("siteCode") && (
                <div className="pm-field"><label className="pm-field-label">Site</label>
                  <select className="pm-input" value={filters.siteCode || ""} onChange={(e) => setFilters({ ...filters, siteCode: e.target.value })}>
                    <option value="">—</option>
                    {opts.sites.map((s) => <option key={s.siteCode} value={s.siteCode}>{s.siteCode} — {s.siteName}</option>)}
                  </select></div>
              )}
              {filterFields.includes("country") && (
                <div className="pm-field"><label className="pm-field-label">Country</label>
                  <select className="pm-input" value={filters.country || ""} onChange={(e) => setFilters({ ...filters, country: e.target.value })}>
                    <option value="">—</option>
                    {opts.countries.map((c) => <option key={c} value={c}>{c}</option>)}
                  </select></div>
              )}
              <div className="pm-field"><label className="pm-field-label">Region</label>
                <select className="pm-input" value={filters.region || ""} onChange={(e) => setFilters({ ...filters, region: e.target.value })}>
                  <option value="">All regions</option>
                  {opts.regions.map((r) => <option key={r} value={r}>{r}</option>)}
                </select></div>
              <div className="pm-field"><label className="pm-field-label">Period</label>
                <select className="pm-input"
                        value={(filters.period && filters.period.kind) || (chosen === "lookahead-4w" ? "next-4w" : chosen === "monthly-summary" ? "last-month" : chosen === "weekly-digest" ? "last-week" : "custom")}
                        onChange={(e) => setFilters({ ...filters, period: { kind: e.target.value } })}>
                  <option value="last-week">Last 7 days</option>
                  <option value="last-month">Last 30 days</option>
                  <option value="next-week">Next 7 days</option>
                  <option value="next-month">Next 30 days</option>
                  <option value="next-4w">Next 4 weeks</option>
                </select></div>
              <div className="flex gap-sm" style={{marginLeft:"auto", alignSelf:"flex-end"}}>
                <button className="btn" onClick={generate}>Re-generate</button>
                <button className="btn" onClick={() => setSavingTemplate(true)}>Save template…</button>
                <button className="btn" onClick={runAndArchive} disabled={!report}>Archive run</button>
                <button className="btn primary" onClick={() => printReport()} disabled={!report}><Glyph name="ext" size={12}/> Export PDF</button>
              </div>
            </div>
          </div>

          {report && <ReportPreview report={report}/>}
          {!report && <Empty title="No preview yet" sub="Pick a template and adjust filters."/>}

          {savingTemplate && (
            <div className="pm-modal-backdrop" onClick={() => setSavingTemplate(false)}>
              <div className="pm-modal" onClick={(e) => e.stopPropagation()} style={{maxWidth:420}}>
                <div className="pm-modal-h"><div className="title">Save report template</div></div>
                <div className="pm-modal-body">
                  <div className="pm-field"><label className="pm-field-label">Template name</label>
                    <input className="pm-input" autoFocus value={templateName} onChange={(e) => setTemplateName(e.target.value)} placeholder="e.g. EMEA Weekly · POs"/></div>
                </div>
                <div className="pm-modal-actions">
                  <button className="btn" onClick={() => setSavingTemplate(false)}>Cancel</button>
                  <button className="btn primary" disabled={!templateName.trim()} onClick={saveTemplate}>Save</button>
                </div>
              </div>
            </div>
          )}
        </main>
      </div>
    </div>
  );
}

/* ------- Print pipeline (PM-21 "browser print/PDF" option) ----- */
function printReport() {
  document.body.classList.add("pm-print-mode");
  // Allow the layout to settle for one frame
  setTimeout(() => {
    window.print();
    setTimeout(() => document.body.classList.remove("pm-print-mode"), 200);
  }, 50);
}

/* =============================================================
   ReportPreview — renders a Report object as graphical HTML
   ============================================================= */
function ReportPreview({ report }) {
  if (!report) return null;
  return (
    <div className="pm-report-doc" id="pm-report-doc">
      <header className="pm-report-head">
        <div>
          <div className="pm-report-brand">1NET · Forti-SDWAN migration</div>
          <h1 className="pm-report-title">{report.title}</h1>
          <div className="pm-report-sub">
            <span>{(report.period && report.period.label) || ""}</span>
            <span className="muted">·</span>
            <span>Generated by <strong>{report.generatedBy}</strong></span>
            <span className="muted">·</span>
            <span className="mono">{(report.generatedAt || "").slice(0, 19).replace("T", " ")}</span>
          </div>
        </div>
        <div className="pm-report-mark">1N</div>
      </header>

      {(report.sections || []).map((s, i) => (
        <section key={i} className={"pm-report-section pm-report-" + s.kind}>
          {s.title && <h2 className="pm-report-h2">{s.title}</h2>}
          {s.sourceLabel && <div className="pm-report-source-label">{s.sourceLabel}</div>}
          {s.kind === "source-meta" && <ReportSourceMeta meta={s.meta}/>}
          {s.kind === "kpi-band" && <ReportKpiBand items={s.items}/>}
          {s.kind === "donut" && <ReportDonut segments={s.segments}/>}
          {s.kind === "bar" && <ReportBar data={s.data}/>}
          {s.kind === "timeline" && <ReportTimeline rows={s.rows}/>}
          {s.kind === "table" && <ReportTable section={s}/>}
          {s.kind === "list" && <ReportList items={s.items}/>}
          {s.kind === "narrative" && safeMarkdown(s.markdown || "")}
          {s.kind === "blockers" && <ReportBlockers section={s}/>}
        </section>
      ))}

      <footer className="pm-report-foot">
        <div className="muted">Generated locally · 1NET portal · {(report.generatedAt || "").slice(0,10)}</div>
        <div className="muted">Confidential — internal use only</div>
      </footer>
    </div>
  );
}

function ReportSourceMeta({ meta }) {
  const m = meta || {};
  const batch = m.workbookBatchScope || {};
  const baseline = m.programBaselineScope || {};
  const filters = m.selectedFilters && m.selectedFilters.length ? m.selectedFilters : ["none"];
  return (
    <div className="pm-report-source-grid">
      <div className="pm-report-source-card accent">
        <span>workbook snapshot</span>
        <strong>{m.workbookSnapshotDate ? m.workbookSnapshotDate.slice(0, 19).replace("T", " ") : "embedded workbook"}</strong>
        <em>{batch.label || "workbook batch scope"}</em>
      </div>
      <div className="pm-report-source-card ok">
        <span>local override</span>
        <strong>{Number(m.localOverrideCount || 0).toLocaleString()}</strong>
        <em>{m.effectiveModelSource || "workbook base + PMStore"}</em>
      </div>
      <div className="pm-report-source-card info">
        <span>generated date</span>
        <strong>{m.reportGeneratedDate ? m.reportGeneratedDate.slice(0, 19).replace("T", " ") : "now"}</strong>
        <em>{(m.migrationPlanCount || 0).toLocaleString()} plans · {(m.changeRequestCount || 0).toLocaleString()} CRs · {(m.commentCount || 0).toLocaleString()} comments</em>
      </div>
      <div className="pm-report-source-card warn">
        <span>workbook batch vs program baseline scope</span>
        <strong>{(batch.sites || 0).toLocaleString()} batch sites · {(baseline.totalLinks || 0).toLocaleString()} program links</strong>
        <em>{baseline.label || "program-level baseline"} · {baseline.batchLabel || "current workbook batch"}</em>
      </div>
      <div className="pm-report-source-card wide">
        <span>selected filters</span>
        <div className="pm-report-filter-chips">
          {filters.map((f, i) => <b key={i}>{f}</b>)}
        </div>
      </div>
    </div>
  );
}

function ReportKpiBand({ items }) {
  return (
    <div className="pm-report-kpis">
      {items.map((it, i) => (
        <div key={i} className="pm-report-kpi" style={{ borderTopColor: it.color || "var(--accent)" }}>
          <div className="pm-report-kpi-label">{it.label}</div>
          <div className="pm-report-kpi-value">{typeof it.value === "number" ? it.value.toLocaleString() : (it.value || "—")}</div>
        </div>
      ))}
    </div>
  );
}

function ReportDonut({ segments }) {
  const total = (segments || []).reduce((a, s) => a + (s.value || 0), 0);
  if (total === 0) return <Empty title="No data"/>;
  const size = 160, r = 70, cx = size/2, cy = size/2;
  let a0 = -Math.PI / 2;
  return (
    <div className="pm-report-donut-wrap">
      <svg width={size} height={size}>
        {segments.filter((s) => s.value > 0).map((s, i) => {
          const sweep = (s.value / total) * 2 * Math.PI;
          const a1 = a0 + sweep;
          const x0 = cx + r * Math.cos(a0), y0 = cy + r * Math.sin(a0);
          const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1);
          const large = sweep > Math.PI ? 1 : 0;
          const d = `M ${cx} ${cy} L ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1} Z`;
          a0 = a1;
          return <path key={i} d={d} fill={s.color} stroke="white" strokeWidth="2"/>;
        })}
        <circle cx={cx} cy={cy} r={36} fill="var(--surface)"/>
        <text x={cx} y={cy} textAnchor="middle" dominantBaseline="middle" fontSize="18" fontWeight="600" fontFamily="var(--font-mono)">{total.toLocaleString()}</text>
      </svg>
      <ul className="pm-report-legend">
        {segments.map((s, i) => (
          <li key={i}>
            <span className="dot" style={{background: s.color}}/>
            <span>{s.label}</span>
            <span className="mono">{s.value}</span>
            <span className="pct">{Math.round((s.value / total) * 100)}%</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

function ReportBar({ data }) {
  if (!data || data.length === 0) return <Empty title="No data"/>;
  const max = Math.max(...data.map((d) => d.value), 1);
  return (
    <div className="pm-report-bars">
      {data.map((d, i) => (
        <div key={i} className="pm-report-bar-row">
          <div className="pm-report-bar-lbl">{d.label}</div>
          <div className="pm-report-bar-track">
            <span style={{ width: (d.value / max * 100).toFixed(1) + "%", background: d.color || "var(--accent)" }}/>
          </div>
          <div className="pm-report-bar-num mono">{d.value}</div>
        </div>
      ))}
    </div>
  );
}

function ReportTimeline({ rows }) {
  if (!rows || rows.length === 0) return <Empty title="No items"/>;
  return (
    <ol className="pm-report-timeline">
      {rows.map((r, i) => (
        <li key={i} className="pm-report-tl-row">
          <span className="pm-report-tl-dot" style={{background: TYPE_COLORS[r.type] || "var(--ink-400)"}}/>
          <span className="pm-report-tl-date mono">{r.date || "—"}</span>
          <span className="pm-report-tl-title">{r.title}</span>
          <span className={"pm-report-tl-status pm-status-" + r.status}>{STATUS_LABEL[r.status] || r.status}</span>
          {r.owner && <span className="muted mono">{r.owner.split("@")[0]}</span>}
        </li>
      ))}
    </ol>
  );
}

function ReportTable({ section, columns, rows }) {
  columns = columns || (section && section.columns) || [];
  rows = rows || (section && section.rows) || [];
  if (!rows || rows.length === 0) return <Empty title="No rows"/>;
  return (
    <div className="pm-report-table-wrap">
      <div className="pm-report-table-meta">{rows.length.toLocaleString()} row{rows.length === 1 ? "" : "s"} shown · no hidden truncation</div>
      <table className="pm-report-table">
        <thead>
          <tr>{columns.map((c) => <th key={c.key} style={{width: c.width}}>{c.label}</th>)}</tr>
        </thead>
        <tbody>
          {rows.map((r, i) => (
            <tr key={i}>
              {columns.map((c) => <td key={c.key}>{r[c.key] || "—"}</td>)}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function ReportList({ items }) {
  if (!items || items.length === 0) return <Empty title="No items"/>;
  return <ul className="pm-report-list">{items.map((it, i) => <li key={i}>{it}</li>)}</ul>;
}

function ReportBlockers({ section, items }) {
  items = items || (section && section.items) || [];
  if (!items || items.length === 0) return <Empty title="No open blockers" sub="That's good."/>;
  return (
    <div className="pm-report-blockers">
      <div className="pm-report-table-meta">{items.length.toLocaleString()} blocker/risk item{items.length === 1 ? "" : "s"} shown · no hidden truncation</div>
      {items.map((b, i) => (
        <div key={i} className={"pm-report-blocker sev-" + (b.severity || "medium")}>
          <div className="pm-report-blocker-h">
            <span className="strong">{b.site}</span>
            <span className="muted">· {b.country}</span>
            <span className={"pm-tag risk"}>{b.severity || "medium"}</span>
            {b.source && <span className="pm-tag">{b.source}</span>}
          </div>
          <div className="pm-report-blocker-body">{b.description}</div>
          <div className="pm-report-blocker-meta muted">
            {b.owner && <span>Owner: {b.owner}</span>}
            {b.opened && <span>· Opened {b.opened.slice(0,10)}</span>}
          </div>
        </div>
      ))}
    </div>
  );
}

/* ---------- expose ---------- */
window.NotificationBell = NotificationBell;
window.MentionTextarea = MentionTextarea;
window.PmTeam = PmTeam;
window.PMReadiness = {
  findSite: pmFindSite,
  summaryForSite: pmReadinessSummaryForSite,
  contextLines: pmReadinessContextLines,
};
window.PmPlanner = PmPlanner;
window.PlanMigrationModal = PlanMigrationModal;
window.PmReports = PmReports;
window.ReportPreview = ReportPreview;
window.printReport = printReport;
