// ── SSO transaction state (CSRF + PKCE) ────────────────────────────────────
//
// An OAuth redirect flow spans two requests (start → callback) with a hop to the
// IdP in between, so we must carry state across it safely:
//
//   - CSRF: an attacker must not be able to forge a callback. We bind the flow to
//     the user's browser with a short-lived, signed, httpOnly cookie (sso_tx) and
//     put only an opaque `nonce` in the URL `state` param. The callback requires
//     state === cookie.nonce, so a callback not initiated by this browser fails.
//   - PKCE: the code_verifier is generated at start, kept ONLY in the httpOnly
//     cookie (never in the URL/logs), and sent to the token endpoint at callback.
//
// The cookie is a signed JWT (reusing the app's JWT signing) with a tight TTL, so
// it cannot be tampered with and expires quickly if a flow is abandoned.

const crypto = require('crypto')
const token = require('./token')

const TX_COOKIE = 'sso_tx'
const TX_TTL = '10m' // a login round-trip is quick; abandon after 10 minutes

// Second leg of an SSO login for an account that has TOTP enabled. The callback
// authenticated the user with the IdP but must NOT bypass their second factor
// (see issue #31), so instead of minting a session it stages this signed,
// httpOnly cookie and routes the browser through the TOTP form — mirroring the
// local password→TOTP gate. TTL matches the local challenge window.
const TOTP_COOKIE = 'sso_totp'
const TOTP_TTL = '5m'

// base64url of random bytes — used for the nonce and the PKCE verifier.
function randomUrlSafe(bytes = 32) {
  return crypto.randomBytes(bytes).toString('base64url')
}

// PKCE S256 challenge for a given verifier.
function codeChallengeFor(verifier) {
  return crypto.createHash('sha256').update(verifier).digest('base64url')
}

// Create a transaction: returns { nonce, verifier, codeChallenge, txToken }.
// `data` = { provider, mode ('login'|'link'), linkUserId?, returnTo? }.
function createTx(data) {
  const nonce = randomUrlSafe(16)
  const verifier = randomUrlSafe(32)
  const codeChallenge = codeChallengeFor(verifier)
  const txToken = token.signToken(
    { id: 'sso' }, // subject is irrelevant; this is a flow token, not a session
    { nonce, verifier, ...data, kind: 'sso_tx' },
    { expiresIn: TX_TTL },
  )
  return { nonce, verifier, codeChallenge, txToken }
}

// Verify a tx cookie against the state param. Returns the tx payload
// ({ nonce, verifier, provider, mode, ... }) or null if missing/expired/mismatched.
function verifyTx(txToken, stateNonce) {
  if (!txToken || !stateNonce) return null
  const decoded = token.verifyToken(txToken)
  if (!decoded || decoded.kind !== 'sso_tx') return null
  // Constant-time compare so a mismatch can't be timed.
  const a = Buffer.from(String(decoded.nonce))
  const b = Buffer.from(String(stateNonce))
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null
  return decoded
}

// Stage the pending second factor for an SSO login. Carries the context the
// callback already resolved (userId, provider, authMethod, returnTo) so that
// presenting a valid code alone finishes the login. It is deliberately NOT a
// session: `stage: 'totp'` makes session validation reject it (same marker the
// local TOTP challenge uses), and `kind: 'sso_totp'` both reinforces that and
// scopes it to the SSO completion endpoint.
//
// `mobileSessionId` is present only for a mobile SSO bridge flow (mode 'mobile'):
// it threads the bridge session through the TOTP form so that, on a correct code,
// the completion mints an authorization code + deep-links back to the app instead
// of setting a web session cookie. Absent for ordinary web SSO.
function createTotpPending({ userId, provider, authMethod, returnTo, mobileSessionId }) {
  return token.signToken(
    { id: userId }, // subject only; identity is re-loaded fresh when the code is verified
    { stage: 'totp', kind: 'sso_totp', provider, authMethod, returnTo, mobileSessionId },
    { expiresIn: TOTP_TTL },
  )
}

// Verify a pending-TOTP cookie. Returns the payload
// ({ id, provider, authMethod, returnTo, ... }) or null if missing/expired/wrong-kind.
function verifyTotpPending(pendingToken) {
  if (!pendingToken) return null
  const decoded = token.verifyToken(pendingToken)
  if (!decoded || decoded.stage !== 'totp' || decoded.kind !== 'sso_totp') return null
  return decoded
}

module.exports = {
  TX_COOKIE,
  TX_TTL,
  TOTP_COOKIE,
  TOTP_TTL,
  createTx,
  verifyTx,
  createTotpPending,
  verifyTotpPending,
  codeChallengeFor,
  randomUrlSafe,
}
