// Auth & session — deviceless (web) email-code sign-in against /auth/*, token
// storage, and the Authorization header all live HTTP clients (chat_api /
// graph_api / install_skill) attach.
//
// The backend has no passwords. Installed clients enroll a device keypair, but a
// browser is a "deviceless" client: it signs in purely with an emailed code via
// the web endpoints, and the only thing it persists is the returned session
// token. No keypair, no WebCrypto, no signing.
//   register/web/start {email, kind:'org'} → 6-digit code → register/web/verify
//   {email, code} → {access_token, account}
// register/web/verify both CREATES a new account and signs in an existing one,
// so to keep this console "enroll into an EXISTING org only, never create" we
// first probe existence with login/challenge (404 → no such org → refuse).
//
// This console is ORG-ONLY. The personal surfaces are deprecated; every session
// is mode 'org'. The backend login endpoints are kind-agnostic, so we also
// guard the returned account.kind === 'org' before storing any session.
//
// Session is persisted in localStorage (consistent with parcle.theme/route).
// Mock mode (no PARCLE_API_BASE) needs no login and stays org-mode by default.
//
// Exposed on window:
//   parcleLiveAuthRequired()              — true when a real backend is configured
//   parcleToken() / parcleAccount() / parcleMode()  (mode is always 'org')
//   parcleAuthHeaders()                   — {Authorization} or {} for fetch merges
//   parcleRestoreSession()                — {token, account, mode} | null
//   parcleWebStart/Resend/Verify          — email-code sign-in (deviceless web)
//   parcleLogout()                        — POST /auth/logout (best-effort) + clear
//   parcleClearSession()
//   parcleOnUnauthorized(cb) / parcleNotifyUnauthorized()  — 401 → back to login
//   <LoginGate onLogin={fn}/>             — the org email + verification-code form
//   <PersonalEmpty label=.../>            — placeholder kept for back-compat

const { useState: useAuthState, useEffect: useAuthEffect } = React;

const PARCLE_TOKEN_KEY   = 'parcle.authToken';
const PARCLE_ACCOUNT_KEY = 'parcle.account';
const PARCLE_MODE_KEY    = 'parcle.mode';

function parcleApiBase() { return window.PARCLE_API_BASE || ''; }
function parcleLiveAuthRequired() { return !!window.PARCLE_API_BASE; }

function parcleToken()   { try { return localStorage.getItem(PARCLE_TOKEN_KEY) || null; } catch (e) { return null; } }
// Org-only console: every session is org mode. Kept as a function for the live
// clients that branch on it (e.g. graph_api).
function parcleMode()    { return 'org'; }
function parcleAccount() {
  try {
    const raw = localStorage.getItem(PARCLE_ACCOUNT_KEY);
    return raw ? JSON.parse(raw) : null;
  } catch (e) { return null; }
}

// Header object to spread into fetch headers. Empty when unauthenticated so
// mock mode and the no-auth endpoints (/files, /skill/events, /skill/skill.md)
// keep working unchanged.
function parcleAuthHeaders() {
  const t = parcleToken();
  return t ? { Authorization: 'Bearer ' + t } : {};
}

function parcleSetSession({ token, account }) {
  try {
    localStorage.setItem(PARCLE_TOKEN_KEY, token);
    localStorage.setItem(PARCLE_ACCOUNT_KEY, JSON.stringify(account));
    localStorage.setItem(PARCLE_MODE_KEY, 'org');
  } catch (e) { /* storage disabled — session lives only for this load */ }
}

function parcleClearSession() {
  try {
    localStorage.removeItem(PARCLE_TOKEN_KEY);
    localStorage.removeItem(PARCLE_ACCOUNT_KEY);
    localStorage.removeItem(PARCLE_MODE_KEY);
  } catch (e) { /* ignore */ }
}

function parcleRestoreSession() {
  const token = parcleToken();
  const account = parcleAccount();
  if (token && account) return { token, account, mode: 'org' };
  return null;
}

async function _parcleParseDetail(res) {
  let msg = 'HTTP ' + res.status;
  try {
    const j = await res.json();
    if (j && j.detail) msg = typeof j.detail === 'string' ? j.detail : JSON.stringify(j.detail);
  } catch (e) { /* non-JSON body — keep generic message */ }
  return msg;
}

async function _parcleErr(res, stage) {
  const err = new Error(await _parcleParseDetail(res));
  err.status = res.status;
  err.stage = stage;
  return err;
}

// This console is organization-only. The backend web verify endpoint is
// kind-agnostic, so a personal (user) account could otherwise sign in and get
// mislabeled as org — failing later on org-only /skill/* calls. Reject it up
// front with a clear message and DON'T store a session (caller surfaces
// err.message; err.code='not_org').
function _parcleRequireOrg(account) {
  if (!account || account.kind !== 'org') {
    const err = new Error('This is a personal account — the Parcle console is for organization workspaces only.');
    err.code = 'not_org';
    throw err;
  }
}

// Best-effort revoke a freshly-issued token we've decided NOT to keep (e.g. a
// non-org account that web/verify signed in anyway). Fire-and-forget so the
// caller can throw immediately; without this the backend would leave a live,
// usable session behind even though we never stored it locally.
function _parcleRevokeToken(token) {
  if (!parcleApiBase() || !token) return;
  try {
    fetch(parcleApiBase() + '/auth/logout', {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + token },
    }).catch(() => { /* offline / already-expired — nothing else to do */ });
  } catch (e) { /* fetch threw synchronously — ignore */ }
}

// ── Existence probe ──
// register/web/verify CREATES an account when the email is new. To keep this
// console "sign into an existing org only, never create one", we first ask
// login/challenge whether an account exists: it returns a nonce (200) for a
// known email and 404 for an unknown one. We never complete the challenge (the
// nonce just expires); we only use the status as an existence check.
async function _parcleAccountExists(email) {
  const res = await fetch(parcleApiBase() + '/auth/login/challenge', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email }),
  });
  if (res.ok) return true;
  if (res.status === 404) return false;
  throw await _parcleErr(res, 'lookup');
}

// ── Deviceless web sign-in (email code) ──
// web/start stashes a pending registration (no device fields) and emails a code;
// web/verify confirms the code and returns a deviceless session — creating the
// account if new, else signing the existing one in. In dev mode (no mail
// provider) start/resend echo the code as `dev_code`.
async function parcleWebStart(email) {
  const res = await fetch(parcleApiBase() + '/auth/register/web/start', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, kind: 'org' }),
  });
  if (!res.ok) throw await _parcleErr(res, 'web-start');
  return await res.json(); // {ok, email, expires_in_seconds, dev_code?}
}

async function parcleWebResend(email) {
  const res = await fetch(parcleApiBase() + '/auth/register/resend', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email }),
  });
  if (!res.ok) throw await _parcleErr(res, 'web-resend');
  return await res.json();
}

async function parcleWebVerify(email, code) {
  const res = await fetch(parcleApiBase() + '/auth/register/web/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, code }),
  });
  if (!res.ok) throw await _parcleErr(res, 'web-verify');
  const data = await res.json();
  const account = data.account || null;
  if (!data.access_token || !account) throw new Error('Malformed verify response');
  // Org-only console. The backend already issued a usable token for this email
  // regardless of kind — if it's not an org account, revoke that token before
  // rejecting so no live session is left behind, then throw.
  if (!account || account.kind !== 'org') _parcleRevokeToken(data.access_token);
  _parcleRequireOrg(account);
  const session = { token: data.access_token, account, mode: 'org' };
  parcleSetSession(session);
  return session;
}

function parcleLogout() {
  // Clear the local session FIRST and synchronously: capture the token, wipe
  // storage now, then fire the server-side revoke as fire-and-forget. Awaiting
  // the request before clearing would race a fast sign-out → sign-in (the late
  // clear could wipe the freshly-issued token, leaving the next /skill/* call
  // tokenless → 401 → bounced back to login).
  const headers = parcleAuthHeaders();
  parcleClearSession();
  if (parcleApiBase() && headers.Authorization) {
    try {
      fetch(parcleApiBase() + '/auth/logout', { method: 'POST', headers })
        .catch(() => { /* offline / already-expired — local clear already done */ });
    } catch (e) { /* fetch threw synchronously — ignore, session is already cleared */ }
  }
}

// 401 channel: live clients call parcleNotifyUnauthorized() when a token is
// rejected; App subscribes to drop its session state and bounce to login.
const _parcleUnauthorizedSubs = new Set();
function parcleOnUnauthorized(cb) {
  _parcleUnauthorizedSubs.add(cb);
  return () => { _parcleUnauthorizedSubs.delete(cb); };
}
function parcleNotifyUnauthorized() {
  parcleClearSession();
  _parcleUnauthorizedSubs.forEach(cb => { try { cb(); } catch (e) { /* isolate */ } });
}

// ──────────────────────────────────────────────────────────────────────────
// LoginGate — shown by App in live mode until a session exists. Org-only and
// passwordless: enter the organization email and we send a 6-digit code.
//   • If no org account exists for the email → we refuse (orgs are provisioned
//     elsewhere; the console never creates them).
//   • Otherwise → email a code → verify → sign in. A personal (user) account is
//     rejected after verify with a clear message.
// ──────────────────────────────────────────────────────────────────────────
function LoginGate({ onLogin }) {
  const [view, setView] = useAuthState('email');   // 'email' | 'verify'
  const [email, setEmail] = useAuthState('');
  const [code, setCode] = useAuthState('');
  const [busy, setBusy] = useAuthState(false);
  const [error, setError] = useAuthState(null);
  const [info, setInfo] = useAuthState(null);
  const [resendIn, setResendIn] = useAuthState(0);  // seconds left before resend allowed

  // Resend cooldown ticker.
  useAuthEffect(() => {
    if (resendIn <= 0) return undefined;
    const t = setTimeout(() => setResendIn(resendIn - 1), 1000);
    return () => clearTimeout(t);
  }, [resendIn]);

  const clearMsgs = () => { setError(null); setInfo(null); };

  const inputStyle = {
    height: 36, padding: '0 10px', fontSize: 13,
    border: '1px solid var(--border)', borderRadius: 8,
    background: 'var(--surface)', color: 'var(--text-primary)', outline: 'none',
  };

  const codeSentMsg = (r, addr) =>
    r && r.dev_code ? `Dev mode — your code is ${r.dev_code}` : `We sent a 6-digit code to ${addr}.`;

  // Enter email → refuse unknown orgs, else email a code and move to verify.
  const submitEmail = async (e) => {
    e.preventDefault();
    if (busy) return;
    setBusy(true); clearMsgs();
    const addr = email.trim();
    try {
      const exists = await _parcleAccountExists(addr);
      if (!exists) {
        setError('No Parcle organization found for this email.');
        setBusy(false);
        return;
      }
      const r = await parcleWebStart(addr);
      setView('verify');
      setResendIn(60);
      setInfo(codeSentMsg(r, addr));
    } catch (err) {
      setError(err.message || 'Sign-in failed');
    }
    setBusy(false);
  };

  const submitVerify = async (e) => {
    e.preventDefault();
    if (busy) return;
    setBusy(true); clearMsgs();
    try {
      const session = await parcleWebVerify(email.trim(), code.trim());
      onLogin(session);
    } catch (err) {
      setError(err.message || 'Verification failed');
      setBusy(false);
    }
  };

  const resend = async () => {
    if (resendIn > 0 || busy) return;
    clearMsgs();
    const addr = email.trim();
    try {
      const r = await parcleWebResend(addr);
      setResendIn(60);
      setInfo(codeSentMsg(r, addr));
    } catch (err) {
      setError(err.message || 'Could not resend code');
    }
  };

  const title = view === 'verify' ? 'Verify your email' : 'Sign in to Parcle';
  const subtitle = view === 'verify'
    ? `Enter the code we sent to ${email.trim() || 'your email'}.`
    : 'Organization workspace.';
  const onSubmit = view === 'verify' ? submitVerify : submitEmail;

  const errorBox = error && (
    <div className="t-body" style={{
      fontSize: 12, color: 'var(--danger, #DC2626)',
      background: 'var(--danger-muted, rgba(220,38,38,.08))',
      border: '1px solid var(--danger-muted, rgba(220,38,38,.2))',
      borderRadius: 8, padding: '8px 10px',
    }}>{error}</div>
  );
  const infoBox = info && (
    <div className="t-body" style={{
      fontSize: 12, color: 'var(--accent-text, var(--text-secondary))',
      background: 'var(--accent-muted, rgba(0,0,0,.04))',
      border: '1px solid var(--border-subtle)',
      borderRadius: 8, padding: '8px 10px',
    }}>{info}</div>
  );

  return (
    <div style={{
      minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center',
      background: 'var(--bg)', padding: 24,
    }}>
      <form onSubmit={onSubmit} style={{
        width: 360, background: 'var(--surface-raised)',
        border: '1px solid var(--border)', borderRadius: 14,
        boxShadow: 'var(--shadow-2)', overflow: 'hidden',
      }}>
        <div style={{ padding: '24px 24px 16px', borderBottom: '1px solid var(--border-subtle)' }}>
          <div style={{
            width: 32, height: 32, borderRadius: 8,
            background: 'var(--accent)', color: 'var(--on-accent)',
            display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 14,
          }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
              <path d="M4 12a8 8 0 0116 0"/><circle cx="12" cy="12" r="3" fill="currentColor"/>
            </svg>
          </div>
          <h1 className="t-h1" style={{ margin: 0, fontSize: 20 }}>{title}</h1>
          <div className="text-secondary t-body" style={{ marginTop: 4, fontSize: 13 }}>{subtitle}</div>
        </div>

        <div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 14 }}>
          {view === 'email' && (
            <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              <span className="t-micro text-tertiary">Organization email</span>
              <input type="email" autoFocus required value={email} onChange={e => setEmail(e.target.value)}
                autoComplete="username" style={inputStyle}/>
            </label>
          )}

          {view === 'verify' && (
            <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              <span className="t-micro text-tertiary">Verification code</span>
              <input type="text" inputMode="numeric" autoFocus required value={code}
                onChange={e => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
                placeholder="123456"
                style={{ ...inputStyle, letterSpacing: 6, fontSize: 16, textAlign: 'center' }}/>
            </label>
          )}

          {infoBox}
          {errorBox}

          <button type="submit" className="btn" disabled={busy}
            style={{ width: '100%', justifyContent: 'center', height: 38, opacity: busy ? 0.7 : 1 }}>
            {busy
              ? (view === 'verify' ? 'Verifying…' : 'Continuing…')
              : (view === 'verify' ? 'Verify & continue' : 'Continue')}
          </button>

          {view === 'verify' && (
            <button type="button" className="btn ghost" disabled={resendIn > 0 || busy}
              onClick={resend}
              style={{ width: '100%', justifyContent: 'center', height: 34, fontSize: 12 }}>
              {resendIn > 0 ? `Resend code in ${resendIn}s` : 'Resend code'}
            </button>
          )}

          {view === 'verify' && (
            <div className="t-small text-tertiary" style={{ textAlign: 'center', fontSize: 12 }}>
              <a href="#" onClick={e => { e.preventDefault(); clearMsgs(); setCode(''); setView('email'); }}
                style={{ color: 'var(--accent-text, var(--accent))' }}>Use a different email</a>
            </div>
          )}
        </div>
      </form>
    </div>
  );
}

// PersonalEmpty — placeholder kept for back-compat with any remaining org-only
// route guards. Personal surfaces are deprecated in this console.
function PersonalEmpty({ label }) {
  return (
    <div style={{
      height: '100%', display: 'flex', flexDirection: 'column',
      alignItems: 'center', justifyContent: 'center', gap: 8, padding: 48,
    }}>
      <div style={{
        width: 44, height: 44, borderRadius: 12,
        background: 'var(--accent-muted)', color: 'var(--accent-text)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}>
        {typeof Icons !== 'undefined' && Icons.Sparkle ? <Icons.Sparkle size={20}/> : null}
      </div>
      <div className="t-h1" style={{ fontSize: 18, margin: 0 }}>{label || 'Coming soon'}</div>
      <div className="text-secondary t-body" style={{ fontSize: 13, textAlign: 'center', maxWidth: 320 }}>
        Nothing here yet.
      </div>
    </div>
  );
}

Object.assign(window, {
  parcleApiBase, parcleLiveAuthRequired,
  parcleToken, parcleAccount, parcleMode, parcleAuthHeaders,
  parcleSetSession, parcleClearSession, parcleRestoreSession,
  parcleWebStart, parcleWebResend, parcleWebVerify,
  parcleLogout,
  parcleOnUnauthorized, parcleNotifyUnauthorized,
  LoginGate, PersonalEmpty,
});
