/**
 * 2FA verification endpoints — auth-2fa (Task 6).
 *
 * POST /api/auth/2fa/verify      — Firebase OTP verification
 * POST /api/auth/2fa/backup-code — Backup code verification
 *
 * Both endpoints:
 *   - Validate the pending session_token (magic_link_tokens with pending_2fa purpose)
 *   - Verify the second factor
 *   - Issue the full JWT + refresh token with two_factor_verified=true
 *   - Optionally set zync_device_trust cookie (30-day remember-device)
 *
 * Rate limits: 5/10min/IP via RATE_LIMITER_AUTH.
 * Backup-code attempt limit: 5 attempts per session_token (KV-tracked), then 429.
 */
import { Hono } from 'hono'
import { setCookie } from 'hono/cookie'
import {
  hashToken,
  generateOpaqueToken,
  timingSafeEqual,
  verifyFirebaseIdToken,
  hashBackupCode,
  hashPhoneE164,
  DEVICE_TRUST_COOKIE_NAME,
  DEVICE_TRUST_TTL_SECONDS,
  BACKUP_CODE_ATTEMPT_KEY_PREFIX,
  BACKUP_CODE_MAX_ATTEMPTS,
} from '@zync/auth'
import {
  createDb,
  findUserById,
  findPendingToken,
  consumeMagicLinkToken,
  consumeBackupCode,
  createTrustedDevice,
  getPrimaryMembershipWithTenant,
  getTenant2FASettings,
} from '@zync/db/queries'
import type { TenantId, UserId } from '@zync/types'
import type { AppEnv } from '../../types'
import { verify2FASchema, backupCodeSchema } from '../../schemas/2fa'
import { issueSessionForTenantWith2FA, SessionCapExceededError } from '../../lib/issue-session'

const DEVICE_TRUST_COOKIE_BASE = {
  httpOnly: true,
  secure: true,
  sameSite: 'Strict' as const,
  domain: '.zync.is',
  path: '/',
  maxAge: DEVICE_TRUST_TTL_SECONDS,
}

export const twoFactorVerifyRoute = new Hono<AppEnv>()

// ── POST /api/auth/2fa/verify ────────────────────────────────────────────────
twoFactorVerifyRoute.post('/2fa/verify', 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_verify:${ip}` }); return _r ?? { success: true }; } catch { return { success: true }; } })()
  if (!rl.success) {
    return c.json({ error: 'Too many requests' }, 429)
  }

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

  const db = createDb(c.env)

  // Validate the pending session token
  const tokenHash = await hashToken(session_token)
  const pendingToken = await findPendingToken(db, tokenHash, 'pending_2fa')
  if (!pendingToken) {
    return c.json({ error: 'session_expired', message: 'Session token expired or invalid' }, 410)
  }

  // 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)
  }

  // Verify phone hash matches stored value (timing-safe)
  const user = await findUserById(db, pendingToken.userId as UserId)
  if (!user?.twoFactorPhone) {
    return c.json({ error: 'invalid_token', message: '2FA not configured for this account' }, 401)
  }

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

  // Consume the pending token (atomic — concurrent verify requests single-winner).
  const consumed = await consumeMagicLinkToken(db, tokenHash)
  if (!consumed) {
    return c.json({ error: 'session_expired', message: 'Session token expired or invalid' }, 410)
  }

  // Determine tenant (from user's primary membership)
  const membership = await getPrimaryMembershipWithTenant(db, pendingToken.userId as UserId)
  if (!membership) {
    return c.json({ error: 'No active workspace' }, 403)
  }

  const tenant2fa = await getTenant2FASettings(db, membership.tenantId as TenantId)
  const enforce2fa = tenant2fa?.enforce2fa ?? false

  let issued
  try {
    issued = await issueSessionForTenantWith2FA(
      c,
      db,
      pendingToken.userId as 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)
  }

  // Remember device if requested and tenant allows it
  if (trust_device && !(tenant2fa?.disable2faRememberDevice ?? false)) {
    const deviceToken = generateOpaqueToken()
    const deviceTokenHash = await hashToken(deviceToken)
    const expiresAt = new Date(Date.now() + DEVICE_TRUST_TTL_SECONDS * 1000)
    await createTrustedDevice(
      db,
      {
        userId: pendingToken.userId as UserId,
        tenantId: membership.tenantId,
        tokenHash: deviceTokenHash,
        userAgent: c.req.header('User-Agent'),
        ipAddress: ip,
        expiresAt,
      },
      { actorIp: ip },
    )
    setCookie(c, DEVICE_TRUST_COOKIE_NAME, deviceToken, DEVICE_TRUST_COOKIE_BASE)
  }

  return c.json({ expiresAt: issued.expiresAt }, 200)
})

// ── POST /api/auth/2fa/backup-code ───────────────────────────────────────────
twoFactorVerifyRoute.post('/2fa/backup-code', 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_backup:${ip}` }); return _r ?? { success: true }; } catch { return { success: true }; } })()
  if (!rl.success) {
    return c.json({ error: 'Too many requests' }, 429)
  }

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

  // Check attempt count in KV (per session_token)
  const attemptKey = `${BACKUP_CODE_ATTEMPT_KEY_PREFIX}${session_token}`
  const attemptsRaw = await c.env.KV.get(attemptKey)
  const attempts = attemptsRaw ? parseInt(attemptsRaw, 10) : 0

  if (attempts >= BACKUP_CODE_MAX_ATTEMPTS) {
    return c.json({ error: 'Too many attempts', message: 'This session has been locked' }, 429)
  }

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

  // Get tenant for audit
  const membership = await getPrimaryMembershipWithTenant(db, pendingToken.userId as UserId)
  if (!membership) {
    return c.json({ error: 'No active workspace' }, 403)
  }

  // Verify backup code (timing-safe via consumeBackupCode)
  const codeHash = await hashBackupCode(backup_code)
  const consumed = await consumeBackupCode(
    db,
    pendingToken.userId as UserId,
    codeHash,
    membership.tenantId,
    ip,
  )

  if (!consumed) {
    // Increment attempt counter
    const newAttempts = attempts + 1
    const ttlMs = pendingToken.expiresAt.getTime() - Date.now()
    const ttlSecs = Math.max(1, Math.ceil(ttlMs / 1000))

    if (newAttempts >= BACKUP_CODE_MAX_ATTEMPTS) {
      // Invalidate the session token on reaching the limit
      await consumeMagicLinkToken(db, tokenHash)
      await c.env.KV.put(attemptKey, String(newAttempts), { expirationTtl: ttlSecs })
      return c.json(
        { error: 'Too many attempts', message: 'Session locked after too many failed attempts' },
        429,
      )
    }

    await c.env.KV.put(attemptKey, String(newAttempts), { expirationTtl: ttlSecs })
    return c.json({ error: 'invalid_code', message: 'Invalid backup code' }, 401)
  }

  // Code consumed — mark session token used and clear attempt counter
  const sessionConsumed = await consumeMagicLinkToken(db, tokenHash)
  if (!sessionConsumed) {
    return c.json({ error: 'session_expired', message: 'Session token expired or invalid' }, 410)
  }
  await c.env.KV.delete(attemptKey)

  const tenant2fa = await getTenant2FASettings(db, membership.tenantId as TenantId)
  const enforce2fa = tenant2fa?.enforce2fa ?? false

  let issued
  try {
    issued = await issueSessionForTenantWith2FA(
      c,
      db,
      pendingToken.userId as 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)
  }

  // Remember device if requested and tenant allows it
  if (trust_device && !(tenant2fa?.disable2faRememberDevice ?? false)) {
    const deviceToken = generateOpaqueToken()
    const deviceTokenHash = await hashToken(deviceToken)
    const expiresAt = new Date(Date.now() + DEVICE_TRUST_TTL_SECONDS * 1000)
    await createTrustedDevice(
      db,
      {
        userId: pendingToken.userId as UserId,
        tenantId: membership.tenantId,
        tokenHash: deviceTokenHash,
        userAgent: c.req.header('User-Agent'),
        ipAddress: ip,
        expiresAt,
      },
      { actorIp: ip },
    )
    setCookie(c, DEVICE_TRUST_COOKIE_NAME, deviceToken, DEVICE_TRUST_COOKIE_BASE)
  }

  return c.json({ expiresAt: issued.expiresAt }, 200)
})
