import type { Handler } from 'hono';
import type { User } from 'types';
import { createRouter } from '../../create-router';
import { userMiddleware, passkeyRateLimiter } from '../../_middlewares';
import { sign } from '../../jwt';
import * as errors from '../../custom-errors';
import { isLockdown, noteBlocked } from './panic';
import {
  getUserByEmail,
  getPasskeysByUserIdAndOrigin,
  getPasskeyByCredentialId,
  createPasskey,
  updatePasskey,
  storeChallenge,
  consumeChallenge,
} from 'officerdb';
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} from '@simplewebauthn/server';
import type { RegistrationResponseJSON, AuthenticationResponseJSON } from '@simplewebauthn/server';

// WebAuthn is defined in terms of an origin — a passkey is registered against one and only verifiable
// against the same one. A caller that sends neither Origin nor Referer cannot do either, so say so
// instead of passing undefined into a SQL parameter and returning a 500.
function requireOrigin(ctx: { get: (k: 'origin') => unknown }): string {
  const origin = ctx.get('origin') as string | undefined;
  if (!origin) throw errors.BAD_REQUEST('Passkey operations require an Origin header');
  return origin;
}

export const passkeyRouter = createRouter();

const CHALLENGE_TTL_MS = 5 * 60 * 1000; // 5 minutes
const RP_NAME = 'Officer';

function getRpId(origin: string): string {
  try {
    const url = new URL(origin);
    return url.hostname;
  } catch {
    return 'officer.dev';
  }
}

// Generate registration options (challenge) for creating a new passkey
const passkeyRouterPostChallenge: Handler = async (ctx) => {
  const { email } = ctx.req.param();
  const origin = requireOrigin(ctx);
  const rpId = getRpId(origin);

  const dbUser = await getUserByEmail(email!);
  if (!dbUser) throw errors.NOT_FOUND('User not found');

  // Get existing passkeys to exclude them
  const existingPasskeys = await getPasskeysByUserIdAndOrigin(dbUser.id, origin);

  const options = await generateRegistrationOptions({
    rpName: RP_NAME,
    rpID: rpId,
    userName: email!,
    userDisplayName: email!,
    attestationType: 'none',
    excludeCredentials: existingPasskeys.map((p) => ({
      id: p.credentialId!,
    })),
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'preferred',
    },
  });

  await storeChallenge(dbUser.id, origin, options.challenge, CHALLENGE_TTL_MS);
  return ctx.json(options);
};
passkeyRouter.post('/challenge/:email', passkeyRateLimiter, passkeyRouterPostChallenge);

// Verify and store new passkey registration
const passkeyRouterPost: Handler = async (ctx) => {
  const origin = requireOrigin(ctx);
  const rpId = getRpId(origin);
  const user = ctx.get('user') as User;
  const response = ctx.get('body') as RegistrationResponseJSON;

  const storedChallenge = await consumeChallenge(user.id, origin);
  if (!storedChallenge) throw errors.BAD_CREDENTIALS();

  const verification = await verifyRegistrationResponse({
    response,
    expectedChallenge: storedChallenge,
    expectedOrigin: origin,
    expectedRPID: rpId,
  });

  if (!verification.verified || !verification.registrationInfo) {
    throw errors.BAD_CREDENTIALS();
  }

  const { credential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo;

  await createPasskey({
    userId: user.id,
    origin,
    credentialId: credential.id,
    publicKey: Buffer.from(credential.publicKey).toString('base64'),
    counter: credential.counter,
  });

  return ctx.json({ ok: true, credentialDeviceType, credentialBackedUp });
};
passkeyRouter.post('/credentials', userMiddleware, passkeyRouterPost);

// Generate authentication options for signing in with passkey
const passkeyRouterGet: Handler = async (ctx) => {
  const { email } = ctx.req.param();
  const origin = requireOrigin(ctx);
  const rpId = getRpId(origin);

  const dbUser = await getUserByEmail(email!);
  if (!dbUser) throw errors.NOT_FOUND('User not found');

  const passkeys = await getPasskeysByUserIdAndOrigin(dbUser.id, origin);

  const options = await generateAuthenticationOptions({
    rpID: rpId,
    allowCredentials: passkeys.map((p) => ({
      id: p.credentialId!,
    })),
    userVerification: 'preferred',
  });

  await storeChallenge(dbUser.id, origin, options.challenge, CHALLENGE_TTL_MS);
  return ctx.json(options);
};
passkeyRouter.get('/signin/:email', passkeyRateLimiter, passkeyRouterGet);

// Verify passkey authentication and issue token
const passkeyRouterPostVerify: Handler = async (ctx) => {
  if (isLockdown()) {
    noteBlocked('passkey login');
    throw errors.UNAUTHORIZED();
  } // duress lockdown blocks passkey logins too
  const { email } = ctx.req.param();
  const origin = requireOrigin(ctx);
  const rpId = getRpId(origin);
  const response = ctx.get('body') as AuthenticationResponseJSON;

  const dbUser = await getUserByEmail(email!);
  if (!dbUser) throw errors.UNAUTHORIZED();

  const storedChallenge = await consumeChallenge(dbUser.id, origin);
  if (!storedChallenge) throw errors.BAD_CREDENTIALS();

  // Find the passkey being used
  const dbPasskey = await getPasskeyByCredentialId(dbUser.id, response.id);

  if (!dbPasskey || !dbPasskey.publicKey) throw errors.BAD_CREDENTIALS();

  const verification = await verifyAuthenticationResponse({
    response,
    expectedChallenge: storedChallenge,
    expectedOrigin: origin,
    expectedRPID: rpId,
    credential: {
      id: dbPasskey.credentialId!,
      publicKey: Buffer.from(dbPasskey.publicKey, 'base64'),
      counter: dbPasskey.counter,
    },
  });

  if (!verification.verified) throw errors.BAD_CREDENTIALS();

  // Update counter to prevent replay attacks
  await updatePasskey(dbPasskey.id, { counter: verification.authenticationInfo.newCounter });

  const passkeys = await getPasskeysByUserIdAndOrigin(dbUser.id, origin);

  const { id, name, username } = dbUser;
  const token = await sign({
    id,
    email,
    name,
    username,
    passkeys: passkeys.length,
  });

  return ctx.json({
    token,
    user: {
      id,
      email,
      name,
      username,
      passkeys: passkeys.length,
    },
  });
};
passkeyRouter.post('/verify/:email', passkeyRateLimiter, passkeyRouterPostVerify);
