/*
Copyright (C) 2023-2026 QuantumNous

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

For commercial licensing, please contact support@quantumnous.com
*/
/**
 * Passkey helper utilities for WebAuthn credential handling.
 *
 * These helpers convert between ArrayBuffer and Base64URL encodings and
 * normalise server-provided credential options into browser-compatible types.
 */

/* eslint-disable @typescript-eslint/no-explicit-any */

/**
 * Convert a base64url string to an ArrayBuffer.
 */
type NodeBufferCtor = {
  from(input: string, encoding: string): { toString(encoding: string): string }
}

export function base64UrlToArrayBuffer(value?: string | null): ArrayBuffer {
  if (!value) return new ArrayBuffer(0)

  const padding = '='.repeat((4 - (value.length % 4)) % 4)
  const base64 = (value + padding).replace(/-/g, '+').replace(/_/g, '/')

  const globalRef = globalThis as typeof globalThis & {
    Buffer?: NodeBufferCtor
  }

  const decode =
    typeof globalRef.atob === 'function'
      ? globalRef.atob.bind(globalRef)
      : (input: string) => {
          if (typeof globalRef.Buffer !== 'undefined') {
            return globalRef.Buffer.from(input, 'base64').toString('binary')
          }
          throw new Error(
            'Base64 decoding is not supported in this environment'
          )
        }

  const binary = decode(base64)
  const buffer = new ArrayBuffer(binary.length)
  const bytes = new Uint8Array(buffer)

  for (let i = 0; i < binary.length; i += 1) {
    bytes[i] = binary.charCodeAt(i)
  }

  return buffer
}

/**
 * Convert an ArrayBuffer to a base64url string.
 */
export function arrayBufferToBase64Url(
  buffer?: ArrayBuffer | ArrayBufferLike | null
): string {
  if (!buffer) return ''

  const globalRef = globalThis as typeof globalThis & {
    Buffer?: NodeBufferCtor
  }

  const bytes = new Uint8Array(buffer)
  let binary = ''
  for (let i = 0; i < bytes.byteLength; i += 1) {
    binary += String.fromCharCode(bytes[i])
  }

  const encode =
    typeof globalRef.btoa === 'function'
      ? globalRef.btoa.bind(globalRef)
      : (input: string) => {
          if (typeof globalRef.Buffer !== 'undefined') {
            return globalRef.Buffer.from(input, 'binary').toString('base64')
          }
          throw new Error(
            'Base64 encoding is not supported in this environment'
          )
        }

  return encode(binary)
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/g, '')
}

/**
 * Prepare credential creation options returned by the backend.
 */
export function prepareCredentialCreationOptions(
  payload: any
): PublicKeyCredentialCreationOptions {
  const options =
    payload?.publicKey ??
    payload?.PublicKey ??
    payload?.response ??
    payload?.Response

  if (!options) {
    throw new Error(
      'Unable to parse Passkey registration options from response'
    )
  }

  const publicKey: PublicKeyCredentialCreationOptions & Record<string, any> = {
    ...options,
    challenge: base64UrlToArrayBuffer(options.challenge),
    user: {
      ...options.user,
      id: base64UrlToArrayBuffer(options.user?.id),
    },
  }

  if (Array.isArray(options.excludeCredentials)) {
    publicKey.excludeCredentials = options.excludeCredentials.map(
      (item: any) => ({
        ...item,
        id: base64UrlToArrayBuffer(item.id),
      })
    )
  }

  if (
    Array.isArray(options.attestationFormats) &&
    options.attestationFormats.length === 0
  ) {
    delete publicKey.attestationFormats
  }

  return publicKey
}

/**
 * Prepare credential request options returned by the backend.
 */
export function prepareCredentialRequestOptions(
  payload: any
): PublicKeyCredentialRequestOptions {
  const options =
    payload?.publicKey ??
    payload?.PublicKey ??
    payload?.response ??
    payload?.Response

  if (!options) {
    throw new Error('Unable to parse Passkey login options from response')
  }

  const publicKey: PublicKeyCredentialRequestOptions & Record<string, any> = {
    ...options,
    challenge: base64UrlToArrayBuffer(options.challenge),
  }

  if (Array.isArray(options.allowCredentials)) {
    publicKey.allowCredentials = options.allowCredentials.map((item: any) => ({
      ...item,
      id: base64UrlToArrayBuffer(item.id),
    }))
  }

  return publicKey
}

/**
 * Build payload for registering a new credential.
 */
export function buildRegistrationResult(
  credential: PublicKeyCredential | null
): Record<string, any> | null {
  if (!credential) return null

  const response = credential.response as AuthenticatorAttestationResponse & {
    getTransports?: () => string[]
  }

  const transports =
    typeof response.getTransports === 'function'
      ? response.getTransports()
      : undefined

  return {
    id: credential.id,
    rawId: arrayBufferToBase64Url(credential.rawId),
    type: credential.type,
    authenticatorAttachment: credential.authenticatorAttachment,
    response: {
      attestationObject: arrayBufferToBase64Url(response.attestationObject),
      clientDataJSON: arrayBufferToBase64Url(response.clientDataJSON),
      transports,
    },
    clientExtensionResults: credential.getClientExtensionResults?.() ?? {},
  }
}

/**
 * Build payload for verifying an existing credential.
 */
export function buildAssertionResult(
  credential: PublicKeyCredential | null
): Record<string, any> | null {
  if (!credential) return null

  const response = credential.response as AuthenticatorAssertionResponse

  return {
    id: credential.id,
    rawId: arrayBufferToBase64Url(credential.rawId),
    type: credential.type,
    authenticatorAttachment: credential.authenticatorAttachment,
    response: {
      authenticatorData: arrayBufferToBase64Url(response.authenticatorData),
      clientDataJSON: arrayBufferToBase64Url(response.clientDataJSON),
      signature: arrayBufferToBase64Url(response.signature),
      userHandle: response.userHandle
        ? arrayBufferToBase64Url(response.userHandle)
        : null,
    },
    clientExtensionResults: credential.getClientExtensionResults?.() ?? {},
  }
}

/**
 * Check if current environment supports Passkey/WebAuthn.
 */
export async function isPasskeySupported(): Promise<boolean> {
  if (typeof window === 'undefined') return false
  const { PublicKeyCredential } = window
  if (!PublicKeyCredential) return false

  if (
    typeof PublicKeyCredential.isConditionalMediationAvailable === 'function'
  ) {
    try {
      const available =
        await PublicKeyCredential.isConditionalMediationAvailable()
      if (available) return true
    } catch {
      // ignore
    }
  }

  if (
    typeof PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable ===
    'function'
  ) {
    try {
      return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
    } catch {
      return false
    }
  }

  return true
}

/**
 * Execute an async Passkey credential creation flow.
 */
export async function createCredential(
  options: PublicKeyCredentialCreationOptions
) {
  return navigator.credentials.create({ publicKey: options })
}

/**
 * Execute an async Passkey credential request flow.
 */
export async function getCredential(
  options: PublicKeyCredentialRequestOptions
) {
  return navigator.credentials.get({ publicKey: options })
}
