// VELOUR — admin sign-in gate.
//
// Wraps the internal dashboard so #admin is no longer a URL anyone can type. Signing
// in here proves nothing on its own: the server re-checks the token AND the
// ADMIN_EMAILS allowlist on every request (api/_auth.js). This screen is the door;
// the lock is server-side. Treat anything it renders as convenience, not security.
//
// Zero dependencies — Supabase Auth is a plain HTTP endpoint, so there is no
// supabase-js and no build step, matching the rest of the repo.
const { Card, Button } = window.VelourDesignSystem_380ed4;

// sessionStorage, not localStorage: an admin session should not outlive the tab.
const VLR_ADMIN_TOKEN = "velour_admin_token";

// Cached /api/config so we fetch the public Supabase values once per page load.
let _configPromise = null;
function adminConfig() {
  if (!_configPromise) {
    _configPromise = fetch("/api/config")
      .then((r) => (r.ok ? r.json() : Promise.reject(new Error("config unavailable"))))
      .catch((e) => { _configPromise = null; throw e; });
  }
  return _configPromise;
}

function adminToken() {
  try { return sessionStorage.getItem(VLR_ADMIN_TOKEN) || null; } catch (e) { return null; }
}
function setAdminToken(t) {
  try { t ? sessionStorage.setItem(VLR_ADMIN_TOKEN, t) : sessionStorage.removeItem(VLR_ADMIN_TOKEN); } catch (e) {}
}

async function adminSignIn(email, password) {
  let supabaseUrl, supabasePublishableKey;
  try {
    ({ supabaseUrl, supabasePublishableKey } = await adminConfig());
  } catch (e) {
    // /api/config is missing or unreachable — running the static site without the
    // serverless functions, or Supabase env vars aren't set on this deployment.
    // Say something a person can act on rather than surfacing the internal reason.
    throw new Error("Sign-in isn't available here. This build has no connection to the server.");
  }
  const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=password`, {
    method: "POST",
    headers: { apikey: supabasePublishableKey, "Content-Type": "application/json" },
    body: JSON.stringify({ email, password }),
  });
  const body = await res.json().catch(() => ({}));
  if (!res.ok || !body.access_token) {
    // Supabase distinguishes "no such user" from "wrong password". We deliberately
    // do not — an admin login should not confirm which addresses exist.
    throw new Error("Those details weren't recognised.");
  }
  setAdminToken(body.access_token);
  return body.access_token;
}

// Fetch wrapper for every admin API call. A 401/403 clears the stored token so the
// UI falls back to the sign-in screen rather than looping on a dead session.
async function adminFetch(path, options) {
  const token = adminToken();
  const opts = Object.assign({}, options);
  opts.headers = Object.assign({}, opts.headers, token ? { Authorization: `Bearer ${token}` } : {});
  const res = await fetch(path, opts);
  if (res.status === 401 || res.status === 403) {
    setAdminToken(null);
    window.dispatchEvent(new Event("velour-admin-signed-out"));
    throw new Error("That session is no longer valid.");
  }
  return res;
}

// ---- choosing a password ------------------------------------------------------
// Added 15 Sep 2026. Admin sign-in is email + password, but members sign in with
// a link, so an account on ADMIN_EMAILS that has only ever used links has NO
// password — and nothing on the site could give it one. Supabase's dashboard can
// only email a reset link, and that link landed on tryvelour.com with nowhere to
// type a new password. This is that nowhere. It never sees or stores a password
// beyond sending it once, over HTTPS, to Supabase.
//
// The reset link comes back as #access_token=…&type=recovery on ?view=admin.
// velourAuth.completeFromUrl (the member handler in _ds_bundle.js) now ignores
// type=recovery, and this screen reads the fragment during its FIRST render —
// before any effect — then clears it so the token never sits in the address bar.

// "What is in the address bar right now", read once. Returns
// { token } for a reset link, { expired: true } for a dead one, or null.
function readRecoveryFromUrl() {
  try {
    const hp = new URLSearchParams((window.location.hash || "").replace(/^#/, ""));
    if (hp.get("type") === "recovery" && hp.get("access_token")) return { token: hp.get("access_token") };
    // Supabase reports an expired or already-used link as an error fragment with
    // no type, so on the admin page any auth error fragment is read as that.
    if (hp.get("error_code") || hp.get("error_description")) return { expired: true };
  } catch (e) {}
  return null;
}

function clearAuthFragment() {
  try { history.replaceState(null, "", window.location.pathname + window.location.search); } catch (e) {}
}

// Ask Supabase to email a reset link that returns to THIS page. Supabase answers
// the same whether or not the address has an account, and so does the screen.
async function adminRequestReset(email) {
  let supabaseUrl, supabasePublishableKey;
  try {
    ({ supabaseUrl, supabasePublishableKey } = await adminConfig());
  } catch (e) {
    throw new Error("This build has no connection to the server.");
  }
  const back = window.location.origin + window.location.pathname + "?view=admin";
  const res = await fetch(`${supabaseUrl}/auth/v1/recover?redirect_to=${encodeURIComponent(back)}`, {
    method: "POST",
    headers: { apikey: supabasePublishableKey, "Content-Type": "application/json" },
    body: JSON.stringify({ email }),
  });
  // 429 is the one answer worth passing on: Supabase limits how often it emails.
  if (res.status === 429) throw new Error("Too many requests just now. Wait a minute and try again.");
  if (!res.ok && res.status >= 500) throw new Error("The email couldn't be sent. Try again shortly.");
}

// Set the password with the reset link's token, then sign in the ordinary way, so
// the admin session is exactly what a normal sign-in produces.
async function adminChoosePassword(recoveryToken, password) {
  const { supabaseUrl, supabasePublishableKey } = await adminConfig();
  const res = await fetch(`${supabaseUrl}/auth/v1/user`, {
    method: "PUT",
    headers: {
      apikey: supabasePublishableKey,
      Authorization: `Bearer ${recoveryToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ password }),
  });
  const body = await res.json().catch(() => ({}));
  if (res.status === 401 || res.status === 403) {
    throw new Error("That link has expired. Ask for a new one below.");
  }
  if (!res.ok || !body.email) {
    // Supabase's own reason is specific and safe to show here ("should be at least
    // N characters", "should be different from the old password").
    throw new Error(body.msg || body.message || body.error_description || "The password couldn't be saved.");
  }
  await adminSignIn(body.email, password);
}

window.VelourAdminAuth = {
  getToken: adminToken,
  signIn: adminSignIn,
  signOut: () => { setAdminToken(null); window.dispatchEvent(new Event("velour-admin-signed-out")); },
  fetch: adminFetch,
};

const MIN_ADMIN_PASSWORD = 12;

function AdminLogin({ onSignedIn, onNavigate }) {
  // Read during the first render, not in an effect: see readRecoveryFromUrl().
  const [recovery] = React.useState(readRecoveryFromUrl);
  const [mode, setMode] = React.useState(recovery && recovery.token ? "choose" : recovery && recovery.expired ? "forgot" : "signin");
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");
  const [confirm, setConfirm] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(recovery && recovery.expired ? "That link has expired or was already used. Ask for a new one." : null);
  const [sentTo, setSentTo] = React.useState(null);
  React.useEffect(() => { if (recovery) clearAuthFragment(); }, []);

  const go = (m) => { setMode(m); setError(null); setPassword(""); setConfirm(""); setBusy(false); };

  const sendReset = async (e) => {
    if (e) e.preventDefault();
    if (busy || !email.trim()) return;
    setBusy(true); setError(null);
    try {
      await adminRequestReset(email.trim());
      setSentTo(email.trim());
      setMode("sent");
    } catch (err) {
      setError(err.message);
    }
    setBusy(false);
  };

  const choose = async (e) => {
    if (e) e.preventDefault();
    if (busy) return;
    if (password.length < MIN_ADMIN_PASSWORD) { setError(`Use at least ${MIN_ADMIN_PASSWORD} characters.`); return; }
    if (password !== confirm) { setError("The two passwords don't match."); return; }
    setBusy(true); setError(null);
    try {
      await adminChoosePassword(recovery.token, password);
      setPassword(""); setConfirm("");
      onSignedIn();
    } catch (err) {
      setError(err.message || "The password couldn't be saved.");
      setBusy(false);
    }
  };

  const submit = async (e) => {
    if (e) e.preventDefault();
    if (busy || !email.trim() || !password) return;
    setBusy(true); setError(null);
    try {
      await adminSignIn(email.trim(), password);
      setPassword("");
      onSignedIn();
    } catch (err) {
      setError(err.message || "Sign-in didn't complete.");
      setBusy(false);
    }
  };

  const field = {
    width: "100%", boxSizing: "border-box", padding: "12px 14px", borderRadius: 10,
    border: "var(--border-width) solid var(--border)", background: "var(--surface-card)",
    fontFamily: "var(--font-sans)", fontSize: 14.5, color: "var(--text-primary)", outline: "none",
  };
  const heading = {
    fontFamily: "var(--font-serif)", fontWeight: 500, fontSize: 21,
    color: "var(--text-primary)", margin: "0 0 6px", letterSpacing: "-0.01em",
  };
  const intro = { fontSize: 13.5, lineHeight: 1.6, color: "var(--text-tertiary)", margin: "0 0 22px" };
  const alertBox = {
    fontSize: 13, lineHeight: 1.55, color: "var(--terracotta-text)",
    background: "var(--terracotta-fill)", border: "var(--border-width) solid var(--terracotta)",
    borderRadius: 10, padding: "10px 13px", marginBottom: 18,
  };
  const quietLink = {
    display: "block", width: "100%", marginTop: 14, background: "none", border: "none", cursor: "pointer",
    fontFamily: "var(--font-sans)", fontSize: 13, color: "var(--text-tertiary)", textAlign: "center",
  };
  const label = {
    display: "block", fontSize: 11.5, letterSpacing: "0.14em", textTransform: "uppercase",
    color: "var(--text-tertiary)", marginBottom: 7,
  };

  return (
    <main style={{
      minHeight: "100vh", background: "var(--parchment)", display: "flex",
      alignItems: "center", justifyContent: "center", padding: 24, fontFamily: "var(--font-sans)",
    }}>
      <div style={{ width: "100%", maxWidth: 380 }}>
        <div style={{ textAlign: "center", marginBottom: 28 }}>
          <div style={{
            fontFamily: "var(--font-serif)", fontSize: 22, letterSpacing: "0.17em",
            color: "var(--ink)", marginBottom: 10,
          }}>VELOUR</div>
          <div style={{
            fontSize: 11.5, letterSpacing: "0.16em", textTransform: "uppercase",
            color: "var(--text-tertiary)",
          }}>Internal</div>
        </div>

        <Card padding={28}>
          {mode === "signin" && (
            <form onSubmit={submit}>
              <h1 style={heading}>Sign in</h1>
              <p style={intro}>This dashboard holds member records. Access is limited to accounts on the admin list.</p>
              <div style={{ marginBottom: 16 }}>
                <label style={label} htmlFor="vlr-admin-email">Email</label>
                <input id="vlr-admin-email" type="email" autoComplete="username" value={email}
                  onChange={(e) => setEmail(e.target.value)} disabled={busy} style={field} />
              </div>
              <div style={{ marginBottom: 22 }}>
                <label style={label} htmlFor="vlr-admin-password">Password</label>
                <input id="vlr-admin-password" type="password" autoComplete="current-password" value={password}
                  onChange={(e) => setPassword(e.target.value)} disabled={busy} style={field} />
              </div>
              {error && <div role="alert" style={alertBox}>{error}</div>}
              <Button type="submit" disabled={busy} style={{ width: "100%" }}>
                {busy ? "One moment" : "Sign in"}
              </Button>
              <button type="button" onClick={() => go("forgot")} style={quietLink}>Forgot your password, or never set one?</button>
            </form>
          )}

          {mode === "forgot" && (
            <form onSubmit={sendReset}>
              <h1 style={heading}>Choose a password</h1>
              <p style={intro}>Enter your admin email and we'll send a link to set a new password. It works once, for a short while.</p>
              <div style={{ marginBottom: 22 }}>
                <label style={label} htmlFor="vlr-admin-reset-email">Email</label>
                <input id="vlr-admin-reset-email" type="email" autoComplete="username" value={email}
                  onChange={(e) => setEmail(e.target.value)} disabled={busy} style={field} />
              </div>
              {error && <div role="alert" style={alertBox}>{error}</div>}
              <Button type="submit" disabled={busy} style={{ width: "100%" }}>
                {busy ? "One moment" : "Send the link"}
              </Button>
              <button type="button" onClick={() => go("signin")} style={quietLink}>Back to sign in</button>
            </form>
          )}

          {mode === "sent" && (
            <div>
              <h1 style={heading}>Check your inbox</h1>
              {/* The same words whether or not the address has an account: this page
                  must not confirm which emails exist. */}
              <p style={intro}>If {sentTo} has an account, a link to choose a password is on its way. Open it on this device. It works once, for a short while.</p>
              <button type="button" onClick={() => go("signin")} style={quietLink}>Back to sign in</button>
            </div>
          )}

          {mode === "choose" && (
            <form onSubmit={choose}>
              <h1 style={heading}>Choose a password</h1>
              <p style={intro}>At least {MIN_ADMIN_PASSWORD} characters. You'll use it with your email to sign in here.</p>
              <div style={{ marginBottom: 16 }}>
                <label style={label} htmlFor="vlr-admin-new-password">New password</label>
                <input id="vlr-admin-new-password" type="password" autoComplete="new-password" value={password}
                  onChange={(e) => setPassword(e.target.value)} disabled={busy} style={field} />
              </div>
              <div style={{ marginBottom: 22 }}>
                <label style={label} htmlFor="vlr-admin-confirm-password">Type it again</label>
                <input id="vlr-admin-confirm-password" type="password" autoComplete="new-password" value={confirm}
                  onChange={(e) => setConfirm(e.target.value)} disabled={busy} style={field} />
              </div>
              {error && <div role="alert" style={alertBox}>{error}</div>}
              <Button type="submit" disabled={busy} style={{ width: "100%" }}>
                {busy ? "One moment" : "Save and sign in"}
              </Button>
              <button type="button" onClick={() => go("forgot")} style={quietLink}>Ask for a new link</button>
            </form>
          )}
        </Card>

        <div style={{ textAlign: "center", marginTop: 18 }}>
          <button onClick={() => onNavigate && onNavigate("home")} style={{
            background: "none", border: "none", cursor: "pointer", fontFamily: "var(--font-sans)",
            fontSize: 13, color: "var(--text-tertiary)",
          }}>Back to the site</button>
        </div>
      </div>
    </main>
  );
}

window.AdminLogin = AdminLogin;

// The gate itself: sign-in screen until there is a session, dashboard after. Listens
// for velour-admin-signed-out so an expired or refused token drops straight back to
// the sign-in screen instead of leaving a dead dashboard on screen.
function AdminGate({ onNavigate }) {
  const [token, setToken] = React.useState(adminToken);
  React.useEffect(() => {
    const signedOut = () => setToken(null);
    window.addEventListener("velour-admin-signed-out", signedOut);
    return () => window.removeEventListener("velour-admin-signed-out", signedOut);
  }, []);

  if (!token) return <AdminLogin onSignedIn={() => setToken(adminToken())} onNavigate={onNavigate} />;
  return <AdminDashboard onNavigate={onNavigate} />;
}

window.AdminGate = AdminGate;
