/* Settings — providers, LLM, privacidad, sistema. Cableado a la API real
 * con fallback a MOCK cuando el backend no responde. */

// Format an ISO timestamp as a short, relative human string in es-ES.
// "hace 12 min" / "hace 3 h" / "ayer 21:14" / "21 abr 09:30".
const formatRelative = (iso) => {
  if (!iso) return "—";
  let raw = iso;
  if (typeof raw === "string" && /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test(raw) && !/[zZ]|[+-]\d{2}:?\d{2}$/.test(raw)) {
    raw = raw.replace(" ", "T") + "Z";
  }
  const d = typeof raw === "string" ? new Date(raw) : raw;
  if (!(d instanceof Date) || isNaN(d.getTime())) return "—";
  const now = new Date();
  const diffMs = now.getTime() - d.getTime();
  const diffMin = Math.round(diffMs / 60000);
  if (diffMin < 1) return "ahora";
  if (diffMin < 60) return `hace ${diffMin} min`;
  const diffH = Math.round(diffMin / 60);
  if (diffH < 24) return `hace ${diffH} h`;
  const sameYear = d.getFullYear() === now.getFullYear();
  const time = d.toLocaleTimeString("es-ES", { hour: "2-digit", minute: "2-digit" });
  if (diffH < 48) return `ayer ${time}`;
  const months = ["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"];
  const m = months[d.getMonth()];
  const dd = String(d.getDate()).padStart(2, "0");
  return sameYear ? `${dd} ${m} ${time}` : `${dd} ${m} ${d.getFullYear()}`;
};

const SETTINGS_AGENTS = {
  coach:   { label: "Coach",         icon: "coach",   color: "var(--accent)" },
  nutri:   { label: "Nutricionista", icon: "apple",   color: "var(--good)" },
  medical: { label: "Médico",        icon: "medical", color: "var(--info)" },
};

const Settings = ({ density, inDemo, onLogout, setRoute }) => {
  const [tab, setTab] = React.useState("providers");
  return (
    <div className="af-padded af-stack-mobile" style={{ padding: "var(--sp-6) var(--sp-8) 80px", display: "grid", gridTemplateColumns: "200px 1fr", gap: 24 }}>
      <nav style={{ display: "flex", flexDirection: "column", gap: 2 }}>
        {[
          { v: "providers", l: "Proveedores" },
          { v: "llm", l: "LLM" },
          { v: "usage", l: "Uso y costes" },
          { v: "privacy", l: "Privacidad" },
          { v: "system", l: "Sistema" },
        ].map(t => (
          <button key={t.v} onClick={() => setTab(t.v)} style={{
            padding: "8px 12px", textAlign: "left",
            background: tab === t.v ? "var(--bg-2)" : "transparent",
            border: "none", borderRadius: "var(--r-2)",
            color: tab === t.v ? "var(--text)" : "var(--text-2)",
            fontSize: 13, fontWeight: tab === t.v ? 500 : 400,
          }}>{t.l}</button>
        ))}
      </nav>

      <div style={{ display: "flex", flexDirection: "column", gap: 14, minWidth: 0 }}>
        {tab === "providers" && <SettingsProviders setRoute={setRoute} />}
        {tab === "llm" && <SettingsLLM />}
        {tab === "usage" && <SettingsUsage />}
        {tab === "privacy" && <SettingsPrivacy />}
        {tab === "system" && <SettingsSystem onLogout={onLogout} setRoute={setRoute} />}
      </div>
    </div>
  );
};

const SettingsProviders = ({ setRoute }) => {
  const [providers, setProviders] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [syncing, setSyncing] = React.useState(null);
  const [connectFor, setConnectFor] = React.useState(null);  // provider name for inline modal

  const load = React.useCallback(async () => {
    if (API.demoMode()) {
      setProviders(MOCK.providers);
      return;
    }
    try {
      const data = await API.listProviders();
      // /providers/ trae sólo el registry (nombre/modos/caps); el estado
      // de conexión vive en /providers/{name}/status. Merge en paralelo.
      const list = Array.isArray(data) ? data : (data?.providers || []);
      const statuses = await Promise.all(
        list.map(p => {
          const name = typeof p === "string" ? p : (p.name || p.id);
          return name
            ? API.providerStatus(name).catch(() => null)
            : Promise.resolve(null);
        })
      );
      const normalized = list.map((p, i) => {
        const base = typeof p === "string"
          ? { name: p, mode: "POLL", caps: [] }
          : {
              name: p.name || p.id || "?",
              mode: p.mode || (p.modes?.[0]) || "POLL",
              caps: p.capabilities || p.caps || [],
            };
        const st = statuses[i] || {};
        const auth = st.auth || st.status;  // /status usa {auth}, /auth/status usa {status}
        const lastSyncIso = st.last_sync || st.lastSync || null;
        return {
          ...base,
          logo: (base.name || "?")[0]?.toUpperCase(),
          state: auth === "logged_in" ? "ok" : "off",
          lastSync: lastSyncIso ? formatRelative(lastSyncIso) : (auth === "logged_in" ? "—" : "—"),
          syncing: !!st.syncing,
        };
      });
      setProviders(normalized);
      setError(null);
    } catch (e) {
      setError(e.message);
      // Si la lista de providers ya estaba cargada antes (re-fetch tras
      // un sync), la dejamos como estaba en lugar de pisarla con MOCK.
      // Si nunca cargó, mostramos lista vacía + mensaje de error abajo.
      if (providers === null) setProviders([]);
    }
  }, [providers]);

  React.useEffect(() => { load(); }, [load]);

  const triggerSync = async (name) => {
    setSyncing(name);
    try {
      await API.sync(name);
      await load();
    } catch (e) {
      alert(`Sync falló: ${e.message}`);
    } finally {
      setSyncing(null);
    }
  };

  // En demo seguimos mostrando MOCK.providers para preview; en producción
  // usamos lo que vino del backend. Mientras carga, lista vacía.
  const list = providers || (API.demoMode() ? MOCK.providers : []);
  const connected = list.filter((p) => p.state === "ok");
  const available = list.filter((p) => p.state !== "ok");
  const [showAddModal, setShowAddModal] = React.useState(false);

  return (
    <>
      {error && (
        <div style={{ padding: "10px 14px", background: "var(--warn-soft)", border: "1px solid color-mix(in srgb, var(--warn) 30%, transparent)", borderRadius: "var(--r-2)", fontSize: 12, color: "var(--text-2)" }}>
          <Icon name="alert" size={12} color="var(--warn)" /> Backend no responde: {error}
        </div>
      )}

      <div style={{ display: "flex", alignItems: "center", marginBottom: 4 }}>
        <div className="eyebrow">PROVEEDORES CONECTADOS</div>
        <div style={{ flex: 1 }} />
        {connected.length > 0 && available.length > 0 && (
          <Btn
            size="sm"
            variant="default"
            icon={<Icon name="plus" size={11} />}
            onClick={() => setShowAddModal(true)}
          >
            Agregar proveedor
          </Btn>
        )}
      </div>

      {connected.length === 0 ? (
        // Empty state — el primer onboarding visible cuando no hay nada.
        <Card style={{ padding: "var(--sp-7) var(--sp-6)" }}>
          <div style={{ textAlign: "center", maxWidth: 520, margin: "0 auto" }}>
            <div style={{
              width: 48, height: 48, borderRadius: "50%",
              background: "var(--bg-2)", color: "var(--text-3)",
              border: "1px solid var(--line)",
              display: "grid", placeItems: "center", margin: "0 auto 16px",
            }}>
              <Icon name="cloud" size={20} color="var(--text-3)" />
            </div>
            <div style={{ fontSize: 17, fontWeight: 500, marginBottom: 8 }}>
              No tenés proveedores conectados aún.
            </div>
            <div style={{ fontSize: 12.5, color: "var(--text-2)", lineHeight: 1.55, marginBottom: 22 }}>
              Conectá Garmin, Eufy u Oura para que el coach trabaje con tus datos reales.
              Sin proveedores, los planes se generan a ciegas.
            </div>
            {available.length > 0 ? (
              <Btn variant="primary" size="lg" icon={<Icon name="plus" size={13} />} onClick={() => setShowAddModal(true)}>
                Agregar tu primer proveedor
              </Btn>
            ) : (
              <div style={{ fontSize: 11, color: "var(--text-3)", fontFamily: "var(--font-mono)" }}>
                no hay proveedores disponibles en el registry
              </div>
            )}
          </div>
        </Card>
      ) : (
        <Card padded={false}>
          {connected.map((p, i) => (
            <ConnectedProviderRow
              key={p.name}
              provider={p}
              syncing={syncing === p.name}
              onSync={() => triggerSync(p.name)}
              last={i === connected.length - 1}
            />
          ))}
        </Card>
      )}

      {showAddModal && (
        <AddProviderModal
          available={available}
          onClose={() => setShowAddModal(false)}
          onPick={(name) => { setShowAddModal(false); setConnectFor(name); }}
        />
      )}

      {connectFor && (
        <ConnectModal
          provider={connectFor}
          onClose={() => setConnectFor(null)}
          onConnected={() => { setConnectFor(null); load(); }}
          setRoute={setRoute}
        />
      )}
    </>
  );
};

// Inline connect flow used by SettingsProviders.
// Garmin: email + password + (MFA code si Garmin lo pide).
// Eufy / Oura: por ahora derivamos al asistente de configuración (que tiene
// el flujo BLE / PAT completo); el botón es un atajo coherente.
/* ConnectedProviderRow — fila por proveedor activo con sync + menú "⋯".
 * Antes había Sync y Desconectar siempre visibles (ruido). Ahora el menú
 * mantiene el row limpio y sólo expone acciones secundarias bajo demanda. */
const ConnectedProviderRow = ({ provider, syncing, onSync, last }) => {
  const p = provider;
  const [menuOpen, setMenuOpen] = React.useState(false);
  React.useEffect(() => {
    if (!menuOpen) return undefined;
    const close = () => setMenuOpen(false);
    window.addEventListener("click", close);
    return () => window.removeEventListener("click", close);
  }, [menuOpen]);

  return (
    <div style={{
      display: "grid",
      gridTemplateColumns: "auto 1fr auto auto",
      gap: 16,
      alignItems: "center",
      padding: "16px 20px",
      borderBottom: last ? "none" : "1px solid var(--line-soft)",
    }}>
      <div style={{
        width: 38, height: 38, borderRadius: 8,
        background: "var(--bg-3)",
        display: "grid", placeItems: "center",
        fontFamily: "var(--font-mono)", fontWeight: 600, fontSize: 16,
        color: "var(--text)",
        border: "1px solid var(--line)",
      }}>
        {p.logo}
      </div>
      <div>
        <div style={{ fontSize: 14, fontWeight: 500, marginBottom: 2 }}>{p.name}</div>
        <div style={{ fontSize: 11.5, color: "var(--text-3)", display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
          <Badge tone="default" style={{ fontSize: 9.5 }}>{p.mode}</Badge>
          {p.caps.slice(0, 4).map((c) => (
            <span key={c} style={{ fontFamily: "var(--font-mono)", fontSize: 10 }}>{c}</span>
          ))}
          {p.caps.length > 4 && (
            <span style={{ fontSize: 10, color: "var(--text-4)" }}>+{p.caps.length - 4}</span>
          )}
        </div>
      </div>
      <div style={{ textAlign: "right", fontSize: 11 }}>
        <div style={{ color: "var(--good)", display: "flex", alignItems: "center", gap: 5, justifyContent: "flex-end" }}>
          <Dot tone="good" pulse /> Activo
        </div>
        <div style={{ color: "var(--text-3)", fontFamily: "var(--font-mono)", fontSize: 10, marginTop: 2 }}>
          sincronizado {p.lastSync}
        </div>
      </div>
      <div style={{ display: "flex", gap: 6, position: "relative" }} onClick={(e) => e.stopPropagation()}>
        <Btn
          size="sm"
          variant="default"
          icon={<Icon name={syncing ? "spinner" : "sync"} size={11} />}
          onClick={onSync}
          disabled={syncing}
        >
          {syncing ? "Sync…" : "Sincronizar"}
        </Btn>
        <Btn
          size="sm"
          variant="ghost"
          onClick={() => setMenuOpen((v) => !v)}
          title="Más opciones"
        >
          ⋯
        </Btn>
        {menuOpen && (
          <div style={{
            position: "absolute",
            top: "calc(100% + 6px)",
            right: 0,
            minWidth: 180,
            background: "var(--bg)",
            border: "1px solid var(--line)",
            borderRadius: "var(--r-2)",
            boxShadow: "0 6px 24px rgba(0,0,0,0.25)",
            zIndex: 10,
            padding: 4,
          }}>
            <button
              type="button"
              onClick={() => {
                setMenuOpen(false);
                if (confirm(`¿Desconectar ${p.name}? Vas a poder volver a conectarlo desde "+ Agregar".`)) {
                  // No hay endpoint /providers/{name}/disconnect aún — flag descriptivo.
                  alert("La desconexión todavía no está cableada al backend. Borrá el secret manualmente o reseteá el contenedor.");
                }
              }}
              style={{
                display: "block", width: "100%", textAlign: "left",
                padding: "8px 12px", background: "transparent", border: "none",
                color: "var(--bad)", fontSize: 12.5, cursor: "pointer", borderRadius: 4,
              }}
              onMouseEnter={(e) => (e.currentTarget.style.background = "var(--bad-soft)")}
              onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
            >
              Desconectar
            </button>
          </div>
        )}
      </div>
    </div>
  );
};

/* AddProviderModal — grilla de proveedores aún no conectados. Reusa los
 * estilos modalBackdrop/modalBody existentes. Click sobre una tarjeta dispara
 * el flujo `setConnectFor(name)` del parent que abre el wizard inline
 * apropiado (Garmin: email+MFA; Eufy: BLE pair; Oura: PAT). Todo dentro del
 * mismo modal — no se sale a otra pantalla. */
const AddProviderModal = ({ available, onClose, onPick }) => (
  <div onClick={onClose} style={modalBackdrop}>
    <div onClick={(e) => e.stopPropagation()} style={{ ...modalBody, width: "min(640px, 100%)" }}>
      <div style={{ display: "flex", alignItems: "center", marginBottom: 16 }}>
        <div>
          <div className="eyebrow">AGREGAR PROVEEDOR</div>
          <div className="display" style={{ fontSize: 22, fontWeight: 400, letterSpacing: "-0.02em", marginTop: 4 }}>
            ¿Qué fuente de datos querés sumar?
          </div>
        </div>
        <div style={{ flex: 1 }} />
        <Btn size="sm" variant="ghost" onClick={onClose}>Cerrar</Btn>
      </div>
      {available.length === 0 ? (
        <div style={{ padding: "30px 10px", textAlign: "center", fontSize: 12, color: "var(--text-3)", fontFamily: "var(--font-mono)" }}>
          No quedan proveedores por sumar — ya tenés conectados todos los disponibles.
        </div>
      ) : (
        <div style={{ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 12 }}>
          {available.map((p) => (
            <button
              key={p.name}
              type="button"
              onClick={() => onPick(p.name)}
              style={{
                display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 10,
                padding: 16,
                background: "var(--bg-1)",
                border: "1px solid var(--line)",
                borderRadius: "var(--r-2)",
                textAlign: "left",
                cursor: "pointer",
                transition: "border-color 0.12s",
              }}
              onMouseEnter={(e) => (e.currentTarget.style.borderColor = "var(--accent)")}
              onMouseLeave={(e) => (e.currentTarget.style.borderColor = "var(--line)")}
            >
              <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                <div style={{
                  width: 36, height: 36, borderRadius: 8,
                  background: "var(--bg-2)",
                  display: "grid", placeItems: "center",
                  fontFamily: "var(--font-mono)", fontWeight: 600, fontSize: 16,
                  border: "1px solid var(--line)",
                }}>
                  {p.logo}
                </div>
                <div>
                  <div style={{ fontSize: 14, fontWeight: 500 }}>{p.name}</div>
                  <Badge tone="default" style={{ fontSize: 9.5 }}>{p.mode}</Badge>
                </div>
              </div>
              {p.caps && p.caps.length > 0 && (
                <div style={{
                  fontSize: 10.5, color: "var(--text-3)",
                  fontFamily: "var(--font-mono)",
                  display: "flex", flexWrap: "wrap", gap: 6,
                }}>
                  {p.caps.slice(0, 5).map((c) => <span key={c}>{c}</span>)}
                  {p.caps.length > 5 && <span style={{ color: "var(--text-4)" }}>+{p.caps.length - 5}</span>}
                </div>
              )}
              <div style={{ marginTop: "auto", fontSize: 11, color: "var(--accent)", fontWeight: 500 }}>
                Conectar →
              </div>
            </button>
          ))}
        </div>
      )}
    </div>
  </div>
);

/* EufyConnectModal — wraps the existing EufyPair (loaded by screen-onboarding)
 * inside a sized modal. Antes derivábamos al asistente full-screen; ahora todo
 * el wizard de scan/configure/listener-restart vive dentro del mismo modal. */
const EufyConnectModal = ({ onClose, onConnected }) => {
  // El sub-componente maneja sus propios pasos (idle / scanning / results)
  // pero pide que el "outer state" se exponga. Acá lo hosteamos.
  const [pairState, setPairState] = React.useState("idle");
  const EufyPairComp = (typeof window !== "undefined") ? window.EufyPair : null;
  return (
    <div onClick={onClose} style={modalBackdrop}>
      <div onClick={(e) => e.stopPropagation()} style={{ ...modalBody, width: "min(640px, 100%)", maxHeight: "88vh", overflowY: "auto" }}>
        <div style={{ display: "flex", alignItems: "center", marginBottom: 14 }}>
          <div className="eyebrow">CONECTAR · EUFY</div>
          <div style={{ flex: 1 }} />
          <Btn size="sm" variant="ghost" onClick={onClose}>Cerrar</Btn>
        </div>
        {EufyPairComp ? (
          <EufyPairComp
            state={pairState}
            setState={setPairState}
            onBack={onClose}
            onDone={onConnected}
          />
        ) : (
          <div style={{ fontSize: 12, color: "var(--text-3)", padding: "20px 0", textAlign: "center" }}>
            Cargando wizard de Eufy…
          </div>
        )}
      </div>
    </div>
  );
};

/* OuraConnectModal — pequeño formulario inline para el PAT (personal access
 * token) de Oura Cloud. El backend valida el token contra /v2/usercollection
 * /personal_info y lo persiste cifrado en common.secrets. */
const OuraConnectModal = ({ onClose, onConnected }) => {
  const [pat, setPat] = React.useState("");
  const [stage, setStage] = React.useState("idle"); // idle | submitting | done | error
  const [errMsg, setErrMsg] = React.useState(null);

  const submit = async () => {
    if (!pat || pat.trim().length < 16) {
      setErrMsg("Pegá tu Personal Access Token (lo generás en cloud.ouraring.com → Personal Access Tokens).");
      return;
    }
    setStage("submitting");
    setErrMsg(null);
    try {
      const res = await API.startAuth("oura", {
        style: "api_key",
        credentials: { personal_access_token: pat.trim() },
      });
      if (res?.status === "logged_in" || res?.auth === "logged_in") {
        setStage("done");
        setTimeout(() => onConnected && onConnected(), 600);
      } else {
        setStage("error");
        setErrMsg(res?.error || "No se pudo validar el PAT con Oura.");
      }
    } catch (e) {
      setStage("error");
      setErrMsg(e.detail?.detail || e.message || "Error verificando el PAT.");
    }
  };

  const fs = (typeof window !== "undefined" && window.fieldStyle3) || {
    width: "100%", padding: "10px 12px", background: "var(--bg-2)",
    border: "1px solid var(--line)", borderRadius: "var(--r-2)",
    fontSize: 13, color: "var(--text)", outline: "none", marginTop: 4,
  };

  return (
    <div onClick={onClose} style={modalBackdrop}>
      <div onClick={(e) => e.stopPropagation()} style={modalBody}>
        <div style={{ display: "flex", alignItems: "center", marginBottom: 14 }}>
          <div className="eyebrow">CONECTAR · OURA</div>
          <div style={{ flex: 1 }} />
          <Btn size="sm" variant="ghost" onClick={onClose}>Cerrar</Btn>
        </div>
        <div className="display" style={{ fontSize: 22, fontWeight: 400, letterSpacing: "-0.02em", marginBottom: 8 }}>
          Pegá tu Personal Access Token
        </div>
        <div style={{ fontSize: 12.5, color: "var(--text-2)", lineHeight: 1.55, marginBottom: 16 }}>
          Generá uno en{" "}
          <a href="https://cloud.ouraring.com/personal-access-tokens" target="_blank" rel="noreferrer" style={{ color: "var(--accent)" }}>
            cloud.ouraring.com → Personal Access Tokens
          </a>. El token se guarda cifrado con Fernet en este servidor; nunca sale del contenedor.
        </div>
        {stage === "done" ? (
          <div style={{ padding: 16, background: "var(--good-soft)", borderRadius: "var(--r-2)", fontSize: 13, color: "var(--good)" }}>
            ✓ Oura conectado. Cerrando…
          </div>
        ) : (
          <>
            <input
              type="password"
              value={pat}
              onChange={(e) => setPat(e.target.value)}
              placeholder="ULNZQ7..."
              autoComplete="off"
              spellCheck={false}
              disabled={stage === "submitting"}
              style={{ ...fs, fontFamily: "var(--font-mono)" }}
            />
            {errMsg && (
              <div style={{
                marginTop: 10, padding: "8px 12px",
                background: "var(--bad-soft)",
                border: "1px solid color-mix(in srgb, var(--bad) 30%, transparent)",
                borderRadius: "var(--r-2)", fontSize: 12, color: "var(--text-2)",
              }}>
                <strong style={{ color: "var(--bad)" }}>Error:</strong> {errMsg}
              </div>
            )}
            <div style={{ marginTop: 18, display: "flex", justifyContent: "flex-end", gap: 8 }}>
              <Btn variant="ghost" onClick={onClose} disabled={stage === "submitting"}>Cancelar</Btn>
              <Btn variant="primary" onClick={submit} disabled={stage === "submitting"}>
                {stage === "submitting" ? "Validando…" : "Conectar"}
              </Btn>
            </div>
          </>
        )}
      </div>
    </div>
  );
};

const ConnectModal = ({ provider, onClose, onConnected, setRoute }) => {
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");
  const [mfa, setMfa] = React.useState("");
  const [stage, setStage] = React.useState("idle");  // idle | submitting | awaiting_mfa | done | error
  const [errMsg, setErrMsg] = React.useState(null);
  const [waitInfo, setWaitInfo] = React.useState(null);  // {elapsedSec} mientras pollea

  const close = () => onClose && onClose();

  // Sondear /auth/status hasta llegar a un estado terminal. Garmin con
  // Cloudflare puede tardar 3-5 min (mobile→widget→portal con backoffs
  // de 13-19s entre intentos). Por eso 5 min de timeout en lugar de 2.
  const pollUntilTerminal = async (timeoutMs = 300000, onTick) => {
    const t0 = Date.now();
    while (Date.now() - t0 < timeoutMs) {
      await new Promise(r => setTimeout(r, 1500));
      try {
        const s = await API.providerStatus("garmin");
        const auth = s?.auth || s?.status;
        const elapsedSec = Math.round((Date.now() - t0) / 1000);
        if (onTick) onTick({ elapsedSec, auth });
        if (auth === "logged_in") return { ok: true, status: auth };
        if (auth === "awaiting_mfa") return { ok: false, mfa: true, status: auth };
        if (auth === "error") return { ok: false, status: auth, error: s.auth_error || s.error };
      } catch (_) { /* sigue sondeando */ }
    }
    return {
      ok: false,
      status: "timeout",
      error: "Garmin tardó más de 5 min — probablemente la IP está rate-limiteada por Cloudflare. El backend sigue intentando en background; podés cerrar y revisar el estado en unos minutos.",
    };
  };

  const startGarmin = async () => {
    if (!email || !password) { setErrMsg("Email y contraseña requeridos."); return; }
    setStage("submitting");
    setErrMsg(null);
    try {
      await API.startAuth("garmin", {
        style: "interactive",
        credentials: { email, password },
      });
      // El POST /auth/start retorna casi inmediato; el auth real corre en
      // background. Polleamos /auth/status hasta estado terminal.
      setWaitInfo({ elapsedSec: 0 });
      const final = await pollUntilTerminal(300000, (tick) => setWaitInfo(tick));
      setWaitInfo(null);
      if (final.ok) {
        setStage("done");
        setTimeout(() => { onConnected && onConnected(); }, 600);
      } else if (final.mfa) {
        setStage("awaiting_mfa");
      } else {
        setStage("error");
        setErrMsg(final.error || "No se pudo iniciar sesión.");
      }
    } catch (e) {
      setStage("error");
      setErrMsg(e.detail?.detail || e.message || "No se pudo iniciar sesión con Garmin.");
    }
  };

  const submitMfa = async () => {
    if (!mfa) { setErrMsg("Pegá el código MFA que te llegó al email."); return; }
    setStage("submitting");
    setErrMsg(null);
    try {
      await API.submitMfa("garmin", { code: mfa });
      // submit_mfa también termina el auth en background; sondear status.
      const final = await pollUntilTerminal(60000);
      if (final.ok) {
        setStage("done");
        setTimeout(() => { onConnected && onConnected(); }, 600);
      } else if (final.mfa) {
        setStage("awaiting_mfa");
        setErrMsg("El backend sigue esperando MFA. Reintentá con el código.");
      } else {
        setStage("error");
        setErrMsg(final.error || "Código MFA rechazado.");
      }
    } catch (e) {
      setStage("error");
      setErrMsg(e.detail?.detail || e.message || "Error verificando MFA.");
    }
  };

  // Eufy: wizard inline reusando window.EufyPair (definido en screen-onboarding).
  // Antes redirigíamos a /onboarding y el flujo se sentía partido.
  if (provider === "eufy") {
    return <EufyConnectModal onClose={close} onConnected={() => { onConnected && onConnected(); }} />;
  }
  if (provider === "oura") {
    return <OuraConnectModal onClose={close} onConnected={() => { onConnected && onConnected(); }} />;
  }

  return (
    <div onClick={close} style={modalBackdrop}>
      <div onClick={(e) => e.stopPropagation()} style={modalBody}>
        <div className="eyebrow" style={{ marginBottom: 6 }}>CONECTAR · GARMIN</div>
        <div className="display" style={{ fontSize: 22, marginBottom: 4 }}>Iniciá sesión con Garmin Connect.</div>
        <div style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 18 }}>
          Tus credenciales nunca salen del contenedor. Garmin puede pedir un código de verificación de dispositivo al primer login.
        </div>

        {stage !== "awaiting_mfa" && stage !== "done" && (
          <>
            <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 14 }}>
              <input
                type="email" autoComplete="username"
                value={email} onChange={(e) => setEmail(e.target.value)}
                placeholder="email@dominio.com" style={modalField}
                disabled={stage === "submitting"}
              />
              <input
                type="password" autoComplete="current-password"
                value={password} onChange={(e) => setPassword(e.target.value)}
                placeholder="contraseña" style={modalField}
                disabled={stage === "submitting"}
                onKeyDown={(e) => { if (e.key === "Enter") startGarmin(); }}
              />
            </div>
            {stage === "submitting" && waitInfo && (
              <div style={{ background: "var(--bg-2)", border: "1px solid var(--line)", borderRadius: "var(--r-2)", padding: 10, marginBottom: 12, fontSize: 12, color: "var(--text-2)", lineHeight: 1.5 }}>
                Conectando contra Garmin · <span style={{ fontFamily: "var(--font-mono)" }}>{waitInfo.elapsedSec}s</span><br />
                Cloudflare a veces nos hace esperar 2-5 min entre intentos. Podés cerrar la modal — el backend sigue intentando y el provider se va a marcar como Activo cuando termine.
              </div>
            )}
            {errMsg && <div style={{ color: "var(--danger)", fontSize: 12, marginBottom: 12 }}>{errMsg}</div>}
            <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
              <Btn variant="ghost" onClick={close}>{stage === "submitting" ? "Cerrar (sigue en background)" : "Cancelar"}</Btn>
              <Btn variant="primary" onClick={startGarmin} disabled={stage === "submitting"}>
                {stage === "submitting" ? "Conectando…" : "Iniciar sesión"}
              </Btn>
            </div>
          </>
        )}

        {stage === "awaiting_mfa" && (
          <>
            <div style={{ background: "var(--bg-2)", border: "1px solid var(--line)", borderRadius: "var(--r-2)", padding: 12, marginBottom: 12, fontSize: 12.5, color: "var(--text-2)", lineHeight: 1.55 }}>
              Garmin te envió un código de verificación. Revisá tu email (incluyendo spam).
            </div>
            <input
              value={mfa} onChange={(e) => setMfa(e.target.value)}
              placeholder="código (6 dígitos)" style={{ ...modalField, marginBottom: 12, fontFamily: "var(--font-mono)" }}
              autoFocus
              onKeyDown={(e) => { if (e.key === "Enter") submitMfa(); }}
            />
            {errMsg && <div style={{ color: "var(--danger)", fontSize: 12, marginBottom: 12 }}>{errMsg}</div>}
            <div style={{ display: "flex", gap: 8, justifyContent: "space-between" }}>
              <Btn variant="ghost" onClick={() => { setStage("idle"); setMfa(""); setErrMsg(null); }}>← volver</Btn>
              <div style={{ display: "flex", gap: 8 }}>
                <Btn variant="ghost" onClick={startGarmin}>Reenviar</Btn>
                <Btn variant="primary" onClick={submitMfa}>Verificar</Btn>
              </div>
            </div>
          </>
        )}

        {stage === "done" && (
          <div style={{ display: "flex", alignItems: "center", gap: 10, color: "var(--good)", fontSize: 14 }}>
            <Icon name="check" size={16} /> Garmin conectado.
          </div>
        )}
      </div>
    </div>
  );
};

const modalBackdrop = {
  position: "fixed", inset: 0, zIndex: 1000,
  background: "color-mix(in srgb, #000 65%, transparent)",
  display: "grid", placeItems: "center",
  padding: 24,
};

const modalBody = {
  width: "min(440px, 100%)",
  background: "var(--bg-1)",
  border: "1px solid var(--line)",
  borderRadius: "var(--r-3, 12px)",
  padding: "20px 22px",
  boxShadow: "0 24px 80px -20px rgba(0,0,0,0.6)",
};

const modalField = {
  background: "var(--bg)",
  border: "1px solid var(--line)",
  borderRadius: "var(--r-2)",
  color: "var(--text)",
  padding: "10px 12px",
  fontSize: 13.5,
  width: "100%",
  outline: "none",
};

const SettingsLLM = () => {
  // provider en lowercase para que matchee el registro del backend
  // (LLM_PROVIDERS["openai"]). Los selects muestran labels capitalizados.
  const [config, setConfig] = React.useState({
    provider: "openai",
    model: "gpt-4o-mini",
    api_key: "",
    base_url: "",
    configured: false,  // true tras un GET /config/llm que devuelve { configured: true }
  });
  const [missing, setMissing] = React.useState(false);
  const [testStatus, setTestStatus] = React.useState({ kind: "idle" });

  React.useEffect(() => {
    if (API.demoMode()) return;
    API.getLLMConfig().then((c) => {
      // Backend devuelve { configured, provider, model_default, models_per_agent, base_url }.
      const isConfigured = c?.configured === true;
      setConfig((prev) => ({
        ...prev,
        provider: (c?.provider || prev.provider).toLowerCase(),
        model: c?.model_default || c?.default_model || c?.model || prev.model,
        base_url: c?.base_url || "",
        api_key: "",  // nunca se pre-rellena; el backend nunca devuelve la key en claro
        configured: isConfigured,
      }));
      setMissing(!isConfigured);
    }).catch((e) => {
      // 401/403 los maneja el evento global; 404/otros marcan "no configurado".
      setMissing(true);
    });
  }, []);

  const testConnection = async () => {
    if (!config.api_key || config.api_key.includes("•")) {
      setTestStatus({ kind: "bad", msg: "Pegá una API key real para probar." });
      return;
    }
    setTestStatus({ kind: "testing" });
    const t0 = performance.now();
    try {
      await API.setLLMConfig({
        provider: config.provider.toLowerCase(),
        model_default: config.model,
        api_key: config.api_key,
        base_url: config.base_url || null,
      });
      setTestStatus({ kind: "ok", ms: Math.round(performance.now() - t0) });
      setConfig((prev) => ({ ...prev, configured: true, api_key: "" }));
      setMissing(false);
    } catch (e) {
      setTestStatus({ kind: "bad", msg: e.message });
    }
  };

  return (
    <>
      {missing && (
        <Card style={{ background: "var(--warn-soft)", border: "1px solid color-mix(in srgb, var(--warn) 30%, transparent)" }}>
          <div style={{ display: "flex", gap: 10, alignItems: "flex-start" }}>
            <Icon name="alert" size={14} color="var(--warn)" />
            <div style={{ fontSize: 12.5, color: "var(--text-2)", lineHeight: 1.5 }}>
              <strong style={{ color: "var(--warn)" }}>LLM sin configurar.</strong> Sin esto el chat con agentes y la generación de planes están deshabilitados. Completá los campos abajo para activar al coach.
            </div>
          </div>
        </Card>
      )}

      <div className="eyebrow">PROVEEDOR LLM</div>
      <Card>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
          <SettingsField label="Provider">
            <select value={config.provider} onChange={(e) => setConfig({ ...config, provider: e.target.value })} style={settingsField}>
              <option value="openai">OpenAI</option>
              {/* otros se enchufan cuando se registren backend-side
                  (anthropic, ollama, gemini, etc.) */}
            </select>
          </SettingsField>
          <SettingsField label="Modelo por defecto">
            <select value={config.model} onChange={(e) => setConfig({ ...config, model: e.target.value })} style={settingsField}>
              <option value="gpt-4o-mini">gpt-4o-mini</option>
              <option value="gpt-4o">gpt-4o</option>
              <option value="gpt-4-turbo">gpt-4-turbo</option>
              <option value="o1">o1</option>
              <option value="o1-mini">o1-mini</option>
            </select>
          </SettingsField>
          <SettingsField label="API key">
            <input
              type="password" autoComplete="off"
              value={config.api_key}
              placeholder={config.configured ? "guardada · pegá una nueva para reemplazar" : "sk-..."}
              onChange={(e) => setConfig({ ...config, api_key: e.target.value })}
              style={settingsField}
            />
          </SettingsField>
          <SettingsField label="Base URL (opcional)">
            <input
              type="text" placeholder="https://api.openai.com/v1"
              value={config.base_url}
              onChange={(e) => setConfig({ ...config, base_url: e.target.value })}
              style={settingsField}
            />
          </SettingsField>
        </div>
        <div style={{ marginTop: 14, display: "flex", gap: 8, alignItems: "center" }}>
          <Btn size="sm" variant="default" icon={<Icon name="check" size={11} />} onClick={testConnection} disabled={testStatus.kind === "testing"}>
            {testStatus.kind === "testing" ? "Probando…" : "Probar conexión"}
          </Btn>
          {testStatus.kind === "ok" && (
            <span style={{ fontSize: 11, color: "var(--good)", display: "flex", alignItems: "center", gap: 5 }}>
              <Dot tone="good" /> ok · {testStatus.ms}ms
            </span>
          )}
          {testStatus.kind === "bad" && (
            <span style={{ fontSize: 11, color: "var(--bad)" }}>{testStatus.msg}</span>
          )}
        </div>
      </Card>

      <div className="eyebrow">MODELO POR AGENTE</div>
      <Card padded={false}>
        {Object.entries(SETTINGS_AGENTS).map(([k, a], i) => (
          <div key={k} style={{ display: "flex", alignItems: "center", gap: 14, padding: "14px 18px", borderBottom: i === 2 ? "none" : "1px solid var(--line-soft)" }}>
            <Icon name={a.icon} size={14} color={a.color} />
            <span style={{ flex: 1, fontSize: 13, fontWeight: 500 }}>{a.label}</span>
            <select style={{ ...settingsField, width: 220 }}>
              <option>gpt-4o-mini (defecto)</option>
              <option>gpt-4o</option>
              <option>claude-haiku-4-5</option>
            </select>
          </div>
        ))}
      </Card>
    </>
  );
};

const SettingsUsage = () => {
  const [usage, setUsage] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [err, setErr] = React.useState(null);

  React.useEffect(() => {
    if (API.demoMode()) {
      setUsage({
        by_agent: [
          { agent: "coach", prompt: 12000, completion: 4500, cost: "0.18" },
          { agent: "briefing.coach", prompt: 8000, completion: 1200, cost: "0.10" },
        ],
        by_model: [{ model: "gpt-4o-mini", prompt: 20000, completion: 5700, cost: "0.28" }],
        total_tokens: 25700,
        total_cost_usd: "0.28",
      });
      setLoading(false);
      return;
    }
    API.getLLMUsage()
      .then((u) => { setUsage(u); setLoading(false); })
      .catch((e) => { setErr(e.message || "No se pudo cargar uso"); setLoading(false); });
  }, []);

  const totalTokens = Number(usage?.total_tokens || 0);
  const totalCost = Number(usage?.total_cost_usd || 0);
  const byAgent = Array.isArray(usage?.by_agent) ? usage.by_agent : [];
  const byModel = Array.isArray(usage?.by_model) ? usage.by_model : [];

  return (
    <>
      <div className="eyebrow">USO LLM · ACUMULADO</div>
      {loading ? (
        <Card>
          <div style={{ padding: 16, textAlign: "center", color: "var(--text-3)", fontFamily: "var(--font-mono)", fontSize: 12 }}>
            cargando uso…
          </div>
        </Card>
      ) : err ? (
        <Card>
          <div style={{ padding: 12, fontSize: 12, color: "var(--warn)" }}>{err}</div>
        </Card>
      ) : totalTokens === 0 ? (
        <Card>
          <div style={{ padding: "20px 10px", textAlign: "center", fontSize: 12, color: "var(--text-3)", fontFamily: "var(--font-mono)" }}>
            Sin llamadas LLM registradas todavía.<br />
            <span style={{ color: "var(--text-4)", fontSize: 10, marginTop: 6, display: "inline-block" }}>
              Generá un briefing o un plan para empezar a ver consumo aquí.
            </span>
          </div>
        </Card>
      ) : (
        <>
          <Card>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 18, marginBottom: 18 }}>
              <UsageStat label="Tokens totales" value={fmtTokens(totalTokens)} unit="" />
              <UsageStat label="Coste total" value={`$${totalCost.toFixed(4)}`} unit="USD" />
            </div>
          </Card>
          {byAgent.length > 0 && (
            <Card>
              <div className="eyebrow" style={{ marginBottom: 10 }}>POR AGENTE</div>
              <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12.5, fontFamily: "var(--font-mono)" }}>
                <thead>
                  <tr style={{ borderBottom: "1px solid var(--line-soft)" }}>
                    {["AGENTE", "PROMPT", "COMPLETION", "USD"].map((h) => (
                      <th key={h} style={{ padding: "6px 4px", textAlign: "left", fontSize: 9.5, color: "var(--text-3)", fontWeight: 500 }}>{h}</th>
                    ))}
                  </tr>
                </thead>
                <tbody>
                  {byAgent.map((row, i) => (
                    <tr key={row.agent} style={{ borderBottom: i === byAgent.length - 1 ? "none" : "1px dashed var(--line-soft)" }}>
                      <td style={{ padding: "6px 4px" }}>{row.agent}</td>
                      <td style={{ padding: "6px 4px" }}>{fmtTokens(Number(row.prompt))}</td>
                      <td style={{ padding: "6px 4px" }}>{fmtTokens(Number(row.completion))}</td>
                      <td style={{ padding: "6px 4px", color: "var(--text-2)" }}>${Number(row.cost || 0).toFixed(4)}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </Card>
          )}
          {byModel.length > 0 && (
            <Card>
              <div className="eyebrow" style={{ marginBottom: 10 }}>POR MODELO</div>
              <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12.5, fontFamily: "var(--font-mono)" }}>
                <thead>
                  <tr style={{ borderBottom: "1px solid var(--line-soft)" }}>
                    {["MODELO", "PROMPT", "COMPLETION", "USD"].map((h) => (
                      <th key={h} style={{ padding: "6px 4px", textAlign: "left", fontSize: 9.5, color: "var(--text-3)", fontWeight: 500 }}>{h}</th>
                    ))}
                  </tr>
                </thead>
                <tbody>
                  {byModel.map((row, i) => (
                    <tr key={row.model} style={{ borderBottom: i === byModel.length - 1 ? "none" : "1px dashed var(--line-soft)" }}>
                      <td style={{ padding: "6px 4px" }}>{row.model}</td>
                      <td style={{ padding: "6px 4px" }}>{fmtTokens(Number(row.prompt))}</td>
                      <td style={{ padding: "6px 4px" }}>{fmtTokens(Number(row.completion))}</td>
                      <td style={{ padding: "6px 4px", color: "var(--text-2)" }}>${Number(row.cost || 0).toFixed(4)}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </Card>
          )}
        </>
      )}
    </>
  );
};

const SettingsPrivacy = () => (
  <>
    <div className="eyebrow">PRIVACIDAD</div>
    <Card>
      <PrivacyRow
        title="Anonimizar antes de enviar al LLM"
        body="Quita nombre, GPS, fechas exactas. Mantiene métricas y tendencias."
        on
      />
      <PrivacyRow
        title="Procesamiento 100% local"
        body="Usar Ollama en lugar de API cloud. Próximamente — beta interna."
        on={false} disabled
      />
      <PrivacyRow
        title="Mostrar contexto enviado"
        body="Modal con el prompt exacto antes de enviar."
        on
      />
      <PrivacyRow
        title="Logs detallados de sync"
        body="Para debug. Aumenta el espacio en disco."
        on={false} last
      />
    </Card>

    <Card style={{ background: "var(--info-soft)", border: "1px solid color-mix(in srgb, var(--info) 30%, transparent)" }}>
      <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
        <Icon name="cloud" size={16} color="var(--info)" />
        <div style={{ fontSize: 12.5, color: "var(--text-2)", lineHeight: 1.55 }}>
          OpenAI procesará tus datos en sus servidores. Si querés procesamiento 100% local, configurá <strong>Ollama</strong> cuando esté disponible. Tus datos crudos nunca salen del contenedor; solo lo que mandás explícitamente al chat.
        </div>
      </div>
    </Card>
  </>
);

const SettingsSystem = ({ onLogout, setRoute }) => {
  const [info, setInfo] = React.useState(null);
  React.useEffect(() => {
    if (API.demoMode()) {
      setInfo({ version: "0.4.2", status: "demo" });
      return;
    }
    Promise.all([API.health().catch(() => null), API.version().catch(() => null)]).then(([h, v]) => {
      setInfo({
        version: v?.version || h?.version || "0.4.2",
        status: h?.status || "running",
      });
    });
  }, []);
  const v = info?.version || "0.4.2";

  return (
    <>
      <div className="eyebrow">SISTEMA</div>
      <Card>
        <SysRow l="Versión" v={v} />
        <SysRow l="Estado" v={info?.status || "—"} />
        <SysRow l="Container" v={`api_fit:${v}`} />
        <SysRow l="Base de datos" v="DuckDB · /data/api_fit.duckdb" />
        <SysRow l="Sesión" v={<>
          <span className="num">{API.getUser()?.email || (API.demoMode() ? "demo" : "—")}</span>
          <Btn size="sm" variant="ghost" style={{ marginLeft: 8 }} onClick={() => { (onLogout ? onLogout() : (async () => { await API.authLogout(); window.location.reload(); })()); }}>Cerrar sesión</Btn>
        </>} />
        <SysRow l="Base URL del backend" v={<>
          <span className="num">{API.getBase() || "(mismo origen)"}</span>
        </>} />
        <SysRow l="Logs" v={<a style={{ color: "var(--accent)" }}>ver últimos 1000 →</a>} last />
      </Card>

      <div className="eyebrow">CONFIGURACIÓN INICIAL</div>
      <Card>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
          <div style={{ flex: 1, minWidth: 240 }}>
            <div style={{ fontWeight: 500, marginBottom: 4 }}>Asistente de configuración</div>
            <div style={{ color: "var(--text-3)", fontSize: 12, lineHeight: 1.5 }}>
              Recorré de nuevo el flujo guiado para conectar proveedores y configurar el LLM.
              Útil cuando agregás un dispositivo nuevo o cambiás de modelo.
            </div>
          </div>
          <Btn
            variant="default"
            onClick={() => setRoute && setRoute("onboarding")}
          >
            Abrir asistente →
          </Btn>
        </div>
      </Card>

      <div className="eyebrow">EXPORTAR</div>
      <Card>
        <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
          <Btn variant="default" icon={<Icon name="arrow" size={12} />}>Descargar dump DuckDB</Btn>
          <Btn variant="default" icon={<Icon name="arrow" size={12} />}>Exportar CSV de actividades</Btn>
          <Btn variant="default" icon={<Icon name="arrow" size={12} />}>Exportar histórico LLM</Btn>
        </div>
      </Card>
    </>
  );
};

const PrivacyRow = ({ title, body, on, last, disabled }) => (
  <div style={{
    display: "flex", gap: 14, alignItems: "flex-start", padding: "14px 0",
    borderBottom: last ? "none" : "1px solid var(--line-soft)",
    opacity: disabled ? 0.5 : 1,
  }}>
    <div style={{ flex: 1 }}>
      <div style={{ fontSize: 13.5, fontWeight: 500, marginBottom: 3, display: "flex", gap: 8, alignItems: "center" }}>
        {title}
        {disabled && <Badge tone="default">próximamente</Badge>}
      </div>
      <div style={{ fontSize: 11.5, color: "var(--text-3)", lineHeight: 1.45 }}>{body}</div>
    </div>
    <Toggle on={on} disabled={disabled} />
  </div>
);

const Toggle = ({ on, disabled }) => (
  <div style={{
    width: 32, height: 18, borderRadius: 999,
    background: on ? "var(--accent)" : "var(--line)",
    display: "flex", alignItems: "center", padding: 2,
    cursor: disabled ? "not-allowed" : "pointer",
  }}>
    <div style={{
      width: 14, height: 14, borderRadius: "50%",
      background: "white",
      transform: on ? "translateX(14px)" : "translateX(0)",
      transition: "transform 0.15s",
    }} />
  </div>
);

const SysRow = ({ l, v, last }) => (
  <div style={{ display: "flex", padding: "10px 0", borderBottom: last ? "none" : "1px dashed var(--line-soft)", fontSize: 13, alignItems: "center" }}>
    <div style={{ width: 200, color: "var(--text-3)", fontSize: 12 }}>{l}</div>
    <div style={{ fontFamily: "var(--font-mono)", fontSize: 12, display: "flex", alignItems: "center", gap: 6, flex: 1 }}>{v}</div>
  </div>
);

const UsageStat = ({ label, value, unit }) => (
  <div>
    <div className="eyebrow" style={{ fontSize: 9.5, marginBottom: 4 }}>{label}</div>
    <div style={{ display: "flex", alignItems: "baseline", gap: 4 }}>
      <span className="num" style={{ fontSize: 22, fontWeight: 500, letterSpacing: "-0.02em" }}>{value}</span>
      {unit && <span style={{ fontSize: 11, color: "var(--text-3)" }}>{unit}</span>}
    </div>
  </div>
);

const settingsField = {
  width: "100%", padding: "8px 10px", background: "var(--bg-2)", border: "1px solid var(--line)",
  borderRadius: "var(--r-2)", fontSize: 12.5, color: "var(--text)", outline: "none", marginTop: 4,
};
const SettingsField = ({ label, children, full }) => (
  <div style={{ gridColumn: full ? "1 / -1" : "auto" }}>
    <div className="eyebrow" style={{ fontSize: 9.5 }}>{label}</div>
    {children}
  </div>
);

function fmtTokens(n) {
  if (n == null) return "—";
  if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
  if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
  return String(n);
}

function maskToken(t) {
  if (!t) return "—";
  if (t.length <= 8) return "•".repeat(t.length);
  return t.slice(0, 4) + "•".repeat(Math.min(12, t.length - 8)) + t.slice(-4);
}

window.Settings = Settings;
