/**
 * Backup-code query helpers — auth-2fa.
 *
 * Backup codes for two-factor authentication recovery. Only hashes are stored;
 * plaintext codes are returned to the user once at enrollment and never again.
 *
 * `consumeBackupCode` uses an atomic UPDATE...WHERE to claim exactly one unused
 * code per call, preventing double-use under concurrent requests.
 */
import { and, eq, isNull, sql } from 'drizzle-orm'
import type { UserId } from '@zync/types'
import type { Db } from '../client'
import { user2faBackupCodes } from '../schema'
import { auditLog } from './_audit-forward'

/**
 * Insert 8 (or N) backup-code hashes for a user. Caller is responsible for
 * generating and hashing the plaintext codes before calling this.
 * Wrapped in a transaction with an audit row (tenant-scoped mutation).
 */
export async function insertBackupCodes(
  db: Db,
  userId: UserId,
  codeHashes: string[],
  tenantId: string,
  actorIp?: string | null,
  requestId?: string | null,
): Promise<void> {
  await db.transaction(async (tx) => {
    await tx.insert(user2faBackupCodes).values(
      codeHashes.map((codeHash) => ({ userId, codeHash })),
    )
    await tx.insert(auditLog).values({
      tenantId,
      actorId: userId,
      actorType: 'user',
      entityType: 'two_factor',
      entityId: userId,
      action: 'two_factor.backup_codes.generated',
      ip: actorIp ?? null,
      requestId: requestId ?? null,
    })
  })
}

/**
 * Atomically consume an unused backup code matching `codeHash` for `userId`.
 * Sets `used_at = now()` on success.
 * Returns true if a code was consumed, false if no unused matching code exists.
 *
 * This is a single UPDATE...WHERE — if two requests race, only one wins.
 */
export async function consumeBackupCode(
  db: Db,
  userId: UserId,
  codeHash: string,
  tenantId: string,
  actorIp?: string | null,
  requestId?: string | null,
): Promise<boolean> {
  return db.transaction(async (tx) => {
    const result = await tx
      .update(user2faBackupCodes)
      .set({ usedAt: new Date() })
      .where(
        and(
          eq(user2faBackupCodes.userId, userId),
          eq(user2faBackupCodes.codeHash, codeHash),
          isNull(user2faBackupCodes.usedAt),
        ),
      )
      .returning({ id: user2faBackupCodes.id })

    if (result.length === 0) return false

    await tx.insert(auditLog).values({
      tenantId,
      actorId: userId,
      actorType: 'user',
      entityType: 'two_factor',
      entityId: userId,
      action: 'two_factor.backup_code.used',
      ip: actorIp ?? null,
      requestId: requestId ?? null,
    })
    return true
  })
}

/** Count unused backup codes for a user. */
export async function countUnusedBackupCodes(db: Db, userId: UserId): Promise<number> {
  const [row] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(user2faBackupCodes)
    .where(and(eq(user2faBackupCodes.userId, userId), isNull(user2faBackupCodes.usedAt)))
  return row?.count ?? 0
}

/**
 * Delete all unused backup codes for a user.
 * Called before regenerating codes, or when 2FA is disabled.
 * Wrapped in transaction with audit row.
 */
export async function deleteUnusedBackupCodes(
  db: Db,
  userId: UserId,
  tenantId: string,
  actorIp?: string | null,
  requestId?: string | null,
): Promise<void> {
  await db.transaction(async (tx) => {
    await tx
      .delete(user2faBackupCodes)
      .where(and(eq(user2faBackupCodes.userId, userId), isNull(user2faBackupCodes.usedAt)))

    await tx.insert(auditLog).values({
      tenantId,
      actorId: userId,
      actorType: 'user',
      entityType: 'two_factor',
      entityId: userId,
      action: 'two_factor.backup_codes.deleted',
      ip: actorIp ?? null,
      requestId: requestId ?? null,
    })
  })
}
