/**
 * MFA requirements from an mfa_required error response
 *
 * @category Errors
 */
export interface MfaRequirements {
  /** Required enrollment types */
  enroll?: Array<{ type: string }>;
  /** Required challenge types */
  challenge?: Array<{ type: string }>;
}

/**
 * Thrown when network requests to the Auth server fail.
 *
 * @category Errors
 */
export class GenericError extends Error {
  /**
   * @param error - The `error` code from the server response.
   * @param error_description - The `error_description` from the server response.
   * @param data - Any other fields from the server's error response (for example `auth_session`).
   */
  constructor(
    public error: string,
    public error_description: string,
    public data?: Record<string, any>
  ) {
    super(error_description);
    Object.setPrototypeOf(this, GenericError.prototype);
  }

  static fromPayload({
    error,
    error_description
  }: {
    error: string;
    error_description: string;
  }) {
    return new GenericError(error, error_description);
  }
}

/**
 * Thrown at construction time for an invalid option combination (e.g.
 * `refreshTokenMode: 'online'` without `useDpop: true`). The `suggestion` names the fix.
 *
 * @category Errors
 */
export class InvalidConfigurationError extends GenericError {
  constructor(message: string, public suggestion: string) {
    super('invalid_configuration', `${message} ${suggestion}`);
    Object.setPrototypeOf(this, InvalidConfigurationError.prototype);
  }
}

/**
 * Thrown when handling the redirect callback fails, will be one of Auth0's
 * Authentication API's Standard Error Responses: https://auth0.com/docs/api/authentication?javascript#standard-error-responses
 *
 * @category Errors
 */
export class AuthenticationError extends GenericError {
  constructor(
    error: string,
    error_description: string,
    public state: string,
    public appState: any = null
  ) {
    super(error, error_description);
    //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
    Object.setPrototypeOf(this, AuthenticationError.prototype);
  }
}

/**
 * Thrown when handling the redirect callback for the connect flow fails, will be one of Auth0's
 * Authentication API's Standard Error Responses: https://auth0.com/docs/api/authentication?javascript#standard-error-responses
 *
 * @category Errors
 */
export class ConnectError extends GenericError {
  constructor(
    error: string,
    error_description: string,
    public connection: string,
    public state: string,
    public appState: any = null
  ) {
    super(error, error_description);
    //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
    Object.setPrototypeOf(this, ConnectError.prototype);
  }
}

/**
 * Thrown when silent auth times out (usually due to a configuration issue) or
 * when network requests to the Auth server timeout.
 *
 * @category Errors
 */
export class TimeoutError extends GenericError {
  constructor() {
    super('timeout', 'Timeout');
    //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
    Object.setPrototypeOf(this, TimeoutError.prototype);
  }
}

/**
 * Error thrown when the login popup times out (if the user does not complete auth)
 *
 * @category Errors
 */
export class PopupTimeoutError extends TimeoutError {
  constructor(public popup: Window) {
    super();
    //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
    Object.setPrototypeOf(this, PopupTimeoutError.prototype);
  }
}

/**
 * Error thrown when the login popup is closed before auth completes.
 *
 * @category Errors
 */
export class PopupCancelledError extends GenericError {
  constructor(public popup: Window) {
    super('cancelled', 'Popup closed');
    //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
    Object.setPrototypeOf(this, PopupCancelledError.prototype);
  }
}

/**
 * Error thrown when a popup cannot be opened (`window.open` returned `null`).
 *
 * @category Errors
 */
export class PopupOpenError extends GenericError {
  constructor() {
    super('popup_open', 'Unable to open a popup for loginWithPopup - window.open returned `null`');
    //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
    Object.setPrototypeOf(this, PopupOpenError.prototype);
  }
}

/**
 * Error thrown when the token exchange results in a `mfa_required` error
 *
 * @category Errors
 */
export class MfaRequiredError extends GenericError {
  constructor(
    error: string,
    error_description: string,
    public mfa_token: string,
    public mfa_requirements: MfaRequirements
  ) {
    super(error, error_description);
    //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
    Object.setPrototypeOf(this, MfaRequiredError.prototype);
  }
}

/**
 * Thrown by `signup()` when the server requires identifier verification before
 * passkey creation. Catch this error, collect OTP codes for each identifier in
 * `verificationRequired`, then call `passkey.continueSignup(codes)` to complete
 * the flow. The SDK retains the auth session internally — you do not need to
 * pass it yourself.
 *
 * @category Errors
 */
export class PasskeyVerificationRequiredError extends GenericError {
  constructor(public readonly verificationRequired: Array<'email' | 'phone'>) {
    super(
      'verification_required',
      'Identifier verification is required before passkey creation. ' +
      'Collect OTP codes for each identifier and call passkey.continueSignup().'
    );
    //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
    Object.setPrototypeOf(this, PasskeyVerificationRequiredError.prototype);
  }
}

/**
 * Error thrown when there is no refresh token to use
 *
 * @category Errors
 */
export class MissingRefreshTokenError extends GenericError {
  constructor(public audience: string, public scope: string) {
    super(
      'missing_refresh_token',
      `Missing Refresh Token (audience: '${valueOrEmptyString(audience, [
        'default'
      ])}', scope: '${valueOrEmptyString(scope)}')`
    );
    Object.setPrototypeOf(this, MissingRefreshTokenError.prototype);
  }
}

/**
 * Error thrown when there are missing scopes after refreshing a token
 *
 * @category Errors
 */
export class MissingScopesError extends GenericError {
  constructor(public audience: string, public scope: string) {
    super(
      'missing_scopes',
      `Missing requested scopes after refresh (audience: '${valueOrEmptyString(audience, [
        'default'
      ])}', missing scope: '${valueOrEmptyString(scope)}')`
    );
    Object.setPrototypeOf(this, MissingScopesError.prototype);
  }
}

/**
 * Error thrown when the wrong DPoP nonce is used and a potential subsequent retry wasn't able to fix it.
 *
 * @category Errors
 */
export class UseDpopNonceError extends GenericError {
  constructor(public newDpopNonce: string | undefined) {
    super('use_dpop_nonce', 'Server rejected DPoP proof: wrong nonce');

    Object.setPrototypeOf(this, UseDpopNonceError.prototype);
  }
}

/**
 * Returns an empty string when value is falsy, or when it's value is included in the exclude argument.
 * @param value The value to check
 * @param exclude An array of values that should result in an empty string.
 * @returns The value, or an empty string when falsy or included in the exclude argument.
 */
function valueOrEmptyString(value: string, exclude: string[] = []) {
  return value && !exclude.includes(value) ? value : '';
}
