import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'

/**
 * HTTPS callable that creates a secure invite code.
 *
 * Issue #9 / review2.md Risk #1 fix: clients are no longer allowed to create
 * invites directly. 6-character document IDs are enumerable, so a direct client
 * write would expose pending invites to scanning.
 *
 * Request body: { code?: string, wrappedCoupleKey?: string, kdfSalt?: string, kdfParams?: string, encryptedRecoveryPhrase?: string }
 * - code: client-supplied 6-character code (Android). The server validates uniqueness and
 *   returns an error if taken so the client can retry with a new code. If omitted (iOS),
 *   the server generates the code.
 * - wrappedCoupleKey: base64-encoded couple key wrapped by the inviter's KDF
 * - kdfSalt: base64 KDF salt
 * - kdfParams: KDF parameter tag (e.g. argon2id;v=19;m=47104;t=3;p=1)
 * - encryptedRecoveryPhrase: Argon2id+AES-GCM blob produced by the client using the invite
 *   code as the KDF input. The server stores it opaquely and never sees the plaintext phrase.
 *
 * Strict E2EE: code, wrappedCoupleKey, kdfSalt, kdfParams, and encryptedRecoveryPhrase are
 * all required. There is no plaintext-couple path.
 *
 * Response: { code: string, expiresAt: Timestamp }
 *
 * Operations (all via Admin SDK, so Firestore rules are bypassed):
 *  1. Verify caller is authenticated and not already paired.
 *  2. Rate-limit the caller to 5 invite creations per rolling hour.
 *  3. Use client-supplied code or generate one server-side; validate uniqueness via transaction.
 *  4. Write the invite document with a 24-hour TTL.
 */

const CODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
const CODE_LENGTH = 6
const INVITE_TTL_MS = 24 * 60 * 60 * 1000
const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000
const RATE_LIMIT_MAX = 5

function generateCode(): string {
  let code = ''
  const randomValues = Buffer.alloc(CODE_LENGTH)
  // crypto.randomBytes is synchronous and suitable for Cloud Functions.
  require('crypto').randomFillSync(randomValues)
  for (let i = 0; i < CODE_LENGTH; i++) {
    code += CODE_CHARS[randomValues[i] % CODE_CHARS.length]
  }
  return code
}

export const createInviteCallable = functions.https.onCall(async (data: any, context) => {
  const callerId = context.auth?.uid
  if (!callerId) {
    throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.')
  }
  if (!context.app) {
    throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.')
  }

  const db = admin.firestore()

  // Caller must not already be paired.
  const callerDoc = await db.collection('users').doc(callerId).get()
  if (callerDoc.exists && callerDoc.data()?.coupleId != null) {
    throw new functions.https.HttpsError('failed-precondition', 'Caller is already paired.')
  }

  const callerDisplayName = callerDoc.data()?.displayName as string | undefined

  // Rate limit: count invites created by this user in the last hour.
  const now = admin.firestore.Timestamp.now()
  const windowStart = admin.firestore.Timestamp.fromMillis(now.toMillis() - RATE_LIMIT_WINDOW_MS)
  const recentInvitesQuery = db
    .collection('invites')
    .where('inviterUserId', '==', callerId)
    .where('createdAt', '>=', windowStart)
    .orderBy('createdAt', 'desc')
    .limit(RATE_LIMIT_MAX + 1)

  const recentInvites = await recentInvitesQuery.get()
  if (recentInvites.size >= RATE_LIMIT_MAX) {
    throw new functions.https.HttpsError('resource-exhausted', 'Too many invites created. Try again later.')
  }

  const clientCode = data?.code as string | undefined
  const wrappedCoupleKey = data?.wrappedCoupleKey as string | undefined
  const kdfSalt = data?.kdfSalt as string | undefined
  const kdfParams = data?.kdfParams as string | undefined
  const encryptedRecoveryPhrase = data?.encryptedRecoveryPhrase as string | undefined

  // Strict E2EE: every couple must be created with a wrapped couple key. The client-supplied
  // code, wrapped key, KDF salt/params, and encrypted recovery phrase are all required.
  if (!clientCode) {
    throw new functions.https.HttpsError('invalid-argument', 'code is required.')
  }
  // Security review Batch 2: validate the code is exactly the 6-char Crockford-style
  // alphabet the client generates (CODE_CHARS, no I/O/0/1). Rejects malformed/oversized
  // codes and anything that could be abused as the document id.
  if (!/^[A-HJ-NP-Z2-9]{6}$/.test(clientCode)) {
    throw new functions.https.HttpsError('invalid-argument', 'code must be 6 valid characters.')
  }
  if (wrappedCoupleKey == null || kdfSalt == null || kdfParams == null || encryptedRecoveryPhrase == null) {
    throw new functions.https.HttpsError(
      'invalid-argument',
      'E2EE fields (wrappedCoupleKey, kdfSalt, kdfParams, encryptedRecoveryPhrase) are required.'
    )
  }

  const expiresAt = admin.firestore.Timestamp.fromMillis(now.toMillis() + INVITE_TTL_MS)

  // Android supplies its own code (used as the KDF input for phrase encryption, so the server
  // must use it as-is). iOS omits the code; the server generates one in that case.
  // Either way, validate uniqueness via transaction and return an error on collision so the
  // client can retry with a fresh code.
  let inviteRef: admin.firestore.DocumentReference | null = null
  let code: string | null = null

  const candidates = clientCode ? [clientCode] : Array.from({ length: 10 }, generateCode)

  for (const candidate of candidates) {
    const candidateRef = db.collection('invites').doc(candidate)

    // eslint-disable-next-line no-await-in-loop
    const created = await db.runTransaction(async (tx) => {
      const snap = await tx.get(candidateRef)
      if (snap.exists) return false
      tx.set(candidateRef, {
        code: candidate,
        inviterUserId: callerId,
        inviterDisplayName: callerDisplayName ?? null,
        status: 'pending',
        createdAt: admin.firestore.FieldValue.serverTimestamp(),
        expiresAt,
        usedAt: null,
        usedByUserId: null,
        wrappedCoupleKey: wrappedCoupleKey ?? null,
        kdfSalt: kdfSalt ?? null,
        kdfParams: kdfParams ?? null,
        encryptedRecoveryPhrase: encryptedRecoveryPhrase ?? null,
      })
      return true
    })

    if (created) {
      code = candidate
      inviteRef = candidateRef
      break
    }
  }

  if (!code || !inviteRef) {
    // Client-supplied code collided; the Android client will retry with a new code.
    throw new functions.https.HttpsError('already-exists', 'Invite code is already taken. Please try again.')
  }

  // Write a server-side audit log entry for the inviter. This is not read by
  // clients and supports the rate-limit count as well as future abuse review.
  try {
    // Do not store the raw code — it is the KDF seed for the couple's recovery phrase.
    await db.collection('users').doc(callerId).collection('notification_queue').add({
      type: 'invite_created',
      createdAt: admin.firestore.FieldValue.serverTimestamp(),
      read: true,
    })
  } catch (err) {
    // Audit write is best-effort; do not fail the invite if it errors.
    console.warn(`[createInviteCallable] audit log failed for ${callerId}:`, err)
  }

  console.log(`[createInviteCallable] ${callerId} created an invite; expires ${expiresAt.toDate().toISOString()}`)

  return { code, expiresAt }
})
