// ── Mobile SSO authorization bridge — logic layer ──────────────────────────
//
// Bridges the browser SSO redirect flow to the native app. A `/start` seeds a
// `mobile_auth_sessions` row carrying the app-supplied PKCE challenge, the app's
// CSRF `state`, and the exact callback URI. The SSO callback (once the account is
// resolved) mints a single-use authorization CODE tied to that session; the app
// redeems the code + its PKCE verifier at `/exchange` for bearer tokens.
//
// Secrets: the raw authorization code is generated here and returned ONCE to the
// caller (to place in the redirect URL); only its sha256 hash is persisted — the
// raw code never touches the DB. The PKCE code_challenge is already a hash.

const crypto = require('crypto')

const db = require('./mobileAuthBridge.db')

// TTLs. A start→callback→exchange round-trip is quick, but the callback may route
// through the TOTP form, so give the session a little room; the code itself is
// very short-lived.
const SESSION_TTL_MS = 10 * 60 * 1000 // 10 min — one redirect round-trip incl. TOTP
const CODE_TTL_MS = 5 * 60 * 1000 //     5 min — the app redeems immediately

// sha256 hex of a raw token — the persisted representation of an authorization code.
function hashCode(raw) {
  return crypto.createHash('sha256').update(String(raw)).digest('hex')
}

// Create a pending bridge session. Returns { sessionId } (opaque uuid) which the
// caller embeds in the signed sso_tx so the callback can find this row. Best-effort
// prune keeps the table trimmed without a cron (same approach as revoked_sessions).
async function startSession({ provider, codeChallenge, redirectUri, state, now = Date.now() }) {
  const sessionId = crypto.randomUUID()
  await db.insertSession({
    sessionId,
    provider,
    codeChallenge,
    redirectUri,
    state,
    expiresAt: new Date(now + SESSION_TTL_MS),
  })
  db.pruneExpired().catch(() => {}) // opportunistic; never blocks the flow
  return { sessionId }
}

// Fetch a bridge session row by session_id (or null).
async function getSession(sessionId) {
  if (!sessionId) return null
  return db.getSession(sessionId)
}

// Mint the one-time authorization code for a resolved account. Marks the owning
// session `completed`; returns { code } (the RAW code, to place in the redirect
// URL) or null if the session isn't pending/eligible. 256 bits of entropy — well
// above the ≥128-bit floor — url-safe and opaque.
async function issueAuthCode({ sessionId, userId, now = Date.now() }) {
  const completed = await db.completeSession(sessionId, userId)
  if (!completed) return null // not pending / expired / already used
  const code = crypto.randomBytes(32).toString('base64url')
  await db.insertCode({
    codeHash: hashCode(code),
    userId,
    sessionId,
    expiresAt: new Date(now + CODE_TTL_MS),
  })
  db.pruneExpired().catch(() => {})
  return { code }
}

// Look up a redeemable code row (unused + unexpired) for a raw code, or null.
async function findRedeemableCode(rawCode) {
  if (!rawCode) return null
  return db.findValidCode(hashCode(rawCode))
}

// Atomically burn a code (single-use). Returns true iff THIS call was the one that
// marked it used — a concurrent double-redeem gets false on the loser.
async function consumeCode(rawCode) {
  const changed = await db.markCodeUsed(hashCode(rawCode))
  return changed > 0
}

// Flag that the user ticked "trust this device" on the Custom Tab TOTP form. The
// exchange step reads this to decide whether to mint the app's own trust token.
// Returns true iff the session was still eligible to be flagged.
async function markTrustRequested(sessionId) {
  if (!sessionId) return false
  return (await db.setTrustDevice(sessionId)) > 0
}

// Mark a session fully consumed after a successful exchange.
async function finishSession(sessionId) {
  return db.consumeSession(sessionId)
}

// Drop expired/used rows from both tables.
async function pruneExpired() {
  return db.pruneExpired()
}

module.exports = {
  SESSION_TTL_MS,
  CODE_TTL_MS,
  startSession,
  getSession,
  issueAuthCode,
  findRedeemableCode,
  consumeCode,
  markTrustRequested,
  finishSession,
  pruneExpired,
}
