// Main app entrypoint
const { useState, useEffect, useMemo, useCallback, useRef } = React;

function nowStamp() {
  const d = new Date();
  const pad = (n) => String(n).padStart(2, "0");
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

function App() {
  // AHP-derived defaults — sourced from SCHEMA.default_weights via dqi_engine.js.
  const initialWeights = useMemo(() => {
    const dw = (typeof DEFAULT_WEIGHTS !== "undefined" && DEFAULT_WEIGHTS) || { accuracy: 35, completeness: 30, consistency: 20, timeliness: 15 };
    return { a: dw.accuracy, c: dw.completeness, co: dw.consistency, t: dw.timeliness };
  }, []);
  const [weights, setWeights] = useState(initialWeights);

  // files[name] = { name, df, profile_name, geo_type, label, auto, confirmed }
  const [files, setFiles] = useState({});
  // Set of history-keys (profile|geo|label or file:<name>) already saved this session.
  // Held in a ref so the save effect can read the current value without needing
  // savedKeys in its dep array (which would cause an infinite save loop).
  const savedKeysRef = useRef(new Set());
  const [historyVersion, setHistoryVersion] = useState(0);
  const [toast, setToast] = useState(null);
  const [showSettings, setShowSettings] = useState(false);
  const [cloudStatus, setCloudStatus] = useState(() => isCloudEnabled() ? "connecting" : "off");

  const totalWeight = weights.a + weights.c + weights.co + weights.t;
  const weightsOk = totalWeight === 100;
  const wa = weights.a / 100, wc = weights.c / 100, wco = weights.co / 100, wt = weights.t / 100;

  useEffect(() => { if (!toast) return; const t = setTimeout(() => setToast(null), 3000); return () => clearTimeout(t); }, [toast]);

  // Initial cloud sync
  useEffect(() => {
    if (!isCloudEnabled()) { setCloudStatus("off"); return; }
    setCloudStatus("connecting");
    syncFromCloud().then(() => {
      setCloudStatus("online");
      setHistoryVersion((v) => v + 1);
    }).catch(() => setCloudStatus("offline"));
  }, []);

  // Periodic re-sync from cloud so other users' writes show up.
  useEffect(() => {
    if (!isCloudEnabled()) return;
    const id = setInterval(() => {
      if (!isCloudEnabled()) return; // user disconnected mid-flight
      syncFromCloud().then(() => {
        if (!isCloudEnabled()) return;
        setCloudStatus("online");
        setHistoryVersion((v) => v + 1);
      }).catch(() => { if (isCloudEnabled()) setCloudStatus("offline"); });
    }, 30000);
    return () => clearInterval(id);
  }, [historyVersion, cloudStatus]);

  // --- Add files: auto-detect profile + geography from filename --------------
  const handleFiles = useCallback(async (fileList) => {
    const updates = { ...files };
    for (const f of fileList) {
      if (updates[f.name]) continue;
      try {
        const df = await parseFile(f);
        const { profile: detected, label: detectedLabel } = matchProfileByFilename(f.name);
        const isDefault = detected.filename === "__default__";
        const stem = f.name.replace(/\.[^.]+$/, "");
        updates[f.name] = {
          name: f.name, df,
          profile_name: detected.name,
          geo_type: isDefault ? "N/A" : (detected.geo_type || "Zone"),
          label: isDefault ? stem : (detectedLabel || stem),
          auto: true,
          confirmed: false,
        };
      } catch (e) {
        setToast({ kind: "error", text: `Failed to read ${f.name}: ${e.message}` });
      }
    }
    setFiles(updates);
  }, [files]);

  const handleDemo = useCallback(() => {
    const updates = { ...files };
    for (const f of window.DEMO_FILES) {
      // Run the same detector against demo filenames so the status pill
      // shows "Auto-detected" for matchable names and "No profile match"
      // for the generic fallback file.
      const { profile: detected, label: detectedLabel } = matchProfileByFilename(f.name);
      const isDefault = detected.filename === "__default__";
      const stem = f.name.replace(/\.[^.]+$/, "");
      updates[f.name] = {
        name: f.name, df: f.df,
        profile_name: detected.name,
        geo_type: isDefault ? "N/A" : (detected.geo_type || "Zone"),
        label: isDefault ? stem : (detectedLabel || stem),
        auto: true,
        confirmed: false,
      };
    }
    setFiles(updates);
    setToast({ kind: "success", text: "Loaded demo data — review detections, then Confirm all & score." });
  }, [files]);

  const removeFile = useCallback((name) => {
    setFiles((cur) => { const n = { ...cur }; delete n[name]; return n; });
  }, []);

  // DetectionRow merges patches into a single file entry. Any user edit
  // flips `auto` off so the status pill switches from green to a manual tag.
  const updateDetection = useCallback((name, patch) => {
    setFiles((cur) => ({ ...cur, [name]: { ...cur[name], ...patch, confirmed: false } }));
  }, []);

  // Single "Confirm all & score" — commits every pending detection at once.
  const confirmAll = useCallback(() => {
    setFiles((cur) => {
      const out = { ...cur };
      for (const k of Object.keys(out)) {
        const e = out[k];
        const stem = e.name.replace(/\.[^.]+$/, "");
        const finalLabel = e.geo_type === "N/A" ? stem : ((e.label || "").trim() || stem);
        out[k] = { ...e, label: finalLabel, confirmed: true };
      }
      return out;
    });
    // Force a fresh save pass with current weights (per Streamlit spec).
    savedKeysRef.current = new Set();
  }, []);

  // --- Computed: parsed (profile -> entries) — only confirmed files ----------
  const parsed = useMemo(() => {
    const out = {};
    for (const f of Object.values(files)) {
      if (!f.confirmed) continue;
      const profile = getProfileByName(f.profile_name);
      const isDefault = profile.filename === "__default__";
      const scores = weightsOk ? runProfileChecks(f.df, profile) : null;
      const detailed = weightsOk ? runProfileChecksDetailed(f.df, profile) : null;
      const dqi = scores ? computeDqi(scores, wa, wc, wco, wt) : null;
      const zoneLabel = f.label || f.name;
      const entry = {
        filename: f.name,
        zone_label: zoneLabel,
        geo_type: f.geo_type,
        profile, df: f.df,
        scores, detailed, dqi, isDefault,
      };
      (out[profile.name] ||= []).push(entry);
    }
    return out;
  }, [files, weightsOk, wa, wc, wco, wt, historyVersion]);

  // History save key: composite (profile|geo_type|label) for zoned profiles,
  // filename for Default profile. Matches the new composite-history reads.
  const historyKey = (e) => (e.geo_type && e.geo_type !== "N/A")
    ? `${e.profile.name}|${e.geo_type}|${e.zone_label}`
    : `file:${e.filename}`;

  // Save scored entries to history (once per key per session — re-saved when
  // weights change because confirmAll() clears savedKeysRef).
  useEffect(() => {
    if (!weightsOk) return;
    const toSave = [];
    for (const entries of Object.values(parsed)) {
      for (const e of entries) {
        if (!e.scores) continue;
        const key = historyKey(e);
        if (savedKeysRef.current.has(key)) continue;
        toSave.push({
          filename: e.filename,
          profile: e.profile.name,
          geo_type: e.isDefault || e.geo_type === "N/A" ? null : e.geo_type,
          geography: e.isDefault || e.geo_type === "N/A" ? null : `${e.geo_type} — ${e.zone_label}`,
          uploaded_at: nowStamp(),
          accuracy_score: e.scores.accuracy,
          completeness_score: e.scores.completeness,
          consistency_score: e.scores.consistency,
          timeliness_score: e.scores.timeliness,
          dqi_score: e.dqi,
        });
        // Mark as saved immediately so subsequent effect runs skip it.
        savedKeysRef.current.add(key);
      }
    }
    if (toSave.length === 0) return;
    if (!isCloudEnabled()) {
      // No cloud configured — nothing to persist, but don't loop.
      return;
    }
    (async () => {
      let anyError = false;
      for (const r of toSave) {
        try { await saveHistoryRecord(r); }
        catch (err) { anyError = true; console.warn(err); }
      }
      if (anyError) {
        setCloudStatus("offline");
        setToast({ kind: "error", text: "Failed to save to cloud database. Check your connection." });
      } else {
        setCloudStatus("online");
      }
      setHistoryVersion((v) => v + 1);
    })();
  }, [parsed, weightsOk]);

  const profileGroups = useMemo(() => {
    const out = {};
    for (const [pn, entries] of Object.entries(parsed)) {
      out[pn] = entries.filter((e) => e.scores);
    }
    return out;
  }, [parsed]);

  const allScored = Object.keys(parsed).length > 0 && Object.values(parsed).every((arr) => arr.every((e) => e.scores));

  // Clear history button per entry. For zoned profiles, clears the composite
  // trend (every row for this profile/geo/label combo). For Default, clears
  // by filename, matching the old behaviour.
  const handleClearHistory = useCallback(async (entry) => {
    if (cloudStatus !== "online") {
      setToast({ kind: "error", text: "Connect to the cloud database to clear history." });
      return;
    }
    if (entry.geo_type && entry.geo_type !== "N/A") {
      await clearCompositeHistory(entry.profile.name, entry.geo_type, entry.zone_label);
      savedKeysRef.current.delete(historyKey(entry));
      setToast({ kind: "success", text: `Trend history cleared for ${entry.geo_type}: ${entry.zone_label}.` });
    } else {
      await clearFileHistory(entry.filename);
      savedKeysRef.current.delete(historyKey(entry));
      setToast({ kind: "success", text: `History cleared for ${entry.filename}.` });
    }
    setHistoryVersion((v) => v + 1);
  }, [cloudStatus]);

  const handleClearAll = useCallback(async () => {
    if (cloudStatus !== "online") {
      setToast({ kind: "error", text: "Connect to the cloud database to clear history." });
      return;
    }
    if (!confirm("Clear ALL upload history? This cannot be undone.")) return;
    await clearAllHistory();
    savedKeysRef.current = new Set();
    setHistoryVersion((v) => v + 1);
    setToast({ kind: "success", text: "All history cleared." });
  }, [cloudStatus]);

  // --- Report export (HTML report — printable to PDF / saved as DOCX-ish) ---
  const handleExportReport = useCallback(() => {
    const win = window.open("", "_blank");
    if (!win) { setToast({ kind: "error", text: "Pop-up blocked — allow pop-ups to export." }); return; }
    const html = buildReportHtml(parsed, profileGroups, weights);
    win.document.open(); win.document.write(html); win.document.close();
  }, [parsed, profileGroups, weights]);

  // --- Render ---------------------------------------------------------------
  const fileEntries = Object.values(files);
  const pendingFiles = fileEntries.filter((f) => !f.confirmed);
  const hasPending = pendingFiles.length > 0;

  // Build summary cards
  const profileCards = useMemo(() => {
    const out = [];
    for (const [pname, entries] of Object.entries(parsed)) {
      const profileObj = entries[0].profile;
      const byGeo = {};
      for (const e of entries) (byGeo[e.geo_type] ||= []).push(e.zone_label);
      out.push({ pname, profileObj, byGeo, entries });
    }
    return out;
  }, [parsed]);

  const zoneProfiles = profileCards.filter((c) => c.profileObj.filename !== "__default__");
  const defaultProfileCards = profileCards.filter((c) => c.profileObj.filename === "__default__");

  const allGeoTypes = zoneProfiles.flatMap((c) => c.entries.map((e) => e.geo_type));
  const geoWordHeader = geoLabel(allGeoTypes);

  // Headline summary
  const headline = useMemo(() => {
    const all = Object.values(parsed).flat().filter((e) => e.scores);
    if (!all.length) return null;
    const meanDqi = all.reduce((a, e) => a + e.dqi, 0) / all.length;
    return { n: all.length, meanDqi };
  }, [parsed]);

  // Pre-compute composite history per entry so ScoreCard renders synchronously.
  const historyByFilename = useMemo(() => {
    const m = {};
    for (const entries of Object.values(parsed)) {
      for (const e of entries) {
        m[e.filename] = getCompositeHistory(e.profile.name, e.geo_type, e.zone_label, e.filename);
      }
    }
    return m;
  }, [parsed, historyVersion]);

  return (
    <div className="app">
      {/* Portal top bar */}
      <div className="portal-bar">
        <div className="portal-bar-inner">
          <div className="portal-brand">
            <div className="portal-logo">
              <img src={(typeof window !== "undefined" && window.__resources && window.__resources.logo) || "logo.png"} alt="" />
            </div>
            <div className="portal-divider"></div>
            <div className="portal-wordmark">
              <span className="portal-wordmark-title">Internal DQI Evaluation Tool</span>
              <span className="portal-wordmark-draft">Draft</span>
            </div>
          </div>
          <div className="portal-actions">
            <span className="portal-env">v0.4 · UAT</span>
            <button className="small" onClick={() => setShowSettings(true)}>
              <span style={{ display: "inline-block", width: 8, height: 8, borderRadius: "50%", marginRight: 6, background: cloudStatus === "online" ? "var(--good)" : cloudStatus === "connecting" ? "var(--warn)" : cloudStatus === "offline" ? "var(--bad)" : "var(--ink-3)" }}></span>
              {cloudStatus === "online" ? "Cloud connected" : cloudStatus === "connecting" ? "Connecting…" : cloudStatus === "offline" ? "Cloud offline" : "Cloud disconnected"}
            </button>
          </div>
        </div>
      </div>

      <div className="app-main">
        {/* Page header */}
        <header className="page-header">
          <div>
            <p className="page-eyebrow">Data Quality Index · ACTC Framework</p>
            <h1 className="app-title">DQI Scoring</h1>
            <p className="app-subtitle">Upload data files, score them across the four ACTC dimensions, and compare across zones.</p>
          </div>
          <div className="header-actions">
            {Object.keys(parsed).length > 0 && allScored && (
              <button className="primary" onClick={handleExportReport}>Export report</button>
            )}
            <button
              className="ghost danger small"
              onClick={handleClearAll}
              disabled={cloudStatus !== "online"}
              title={cloudStatus !== "online" ? "Connect to the cloud database to enable" : ""}
            >Clear all history</button>
          </div>
        </header>

        {toast && <Banner kind={toast.kind}>{toast.text}</Banner>}

        {showSettings && <SettingsModal status={cloudStatus} onClose={() => setShowSettings(false)} onChange={(status) => { setCloudStatus(status); setHistoryVersion((v) => v + 1); }} />}

        {/* Step 1: Upload */}
        <div className="card">
          <h3 className="section-title">1. Upload files</h3>
          <p className="section-caption">Drop in Excel or CSV files. We'll auto-detect the profile and geography from each filename — review and confirm below.</p>
          <UploadZone onFiles={handleFiles} onDemo={handleDemo} />
        </div>

        {/* Step 2: Methodology / Weights (collapsed by default) */}
        <WeightSliders weights={weights} setWeights={setWeights} />

        {/* Step 3: Detected files + single Confirm-all button */}
        {fileEntries.length > 0 && (
          <div className="card">
            <h3 className="section-title">2. Detected files</h3>
            <p className="section-caption">Review the auto-detected profile and geography for each file. Adjust if needed, then confirm all to score.</p>
            {fileEntries.map((f) => (
              <DetectionRow key={f.name} entry={f} onChange={updateDetection} onRemove={removeFile} />
            ))}
            <div className="footer-actions" style={{ marginTop: 12, alignItems: "center" }}>
              <button
                className="primary"
                onClick={confirmAll}
                disabled={!weightsOk || !hasPending}
                title={!weightsOk ? "Fix the weights — must sum to 100%" : (!hasPending ? "All files are already scored" : "")}
              >
                {hasPending ? `Confirm all & score (${pendingFiles.length})` : "All files scored"}
              </button>
              {!weightsOk && (
                <span style={{ fontSize: 12, color: "var(--bad)", alignSelf: "center" }}>Weights must sum to 100% — open the methodology expander to fix.</span>
              )}
            </div>
          </div>
        )}

        {/* Headline DQI summary */}
        {headline && allScored && (
          <div className="card" style={{ display: "flex", gap: 32, alignItems: "center" }}>
            <div>
              <div style={{ fontSize: 11, color: "var(--ink-3)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}>Files scored</div>
              <div style={{ fontSize: 32, fontWeight: 600, letterSpacing: "-0.01em" }}>{headline.n}</div>
            </div>
            <div>
              <div style={{ fontSize: 11, color: "var(--ink-3)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}>Mean DQI</div>
              <div style={{ fontSize: 32, fontWeight: 600, letterSpacing: "-0.01em" }}>{headline.meanDqi.toFixed(1)}<span style={{ fontSize: 16, color: "var(--ink-3)", marginLeft: 2 }}>%</span></div>
            </div>
            <div style={{ flex: 1, fontSize: 12, color: "var(--ink-3)" }}>
              Detail per profile below. Comparison appears when 2+ files share a profile.
            </div>
          </div>
        )}

        {/* Profile summary cards */}
        {profileCards.length > 0 && (
          <>
            <hr className="divider" />
            {profileCards.map((c) => (
              <div className="profile-section" key={c.pname}>
                <div className="profile-header">
                  <h2 className="profile-name">{c.pname}</h2>
                  <p className="profile-desc">{c.profileObj.description}</p>
                  {c.profileObj.filename !== "__default__" && (
                    <div className="profile-tag-line">
                      {Object.entries(c.byGeo).map(([gt, labels], i) => (
                        <span key={gt}>
                          {i > 0 && " | "}
                          <strong>{gt === "N/A" ? "Files" : `${gt}s`}:</strong> {labels.join(", ")}
                        </span>
                      ))}
                    </div>
                  )}
                </div>
              </div>
            ))}
          </>
        )}

        {/* Comparison */}
        {Object.entries(profileGroups).filter(([, e]) => e.length >= 2).length > 0 && (
          <>
            <h2 className="section-title" style={{ fontSize: 18, marginTop: 8, marginBottom: 12 }}>Comparison</h2>
            {Object.entries(profileGroups).filter(([, e]) => e.length >= 2).map(([pname, entries]) => (
              <ComparisonBlock key={pname} profileName={pname} entries={entries} />
            ))}
          </>
        )}

        {/* Zone-wise details */}
        {zoneProfiles.length > 0 && (
          <>
            <h2 className="section-title" style={{ fontSize: 18, marginTop: 24, marginBottom: 12 }}>{geoWordHeader}-wise Details</h2>
            {zoneProfiles.map((c) => (
              <div className="card" key={c.pname}>
                <h3 className="section-title" style={{ fontSize: 15 }}>{c.pname}</h3>
                {c.entries.map((e) => (
                  <ScoreCard key={e.filename} entry={e} weights={weights} totalOk={weightsOk}
                    onClearHistory={handleClearHistory}
                    cloudOnline={cloudStatus === "online"}
                    history={historyByFilename[e.filename] || []} />
                ))}
              </div>
            ))}
          </>
        )}

        {/* Default-profile file details */}
        {defaultProfileCards.length > 0 && (
          <>
            <h2 className="section-title" style={{ fontSize: 18, marginTop: 24, marginBottom: 12 }}>File Details</h2>
            {defaultProfileCards.map((c) => (
              <div className="card" key={c.pname}>
                {c.entries.map((e) => (
                  <ScoreCard key={e.filename} entry={e} weights={weights} totalOk={weightsOk}
                    onClearHistory={handleClearHistory}
                    cloudOnline={cloudStatus === "online"}
                    history={historyByFilename[e.filename] || []} />
                ))}
              </div>
            ))}
          </>
        )}

        {/* Report generation */}
        {Object.keys(parsed).length > 0 && (
          <div className="card">
            <h3 className="section-title">Generate Report</h3>
            <p className="section-caption">Exports a printable summary of all uploaded files with ACTC dimension scores, DQI totals, and the comparison section.</p>
            {allScored ? (
              <div className="footer-actions">
                <button className="primary" onClick={handleExportReport}>Open printable report</button>
                <span style={{ fontSize: 12, color: "var(--ink-3)", alignSelf: "center" }}>Tip: use your browser's "Save as PDF" in the print dialog.</span>
              </div>
            ) : (
              <Banner kind="info">Fix the weights above (must sum to 100%) to enable report generation.</Banner>
            )}
          </div>
        )}

        {Object.keys(parsed).length === 0 && fileEntries.length === 0 && (
          <div className="empty">No files uploaded yet. Drop a file above or click "Load demo data" to explore.</div>
        )}
      </div>
    </div>
  );
}

// --- Settings modal (Supabase config) ---------------------------------------
function SettingsModal({ onClose, onChange, status }) {
  // Three modes:
  //   "connected"    — we have working credentials. Show source, hide values, offer Disconnect + Re-test.
  //   "configured"   — credentials present but last call failed. Same UI as connected, status banner explains.
  //   "disconnected" — no usable credentials. Offer paste form (+ "Use env.js" if env has values).
  const cur = loadSupabaseConfig();
  const envAvailable = hasEnvCredentials();
  const disconnected = isDisconnected();
  const mode = cur ? (status === "online" ? "connected" : "configured") : "disconnected";
  const source = cur ? cur.source : null;

  const [url, setUrl] = useState("");
  const [anonKey, setAnonKey] = useState("");
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState(null);

  // Connect from pasted credentials
  const connectPasted = async () => {
    setBusy(true); setMsg(null);
    saveSupabaseConfig({ url, anonKey });
    const r = await testSupabaseConnection();
    setBusy(false);
    if (r.ok) {
      setMsg({ kind: "success", text: "Connected ✓ — pulling history…" });
      await syncFromCloud();
      onChange("online");
      setTimeout(onClose, 700);
    } else {
      setMsg({ kind: "error", text: r.error });
      onChange("offline");
    }
  };

  // Re-enable env.js (clear the "disconnected" override)
  const useEnv = async () => {
    setBusy(true); setMsg(null);
    setDisconnected(false);
    const r = await testSupabaseConnection();
    setBusy(false);
    if (r.ok) {
      setMsg({ kind: "success", text: "Connected via env.js ✓ — pulling history…" });
      await syncFromCloud();
      onChange("online");
      setTimeout(onClose, 700);
    } else {
      setMsg({ kind: "error", text: r.error });
      onChange("offline");
    }
  };

  // Re-test currently-loaded credentials
  const retest = async () => {
    setBusy(true); setMsg(null);
    const r = await testSupabaseConnection();
    setBusy(false);
    if (r.ok) {
      setMsg({ kind: "success", text: "Connected ✓" });
      await syncFromCloud();
      onChange("online");
      setTimeout(onClose, 700);
    } else {
      setMsg({ kind: "error", text: r.error });
      onChange("offline");
    }
  };

  // Disconnect: clear localStorage config AND set the override so env.js
  // doesn't immediately reconnect us on the next render.
  const disconnect = () => {
    saveSupabaseConfig(null);
    setDisconnected(true);
    onChange("off");
    onClose();
  };

  const wrapStyle = { position: "fixed", inset: 0, background: "rgba(15,20,30,0.45)", display: "grid", placeItems: "center", zIndex: 100 };
  const cardStyle = { width: 520, maxWidth: "90vw", margin: 0 };

  // ----- CONNECTED / CONFIGURED VIEW -----
  if (mode !== "disconnected") {
    const statusDot = status === "online" ? "var(--good)" : status === "connecting" ? "var(--warn)" : "var(--bad)";
    const statusLabel = status === "online" ? "Connected" : status === "connecting" ? "Connecting…" : "Last call failed";
    return (
      <div style={wrapStyle} onClick={onClose}>
        <div className="card" style={cardStyle} onClick={(e) => e.stopPropagation()}>
          <h3 className="section-title">Cloud database</h3>
          <p className="section-caption">Supabase history sync for this workspace.</p>

          <div style={{ border: "1px solid var(--border)", borderRadius: 8, padding: 14, background: "var(--surface-2)", marginBottom: 14 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }}>
              <span style={{ width: 8, height: 8, borderRadius: "50%", background: statusDot }}></span>
              <span style={{ fontSize: 13, fontWeight: 600 }}>{statusLabel}</span>
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "100px 1fr", rowGap: 6, fontSize: 12 }}>
              <span style={{ color: "var(--ink-3)" }}>Source</span>
              <span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace" }}>
                {source === "env" ? "env.js (local file)" : "Browser settings (localStorage)"}
              </span>
              <span style={{ color: "var(--ink-3)" }}>Project</span>
              <span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", color: "var(--ink-2)" }}>
                {cur.url.replace(/^https?:\/\//, "").split(".")[0]}
                <span style={{ color: "var(--ink-3)" }}>.supabase.co</span>
              </span>
              <span style={{ color: "var(--ink-3)" }}>Anon key</span>
              <span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", color: "var(--ink-2)" }}>
                •••••••• <span style={{ color: "var(--ink-3)" }}>(hidden)</span>
              </span>
            </div>
          </div>

          {source === "env" && (
            <p style={{ fontSize: 12, color: "var(--ink-3)", margin: "0 0 12px" }}>
              To change the URL or key, edit <code className="fname">env.js</code> and reload. Disconnecting here stops the app from using those values until you reconnect.
            </p>
          )}

          {msg && <Banner kind={msg.kind}>{msg.text}</Banner>}

          <div style={{ display: "flex", gap: 8, marginTop: 12, justifyContent: "flex-end", alignItems: "center" }}>
            <button onClick={disconnect} className="danger small">Disconnect</button>
            <div style={{ flex: 1 }}></div>
            <button onClick={onClose}>Close</button>
            <button className="primary" disabled={busy} onClick={retest}>{busy ? "Testing…" : "Re-test connection"}</button>
          </div>
        </div>
      </div>
    );
  }

  // ----- DISCONNECTED VIEW -----
  return (
    <div style={wrapStyle} onClick={onClose}>
      <div className="card" style={cardStyle} onClick={(e) => e.stopPropagation()}>
        <h3 className="section-title">Connect cloud database</h3>
        <p className="section-caption">
          {envAvailable && disconnected
            ? <>Reconnect using <code className="fname">env.js</code>, or paste different credentials below. See <code className="fname">SETUP.md</code>.</>
            : <>Paste your Supabase project URL and anon key, or put them in <code className="fname">env.js</code> to skip this dialog next time. See <code className="fname">SETUP.md</code>.</>}
        </p>

        {envAvailable && disconnected && (
          <div style={{ border: "1px solid var(--border)", borderRadius: 8, padding: 12, background: "var(--accent-soft)", marginBottom: 14, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
            <div style={{ fontSize: 12 }}>
              <div style={{ fontWeight: 600, color: "var(--accent-ink)" }}>env.js credentials available</div>
              <div style={{ color: "var(--ink-3)", marginTop: 2 }}>Reconnect to use them — no need to paste.</div>
            </div>
            <button className="primary small" disabled={busy} onClick={useEnv}>{busy ? "…" : "Use env.js"}</button>
          </div>
        )}

        <div className="field" style={{ marginBottom: 10 }}>
          <label>Project URL</label>
          <input type="text" value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://xxxxx.supabase.co" />
        </div>
        <div className="field" style={{ marginBottom: 10 }}>
          <label>Anon (public) key</label>
          <input type="text" value={anonKey} onChange={(e) => setAnonKey(e.target.value)} placeholder="eyJhbGciOi..." />
        </div>
        {msg && <Banner kind={msg.kind}>{msg.text}</Banner>}
        <div style={{ display: "flex", gap: 8, marginTop: 12, justifyContent: "flex-end" }}>
          <button onClick={onClose}>Cancel</button>
          <button className="primary" disabled={busy || !url || !anonKey} onClick={connectPasted}>{busy ? "Testing…" : "Connect & test"}</button>
        </div>
      </div>
    </div>
  );
}

// --- Printable HTML report --------------------------------------------------
// SVG chart helpers (inline strings — no external libs needed in the report)
function svgBar(data, opts = {}) {
  const w = opts.width || 640, h = opts.height || 240;
  const padL = 44, padR = 14, padT = 14, padB = 46;
  const innerW = w - padL - padR, innerH = h - padT - padB;
  const yMax = 105;
  const n = Math.max(data.length, 1);
  const slot = innerW / n;
  const barW = Math.min(80, slot * 0.65);
  const color = opts.color || "#4C72B0";
  let s = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="100%" style="max-width:${w}px;">`;
  for (const g of [0, 25, 50, 75, 100]) {
    const y = padT + innerH - (g / yMax) * innerH;
    s += `<line x1="${padL}" x2="${w - padR}" y1="${y}" y2="${y}" stroke="#e4e6eb" stroke-dasharray="${g === 0 ? "" : "3 3"}"/>`;
    s += `<text x="${padL - 8}" y="${y + 3}" text-anchor="end" font-size="10" fill="#767c85" font-family="Inter,sans-serif">${g}</text>`;
  }
  data.forEach((d, i) => {
    const cx = padL + slot * i + slot / 2;
    const bh = (Math.max(0, d.value) / yMax) * innerH;
    const x = cx - barW / 2, y = padT + innerH - bh;
    s += `<rect x="${x}" y="${y}" width="${barW}" height="${bh}" rx="3" fill="${color}"/>`;
    s += `<text x="${cx}" y="${padT + innerH + 16}" text-anchor="middle" font-size="11" fill="#4a4f57" font-family="Inter,sans-serif">${escapeHtml(d.label)}</text>`;
    s += `<text x="${cx}" y="${y - 4}" text-anchor="middle" font-size="10" fill="#1a1d23" font-family="Inter,sans-serif" font-weight="600">${d.value.toFixed(1)}</text>`;
  });
  s += `</svg>`;
  return s;
}
function svgGrouped(groups, series, opts = {}) {
  const w = opts.width || 640, h = opts.height || 280;
  const padL = 44, padR = 14, padT = 14, padB = 64;
  const innerW = w - padL - padR, innerH = h - padT - padB;
  const yMax = 105;
  const colors = ["#4C72B0", "#DD8452", "#55A868", "#C44E52", "#8172B3"];
  const nG = Math.max(groups.length, 1);
  const nS = Math.max(series.length, 1);
  const slot = innerW / nG;
  const groupW = slot * 0.78, barW = groupW / nS;
  let s = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="100%" style="max-width:${w}px;">`;
  for (const g of [0, 25, 50, 75, 100]) {
    const y = padT + innerH - (g / yMax) * innerH;
    s += `<line x1="${padL}" x2="${w - padR}" y1="${y}" y2="${y}" stroke="#e4e6eb" stroke-dasharray="${g === 0 ? "" : "3 3"}"/>`;
    s += `<text x="${padL - 8}" y="${y + 3}" text-anchor="end" font-size="10" fill="#767c85" font-family="Inter,sans-serif">${g}</text>`;
  }
  groups.forEach((label, gi) => {
    const cx = padL + slot * gi + slot / 2;
    series.forEach((sr, si) => {
      const v = sr.values[gi] ?? 0;
      const bh = (Math.max(0, v) / yMax) * innerH;
      const x = cx - groupW / 2 + si * barW;
      const y = padT + innerH - bh;
      s += `<rect x="${x}" y="${y}" width="${barW - 2}" height="${bh}" rx="2" fill="${colors[si % colors.length]}"/>`;
    });
    s += `<text x="${cx}" y="${padT + innerH + 16}" text-anchor="middle" font-size="11" fill="#4a4f57" font-family="Inter,sans-serif">${escapeHtml(label)}</text>`;
  });
  series.forEach((sr, si) => {
    const x = padL + si * 110, y = h - 22;
    s += `<rect x="${x}" y="${y}" width="10" height="10" rx="2" fill="${colors[si % colors.length]}"/>`;
    s += `<text x="${x + 14}" y="${y + 9}" font-size="11" fill="#4a4f57" font-family="Inter,sans-serif">${escapeHtml(sr.label)}</text>`;
  });
  s += `</svg>`;
  return s;
}
function svgLine(xLabels, series, opts = {}) {
  const w = opts.width || 640, h = opts.height || 240;
  const padL = 44, padR = 14, padT = 14, padB = 56;
  const innerW = w - padL - padR, innerH = h - padT - padB;
  const yMax = 105;
  const colors = ["#4C72B0", "#DD8452", "#55A868", "#C44E52"];
  const n = Math.max(xLabels.length, 1);
  const xFor = (i) => padL + (n === 1 ? innerW / 2 : (i / (n - 1)) * innerW);
  const yFor = (v) => padT + innerH - (Math.max(0, v) / yMax) * innerH;
  let s = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="100%" style="max-width:${w}px;">`;
  for (const g of [0, 25, 50, 75, 100]) {
    const y = padT + innerH - (g / yMax) * innerH;
    s += `<line x1="${padL}" x2="${w - padR}" y1="${y}" y2="${y}" stroke="#e4e6eb" stroke-dasharray="${g === 0 ? "" : "3 3"}"/>`;
    s += `<text x="${padL - 8}" y="${y + 3}" text-anchor="end" font-size="10" fill="#767c85" font-family="Inter,sans-serif">${g}</text>`;
  }
  series.forEach((sr, si) => {
    const color = colors[si % colors.length];
    const pts = sr.values.map((v, i) => `${xFor(i)},${yFor(v)}`).join(" ");
    s += `<polyline points="${pts}" fill="none" stroke="${color}" stroke-width="2"/>`;
    sr.values.forEach((v, i) => {
      s += `<circle cx="${xFor(i)}" cy="${yFor(v)}" r="3" fill="${color}"/>`;
    });
  });
  xLabels.forEach((lbl, i) => {
    const x = xFor(i), y = padT + innerH + 16;
    s += `<text x="${x}" y="${y}" text-anchor="middle" font-size="10" fill="#4a4f57" font-family="Inter,sans-serif" transform="rotate(-25 ${x} ${y})">${escapeHtml(lbl)}</text>`;
  });
  if (series.length > 1) {
    series.forEach((sr, si) => {
      const x = padL + si * 110, y = h - 18;
      s += `<rect x="${x}" y="${y}" width="10" height="10" rx="2" fill="${colors[si % colors.length]}"/>`;
      s += `<text x="${x + 14}" y="${y + 9}" font-size="11" fill="#4a4f57" font-family="Inter,sans-serif">${escapeHtml(sr.label)}</text>`;
    });
  }
  s += `</svg>`;
  return s;
}
function escapeHtml(s) {
  return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}

function buildReportHtml(parsed, profileGroups, weights) {
  const wa = weights.a, wc = weights.c, wco = weights.co, wt = weights.t;
  const css = `
    body{font-family:'Inter',-apple-system,sans-serif;color:#1a1d23;padding:32px;line-height:1.5;}
    h1,h2,h3{margin:0 0 8px;}
    h1{font-size:24px;}
    h2{font-size:18px;margin-top:24px;border-bottom:2px solid #1a1d23;padding-bottom:6px;}
    h3{font-size:14px;color:#4a4f57;margin-top:18px;}
    table{border-collapse:collapse;width:100%;margin:8px 0;font-size:12px;}
    th,td{border:1px solid #d0d4da;padding:6px 10px;text-align:left;}
    th{background:#f6f7f9;}
    td.num{text-align:right;font-variant-numeric:tabular-nums;}
    .meta{color:#767c85;font-size:12px;}
    .pagebreak{page-break-after:always;}
    .chart-block{margin:10px 0 18px;}
    .chart-title{font-size:12px;color:#4a4f57;margin:6px 0;font-weight:600;text-transform:uppercase;letter-spacing:0.04em;}
    .chart-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px;}
    @media print { .no-print { display: none !important; } .pagebreak { page-break-after: always; } }
  `;
  const fmt = (n) => Number(n).toFixed(1);
  let body = "";
  body += `<h1>DQI Report</h1>`;
  body += `<div class="meta">Generated: ${new Date().toLocaleString()} · Weights — Accuracy: ${wa}% · Completeness: ${wc}% · Consistency: ${wco}% · Timeliness: ${wt}%</div>`;

  for (const [pname, entries] of Object.entries(parsed)) {
    body += `<h2>${pname}</h2>`;
    body += `<table><thead><tr><th>File / Label</th><th>Accuracy %</th><th>Completeness %</th><th>Consistency %</th><th>Timeliness %</th><th>DQI %</th></tr></thead><tbody>`;
    for (const e of entries) {
      if (!e.scores) continue;
      body += `<tr><td>${e.zone_label}</td><td class="num">${fmt(e.scores.accuracy)}</td><td class="num">${fmt(e.scores.completeness)}</td><td class="num">${fmt(e.scores.consistency)}</td><td class="num">${fmt(e.scores.timeliness)}</td><td class="num">${fmt(e.dqi)}</td></tr>`;
    }
    body += `</tbody></table>`;
  }

  const multi = Object.entries(profileGroups).filter(([, e]) => e.length >= 2);
  if (multi.length) {
    body += `<h2>Comparison</h2>`;
    for (const [pname, entries] of multi) {
      const geoWord = geoLabel(entries.map((e) => e.geo_type));
      body += `<h3>${pname} — ${geoWord}s</h3>`;
      body += `<table><thead><tr><th>${geoWord}</th><th>Accuracy %</th><th>Completeness %</th><th>Consistency %</th><th>Timeliness %</th><th>DQI %</th></tr></thead><tbody>`;
      for (const e of entries) {
        body += `<tr><td>${e.zone_label}</td><td class="num">${fmt(e.scores.accuracy)}</td><td class="num">${fmt(e.scores.completeness)}</td><td class="num">${fmt(e.scores.consistency)}</td><td class="num">${fmt(e.scores.timeliness)}</td><td class="num">${fmt(e.dqi)}</td></tr>`;
      }
      body += `</tbody></table>`;

      const zones = entries.map((e) => e.zone_label);
      const dqiData = entries.map((e) => ({ label: e.zone_label, value: Number(e.dqi.toFixed(2)) }));
      body += `<div class="chart-grid">`;
      body += `<div class="chart-block"><div class="chart-title">Overall DQI Score by ${geoWord}</div>${svgBar(dqiData, { width: 520, height: 240 })}</div>`;
      const series = zones.map((z, i) => ({
        label: z,
        values: DIMENSIONS.map((d) => Number(entries[i].scores[d].toFixed(2))),
      }));
      body += `<div class="chart-block"><div class="chart-title">ACTC Dimension Scores by ${geoWord}</div>${svgGrouped(DIMENSIONS.map((d) => DIM_LABELS[d]), series, { width: 520, height: 280 })}</div>`;
      body += `</div>`;
    }
  }

  // File-wise breakdown
  for (const entries of Object.values(parsed)) {
    for (const e of entries) {
      if (!e.scores) continue;
      const geoPrefix = e.geo_type !== "N/A" ? `[${e.geo_type}] ` : "";
      body += `<div class="pagebreak"></div>`;
      body += `<h2>${geoPrefix}${e.zone_label} — DQI: ${fmt(e.dqi)}%</h2>`;
      body += `<div class="meta">File: ${e.filename} · Geography Type: ${e.geo_type}</div>`;
      body += `<table><thead><tr><th>Dimension</th><th>Score (%)</th><th>Weight (%)</th><th>Check</th></tr></thead><tbody>`;
      const wList = [wa, wc, wco, wt];
      DIMENSIONS.forEach((dim, i) => {
        const dimChecks = e.profile.checks[dim] || [];
        const ck = dimChecks.length > 1 ? `${dimChecks.length} checks — avg` : (dimChecks[0]?.description || "");
        body += `<tr><td>${DIM_LABELS[dim]}</td><td class="num">${e.scores[dim].toFixed(2)}</td><td class="num">${wList[i]}</td><td>${ck}</td></tr>`;
      });
      body += `</tbody></table>`;

      const nullCounts = nullCountsByColumn(e.df);
      const nulls = Object.entries(nullCounts).filter(([, v]) => v > 0);
      if (nulls.length) {
        body += `<h3>Null Values per Column</h3><table><thead><tr><th>Column</th><th>Null Count</th></tr></thead><tbody>`;
        for (const [col, cnt] of nulls) body += `<tr><td>${col.replace(/_/g, " ")}</td><td class="num">${cnt}</td></tr>`;
        body += `</tbody></table>`;
      }

      const history = getCompositeHistory(e.profile.name, e.geo_type, e.zone_label, e.filename);
      if (history.length > 1) {
        body += `<h3>Historical Data — ${e.zone_label}</h3>`;
        body += `<table><thead><tr><th>Uploaded At</th><th>Accuracy %</th><th>Completeness %</th><th>Consistency %</th><th>Timeliness %</th><th>DQI %</th><th>Geography</th></tr></thead><tbody>`;
        for (const h of history) {
          body += `<tr><td>${h.uploaded_at}</td><td class="num">${fmt(h.accuracy_score)}</td><td class="num">${fmt(h.completeness_score)}</td><td class="num">${fmt(h.consistency_score)}</td><td class="num">${fmt(h.timeliness_score)}</td><td class="num">${fmt(h.dqi_score)}</td><td>${h.geography || ""}</td></tr>`;
        }
        body += `</tbody></table>`;

        const xLabels = history.map((h) => h.uploaded_at.slice(5, 16));
        body += `<div class="chart-block"><div class="chart-title">DQI Score Trend — ${escapeHtml(e.zone_label)}</div>${svgLine(xLabels, [{ label: "DQI", values: history.map((h) => h.dqi_score) }], { width: 640, height: 240 })}</div>`;
        body += `<div class="chart-block"><div class="chart-title">ACTC Dimensions Trend — ${escapeHtml(e.zone_label)}</div>${svgLine(xLabels, DIMENSIONS.map((d) => ({ label: DIM_LABELS[d], values: history.map((h) => h[`${d}_score`]) })), { width: 640, height: 260 })}</div>`;
      }
    }
  }

  return `<!doctype html><html><head><meta charset="utf-8"><title>DQI Report</title><style>${css}</style></head><body>
    <div class="no-print" style="margin-bottom:16px"><button onclick="window.print()" style="padding:6px 12px;cursor:pointer;">Print / Save as PDF</button></div>
    ${body}
  </body></html>`;
}

// --- Error boundary -------------------------------------------------------
class ErrorBoundary extends React.Component {
  constructor(p) { super(p); this.state = { err: null, info: null }; }
  static getDerivedStateFromError(err) { return { err }; }
  componentDidCatch(err, info) {
    this.setState({ info });
    console.error("App crashed:", err, info);
  }
  render() {
    if (!this.state.err) return this.props.children;
    return (
      <div style={{ padding: 24, fontFamily: "Inter, sans-serif" }}>
        <h2 style={{ color: "#C0392B", marginTop: 0 }}>Something went wrong.</h2>
        <p style={{ color: "#4a4f57" }}>The DQI app hit an unrecoverable render error. Details below — please send this to the maintainer.</p>
        <pre style={{ background: "#fff5f5", border: "1px solid #f0c0c0", padding: 12, borderRadius: 6, fontSize: 12, whiteSpace: "pre-wrap", overflowX: "auto" }}>{String(this.state.err && (this.state.err.stack || this.state.err.message || this.state.err))}
          {this.state.info && this.state.info.componentStack}</pre>
        <button onClick={() => location.reload()} style={{ padding: "6px 12px", cursor: "pointer", marginTop: 12 }}>Reload</button>
      </div>
    );
  }
}

ReactDOM.createRoot(document.getElementById("root")).render(<ErrorBoundary><App /></ErrorBoundary>);
