export interface HttpError extends Error {
  httpStatus: number;
}

export interface HttpProblem extends HttpError {

  type: string | null;
  title: string;
  detail: string | null;
  instance: string | null;

}

export function isHttpError(e: unknown): e is HttpError {

  if (!e) return false;
  return Number.isInteger((e as HttpError).httpStatus);

}
export function isHttpProblem(e: unknown): e is HttpProblem {

  if (!e) return false;
  return (e as HttpProblem).title !== undefined && isHttpError(e);

}

export class HttpErrorBase extends Error implements HttpProblem {

  type: string | null = null;
  httpStatus: number = 500;
  title: string = 'Internal Server Error';
  detail: string|null = null;
  instance: string | null = null;

  constructor(detail: string|null = null) {

    super(detail || 'HTTP error');
    this.detail = detail;

  }

}

export function isClientError(e: Error): boolean {

  return isHttpError(e) && e.httpStatus >= 400 && e.httpStatus <= 499;

}

export function isServerError(e: Error): boolean {

  return isHttpError(e) && e.httpStatus >= 500 && e.httpStatus <= 599;

}

export class BadRequest extends HttpErrorBase {
  httpStatus = 400;
  title = 'Bad Request';
}

type AuthenticateChallenge = string | string[];

/**
 * Emits a 401 Unauthorized.
 *
 * This response must come with a WWW-Authenticate header. This challenge can
 * optionally be provided via the constructor.
 *
 * examples:
 *    new Unauthorized('Login failed', 'Basic');
 *    new Unauthorized('Login failed', 'Basic; realm="secret area"');
 *    new Unauthorized('Login failed', ['Basic; realm="secret area', 'Bearer']);
 */
export class Unauthorized extends HttpErrorBase {

  httpStatus = 401;
  title = 'Unauthorized';

  wwwAuthenticate?: AuthenticateChallenge;

  constructor(detail: string|null = null, wwwAuthenticate?: AuthenticateChallenge) {

    super(detail);
    this.wwwAuthenticate = wwwAuthenticate;

  }

}

/**
 * Emits 402 Payment Required
 */
export class PaymentRequired extends HttpErrorBase {
  httpStatus = 402;
  title = 'Payment Required';
}

/**
 * Emits 403 Forbidden
 */
export class Forbidden extends HttpErrorBase {
  httpStatus = 403;
  title = 'Forbidden';
}

/**
 * Emits 404 Not Found
 */
export class NotFound extends HttpErrorBase {
  httpStatus = 404;
  title = 'Not Found';
}

/**
 * Emits 405 Method Not Allowed.
 *
 * The 405 Method Not Allowed HTTP response requires an Allow header.
 * You can optionally use the second argument to provide this.
 *
 * Example:
 *   new MethodNotAllowed('This resource is read-only', ['GET', 'HEAD', 'OPTIONS']);
 */
export class MethodNotAllowed extends HttpErrorBase {
  httpStatus = 405;
  title = 'Method Not Allowed';
  allow?: string[];

  constructor(detail: string|null = null, allow?: string[]) {

    super(detail);
    this.allow = allow;

  }

}

/**
 * Emits 406 Not Acceptable
 */
export class NotAcceptable extends HttpErrorBase {
  httpStatus = 406;
  title = 'Not Acceptable';
}

/**
 * Emits a 407 Proxy Autentication Required
 *
 * This response must come with a Proxy-Authenticate header. This challenge can
 * optionally be provided via the constructor.
 *
 * examples:
 *    new ProxyAuthenticationRequired('Login failed', 'Basic');
 *    new ProxyAuthenticationRequired('Login failed', 'Basic; realm="secret area"');
 *    new ProxyAuthenticationRequired('Login failed', ['Basic; realm="secret area', 'Bearer']);
 */
export class ProxyAuthenticationRequired extends HttpErrorBase {

  httpStatus = 407;
  title = 'Proxy Authentication Required';

  proxyAuthenticate?: AuthenticateChallenge;

  constructor(detail: string|null = null, proxyAuthenticate?: AuthenticateChallenge) {

    super(detail);
    this.proxyAuthenticate = proxyAuthenticate;

  }

}

/**
 * Emits 408 Request Timeout
 */
export class RequestTimeout extends HttpErrorBase {
  httpStatus = 408;
  title = 'Request Timeout';
}

/**
 * Emits 409 Conflict
 */
export class Conflict extends HttpErrorBase {
  httpStatus = 409;
  title = 'Conflict';
}

/**
 * Emits 410 Gone
 */
export class Gone extends HttpErrorBase {
  httpStatus = 410;
  title = 'Gone';
}

/**
 * Emits 411 Length Required
 */
export class LengthRequired extends HttpErrorBase {
  httpStatus = 411;
  title = 'LengthRequired';
}

/**
 * Emits 412 Precondition Failed
 */
export class PreconditionFailed extends HttpErrorBase {
  httpStatus = 412;
  title = 'PreconditionFailed';
}

/**
 * Emits 413 Content Too Large
 *
 * If the status is temporary, it's possible for a server to send a
 * Retry-After header to try again. This value may be embeded in this
 * exception.
 *
 * Example:
 *
 * throw new PayloadTooLarge('Send the large file again in 10 minutes', 600);
 */
export class ContentTooLarge extends HttpErrorBase {
  httpStatus = 413;
  title = 'Content Too Large';

  retryAfter: number | null;

  constructor(detail: string|null = null, retryAfter: number|null = null) {

    super(detail);
    this.retryAfter = retryAfter;

  }
}

/**
  * Payload Too Large was the old name of this error, but all instances of
  * Payload have been renamed to Content in RFC9110.
  *
  * @deprecated
 */
export class PayloadTooLarge extends ContentTooLarge {}

/**
 * Emits 414 URI Too Long
 */
export class UriTooLong extends HttpErrorBase {
  httpStatus = 414;
  title = 'URI Too Long';
}

/**
 * Emits 415 Unsupported Media Type
 */
export class UnsupportedMediaType extends HttpErrorBase {
  httpStatus = 415;
  title = 'Unsupported Media Type';
}

/**
 * Emits 416 Range Not Satisfiable
 */
export class RangeNotSatisfiable extends HttpErrorBase {
  httpStatus = 416;
  title = 'Range Not Satisfiable';
}

/**
 * Emits 417 Expectation Failed
 */
export class ExpectationFailed extends HttpErrorBase {
  httpStatus = 417;
  title = 'Expectation Failed';
}

/**
 * Emits 421 Misdirected Request
 */
export class MisdirectedRequest extends HttpErrorBase {
  httpStatus = 421;
  title = 'Misdirected Request';
}

/**
 * Emits 422 Unprocessable Content
 */
export class UnprocessableContent extends HttpErrorBase {
  httpStatus = 422;
  title = 'Unprocessable Entity';
}

/**
 * RFC9110 renamed this to Unprocessable Content.
 *
 * @deprecated
 */
export class UnprocessableEntity extends UnprocessableContent {
}

/**
 * Emits 423 Locked
 */
export class Locked extends HttpErrorBase {
  httpStatus = 423;
  title = 'Locked';
}

/**
 * Emits 424 FailedDependency
 */
export class FailedDependency extends HttpErrorBase {
  httpStatus = 424;
  title = 'Failed Dependency';
}

/**
 * Emits 425 Too Early
 */
export class TooEarly extends HttpErrorBase {
  httpStatus = 425;
  title = 'Too Early';
}

/**
 * Emits 426 Upgrade Required
 */
export class UpgradeRequired extends HttpErrorBase {
  httpStatus = 426;
  title = 'Upgrade Required';
}

/**
 * Emits 428 Precondition Required
 */
export class PreconditionRequired extends HttpErrorBase {
  httpStatus = 428;
  title = 'Precondition Required';
}

/**
 * Emits 429 Too Many Requests
 *
 * When sending this status the server may also send a Retry-After header
 * indicating when it's safe try again.
 *
 * It's possible to supply this information via the second argument.
 *
 * Example:
 *   throw new TooManyRequests('Try again in 10 minutes', 600)
 *
 */
export class TooManyRequests extends HttpErrorBase {
  httpStatus = 429;
  title = 'Too Many Requests';

  retryAfter: number | null;

  constructor(detail: string|null = null, retryAfter: number|null = null) {

    super(detail);
    this.retryAfter = retryAfter;

  }
}

/**
 * Emits 431 Request Header Fields Too Large
 */
export class RequestHeaderFieldsTooLarge extends HttpErrorBase {
  httpStatus = 431;
  title = 'Request Header Fields Too Large';
}

/**
 * Emits 451 Unavailable For Legal Reasons
 */
export class UnavailableForLegalReasons extends HttpErrorBase {
  httpStatus = 451;
  title = 'Unavailable For Legal Reasons';
}

/**
 * Emits 500 Internal Server Error
 */
export class InternalServerError  extends HttpErrorBase {
  httpStatus = 500;
  title = 'Internal Server Error';
}

/**
 * Emits 501 Not Implemented
 */
export class NotImplemented extends HttpErrorBase {
  httpStatus = 501;
  title = 'Not Implemented';
}

/**
 * Emits 502 Bad Gateway
 */
export class BadGateway extends HttpErrorBase {
  httpStatus = 502;
  title = 'Bad Gateway';
}

/**
 * Emits 503 Service Unavailable.
 *
 * When sending this status the server may also send a Retry-After header
 * indicating when it's safe try again.
 *
 * It's possible to supply this information via the second argument.
 *
 * Example:
 *   throw new ServiceUnavailable('We\'re down temporarily', 600)
 *
 */
export class ServiceUnavailable extends HttpErrorBase {
  httpStatus = 503;
  title = 'Service Unavailable';

  retryAfter: number | null;

  constructor(detail: string|null = null, retryAfter: number|null = null) {

    super(detail);
    this.retryAfter = retryAfter;

  }
}

/**
 * Emits 504 Gateway Timeout
 */
export class GatewayTimeout extends HttpErrorBase {
  httpStatus = 504;
  title = 'Gateway Timeout';
}

/**
 * Emits 505 HTTP Version Not Supported
 */
export class HttpVersionNotSupported extends HttpErrorBase {
  httpStatus = 505;
  title = 'HTTP Version Not Supported';
}

/**
 * Emits 506 Variant Also Negotiates
 */
export class VariantAlsoNegotiates extends HttpErrorBase {
  httpStatus = 506;
  title = 'Variant Also Negotiates';
}

/**
 * Emits 507 Insufficient Storage
 */
export class UnsufficientStorage extends HttpErrorBase {
  httpStatus = 507;
  title = 'Unsufficient Storage';
}

/**
 * Emits 508 Loop Detected
 */
export class LoopDetected extends HttpErrorBase {
  httpStatus = 508;
  title = 'Loop Detected';
}

/**
 * Emits 510 Not Extended.
 *
 * This status code has been marked as obsolute.
 *
 * @deprecated
 */
export class NotExtended extends HttpErrorBase {
  httpStatus = 510;
  title = 'Not Extended';
}

/**
 * Emits 511 Network Authentication Required
 */
export class NetworkAuthenticationRequired extends HttpErrorBase {
  httpStatus = 511;
  title = 'Network Authentication Required';
}
