import { Buffer } from 'node:buffer';
import { error, json, type RequestHandler } from '@sveltejs/kit';
import type { VerifiedAuthenticationResponse, VerifyAuthenticationResponseOpts } from '@simplewebauthn/server';
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import jwt from 'jsonwebtoken';
import { NoResultError } from 'kysely';
import { env } from '$env/dynamic/private';
import { getAuthSessionIdFromCookie, setJwtCookie } from '$lib/server/auth/utilities';
import {
  type Authenticator,
  findAuthenticatorByIdentifier,
  updateAuthenticator
} from '$lib/server/data/authentication/authenticator';
import { deleteChallenges, resolveCurrentChallenge } from '$lib/server/data/authentication/challenge';

export const POST: RequestHandler = async function handler({
  url,
  request,
  cookies,
  locals: { database }
}) {
  const sessionId = getAuthSessionIdFromCookie(cookies);

  if (!sessionId) {
    throw error(401, 'Not authenticated');
  }

  let challenge: string;

  try {
    challenge = await resolveCurrentChallenge(database, sessionId);
  } catch (err) {
    if (!(err instanceof Error)) {
      throw err;
    }

    throw error(400, `Failed to resolve challenge: ${err.message}`);
  }

  let response: AuthenticationResponseJSON;

  try {
    response = await request.json() as AuthenticationResponseJSON;
  } catch (err) {
    if (!(err instanceof Error)) {
      throw err;
    }

    await deleteChallenges(database, sessionId);

    return error(400, `Invalid request body: ${err.message}`);
  }

  const userId = response.response.userHandle;

  if (!userId) {
    await deleteChallenges(database, sessionId);

    return error(400, `Invalid payload: Missing user handle`);
  }

  let authenticator: Authenticator | null;

  try {
    authenticator = await findAuthenticatorByIdentifier(
      database,
      response.rawId
    );
  } catch (err) {
    if (!(err instanceof NoResultError)) {
      await deleteChallenges(database, sessionId);

      throw err;
    }

    authenticator = null;
  }

  if (!authenticator || authenticator.user_id !== userId) {
    await deleteChallenges(database, sessionId);

    return error(400, 'Authenticator is not registered with this site');
  }

  let verification: VerifiedAuthenticationResponse;

  try {
    verification = await verifyAuthenticationResponse({
      response,
      expectedChallenge: `${challenge}`,
      expectedOrigin: url.origin, // <-- TODO: Use origin from RP ID instead
      expectedRPID: url.hostname, // <-- TODO: Use hostname from env instead
      authenticator: {
        credentialPublicKey: Buffer.from(authenticator.public_key, 'base64url'),
        credentialID: Buffer.from(authenticator.identifier, 'base64url'),
        counter: Number(authenticator.counter),
        transports: authenticator.transports
      },
      requireUserVerification: true
    } satisfies VerifyAuthenticationResponseOpts);
  } catch (err) {
    if (!(err instanceof Error)) {
      throw err;
    }

    await deleteChallenges(database, sessionId);

    return error(400, err.message);
  }

  const { verified, authenticationInfo } = verification;
  const { newCounter: counter } = authenticationInfo;

  if (verified) {
    // Update the authenticator's counter in the DB to the newest count in the authentication
    await updateAuthenticator(database, response.rawId, {
      counter: counter.toString(),
      last_used_at: new Date()
    });
  }

  await deleteChallenges(database, sessionId);

  // Sign the user token: We have authenticated the user successfully using the passcode, so they
  // may use this JWT to create their pass *key*.
  const token = jwt.sign({
    authenticator: authenticator.id
  }, env.JWT_SECRET, {
    subject: authenticator.user_id
  });

  // Set the cookie on the response: It will be included in any requests to the server, including
  // for tRPC. This makes for a nice, transparent, and "just works" authentication scheme.
  setJwtCookie(cookies, token);

  return json({ verified });
};