import type { Auth0Client } from '../Auth0Client';
import type { TokenEndpointResponse } from '../global';
import type {
  PasskeySignupOptions,
  PasskeyLoginOptions,
  PasskeyCredentialResponse,
  PasskeyCreationOptions,
  PasskeyRequestOptions,
  PasskeySignupChallengeOptions,
  PasskeyLoginChallengeOptions,
  PasskeySignupChallenge,
  PasskeyLoginChallenge,
  PasskeyGetTokenOptions
} from './types';
import type { PasskeyClient } from '@auth0/auth0-auth-js';
import { PasskeyError } from './errors';

/**
 * Client for Auth0 Passkey operations.
 *
 * Provides 2 public methods:
 * - `signup` — Register a new user with a passkey (full flow: challenge → WebAuthn → token exchange)
 * - `login` — Sign in with a passkey (full flow: challenge → WebAuthn → token exchange)
 *
 * @example
 * ```typescript
 * // Signup — single call handles everything
 * const tokens = await auth0.passkey.signup({ email: 'user@example.com' });
 *
 * // Login — single call handles everything
 * const tokens = await auth0.passkey.login();
 * ```
 */
export class PasskeyApiClient {
  #passkeyClient: PasskeyClient;
  #auth0Client: Auth0Client;

  /**
   * @internal
   * Do not instantiate directly. Use Auth0Client.passkey instead.
   */
  constructor(passkeyClient: PasskeyClient, auth0Client: Auth0Client) {
    this.#passkeyClient = passkeyClient;
    this.#auth0Client = auth0Client;
  }

  /**
   * Register a new user with a passkey.
   *
   * Handles the full flow: requests a signup challenge, triggers the browser
   * WebAuthn credential creation ceremony, serializes the result, and exchanges
   * it for tokens.
   *
   * @param options - Passkey signup options (user identifier, optional scope/audience)
   * @returns A promise that resolves to the token endpoint response containing access/ID tokens
   * @throws {PasskeyError} If WebAuthn is not supported in the browser
   * @throws {PasskeyRegisterError} If the challenge request fails
   * @throws {GenericError} If the token exchange fails
   * @throws {PasskeyError} If the user cancels the WebAuthn prompt
   */
  async signup(options: PasskeySignupOptions): Promise<TokenEndpointResponse> {
    if (!window.PublicKeyCredential) {
      throw new PasskeyError('passkey_not_supported', 'WebAuthn is not supported in this browser.');
    }

    const { scope, audience, ...challengeOptions } = options;

    const challenge = await this.#passkeyClient.register(challengeOptions);

    const publicKeyOptions = prepareCreationOptions(
      challenge.authnParamsPublicKey
    );
    const credential = await navigator.credentials.create({
      publicKey: publicKeyOptions
    });

    if (!credential) {
      throw new PasskeyError(
        'passkey_cancelled',
        'Passkey creation was cancelled or no credential was returned.'
      );
    }

    const serialized = serializeCreationCredential(
      credential as PublicKeyCredential
    );

    return this.#auth0Client._requestTokenForPasskey({
      authSession: challenge.authSession,
      credential: serialized,
      realm: challengeOptions.realm,
      organization: challengeOptions.organization,
      scope,
      audience
    });
  }

  /**
   * Sign in with an existing passkey.
   *
   * Handles the full flow: requests a login challenge, triggers the browser
   * WebAuthn assertion ceremony, serializes the result, and exchanges it
   * for tokens.
   *
   * @param options - Optional passkey login options (optional scope/audience/realm/organization)
   * @returns A promise that resolves to the token endpoint response containing access/ID tokens
   * @throws {PasskeyError} If WebAuthn is not supported in the browser
   * @throws {PasskeyChallengeError} If the challenge request fails
   * @throws {GenericError} If the token exchange fails
   * @throws {PasskeyError} If the user cancels the WebAuthn prompt
   */
  async login(options?: PasskeyLoginOptions): Promise<TokenEndpointResponse> {
    if (!window.PublicKeyCredential) {
      throw new PasskeyError('passkey_not_supported', 'WebAuthn is not supported in this browser.');
    }

    const { scope, audience, ...challengeOptions } = options || {};

    const challenge = await this.#passkeyClient.challenge(
      Object.keys(challengeOptions).length > 0 ? challengeOptions : undefined
    );

    const publicKeyOptions = prepareRequestOptions(
      challenge.authnParamsPublicKey
    );
    const credential = await navigator.credentials.get({
      publicKey: publicKeyOptions
    });

    if (!credential) {
      throw new PasskeyError(
        'passkey_cancelled',
        'Passkey authentication was cancelled or no credential was returned.'
      );
    }

    const serialized = serializeAssertionCredential(
      credential as PublicKeyCredential
    );

    return this.#auth0Client._requestTokenForPasskey({
      authSession: challenge.authSession,
      credential: serialized,
      realm: challengeOptions.realm,
      organization: challengeOptions.organization,
      scope,
      audience
    });
  }

  /**
   * Request a passkey signup challenge.
   *
   * Step 1 of the granular signup flow. Returns the auth session and a
   * decoded `PublicKeyCredentialCreationOptions`. Run the WebAuthn credential
   * creation ceremony with `publicKey`, then hand the resulting credential and `authSession` to
   * `getTokenWithPasskey()`.
   *
   * @param options - Signup challenge options (user identifier, optional realm/organization/metadata)
   * @returns A promise resolving to `{ authSession, publicKey }`
   * @throws {PasskeyError} If WebAuthn is not supported in the browser
   * @throws {PasskeyRegisterError} If the challenge request fails
   */
  async getSignupChallenge(
    options: PasskeySignupChallengeOptions
  ): Promise<PasskeySignupChallenge> {
    if (!window.PublicKeyCredential) {
      throw new PasskeyError('passkey_not_supported', 'WebAuthn is not supported in this browser.');
    }

    const challenge = await this.#passkeyClient.register(options);
    return {
      authSession: challenge.authSession,
      publicKey: prepareCreationOptions(challenge.authnParamsPublicKey)
    };
  }

  /**
   * Request a passkey login challenge.
   *
   * Step 1 of the granular login flow. Returns the auth session and a
   * decoded `PublicKeyCredentialRequestOptions`. Run the WebAuthn assertion
   * ceremony with `publicKey`, then hand the resulting credential and `authSession` to
   * `getTokenWithPasskey()`.
   *
   * @param options - Optional login challenge options (realm/organization)
   * @returns A promise resolving to `{ authSession, publicKey }`
   * @throws {PasskeyError} If WebAuthn is not supported in the browser
   * @throws {PasskeyChallengeError} If the challenge request fails
   */
  async getLoginChallenge(
    options?: PasskeyLoginChallengeOptions
  ): Promise<PasskeyLoginChallenge> {
    if (!window.PublicKeyCredential) {
      throw new PasskeyError('passkey_not_supported', 'WebAuthn is not supported in this browser.');
    }

    const challenge = await this.#passkeyClient.challenge(options);

    return {
      authSession: challenge.authSession,
      publicKey: prepareRequestOptions(challenge.authnParamsPublicKey)
    };
  }

  /**
   * Exchange a signed passkey credential for tokens.
   *
   * Step 2 of the granular flow. Serializes the raw `PublicKeyCredential`
   * produced by the WebAuthn ceremony — either a creation (signup) or an
   * assertion (login) credential — and exchanges it for tokens. The credential
   * type (attestation vs assertion) is detected automatically.
   *
   * @param options - The auth session, raw credential, and optional realm/organization/scope/audience
   * @returns A promise resolving to the token endpoint response
   * @throws {PasskeyError} If WebAuthn is not supported in the browser
   * @throws {PasskeyError} If the credential is not a valid attestation or assertion response
   * @throws {GenericError} If the token exchange fails
   */
  async getTokenWithPasskey(
    options: PasskeyGetTokenOptions
  ): Promise<TokenEndpointResponse> {
    if (!window.PublicKeyCredential) {
      throw new PasskeyError('passkey_not_supported', 'WebAuthn is not supported in this browser.');
    }

    const { authSession, credential, realm, organization, scope, audience } = options;
    const response = credential.response;

    let serialized: PasskeyCredentialResponse;

    if (response instanceof AuthenticatorAttestationResponse) {
      serialized = serializeCreationCredential(credential);
    } else if (response instanceof AuthenticatorAssertionResponse) {
      serialized = serializeAssertionCredential(credential);
    } else {
      throw new PasskeyError(
        'passkey_invalid_credential',
        'The provided credential is not a valid attestation or assertion response.'
      );
    }

    return this.#auth0Client._requestTokenForPasskey({
      authSession,
      credential: serialized,
      realm,
      organization,
      scope,
      audience
    });
  }

}

function bufferToBase64url(buffer: ArrayBuffer): string {
  const bytes = new Uint8Array(buffer);
  const binary = Array.from(bytes, b => String.fromCharCode(b)).join('');
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function base64urlToBuffer(base64url: string): ArrayBuffer {
  const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
  const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
  const binary = atob(padded);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
}

function prepareCreationOptions(
  publicKey: PasskeyCreationOptions
): PublicKeyCredentialCreationOptions {
  return {
    ...publicKey,
    challenge: base64urlToBuffer(publicKey.challenge),
    user: {
      ...publicKey.user,
      id: base64urlToBuffer(publicKey.user.id)
    },
    pubKeyCredParams: publicKey.pubKeyCredParams as PublicKeyCredentialParameters[],
    authenticatorSelection: publicKey.authenticatorSelection as AuthenticatorSelectionCriteria | undefined
  };
}

function prepareRequestOptions(
  publicKey: PasskeyRequestOptions
): PublicKeyCredentialRequestOptions {
  return {
    ...publicKey,
    challenge: base64urlToBuffer(publicKey.challenge)
  } as PublicKeyCredentialRequestOptions;
}

function serializeCreationCredential(
  credential: PublicKeyCredential
): PasskeyCredentialResponse {
  const response = credential.response as AuthenticatorAttestationResponse;
  return {
    id: credential.id,
    rawId: bufferToBase64url(credential.rawId),
    type: credential.type,
    authenticatorAttachment: credential.authenticatorAttachment ?? undefined,
    response: {
      clientDataJSON: bufferToBase64url(response.clientDataJSON),
      attestationObject: bufferToBase64url(response.attestationObject)
    },
    clientExtensionResults: credential.getClientExtensionResults() as Record<string, unknown>
  };
}

function serializeAssertionCredential(
  credential: PublicKeyCredential
): PasskeyCredentialResponse {
  const response = credential.response as AuthenticatorAssertionResponse;
  return {
    id: credential.id,
    rawId: bufferToBase64url(credential.rawId),
    type: credential.type,
    authenticatorAttachment: credential.authenticatorAttachment ?? undefined,
    response: {
      clientDataJSON: bufferToBase64url(response.clientDataJSON),
      authenticatorData: bufferToBase64url(response.authenticatorData),
      signature: bufferToBase64url(response.signature),
      userHandle: response.userHandle
        ? bufferToBase64url(response.userHandle)
        : undefined
    },
    clientExtensionResults: credential.getClientExtensionResults() as Record<string, unknown>
  };
}
