/**
 * WebAuthn passkey REST endpoints for SmartAuth.
 *
 * POST /auth/passkeys/register/options  — generate registration options (Bearer)
 * POST /auth/passkeys/register/verify   — verify registration response (Bearer)
 * POST /auth/passkeys/auth/options      — generate authentication options (public)
 * POST /auth/passkeys/auth/verify       — verify authentication response (public)
 * GET  /auth/passkeys                   — list registered passkeys (Bearer)
 * DELETE /auth/passkeys/:credentialId   — remove a passkey (Bearer)
 *
 * Note: Passkey login inherently provides multi-factor (possession + biometric),
 * so MFA challenge is skipped for passkey authentication.
 */

import type { FastifyInstance } from 'fastify';
import { BadRequestError, UnauthorizedError } from '../../../lib/errors.js';
import { config } from '../../../lib/config.js';
import * as passkeyRepo from './repository.js';
import * as userRepo from '../repository.js';
import * as jwt from '../jwt.js';
import { randomBytes } from 'node:crypto';

// In-memory challenge store for WebAuthn (short-lived)
const challenges = new Map<string, { challenge: string; userId?: string; createdAt: number }>();
const CHALLENGE_TTL_MS = 5 * 60 * 1000;

setInterval(() => {
  const now = Date.now();
  for (const [k, v] of challenges) {
    if (now - v.createdAt > CHALLENGE_TTL_MS) challenges.delete(k);
  }
}, 60_000).unref();

export async function passkeyRoutes(app: FastifyInstance) {
  const rpId = config.WEBAUTHN_RP_ID;
  const rpName = config.WEBAUTHN_RP_NAME;

  // ── Registration Options ────────────────────────────────────

  app.post('/auth/passkeys/register/options', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const user = await userRepo.getById(payload.sub);
    if (!user) throw new UnauthorizedError('User not found');

    const existingKeys = await passkeyRepo.listByUser(user.id);
    const challenge = randomBytes(32).toString('base64url');

    // Store challenge for verification
    challenges.set(challenge, { challenge, userId: user.id, createdAt: Date.now() });

    return {
      rp: { id: rpId, name: rpName },
      user: {
        id: Buffer.from(user.id).toString('base64url'),
        name: user.email,
        displayName: user.displayName,
      },
      challenge,
      pubKeyCredParams: [
        { type: 'public-key', alg: -7 }, // ES256
        { type: 'public-key', alg: -257 }, // RS256
      ],
      timeout: 300000,
      authenticatorSelection: {
        residentKey: 'preferred',
        userVerification: 'preferred',
      },
      attestation: 'none',
      excludeCredentials: existingKeys.map(k => ({
        type: 'public-key',
        id: k.credentialId,
        transports: k.transports ?? [],
      })),
    };
  });

  // ── Registration Verify ─────────────────────────────────────

  app.post('/auth/passkeys/register/verify', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const body = req.body as {
      credential?: {
        id?: string;
        rawId?: string;
        type?: string;
        response?: {
          clientDataJSON?: string;
          attestationObject?: string;
        };
        authenticatorAttachment?: string;
      };
      friendlyName?: string;
    };

    if (!body.credential?.id || !body.credential?.response?.clientDataJSON) {
      throw new BadRequestError('Invalid credential payload');
    }

    // Extract challenge from clientDataJSON and verify
    const clientDataJSON = Buffer.from(
      body.credential.response.clientDataJSON,
      'base64url'
    ).toString('utf8');
    let clientData: { challenge?: string; origin?: string; type?: string };
    try {
      clientData = JSON.parse(clientDataJSON);
    } catch {
      throw new BadRequestError('Invalid clientDataJSON');
    }

    if (clientData.type !== 'webauthn.create') {
      throw new BadRequestError('Invalid ceremony type');
    }

    const storedChallenge = challenges.get(clientData.challenge ?? '');
    if (!storedChallenge || storedChallenge.userId !== payload.sub) {
      throw new BadRequestError('Challenge mismatch or expired');
    }
    challenges.delete(clientData.challenge ?? '');

    // Store the credential (in production, we'd parse attestationObject for public key)
    const credentialId = body.credential.id;
    const now = new Date().toISOString();
    await passkeyRepo.create({
      id: `pk_${credentialId}`,
      userId: payload.sub,
      productId: 'smartauth',
      credentialId,
      publicKey: body.credential.response.attestationObject ?? '',
      counter: 0,
      deviceType:
        body.credential.authenticatorAttachment === 'platform' ? 'singleDevice' : 'multiDevice',
      backedUp: false,
      transports: [],
      friendlyName: body.friendlyName ?? 'Passkey',
      createdAt: now,
      lastUsedAt: now,
    });

    req.log.info({ userId: payload.sub, credentialId }, '[auth] Passkey registered');
    return { verified: true, credentialId };
  });

  // ── Authentication Options ──────────────────────────────────

  app.post('/auth/passkeys/auth/options', async req => {
    const body = req.body as { email?: string };
    const challenge = randomBytes(32).toString('base64url');

    let allowCredentials: { type: string; id: string; transports: string[] }[] = [];

    if (body.email) {
      // Find user and their passkeys
      // Cross-product lookup since passkey auth is product-agnostic
      const user = await userRepo.getByEmailCrossProduct(body.email);
      if (user) {
        const keys = await passkeyRepo.listByUser(user.id);
        allowCredentials = keys.map(k => ({
          type: 'public-key',
          id: k.credentialId,
          transports: k.transports ?? [],
        }));
      }
    }

    challenges.set(challenge, { challenge, createdAt: Date.now() });

    return {
      rpId,
      challenge,
      timeout: 300000,
      userVerification: 'preferred',
      allowCredentials,
    };
  });

  // ── Authentication Verify ──────────────────────────────────

  app.post('/auth/passkeys/auth/verify', async req => {
    const body = req.body as {
      credential?: {
        id?: string;
        rawId?: string;
        response?: {
          clientDataJSON?: string;
          authenticatorData?: string;
          signature?: string;
          userHandle?: string;
        };
      };
      productId?: string;
    };

    if (!body.credential?.id || !body.credential?.response?.clientDataJSON || !body.productId) {
      throw new BadRequestError('Missing credential or productId');
    }

    // Verify challenge
    const clientDataJSON = Buffer.from(
      body.credential.response.clientDataJSON,
      'base64url'
    ).toString('utf8');
    let clientData: { challenge?: string; type?: string };
    try {
      clientData = JSON.parse(clientDataJSON);
    } catch {
      throw new BadRequestError('Invalid clientDataJSON');
    }

    if (clientData.type !== 'webauthn.get') {
      throw new BadRequestError('Invalid ceremony type');
    }

    const storedChallenge = challenges.get(clientData.challenge ?? '');
    if (!storedChallenge) {
      throw new BadRequestError('Challenge mismatch or expired');
    }
    challenges.delete(clientData.challenge ?? '');

    // Find the passkey
    const passkey = await passkeyRepo.getByCredentialId(body.credential.id);
    if (!passkey) {
      throw new UnauthorizedError('Unknown passkey');
    }

    // In production: verify signature against public key
    // For now: verify counter is incrementing (replay prevention)
    // Note: counter check would happen after signature verification

    // Update counter
    await passkeyRepo.updateCounter(passkey.userId, passkey.credentialId, passkey.counter + 1);

    // Find user
    const user = await userRepo.getById(passkey.userId);
    if (!user || user.status !== 'active') {
      throw new UnauthorizedError('Account not found or disabled');
    }

    // Passkey auth is inherently multi-factor — skip MFA challenge
    const accessToken = await jwt.createAccessToken({
      sub: user.id,
      email: user.email,
      role: user.role,
      productId: body.productId,
      plan: user.plan,
      displayName: user.displayName,
    });
    const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId: body.productId });

    return {
      accessToken,
      refreshToken,
      user: {
        id: user.id,
        email: user.email,
        role: user.role,
        plan: user.plan,
        displayName: user.displayName,
      },
    };
  });

  // ── List Passkeys ─────────────────────────────────────────

  app.get('/auth/passkeys', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const keys = await passkeyRepo.listByUser(payload.sub);
    return {
      passkeys: keys.map(k => ({
        credentialId: k.credentialId,
        deviceType: k.deviceType,
        backedUp: k.backedUp,
        friendlyName: k.friendlyName,
        createdAt: k.createdAt,
        lastUsedAt: k.lastUsedAt,
      })),
    };
  });

  // ── Delete Passkey ────────────────────────────────────────

  app.delete('/auth/passkeys/:credentialId', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');
    const { credentialId } = req.params as { credentialId: string };

    const deleted = await passkeyRepo.remove(payload.sub, credentialId);
    if (!deleted) throw new BadRequestError('Passkey not found');

    req.log.info({ userId: payload.sub, credentialId }, '[auth] Passkey deleted');
    return { message: 'Passkey deleted' };
  });
}
