/**
 * 2FA enrollment endpoints — auth-2fa (Task 7).
 *
 * PUBLIC (no authMiddleware — self-authorize via session cookie OR pending_2fa_setup token):
 *   POST /api/auth/2fa/enroll/start    — rate-limit gate (no-op, 204)
 *   POST /api/auth/2fa/enroll/verify   — complete enrollment via Firebase ID token
 *
 * PROTECTED (behind authMiddleware + require2FAIfEnforced — real session required):
 *   POST /api/auth/2fa/backup-codes/regenerate  — regenerate backup codes (requires current OTP)
 *   DELETE /api/auth/2fa                        — disable 2FA (requires current OTP)
 *
 * Enrollment endpoints are on BYPASS_2FA_GUARD so unenrolled users in enforcing
 * tenants can still reach them without a verified 2FA session.
 */
import { Hono } from 'hono'
import {
  hashToken,
  timingSafeEqual,
  verifyFirebaseIdToken,
  hashBackupCode,
  hashPhoneE164,
  phoneSuffix,
  generateBackupCodes,
} from '@zync/auth'
import {
  createDb,
  findUserById,
  findPendingToken,
  consumeMagicLinkToken,
  insertBackupCodes,
  consumeBackupCode,
  deleteUnusedBackupCodes,
  enable2FA,
  disable2FA,
  getPrimaryMembershipWithTenant,
  getTenant2FASettings,
} from '@zync/db/queries'
import type { TenantId, UserId } from '@zync/types'
import type { AppEnv } from '../../types'
import { enrollStartSchema, enrollVerifySchema, delete2FASchema, regenerateBackupCodesSchema } from '../../schemas/2fa'
import { issueSessionForTenantWith2FA, SessionCapExceededError } from '../../lib/issue-session'

/** Public enrollment routes — reachable without a valid session (pending token auth). */
export const twoFactorEnrollPublicRoute = new Hono<AppEnv>()

/** Protected enrollment routes — require authenticated session (behind authMiddleware). */
export const twoFactorEnrollRoute = new Hono<AppEnv>()

// ── POST /api/auth/2fa/enroll/start ─────────────────────────────────────────
// Rate-limit gate only — Firebase SMS is sent client-side.
// On BYPASS_2FA_GUARD so unenrolled users in enforcing tenants can reach it.
twoFactorEnrollPublicRoute.post('/2fa/enroll/start', async (c) => {
  const ip = c.req.header('CF-Connecting-IP') ?? 'unknown'
  const rl = await (async () => { try { const _r = await c.env.RATE_LIMITER_AUTH?.limit({ key: `2fa_enroll:${ip}` }); return _r ?? { success: true }; } catch { return { success: true }; } })()
  if (!rl.success) {
    return c.json({ error: 'Too many requests' }, 429)
  }

  const parsed = enrollStartSchema.safeParse(await c.req.json().catch(() => ({})))
  if (!parsed.success) {
    return c.json({ error: 'Invalid request', details: parsed.error.issues }, 400)
  }

  const session = c.get('session')
  if (!session || session.type !== 'user') {
    const sessionToken = parsed.data.session_token
    if (!sessionToken) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const db = createDb(c.env)
    const tokenHash = await hashToken(sessionToken)
    const pendingToken = await findPendingToken(db, tokenHash, 'pending_2fa_setup')
    if (!pendingToken) {
      return c.json({ error: 'session_expired', message: 'Session token expired or invalid' }, 410)
    }
  }

  return new Response(null, { status: 204 })
})

// ── POST /api/auth/2fa/enroll/verify ────────────────────────────────────────
// Completes enrollment: verifies Firebase ID token, stores phone hash + suffix,
// generates 8 backup codes, and if coming from a forced setup flow, issues the
// full JWT (pending_2fa_setup session token) after enrollment completes.
twoFactorEnrollPublicRoute.post('/2fa/enroll/verify', async (c) => {
  const ip = c.req.header('CF-Connecting-IP') ?? 'unknown'
  // Rate-limit: 3/10min/user (keyed by session or IP)
  const rl = await (async () => { try { const _r = await c.env.RATE_LIMITER_AUTH?.limit({ key: `2fa_enroll_verify:${ip}` }); return _r ?? { success: true }; } catch { return { success: true }; } })()
  if (!rl.success) {
    return c.json({ error: 'Too many requests' }, 429)
  }

  const parsed = enrollVerifySchema.safeParse(await c.req.json())
  if (!parsed.success) {
    return c.json({ error: 'Invalid request', details: parsed.error.issues }, 400)
  }
  const { firebase_id_token, session_token } = parsed.data

  // Verify Firebase ID token
  let firebaseUser: { uid: string; phoneNumber: string }
  try {
    firebaseUser = await verifyFirebaseIdToken(firebase_id_token, c.env)
  } catch {
    return c.json({ error: 'invalid_token', message: 'Firebase token verification failed' }, 401)
  }

  const db = createDb(c.env)

  // Determine which user is enrolling:
  // - Regular session: c.get('session').sub
  // - Forced setup (pending_2fa_setup token): validate token, get userId
  let userId: UserId
  let isForcedSetup = false
  let forcedSetupTokenHash: string | null = null

  const session = c.get('session')
  if (session && session.type === 'user') {
    userId = session.sub as UserId
  } else if (session_token) {
    // Forced setup flow: validate pending_2fa_setup token
    forcedSetupTokenHash = await hashToken(session_token)
    const pendingToken = await findPendingToken(db, forcedSetupTokenHash, 'pending_2fa_setup')
    if (!pendingToken) {
      return c.json({ error: 'session_expired', message: 'Session token expired or invalid' }, 410)
    }
    userId = pendingToken.userId as UserId
    isForcedSetup = true
  } else {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const membership = await getPrimaryMembershipWithTenant(db, userId)
  if (!membership) {
    return c.json({ error: 'No active workspace' }, 403)
  }

  // Compute phone hash + suffix
  const phoneHash = await hashPhoneE164(firebaseUser.phoneNumber)
  const suffix = phoneSuffix(firebaseUser.phoneNumber)

  // Enable 2FA on the user account
  await enable2FA(db, {
    userId,
    tenantId: membership.tenantId as TenantId,
    phoneHash,
    phoneSuffix: suffix,
    actorIp: ip,
  })

  // Generate 8 backup codes and store their hashes
  const plainCodes = generateBackupCodes(8)
  const codeHashes = await Promise.all(plainCodes.map(hashBackupCode))
  await insertBackupCodes(db, userId, codeHashes, membership.tenantId, ip)

  // If this was a forced setup flow, mark the temp token used and issue full JWT
  if (isForcedSetup && forcedSetupTokenHash) {
    const consumed = await consumeMagicLinkToken(db, forcedSetupTokenHash)
    if (!consumed) {
      return c.json({ error: 'session_expired', message: 'Session token expired or invalid' }, 410)
    }
    const tenant2fa = await getTenant2FASettings(db, membership.tenantId as TenantId)
    const enforce2fa = tenant2fa?.enforce2fa ?? false
    let issued
    try {
      issued = await issueSessionForTenantWith2FA(
        c,
        db,
        userId,
        membership.tenantId as TenantId,
        { enforce2fa, twoFactorVerified: true },
      )
    } catch (error) {
      if (error instanceof SessionCapExceededError) {
        return c.json({ error: error.message }, 429)
      }
      throw error
    }
    if (!issued) {
      return c.json({ error: 'No active workspace' }, 403)
    }
    // Return backup codes + session expiry (full JWT cookies already set)
    return c.json({ backup_codes: plainCodes, expiresAt: issued.expiresAt }, 200)
  }

  return c.json({ backup_codes: plainCodes }, 200)
})

// ── POST /api/auth/2fa/backup-codes/regenerate ──────────────────────────────
// Requires a valid current OTP (Firebase ID token of current phone).
// Invalidates all prior unused codes and issues 8 new ones.
twoFactorEnrollRoute.post('/2fa/backup-codes/regenerate', async (c) => {
  const ip = c.req.header('CF-Connecting-IP') ?? 'unknown'
  const rl = await (async () => { try { const _r = await c.env.RATE_LIMITER_AUTH?.limit({ key: `2fa_regen:${ip}` }); return _r ?? { success: true }; } catch { return { success: true }; } })()
  if (!rl.success) {
    return c.json({ error: 'Too many requests' }, 429)
  }

  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.two_factor_verified) {
    return c.json({ error: 'Unauthorized — 2FA must be verified' }, 401)
  }

  const parsed = regenerateBackupCodesSchema.safeParse(await c.req.json())
  if (!parsed.success) {
    return c.json({ error: 'Invalid request', details: parsed.error.issues }, 400)
  }
  const { current_otp } = parsed.data

  // current_otp is a Firebase ID token for OTP re-verification
  let firebaseUser: { uid: string; phoneNumber: string }
  try {
    firebaseUser = await verifyFirebaseIdToken(current_otp, c.env)
  } catch {
    return c.json({ error: 'invalid_token', message: 'OTP verification failed' }, 401)
  }

  const db = createDb(c.env)
  const userId = session.sub as UserId

  const user = await findUserById(db, userId)
  if (!user?.twoFactorPhone) {
    return c.json({ error: 'invalid_token', message: '2FA not configured' }, 401)
  }

  // Verify phone matches
  const computedHash = await hashPhoneE164(firebaseUser.phoneNumber)
  if (!timingSafeEqual(computedHash, user.twoFactorPhone)) {
    return c.json({ error: 'invalid_token', message: 'Phone number does not match' }, 401)
  }

  const membership = await getPrimaryMembershipWithTenant(db, userId)
  if (!membership) {
    return c.json({ error: 'No active workspace' }, 403)
  }

  // Delete old unused codes and generate new ones
  await deleteUnusedBackupCodes(db, userId, membership.tenantId, ip)

  const plainCodes = generateBackupCodes(8)
  const codeHashes = await Promise.all(plainCodes.map(hashBackupCode))
  await insertBackupCodes(db, userId, codeHashes, membership.tenantId, ip)

  return c.json({ backup_codes: plainCodes }, 200)
})

// ── DELETE /api/auth/2fa ─────────────────────────────────────────────────────
// Disable 2FA. Requires current OTP (Firebase ID token) or a valid backup code.
twoFactorEnrollRoute.delete('/2fa', async (c) => {
  const ip = c.req.header('CF-Connecting-IP') ?? 'unknown'

  const session = c.get('session')
  if (!session || session.type !== 'user') {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const parsed = delete2FASchema.safeParse(await c.req.json())
  if (!parsed.success) {
    return c.json({ error: 'Invalid request', details: parsed.error.issues }, 400)
  }
  const { current_otp } = parsed.data

  const db = createDb(c.env)
  const userId = session.sub as UserId

  const user = await findUserById(db, userId)
  if (!user?.twoFactorEnabled || !user.twoFactorPhone) {
    return c.json({ error: '2FA not enabled' }, 400)
  }

  // Verify current OTP — try Firebase ID token first, then backup code fallback
  let otpVerified = false

  // Try Firebase ID token verification
  try {
    const firebaseUser = await verifyFirebaseIdToken(current_otp, c.env)
    const computedHash = await hashPhoneE164(firebaseUser.phoneNumber)
    if (timingSafeEqual(computedHash, user.twoFactorPhone)) {
      otpVerified = true
    }
  } catch {
    // Not a Firebase ID token — try as backup code
    const codeHash = await hashBackupCode(current_otp)
    const membership = await getPrimaryMembershipWithTenant(db, userId)
    if (membership) {
      const consumed = await consumeBackupCode(db, userId, codeHash, membership.tenantId, ip)
      if (consumed) otpVerified = true
    }
  }

  if (!otpVerified) {
    return c.json({ error: 'invalid_token', message: 'Invalid OTP or backup code' }, 401)
  }

  const membership = await getPrimaryMembershipWithTenant(db, userId)
  if (!membership) {
    return c.json({ error: 'No active workspace' }, 403)
  }

  // Disable 2FA and clear all unused backup codes
  await disable2FA(db, {
    userId,
    tenantId: membership.tenantId as TenantId,
    actorIp: ip,
  })
  await deleteUnusedBackupCodes(db, userId, membership.tenantId, ip)

  return new Response(null, { status: 204 })
})
