import type { WebAuthnProviderType } from "../../providers/webauthn";
import type { Account, Authenticator, Awaited, InternalOptions, RequestInternal, ResponseInternal, User } from "../../types";
import type { Cookie } from "./cookie";
import { AdapterError, AuthError, InvalidProvider, MissingAdapter, WebAuthnVerificationError } from "../../errors";
import { webauthnChallenge } from "../actions/callback/oauth/checks";
import type {
  AuthenticationResponseJSON,
  PublicKeyCredentialCreationOptionsJSON,
  PublicKeyCredentialRequestOptionsJSON,
  RegistrationResponseJSON,
} from "@simplewebauthn/server/script/deps"
import type { Adapter, AdapterAccount, AdapterAuthenticator } from "../../adapters";
import type { GetUserInfo } from "../../providers/webauthn";
import { randomString } from "./web";
import type { VerifiedAuthenticationResponse, VerifiedRegistrationResponse } from "@simplewebauthn/server";

export type WebAuthnRegister = "register"
export type WebAuthnAuthenticate = "authenticate"
export type WebAuthnAction = WebAuthnRegister | WebAuthnAuthenticate

type InternalOptionsWebAuthn = InternalOptions<WebAuthnProviderType> & { adapter: Required<Adapter> }
export type WebAuthnOptionsResponseBody = {
  action: WebAuthnAuthenticate,
  options: PublicKeyCredentialRequestOptionsJSON
} | {
  action: WebAuthnRegister,
  options: PublicKeyCredentialCreationOptionsJSON
}
type WebAuthnOptionsResponse = ResponseInternal & {
  body: WebAuthnOptionsResponseBody
}

export type CredentialDeviceType = "singleDevice" | "multiDevice"
interface InternalAuthenticator {
  providerAccountId: string
  credentialID: Uint8Array
  credentialPublicKey: Uint8Array
  counter: number
  credentialDeviceType: CredentialDeviceType
  credentialBackedUp: boolean
  transports?: AuthenticatorTransport[]
}

type RGetUserInfo = Awaited<ReturnType<GetUserInfo>>

/**
 * Infers the WebAuthn options based on the provided parameters.
 * 
 * @param action - The WebAuthn action to perform (optional).
 * @param loggedInUser - The logged-in user (optional).
 * @param userInfoResponse - The response containing user information (optional).
 * 
 * @returns The WebAuthn action to perform, or null if no inference could be made.
 */
export function inferWebAuthnOptions(
  action: WebAuthnAction | undefined,
  loggedIn: boolean,
  userInfoResponse: RGetUserInfo
): WebAuthnAction | null {
  const { user, exists = false } = userInfoResponse ?? {}

  switch (action) {
    case "authenticate": {
      /**
       * Always allow explicit authentication requests.
       */
      return "authenticate"
    }
    case "register": {
      /**
       * Registration is only allowed if:
       * - The user is logged in, meaning the user wants to register a new authenticator.
       * - The user is not logged in and provided user info that does NOT exist, meaning the user wants to register a new account.
       */
      if (user && loggedIn === exists)
        return "register"
      break
    }
    case undefined: {
      /**
       * When no explicit action is provided, we try to infer it based on the user info provided. These are the possible cases:
       * - Logged in users must always send an explit action, so we bail out in this case.
       * - Otherwise, if no user info is provided, the desired action is authentication without pre-defined authenticators.
       * - Otherwise, if the user info provided is of an existing user, the desired action is authentication with their pre-defined authenticators.
       * - Finally, if the user info provided is of a non-existing user, the desired action is registration.
       */
      if (!loggedIn) {
        if (user) {
          if (exists) {
            return "authenticate"
          } else {
            return "register"
          }
        } else {
          return "authenticate"
        }
      }
      break
    }
  }

  // No decision could be made
  return null
}

/**
 * Retrieves the registration response for WebAuthn options request.
 * 
 * @param options - The internal options for WebAuthn.
 * @param request - The request object.
 * @param user - The user information.
 * @param resCookies - Optional cookies to be included in the response.
 * @returns A promise that resolves to the WebAuthnOptionsResponse.
 */
export async function getRegistrationResponse(
  options: InternalOptionsWebAuthn,
  request: RequestInternal,
  user: User & { email: string },
  resCookies?: Cookie[]
): Promise<WebAuthnOptionsResponse> {
  // Get registration options
  const regOptions = await getRegistrationOptions(options, request, user)
  // Get signed cookie
  const { cookie } = await webauthnChallenge.create(options, regOptions.challenge, user)

  return {
    status: 200,
    cookies: [...(resCookies ?? []), cookie],
    body: {
      action: "register" as const,
      options: regOptions,
    },
    headers: {
      "Content-Type": "application/json",
    },
  }
}

/**
 * Retrieves the authentication response for WebAuthn options request.
 * 
 * @param options - The internal options for WebAuthn.
 * @param request - The request object.
 * @param user - Optional user information.
 * @param resCookies - Optional array of cookies to be included in the response.
 * @returns A promise that resolves to a WebAuthnOptionsResponse object.
 */
export async function getAuthenticationResponse(
  options: InternalOptionsWebAuthn,
  request: RequestInternal,
  user?: User,
  resCookies?: Cookie[]
): Promise<WebAuthnOptionsResponse> {
  // Get authentication options
  const authOptions = await getAuthenticationOptions(options, request, user)
  // Get signed cookie
  const { cookie } = await webauthnChallenge.create(options, authOptions.challenge)

  return {
    status: 200,
    cookies: [...(resCookies ?? []), cookie],
    body: {
      action: "authenticate" as const,
      options: authOptions,
    },
    headers: {
      "Content-Type": "application/json",
    },
  }
}

export async function verifyAuthenticate(
  options: InternalOptionsWebAuthn,
  request: RequestInternal,
  resCookies: Cookie[]
): Promise<{ account: AdapterAccount, user: User }> {
  const { adapter, provider } = options

  // Get WebAuthn response from request body
  const data = request.body && typeof request.body.data === "string" ? JSON.parse(request.body.data) as unknown : undefined
  if (!data || typeof data !== "object" || !("id" in data) || typeof data.id !== "string") {
    throw new AuthError("Invalid WebAuthn Authentication response.")
  }

  // Reset the ID so we smooth out implementation differences
  const credentialID = toBase64(fromBase64(data.id))

  // Get authenticator from database
  const authenticator = await adapter.getAuthenticator(credentialID)
  if (!authenticator) {
    throw new AuthError(`WebAuthn authenticator not found in database: ${JSON.stringify({ credentialID })}`)
  }

  // Get challenge from request cookies
  const { challenge: expectedChallenge } = await webauthnChallenge.use(options, request.cookies, resCookies)

  // Verify the response
  let verification: VerifiedAuthenticationResponse
  try {
    const relayingParty = provider.getRelayingParty(options, request)
    verification = await provider.simpleWebAuthn.verifyAuthenticationResponse({
      ...provider.verifyAuthenticationOptions,
      expectedChallenge,
      response: data as AuthenticationResponseJSON,
      authenticator: fromAdapterAuthenticator(authenticator),
      expectedOrigin: relayingParty.origin,
      expectedRPID: relayingParty.id,
    })
  } catch (e: any) {
    throw new WebAuthnVerificationError(e)
  }

  const { verified, authenticationInfo } = verification

  // Make sure the response was verified
  if (!verified) {
    throw new WebAuthnVerificationError("WebAuthn authentication response could not be verified.")
  }

  // Update authenticator counter
  try {
    const { newCounter } = authenticationInfo
    await adapter.updateAuthenticatorCounter(authenticator.credentialID, newCounter)
  } catch (e: any) {
    throw new AdapterError(
      `Failed to update authenticator counter. This may cause future authentication attempts to fail. ${JSON.stringify({
        credentialID,
        oldCounter: authenticator.counter,
        newCounter: authenticationInfo.newCounter,
      })}`, e)
  }

  // Get the account and user
  const account = await adapter.getAccount(authenticator.providerAccountId, provider.id)
  if (!account) {
    throw new AuthError(`WebAuthn account not found in database: ${JSON.stringify({ credentialID, providerAccountId: authenticator.providerAccountId })}`)
  }

  const user = await adapter.getUser(account.userId)
  if (!user) {
    throw new AuthError(
      `WebAuthn user not found in database: ${JSON.stringify(
        { credentialID, providerAccountId: authenticator.providerAccountId, userID: account.userId }
      )}`
    )
  }

  return {
    account,
    user,
  }
}

export async function verifyRegister(
  options: InternalOptions<WebAuthnProviderType>,
  request: RequestInternal,
  resCookies: Cookie[],
): Promise<{ account: Account, user: User; authenticator: Authenticator }> {
  const { provider } = options

  // Get WebAuthn response from request body
  const data = request.body && typeof request.body.data === "string" ? JSON.parse(request.body.data) as unknown : undefined
  if (!data || typeof data !== "object" || !("id" in data) || typeof data.id !== "string") {
    throw new AuthError("Invalid WebAuthn Registration response.")
  }

  // Get challenge from request cookies
  const { challenge: expectedChallenge, registerData: user } = await webauthnChallenge.use(options, request.cookies, resCookies)
  if (!user) {
    throw new AuthError("Missing user registration data in WebAuthn challenge cookie.")
  }

  // Verify the response
  let verification: VerifiedRegistrationResponse
  try {
    const relayingParty = provider.getRelayingParty(options, request)
    verification = await provider.simpleWebAuthn.verifyRegistrationResponse({
      ...provider.verifyRegistrationOptions,
      expectedChallenge,
      response: data as RegistrationResponseJSON,
      expectedOrigin: relayingParty.origin,
      expectedRPID: relayingParty.id,
    })
  } catch (e: any) {
    throw new WebAuthnVerificationError(e)
  }

  // Make sure the response was verified
  if (!verification.verified || !verification.registrationInfo) {
    throw new WebAuthnVerificationError("WebAuthn registration response could not be verified.")
  }

  // Build a new account
  const account = {
    providerAccountId: toBase64(verification.registrationInfo.credentialID),
    provider: options.provider.id,
    type: provider.type,
  }

  // Build a new authenticator
  const authenticator = {
    providerAccountId: account.providerAccountId,
    counter: verification.registrationInfo.counter,
    credentialID: toBase64(verification.registrationInfo.credentialID),
    credentialPublicKey: toBase64(verification.registrationInfo.credentialPublicKey),
    credentialBackedUp: verification.registrationInfo.credentialBackedUp,
    credentialDeviceType: verification.registrationInfo.credentialDeviceType,
    transports: transportsToString((data as RegistrationResponseJSON).response.transports as AuthenticatorTransport[])
  }

  // Return created stuff
  return {
    user,
    account,
    authenticator,
  }
}


/**
 * Generates WebAuthn authentication options.
 * 
 * @param options - The internal options for WebAuthn.
 * @param request - The request object.
 * @param user - Optional user information.
 * @returns The authentication options.
 */
async function getAuthenticationOptions(options: InternalOptionsWebAuthn, request: RequestInternal, user?: User) {
  const { provider, adapter } = options

  // Get the user's authenticators.
  const authenticators = user && user["id"] ?
    await adapter.listAuthenticatorsByUserId(user.id) :
    null

  const relayingParty = provider.getRelayingParty(options, request)

  // Return the authentication options.
  return await provider.simpleWebAuthn.generateAuthenticationOptions({
    ...provider.authenticationOptions,
    rpID: relayingParty.id,
    allowCredentials: authenticators?.map((a) => ({
      id: fromBase64(a.credentialID),
      type: "public-key",
      transports: stringToTransports(a.transports),
    })),
  })
}


/**
 * Generates WebAuthn registration options.
 * 
 * @param options - The internal options for WebAuthn.
 * @param request - The request object.
 * @param user - The user information.
 * @returns The registration options.
 */
async function getRegistrationOptions(
  options: InternalOptionsWebAuthn,
  request: RequestInternal,
  user: User & { email: string }
) {
  const { provider, adapter } = options

  // Get the user's authenticators.
  const authenticators = user["id"] ? await adapter.listAuthenticatorsByUserId(user.id) : null

  // Generate a random user ID for the credential.
  // We can do this because we don't use this user ID to link the
  // credential to the user. Instead, we store actual userID in the
  // Authenticator object and fetch it via it's credential ID.
  const userID = randomString(32)

  const relayingParty = provider.getRelayingParty(options, request)

  // Return the registration options.
  return await provider.simpleWebAuthn.generateRegistrationOptions({
    ...provider.registrationOptions,
    userID,
    userName: user.email,
    userDisplayName: user.name ?? undefined,
    rpID: relayingParty.id,
    rpName: relayingParty.name,
    excludeCredentials: authenticators?.map((a) => ({
      id: fromBase64(a.credentialID),
      type: "public-key",
      transports: stringToTransports(a.transports),
    })),
  })
}

export function assertInternalOptionsWebAuthn(options: InternalOptions): InternalOptionsWebAuthn {
  const { provider, adapter } = options

  // Adapter is required for WebAuthn
  if (!adapter) throw new MissingAdapter(
    "An adapter is required for the WebAuthn provider"
  )
  // Provider must be WebAuthn
  if (!provider || provider.type !== "webauthn") {
    throw new InvalidProvider("Provider must be WebAuthn")
  }
  // Narrow the options type for typed usage later
  return { ...options, provider, adapter }
}

function fromAdapterAuthenticator(authenticator: AdapterAuthenticator): InternalAuthenticator {
  return {
    ...authenticator,
    credentialDeviceType: authenticator.credentialDeviceType as InternalAuthenticator["credentialDeviceType"],
    transports: stringToTransports(authenticator.transports),
    credentialID: fromBase64(authenticator.credentialID),
    credentialPublicKey: fromBase64(authenticator.credentialPublicKey),
  }
}

export function fromBase64(base64: string): Uint8Array {
  return new Uint8Array(Buffer.from(base64, "base64"))
}

export function toBase64(bytes: Uint8Array): string {
  return Buffer.from(bytes).toString("base64")
}

export function transportsToString(transports: InternalAuthenticator["transports"]) {
  return transports?.join(",")
}

export function stringToTransports(tstring: string | undefined): InternalAuthenticator["transports"] {
  return tstring ? tstring.split(",") as InternalAuthenticator["transports"] : undefined
}
