export interface ExceptionSchema {
  name: string
  message: string
  status?: number
  code?: string
  help?: string
  stack?: string
  [key: string]: string | number | undefined
}

/**
 * Create a custom error class with the ability to define the error
 * code, status, and the help text.
 *
 * ```js
 * export class FileNotFoundException extends Exception {
 *   static status = 500
 *   static code = 'E_FILE_NOT_FOUND'
 * }
 *
 * throw new FileNotFoundException(
 *  `Unable to find file from ${filePath} location`
 * )
 * ```
 */
export class Exception extends Error {
  /**
   * Define the error metadata as static properties to avoid
   * setting them repeatedly on the error instance
   */
  declare static help?: string
  declare static code?: string
  declare static status?: number
  declare static message?: string

  /**
   * Name of the class that raised the exception.
   */
  override name: string

  /**
   * Optional help description for the error. You can use it to define additional
   * human readable information for the error.
   */
  declare help?: string

  /**
   * A machine readable error code. This will allow the error handling logic
   * to narrow down exceptions based upon the error code.
   */
  declare code?: string

  /**
   * A status code for the error. Usually helpful when converting errors
   * to HTTP responses.
   */
  status: number

  /**
   * Create a new exception instance.
   * It tries to extract the error code and status from the
   * `init` object. If not found, it uses the default message.
   * @param init - An object containing error details
   * @param defaultMessage - A default message to use if `init.message` is not provided
   * @returns An instance of the Exception class
   * @example
   * ```js
   * const error = Exception.fromApiError({
   *   code: 'E_FILE_NOT_FOUND',
   *   status: 404,
   *   message: 'File not found',
   *   help: 'Please check the file path and try again'
   * }, 'An error occurred while processing your request')
   * ```
   */
  static fromRawException(init: object, defaultMessage: string): Exception {
    const typed = init as Record<string, string | number>
    const options: ErrorOptions & { code?: string; status?: number } = {}
    if (typed.code) {
      options.code = typed.code as string
    }
    if (typed.status) {
      options.status = Number(typed.status)
    }
    const message = typed.message ?? defaultMessage
    const result = new this(message as string, options)
    if (typed.help) {
      result.setHelp(typed.help as string)
    }
    if (typed.name) {
      result.name = typed.name as string
    }
    return result
  }

  constructor(message?: string, options?: ErrorOptions & { code?: string; status?: number; help?: string }) {
    super(message, options)

    const ErrorConstructor = this.constructor as typeof Exception

    this.name = ErrorConstructor.name
    this.message = message || ErrorConstructor.message || ''
    this.status = options?.status || ErrorConstructor.status || 500

    const code = options?.code || ErrorConstructor.code
    if (code !== undefined) {
      this.code = code
    }

    const help = options?.help || ErrorConstructor.help
    if (help !== undefined) {
      this.help = help
    }

    Error.captureStackTrace(this, ErrorConstructor)
  }

  setHelp(help: string): this {
    this.help = help
    return this
  }

  setCode(code: string): this {
    this.code = code
    return this
  }

  setStatus(status: number): this {
    this.status = status
    return this
  }

  get [Symbol.toStringTag]() {
    return this.constructor.name
  }

  override toString(): string {
    if (this.code) {
      return `${this.name} [${this.code}]: ${this.message}`
    }
    return `${this.name}: ${this.message}`
  }

  toJSON(): ExceptionSchema {
    const result: ExceptionSchema = {
      name: this.name,
      message: this.message,
    }
    if (this.status) {
      result.status = this.status
    }
    if (this.code) {
      result.code = this.code
    }
    if (this.help) {
      result.help = this.help
    }
    return result
  }
}
