/* =============================================================
   page-overlay.jsx — Overlay tracker (hardware + tasks + migration)
   ============================================================= */
function PageOverlay({ model, filters, setFilters, onOpenSite, onOpenInPM, role }) {
  const { overlay } = model;
  const [search, setSearch] = React.useState("");
  const [view, setView] = React.useState("table");
  const [editingRow, setEditingRow] = React.useState(null);
  const [editingReadiness, setEditingReadiness] = React.useState(null);
  const canEdit = role === "engineer" || role === "po" || role === "admin";
  const resolveNewUnderlayReadiness = React.useMemo(() => buildOverlayNewUnderlayResolver(model), [model]);

  const opts = React.useMemo(() => {
    return {
      region: PRData.uniqueValues(overlay, "region"),
      country: PRData.uniqueValues(overlay, "country"),
      classification: PRData.uniqueValues(overlay, "classification"),
      criticality: PRData.uniqueValues(overlay, "criticality"),
      techno: PRData.uniqueValues(overlay, "techno"),
      migrationStatus: PRData.uniqueValues(overlay, (o) => o.migrationStatus || o.migrationSchedule),
      hwDelivery: PRData.uniqueValues(overlay, "hwDelivery"),
    };
  }, [overlay]);

  const filtered = React.useMemo(() => {
    const s = search.trim().toLowerCase();
    return overlay.filter((o) => {
      if (filters.region && o.region !== filters.region) return false;
      if (filters.country && o.country !== filters.country) return false;
      if (filters.classification && o.classification !== filters.classification) return false;
      if (filters.criticality && o.criticality !== filters.criticality) return false;
      if (filters.techno && o.techno !== filters.techno) return false;
      if (filters.migrationStatus && (o.migrationStatus || o.migrationSchedule) !== filters.migrationStatus) return false;
      if (filters.hwDelivery && o.hwDelivery !== filters.hwDelivery) return false;
      if (s) {
        const hay = [o.siteCode, o.siteName, o.shortLabel, o.country, o.techno, o.comment, o.changeNo].join(" ").toLowerCase();
        if (!hay.includes(s)) return false;
      }
      return true;
    });
  }, [overlay, filters, search]);

  // KPI
  const stats = PRData.overlayStats(filtered);

  // Hardware funnel
  const hwFunnel = [
    { label: "Quote Provided",  value: stats.hwQuoteProvided },
    { label: "Quote Signed",    value: stats.quoteSigned },
    { label: "PO Released",     value: stats.poReleased },
    { label: "HW On-going",     value: stats.hwInProgress },
    { label: "HW Delivered",    value: stats.hwDelivered },
  ];

  // Task readiness rollup
  const taskRoll = React.useMemo(() => PRData.overlayTaskRollup(filtered), [filtered]);

  // TECHNO mix
  const technoMix = React.useMemo(() => {
    const m = new Map();
    for (const o of filtered) if (o.techno) m.set(o.techno, (m.get(o.techno) || 0) + 1);
    const palette = ["var(--accent)", "#558997", "#85AEBA", "#B8CFD6", "#D48E3E", "var(--info)", "var(--ink-300)"];
    return [...m.entries()].sort((a, b) => b[1] - a[1]).map(([k, v], i) => ({
      label: k, value: v, color: palette[i] || "var(--ink-300)",
    }));
  }, [filtered]);

  // Migration schedule mix
  const migrationMix = React.useMemo(() => {
    const m = new Map();
    for (const o of filtered) {
      const k = o.migrationStatus || o.migrationSchedule || "Not Started";
      m.set(k, (m.get(k) || 0) + 1);
    }
    return [...m.entries()].sort((a, b) => b[1] - a[1])
      .map(([label, value]) => ({
        label, value,
        color: PRData.statusBucket(label, "task") === "ok" ? "var(--ok)" :
               PRData.statusBucket(label, "task") === "risk" ? "var(--risk)" :
               PRData.statusBucket(label, "task") === "warn" ? "var(--warn)" : "var(--ink-300)",
      }));
  }, [filtered]);

  // Table columns
  const columns = [
    { key: "siteCode", label: "Code", width: 130, cellClass: "mono strong", render: (r) => r.siteCode || "—" },
    { key: "siteName", label: "Site", render: (r) => (
        <div style={{display:"flex", flexDirection:"column"}}>
          <span className="strong">{r.shortLabel || r.siteName || "—"}</span>
          <span className="muted" style={{fontSize: 11}}>{r.country} · {r.region}</span>
        </div>
      )},
    { key: "criticality", label: "Crit.", width: 70, render: (r) => <CriticalityPill value={r.criticality}/> },
    { key: "classification", label: "Type", width: 110, render: (r) => r.classification || <span className="muted">—</span> },
    { key: "techno", label: "TECHNO", width: 130, render: (r) => r.techno ? <Pill kind="info">{r.techno}</Pill> : <span className="muted">—</span> },
    { key: "hwDelivery", label: "Hardware", width: 130, render: (r) => <StatusCell value={r.hwDelivery} kind="task"/>, sortVal: (r) => r.hwDelivery },
    { key: "migrationStatus", label: "Migration", width: 130, render: (r) => <StatusCell value={r.migrationStatus || r.migrationSchedule} kind="task"/>, sortVal: (r) => r.migrationStatus || r.migrationSchedule },
    { key: "changeNo", label: "CR / change", width: 120, cellClass: "mono",
      render: (r) => r.changeNo || <span className="muted">â€”</span>, sortVal: (r) => r.changeNo || "" },
    { key: "newUnderlayReadiness", label: "New underlay", width: 150, sortable: false,
      render: (r) => <OverlayNewUnderlaySummary resolution={resolveNewUnderlayReadiness(r)}/> },
    { key: "relationshipWarnings", label: "Warnings", width: 120, render: (r) => <RelationshipWarningCell row={r}/>, sortVal: (r) => (r._relationshipWarnings || []).length },
    { key: "tasksReady", label: "Tasks ready", width: 120,
      render: (r) => {
        let ok = 0, total = 0;
        for (const t of PRData.OVERLAY_TASK_FIELDS) {
          total++;
          if (PRData.statusBucket(r.tasks[t.key], "task") === "ok") ok++;
        }
        return (
          <div style={{display:"flex", alignItems:"center", gap: 6}}>
            <div style={{flex: 1, maxWidth: 80}}>
              <Progress value={ok} max={total} thin/>
            </div>
            <span className="mono" style={{fontSize: 11, color: "var(--ink-500)"}}>{ok}/{total}</span>
          </div>
        );
      },
      sortVal: (r) => {
        let ok = 0;
        for (const t of PRData.OVERLAY_TASK_FIELDS) if (PRData.statusBucket(r.tasks[t.key], "task") === "ok") ok++;
        return ok;
      }
    },
    { key: "confirmedMigration", label: "Migration date", width: 110, cellClass: "mono",
      render: (r) => PRData.fmtDate(r.confirmedMigration), sortVal: (r) => r.confirmedMigration },
    { key: "_actions", label: "Actions", width: 325, sortable: false, render: (r) => {
      const readinessResolution = resolveNewUnderlayReadiness(r);
      const openReadinessPanel = (event) => {
        event.preventDefault();
        event.stopPropagation();
        setEditingReadiness({ overlayRow: r, resolution: readinessResolution });
      };
      return (
        <div className="flex gap-sm" onClick={(e) => e.stopPropagation()}>
          {canEdit && <button className="btn" style={{ padding: "2px 8px", fontSize: 11 }} onClick={() => setEditingRow(r)}><Glyph name="edit" size={11}/> Edit status</button>}
          <button
            className="btn ov-readiness-action"
            data-count={readinessResolution.rows.length}
            data-has-links={readinessResolution.rows.length ? "true" : "false"}
            style={{ padding: "2px 8px", fontSize: 11 }}
            onMouseDown={openReadinessPanel}
            onClick={openReadinessPanel}
          >
            <Glyph name="edit" size={11}/> New underlay readiness
          </button>
          {onOpenInPM && <button className="btn ghost" style={{ padding: "2px 8px", fontSize: 11 }} onClick={() => onOpenInPM("overlay", r.siteCode || r.shortLabel || r.siteName)}>Open in PM</button>}
          {/* G6: jump straight to the underlay links of this site (linkRef[0] gets the editor onto the right sub-tab) */}
          {onOpenInPM && (() => {
            const siteUnderlay = (model.underlay || []).filter((u) => u.siteName && r.siteName && u.siteName.toLowerCase() === r.siteName.toLowerCase());
            const firstRef = siteUnderlay[0] && siteUnderlay[0].linkRef;
            if (!firstRef) return null;
            return <button className="btn ghost" style={{ padding: "2px 8px", fontSize: 11 }} onClick={() => onOpenInPM("underlay", firstRef)}>Underlay ({siteUnderlay.length})</button>;
          })()}
        </div>
      );
    } },
  ];

  return (
    <div className="page">
      <PageHeader
        title="Overlay tracker"
        subtitle="Site readiness — hardware, network prep, and migration status."
        right={
          <SearchInput value={search} onChange={setSearch} placeholder="Search site, code, TECHNO, comment…"/>
        }
      />

      <div className="grid g-4 mb-md">
        <KPI accent label="Sites in view" value={stats.totalSites.toLocaleString()} hint={`of ${overlay.length}`} delta=""/>
        <KPI label="HW delivered" value={stats.hwDelivered} hint={`${Math.round((stats.hwDelivered/Math.max(1,stats.totalSites))*100)}%`} delta={`${stats.hwOrdered} PO+signed`}/>
        <KPI label="Migrated" value={stats.migrated} hint={`${Math.round((stats.migrated/Math.max(1,stats.totalSites))*100)}%`} delta=""/>
        <KPI label="With blocked task" value={stats.blocked} hint="any prep step blocked" delta=""/>
      </div>

      <div className="flex gap-sm mb-md" style={{flexWrap: "wrap"}}>
        <FilterSelect label="Region" value={filters.region} onChange={(v) => setFilters({ ...filters, region: v })} options={opts.region}/>
        <FilterSelect label="Country" value={filters.country} onChange={(v) => setFilters({ ...filters, country: v })} options={opts.country}/>
        <FilterSelect label="Type" value={filters.classification} onChange={(v) => setFilters({ ...filters, classification: v })} options={opts.classification}/>
        <FilterSelect label="Criticality" value={filters.criticality} onChange={(v) => setFilters({ ...filters, criticality: v })} options={opts.criticality}/>
        <FilterSelect label="TECHNO" value={filters.techno} onChange={(v) => setFilters({ ...filters, techno: v })} options={opts.techno}/>
        <FilterSelect label="Migration" value={filters.migrationStatus} onChange={(v) => setFilters({ ...filters, migrationStatus: v })} options={opts.migrationStatus}/>
        <FilterSelect label="Hardware" value={filters.hwDelivery} onChange={(v) => setFilters({ ...filters, hwDelivery: v })} options={opts.hwDelivery}/>
        {(filters.region||filters.country||filters.classification||filters.criticality||filters.techno||filters.migrationStatus||filters.hwDelivery||search) && (
          <button className="btn ghost" onClick={() => { setFilters({}); setSearch(""); }} style={{fontSize: 11.5}}>Clear all</button>
        )}
      </div>

      <div className="grid g-3 mb-md">
        <div className="card">
          <div className="card-h"><div className="title">Hardware funnel</div></div>
          <Funnel steps={hwFunnel} />
        </div>
        <div className="card">
          <div className="card-h"><div className="title">Migration status</div></div>
          <BarChart data={migrationMix} />
        </div>
        <div className="card">
          <div className="card-h"><div className="title">TECHNO mix</div></div>
          {technoMix.length === 0 ? <Empty title="No TECHNO data"/> : <StackedBar segments={technoMix} />}
        </div>
      </div>

      <div className="card mb-md">
        <div className="card-h">
          <div className="title">Task readiness across all sites</div>
          <div className="hint">{filtered.length} sites · ready / partial / blocked / not started</div>
        </div>
        <div style={{display: "flex", flexDirection: "column", gap: 6, marginTop: 6}}>
          {taskRoll.map((t) => {
            const okPct = t.total ? (t.ok / t.total) * 100 : 0;
            const partPct = t.total ? (t.partial / t.total) * 100 : 0;
            const blockedPct = t.total ? (t.blocked / t.total) * 100 : 0;
            return (
              <div key={t.key} style={{display: "grid", gridTemplateColumns: "200px 1fr 110px", gap: 12, alignItems: "center"}}>
                <div style={{fontSize: 12.5, color: "var(--ink-700)"}}>{t.label}</div>
                <div className="stack-bar">
                  <span style={{width: okPct + "%", background: "var(--ok)"}}/>
                  <span style={{width: partPct + "%", background: "var(--warn)"}}/>
                  <span style={{width: blockedPct + "%", background: "var(--risk)"}}/>
                </div>
                <div className="mono" style={{fontSize: 11.5, color: "var(--ink-500)", textAlign: "right"}}>
                  <span style={{color: "var(--ok)", fontWeight: 600}}>{t.ok}</span>
                  {t.partial > 0 && <> · <span style={{color: "var(--warn)"}}>{t.partial}</span></>}
                  {t.blocked > 0 && <> · <span style={{color: "var(--risk)"}}>{t.blocked}</span></>}
                </div>
              </div>
            );
          })}
        </div>
      </div>

      <div className="table-wrap">
        <div className="table-toolbar">
          <span className="mono" style={{fontSize: 11.5, color: "var(--ink-500)"}}>{filtered.length.toLocaleString()} of {overlay.length.toLocaleString()} sites</span>
          <span style={{flex: 1}}/>
          <span style={{fontSize: 11.5, color: "var(--ink-400)"}}>Click any row for full site detail</span>
        </div>
        <DataTable columns={columns} rows={filtered} onRowClick={(r) => onOpenSite({ site: { id: r.siteCode || r.siteName, siteCode: r.siteCode, siteName: r.siteName, country: r.country, region: r.region, criticality: r.criticality, classification: r.classification, techno: r.techno, overlay: r, links: [] } })} maxHeight={"65vh"}/>
      </div>

      {editingRow && (
        <TrackerRowEditor
          row={editingRow}
          entity="overlay"
          idResolver={(r) => PRData.resolveOperationalEntityId("overlay", r, model)}
          fields={[
            { key: "hwDelivery", label: "HW delivery", type: "enum", options: ["Not Started", "Quote Provided", "Quote Signed", "PO Released", "HW on-going", "HW Delivered", "HW Returned"] },
            { key: "quoteProvided", label: "Quote provided", type: "enum", options: ["Not Started", "In Progress", "Done"] },
            { key: "quoteSignedDate", label: "Quote signed date", type: "date" },
            { key: "poDate", label: "PO date", type: "date" },
            { key: "hwDelivered", label: "HW delivered date", type: "date" },
            { key: "lanConfig", label: "LAN config", type: "enum", options: ["Not Started", "In Progress", "Done"] },
            { key: "factoryRulebook", label: "Factory rulebook", type: "enum", options: ["Not Started", "In Progress", "Done"] },
            { key: "azureWanRouter", label: "Azure WAN router", type: "enum", options: ["Not Started", "In Progress", "Done"] },
            { key: "vrfAlloc", label: "VRF allocation", type: "enum", options: ["Not Started", "In Progress", "Done"] },
            { key: "infobloxIp", label: "Infoblox IP", type: "enum", options: ["Not Started", "In Progress", "Done"] },
            { key: "infobloxDns", label: "Infoblox DNS", type: "enum", options: ["Not Started", "In Progress", "Done"] },
            { key: "tacacsConfig", label: "TACACS config", type: "enum", options: ["Not Started", "In Progress", "Done"] },
            { key: "smartzoneAlloc", label: "Smartzone alloc", type: "enum", options: ["Not Started", "In Progress", "Done"] },
            { key: "migrationSchedule", label: "Migration schedule", type: "enum", options: ["Not Scheduled", "Scheduled", "Postponed", "Cancelled"] },
            { key: "migrationStatus", label: "Migration status", type: "enum", options: ["Not Started", "In Progress", "Migrated", "On-hold", "Postponed", "Failed"] },
            { key: "confirmedMigration", label: "Migration date (confirmed)", type: "date" },
            { key: "changeNo", label: "Change No.", type: "text" },
            { key: "comment", label: "Comment", type: "longtext" },
          ]}
          canEdit={canEdit}
          openInPM={onOpenInPM}
          onClose={() => setEditingRow(null)}
          onSaved={() => {/* GA-13: autosave — model re-renders via PMClient.subscribe; editor stays open */}}
        />
      )}

      {editingReadiness && (
        <OverlayNewUnderlayReadinessPanel
          overlayRow={editingReadiness.overlayRow}
          resolution={editingReadiness.resolution}
          canEdit={canEdit}
          openInPM={onOpenInPM}
          onClose={() => setEditingReadiness(null)}
          onSaved={() => {/* GA-13: autosave — editor stays open; model re-renders via PMClient.subscribe */}}
        />
      )}
    </div>
  );
}

function overlayNuTextKey(value) {
  return String(value || "").trim().toLowerCase().replace(/\s+/g, " ");
}

function buildOverlayNewUnderlayResolver(model) {
  // PMNU-15: delegate the actual site -> New Underlay resolution to the ONE shared resolver
  // (PRData.resolveSiteNewUnderlay) so multi-link overlay sites stay isolated by Link Ref and
  // A/B link data is never merged destructively. An overlay row is not a canonical site, so the
  // overlay tracker's only job here is to enrich it with the site's known Link Refs (canonical
  // site aggregate + underlay links + the overlay row's own Link Ref columns); the shared
  // resolver then owns matching, per-link isolation, and diagnostics. No bespoke merge-by-site.
  const canonicalSites = (model && (model.canonicalSites || model.sites)) || [];
  const underlay = (model && model.underlay) || [];
  const canonicalByCode = new Map();
  const canonicalByName = new Map();
  canonicalSites.forEach((site) => {
    const code = overlayNuTextKey(site.siteCode);
    const name = overlayNuTextKey(site.siteName || site.id);
    if (code && !canonicalByCode.has(code)) canonicalByCode.set(code, site);
    if (name && !canonicalByName.has(name)) canonicalByName.set(name, site);
  });
  const underlayRefsByCode = new Map();
  const underlayRefsByName = new Map();
  const addUnderlayRef = (map, key, ref) => {
    if (!key || !ref) return;
    if (!map.has(key)) map.set(key, []);
    map.get(key).push(ref);
  };
  underlay.forEach((link) => {
    addUnderlayRef(underlayRefsByCode, overlayNuTextKey(link.siteCode), link.linkRef);
    addUnderlayRef(underlayRefsByName, overlayNuTextKey(link.siteName), link.linkRef);
  });

  const resolveFn = PRData.resolveSiteNewUnderlay;

  return (overlayRow) => {
    const codeKey = overlayNuTextKey(overlayRow && overlayRow.siteCode);
    const nameKey = overlayNuTextKey((overlayRow && (overlayRow.siteName || overlayRow.shortLabel)) || "");
    const linkRefs = new Set();
    const addRef = (ref) => {
      const v = ref === 0 ? "0" : ref;
      if (v) linkRefs.add(String(v).trim());
    };
    // The overlay row's own Link Ref columns (e.g. Link A Ref No).
    addRef(overlayRow && overlayRow.linkARef);
    Object.entries((overlayRow && overlayRow._raw) || {}).forEach(([key, value]) => {
      if (/link.*ref/i.test(key)) addRef(value);
    });
    // Canonical site link refs — the authoritative aggregate (includes New Underlay rows).
    const canonicalSite = (codeKey && canonicalByCode.get(codeKey)) || (nameKey && canonicalByName.get(nameKey)) || null;
    ((canonicalSite && (canonicalSite.canonicalLinkRefs || canonicalSite.linkRefs)) || []).forEach(addRef);
    // Underlay links matched by the same site identity.
    (underlayRefsByCode.get(codeKey) || []).forEach(addRef);
    (underlayRefsByName.get(nameKey) || []).forEach(addRef);

    const site = {
      siteCode: overlayRow && overlayRow.siteCode,
      siteName: (overlayRow && (overlayRow.siteName || overlayRow.shortLabel)) || "",
      region: overlayRow && overlayRow.region,
      linkRefs: [...linkRefs],
      canonicalLinkRefs: [...linkRefs],
    };

    const resolved = resolveFn
      ? resolveFn(site, model)
      : { rows: [], blocks: [], diagnostics: { noMatch: true, duplicateLinkRefs: [], ambiguousFallback: [], orphans: [] } };
    const diagnostics = resolved.diagnostics || {};
    let reason = "";
    if (!(resolved.rows && resolved.rows.length)) {
      reason = (diagnostics.ambiguousFallback && diagnostics.ambiguousFallback.length)
        ? "New Underlay rows exist for this site under a different Link Ref and were not merged. Open the exact link in Project Management."
        : "No linked New Underlay rows were found for this overlay site.";
    }
    return {
      rows: resolved.rows || [],
      blocks: resolved.blocks || [],
      diagnostics,
      reason,
    };
  };
}

function overlayNewUnderlayGroups() {
  // PMNU-15: derive the four phase blocks from the canonical classifier
  // (PRData.newUnderlayReadinessPhase) and phase labels (PRData.NEW_UNDERLAY_PHASES) — exactly
  // the grouping the PM site editor and Underlay tracker use. Deriving instead of hard-coding
  // keeps overlay phase membership from drifting out of sync with PM / the New underlay editor.
  const phases = PRData.NEW_UNDERLAY_PHASES || {};
  const order = [
    { key: "preChangeReadiness", label: phases.preChangeReadiness || "Pre-Change Readiness" },
    { key: "changeManagement",   label: phases.changeManagement   || "Change Management" },
    { key: "linkMigration",      label: phases.linkMigration      || "Link Migration" },
    { key: "postMigration",      label: phases.postMigration      || "Post Migration" },
  ];
  const classify = PRData.newUnderlayReadinessPhase || (() => "postMigration");
  const fields = PRData.NEW_UNDERLAY_READINESS || [];
  return order.map((group) => ({
    ...group,
    fields: fields.filter((field) => classify(field.key) === group.key),
  }));
}

function overlayNewUnderlayStats(row) {
  // PMNU-15: delegate to the one canonical New Underlay readiness classifier so the overlay
  // tracker reports exactly the same readiness %, ready/total, blocked count, and per-phase
  // breakdown as the PM site editor, Underlay tracker, and monitoring pages (no drift).
  if (PRData.newUnderlayReadinessStats) {
    const s = PRData.newUnderlayReadinessStats(row || {});
    return {
      ready: s.readyReadiness || 0,
      blocked: s.blockedReadinessCount || 0,
      total: s.totalReadiness || 1,
      pct: s.readinessPercent || 0,
      phases: s.phases || {},
    };
  }
  // Fallback only if the canonical helper is unavailable (keeps the tracker resilient).
  const fields = PRData.NEW_UNDERLAY_READINESS || [];
  let ready = 0;
  let blocked = 0;
  fields.forEach((field) => {
    const value = row && row.readiness ? row.readiness[field.key] : "";
    const bucket = PRData.statusBucket(value, "task");
    if (bucket === "ok") ready++;
    if (bucket === "risk") blocked++;
  });
  const total = fields.length || 1;
  return { ready, blocked, total, pct: Math.round((ready / total) * 100), phases: {} };
}

function overlayNewUnderlayLastChanged(row) {
  const meta = (row && row._overrideMeta) || {};
  return Object.entries(meta)
    .filter(([field, value]) => field.indexOf("readiness.") === 0 && value && (value.changedAt || value.changedBy))
    .map(([field, value]) => ({ field, ...value }))
    .sort((a, b) => String(b.changedAt || "").localeCompare(String(a.changedAt || "")))[0] || null;
}

function overlayFormatChangedAt(value) {
  if (!value) return "";
  const date = new Date(value);
  if (isNaN(date.getTime())) return String(value);
  return date.toLocaleString([], { month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" });
}

function OverlayNewUnderlaySummary({ resolution }) {
  const rows = (resolution && resolution.rows) || [];
  if (!rows.length) return <span className="ul-readiness-missing" title={resolution && resolution.reason}>No linked rows</span>;
  const totals = rows.reduce((acc, row) => {
    const stats = overlayNewUnderlayStats(row);
    acc.ready += stats.ready;
    acc.blocked += stats.blocked;
    acc.total += stats.total;
    return acc;
  }, { ready: 0, blocked: 0, total: 0 });
  const pct = totals.total ? Math.round((totals.ready / totals.total) * 100) : 0;
  return (
    <div className="ul-readiness-progress ov-readiness-summary" title={`${rows.length} linked New Underlay row(s)`}>
      <div className="ul-readiness-top">
        <span className="mono">{rows.length} link{rows.length === 1 ? "" : "s"}</span>
        {totals.blocked > 0 && <span className="ul-readiness-blocked">{totals.blocked} blocked</span>}
      </div>
      <div className="ul-readiness-bar"><span style={{ width: `${pct}%` }}/></div>
      <div className="ul-readiness-pct">{pct}% ready</div>
    </div>
  );
}

// GA-13 fix: OverlayNewUnderlayReadinessPanel converted from batched draft+Save button to
// per-field autosave. Each enum select commits immediately via PMClient.patchNewUnderlay
// (single field, with __meta provenance) for the selected link only — no explicit Save
// click needed. Keeps 9 mirror fields read-only (rendered as display-only, never patched).
// Editor stays open after each autosave (onSaved is a no-op in the parent).
function OverlayNewUnderlayReadinessPanel({ overlayRow, resolution, canEdit, openInPM, onClose, onSaved }) {
  const rows = (resolution && resolution.rows) || [];
  const [selectedRef, setSelectedRef] = React.useState(() => rows[0] && (rows[0].linkRef || rows[0]._id) || "");
  const selected = rows.find((row) => (row.linkRef || row._id) === selectedRef) || rows[0] || null;
  // GA-13: per-field local state + autosave — no batched draft, no Save button.
  const [localValues, setLocalValues] = React.useState(() => ({ ...((selected && selected.readiness) || {}) }));
  const [savingField, setSavingField] = React.useState(null);
  const [savedFlash, setSavedFlash] = React.useState(null);
  const [err, setErr] = React.useState("");
  const groups = React.useMemo(() => overlayNewUnderlayGroups(), []);
  const options = PRData.NEW_UNDERLAY_WORKFLOW_STATUS_OPTIONS || ["Not Started", "In Progress", "Ready", "Completed", "Blocked", "Failed", "Yes", "No"];
  const stats = overlayNewUnderlayStats({ readiness: localValues });
  const canWrite = canEdit && selected && selected.linkRef;

  React.useEffect(() => {
    if (!rows.length) {
      setSelectedRef("");
      return;
    }
    if (!rows.some((row) => (row.linkRef || row._id) === selectedRef)) {
      setSelectedRef(rows[0].linkRef || rows[0]._id || "");
    }
  }, [rows, selectedRef]);

  // Reset local values and error when the selected link changes.
  React.useEffect(() => {
    setLocalValues({ ...((selected && selected.readiness) || {}) });
    setErr("");
    setSavingField(null);
    setSavedFlash(null);
  }, [selected && (selected.linkRef || selected._id)]);

  // Autosave a single readiness field immediately on change for the selected link.
  const saveField = async (key, value) => {
    if (!canWrite) return;
    setErr("");
    setSavingField(key);
    const patchKey = `readiness.${key}`;
    const oldValue = (selected && selected.readiness) ? (selected.readiness[key] || "") : "";
    try {
      await PMClient.patchNewUnderlay(selected.linkRef, {
        [patchKey]: value,
        __meta: {
          [patchKey]: {
            workbookValue: oldValue,
            sourceEntity: "newUnderlay",
            sourceField: patchKey,
          },
        },
      });
      setSavedFlash(key);
      setTimeout(() => setSavedFlash((cur) => cur === key ? null : cur), 1500);
      onSaved && onSaved();
    } catch (error) {
      setErr("Save failed: " + (error.message || error));
    } finally {
      setSavingField((cur) => cur === key ? null : cur);
    }
  };

  return (
    <div className="pm-modal-backdrop" onClick={onClose}>
      <div className="pm-modal large ov-readiness-modal" onClick={(e) => e.stopPropagation()}>
        <div className="pm-modal-h">
          <div className="title">
            New underlay readiness
            <span className="mono" style={{ marginLeft: 8, color: "var(--ink-500)" }}>
              - {overlayRow.siteCode || overlayRow.siteName || "overlay site"}
            </span>
          </div>
          <button className="btn ghost" onClick={onClose}><Glyph name="close" size={14}/></button>
        </div>
        <div className="pm-modal-body">
          {/* GA-18: shared local-only vs server-backed indicator (same component as PM). */}
          <StorageModePill/>
          {!canEdit && <div className="pm-readonly-pill" style={{ marginBottom: 8 }}>Read-only - management cannot edit</div>}
          {canEdit && canWrite && (
            <div className="pm-readonly-pill" style={{ marginBottom: 8 }}>Changes autosave as you edit — no Save button.</div>
          )}
          {!rows.length && (
            <div className="pm-readonly-pill" style={{ marginBottom: 10, background: "var(--warn-soft)", color: "var(--warn)" }}>
              {resolution && resolution.reason ? resolution.reason : "No linked New Underlay rows were found for this overlay site."}
              <div style={{ marginTop: 4 }}>No fake New Underlay data was created. Add or correct the workbook row first, then reload/import.</div>
            </div>
          )}
          {!!rows.length && (
            <>
              <div className="ov-readiness-card-grid">
                {rows.map((row) => (
                  <OverlayNewUnderlayLinkCard
                    key={row.linkRef || row._id}
                    row={row}
                    groups={groups}
                    selected={(row.linkRef || row._id) === (selected && (selected.linkRef || selected._id))}
                    onSelect={() => setSelectedRef(row.linkRef || row._id)}
                  />
                ))}
              </div>

              <div className="ul-readiness-summary">
                <div>
                  <div className="pm-field-label">Selected link</div>
                  <strong className="mono">{selected && selected.linkRef || "-"}</strong>
                </div>
                <div>
                  <div className="pm-field-label">Ready</div>
                  <strong>{stats.ready}/{stats.total}</strong>
                </div>
                <div>
                  <div className="pm-field-label">Blocked</div>
                  <strong className={stats.blocked ? "risk-text" : ""}>{stats.blocked}</strong>
                </div>
                <div>
                  <div className="pm-field-label">Completion</div>
                  <strong>{stats.pct}%</strong>
                </div>
              </div>

              <div className="pm-fieldgroup">
                <div className="pm-fieldgroup-h">Underlay mirrored values</div>
                <div className="pm-fieldgroup-desc">Read-only values from the Underlay link. Edit these on the linked Underlay row.</div>
                <div className="ul-readiness-mirror-grid">
                  <OverlayNewUnderlayMirrorValue label="Link Ref." value={selected && selected.linkRef}/>
                  <OverlayNewUnderlayMirrorValue label="Link A/B" value={selected && selected.linkAB}/>
                  <OverlayNewUnderlayMirrorValue label="ISP / vendor" value={selected && selected.isp}/>
                  <OverlayNewUnderlayMirrorValue label="Local partner" value={selected && selected.partner}/>
                  <OverlayNewUnderlayMirrorValue label="Circuit delivered" value={selected && selected.circuitDelivered}/>
                  <OverlayNewUnderlayMirrorValue label="Change No." value={selected && selected.changeNo}/>
                </div>
              </div>

              {groups.map((group) => (
                <div key={group.key} className="pm-fieldgroup">
                  <div className="pm-fieldgroup-h">{group.label}</div>
                  <div className="ul-readiness-grid">
                    {group.fields.map((field) => (
                      <div key={field.key} className="pm-field" data-field-key={`readiness.${field.key}`}>
                        <label className="pm-field-label">{field.label}</label>
                        <div className="pm-field-input">
                          <select
                            className="pm-input"
                            aria-label={field.label}
                            value={localValues[field.key] || ""}
                            disabled={!canWrite || savingField === field.key}
                            onChange={(event) => {
                              const v = event.target.value;
                              setLocalValues((cur) => ({ ...cur, [field.key]: v }));
                              saveField(field.key, v);
                            }}
                          >
                            <option value="">-</option>
                            {options.map((option) => <option key={option} value={option}>{option}</option>)}
                          </select>
                          {savedFlash === field.key && <span className="pm-saved-pill">Saved</span>}
                          {savingField === field.key && <span className="pm-saving-spinner"/>}
                        </div>
                      </div>
                    ))}
                  </div>
                </div>
              ))}
            </>
          )}
          {err && <div className="pm-readonly-pill" style={{ marginTop: 10, background: "var(--risk-soft)", color: "var(--risk)" }}>{err}</div>}
        </div>
        <div className="pm-modal-actions">
          {openInPM && selected && selected.linkRef && (
            <button className="btn" onClick={() => { onClose(); openInPM("newUnderlay", selected.linkRef); }}>
              <Glyph name="ext" size={12}/> Open selected link in PM
            </button>
          )}
          <span style={{ flex: 1 }}/>
          <button className="btn primary" onClick={onClose}>Done</button>
        </div>
      </div>
    </div>
  );
}

function OverlayNewUnderlayLinkCard({ row, groups, selected, onSelect }) {
  const stats = overlayNewUnderlayStats(row);
  const lastChanged = overlayNewUnderlayLastChanged(row);
  const phaseGroups = groups || overlayNewUnderlayGroups();
  return (
    <button className={"ov-readiness-card" + (selected ? " selected" : "")} onClick={onSelect}>
      <div className="ov-readiness-card-head">
        <strong className="mono">{row.linkRef || "-"}</strong>
        {row.linkAB && <span>{row.linkAB}</span>}
      </div>
      <div className="ov-readiness-card-meta">
        <span>{row.isp || row.partner || "-"}</span>
        <span>{stats.pct}% ready</span>
      </div>
      <div className="ul-readiness-bar"><span style={{ width: `${stats.pct}%` }}/></div>
      {/* PMNU-15: per-link phase summary using the same four-phase grouping as the PM site editor. */}
      <div className="pm-nu-phasegrid ov-readiness-card-phases" data-nu-phasegrid="true">
        {phaseGroups.map((group) => {
          const p = (stats.phases && stats.phases[group.key]) || {};
          return (
            <div key={group.key} className="pm-nu-phase" data-phase={group.key}>
              <span>{group.label}</span>
              <strong>{(p.ready || 0)}/{(p.total || 0)}</strong>
              <div className="pm-nu-bar"><i style={{ width: `${Math.max(0, Math.min(100, p.percent || 0))}%` }}/></div>
              {(p.blocked || 0) > 0 && <em>{p.blocked} blocked</em>}
            </div>
          );
        })}
      </div>
      <div className="ov-readiness-card-foot">
        <span className={stats.blocked ? "risk-text" : ""}>{stats.blocked} blocked</span>
        <span>{lastChanged ? [lastChanged.changedBy, overlayFormatChangedAt(lastChanged.changedAt)].filter(Boolean).join(" - ") : "No local change"}</span>
      </div>
    </button>
  );
}

function OverlayNewUnderlayMirrorValue({ label, value }) {
  return (
    <div className="ul-readiness-mirror">
      <span>{label}</span>
      <strong>{value || "-"}</strong>
      <em>from Underlay link</em>
    </div>
  );
}

window.PageOverlay = PageOverlay;
