/**
 * MFA requirements from an mfa_required error response
 */
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.
 */
export class GenericError extends Error {
  constructor(public error: string, public error_description: string) {
    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 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
 */
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
 */
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.
 */
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)
 */
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);
  }
}

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);
  }
}

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
 */
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);
  }
}

/**
 * Error thrown when there is no refresh token to use
 */
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
 */
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.
 */
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 : '';
}
