// Google OAuth (OIDC) sign-in helpers.
//
// Entirely config-gated: if GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET /
// OAUTH_REDIRECT_URL aren't set, isConfigured() is false and the routes 404, so
// a deployment without Google SSO behaves exactly as before. google-auth-library
// is imported lazily so the dependency is only required when the feature is on.
//
// Flow: /auth/google redirects to Google's consent screen with a signed `state`;
// /auth/google/callback exchanges the code, verifies the ID token, enforces the
// allowed Workspace domain, and auto-provisions a viewer account on first login.

const SCOPES = ['openid', 'email', 'profile'];

export function isConfigured() {
  return !!(process.env.GOOGLE_CLIENT_ID
         && process.env.GOOGLE_CLIENT_SECRET
         && process.env.OAUTH_REDIRECT_URL);
}

export function allowedDomain() {
  return (process.env.GOOGLE_ALLOWED_DOMAIN || '').trim().toLowerCase() || null;
}

// Lazily build an OAuth2 client (throws a clear error if the dep is missing).
async function makeClient() {
  let OAuth2Client;
  try {
    ({ OAuth2Client } = await import('google-auth-library'));
  } catch {
    const err = new Error('google-auth-library is not installed');
    err.status = 500;
    throw err;
  }
  return new OAuth2Client({
    clientId:     process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    redirectUri:  process.env.OAUTH_REDIRECT_URL,
  });
}

// URL of Google's consent screen. `state` is an opaque anti-CSRF token we also
// stash in the session and re-check on callback.
export async function buildAuthUrl(state) {
  const client = await makeClient();
  return client.generateAuthUrl({
    access_type: 'online',
    scope: SCOPES,
    state,
    prompt: 'select_account',
    // If a Workspace domain is configured, hint Google to scope the picker to it.
    ...(allowedDomain() ? { hd: allowedDomain() } : {}),
  });
}

// Exchange the authorization code and verify the returned ID token. Returns the
// verified { sub, email, name, hd } payload. Throws { status } on any failure.
export async function exchangeAndVerify(code) {
  const client = await makeClient();
  const { tokens } = await client.getToken(code);
  if (!tokens.id_token) {
    const err = new Error('no id_token from Google'); err.status = 401; throw err;
  }
  const ticket = await client.verifyIdToken({
    idToken: tokens.id_token,
    audience: process.env.GOOGLE_CLIENT_ID,
  });
  const p = ticket.getPayload();
  if (!p || !p.sub) {
    const err = new Error('invalid id_token'); err.status = 401; throw err;
  }
  // Require an explicitly verified email — a missing/undefined claim is NOT
  // treated as verified, since the email drives account linking/provisioning.
  if (!p.email || p.email_verified !== true) {
    const err = new Error('email not verified'); err.status = 403; throw err;
  }
  const domain = allowedDomain();
  if (domain) {
    // ONLY trust Google's `hd` (hosted-domain) claim — it's present iff the
    // account is a member of a Google Workspace domain that Google itself
    // has verified. The email-suffix fallback we used to allow let any
    // non-Workspace account with a spoof-friendly email through; if a
    // GOOGLE_ALLOWED_DOMAIN is set, the operator means "only this Workspace,"
    // and consumer accounts (no hd) must be rejected.
    const hd = (p.hd || '').toLowerCase();
    if (hd !== domain) {
      const err = new Error('domain not allowed'); err.status = 403; throw err;
    }
  }
  return { sub: p.sub, email: p.email, name: p.name || p.email, hd: p.hd || null };
}
