import { IHttpRequest, HttpRequest } from '../models/HttpRequest.js'
import { ISentRequest, SentRequest } from '../models/SentRequest.js'
import { IHttpResponse, HttpResponse } from '../models/HttpResponse.js'
import { IErrorResponse, ErrorResponse } from '../models/ErrorResponse.js'
import { Headers } from '../lib/headers/Headers.js'
import { DataReader } from './DataReader.js'
import { JmespathReader } from './JmespathReader.js'
import { XmlReader } from './XmlReader.js'
import { UrlEncodedReader } from './UrlEncodedReader.js'
import { CookieParser } from '../cookies/CookieParser.js'

type DataInput =
  | HttpRequest
  | SentRequest
  | IHttpRequest
  | ISentRequest
  | IHttpResponse
  | HttpResponse
  | IErrorResponse
  | ErrorResponse
  | undefined

/**
 * This is a factory class for the `RequestDataExtractor`.
 */
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class DataExtractor {
  /**
   * Reads the value of a cookie.
   * @param name The name of the cookie to read.
   * @param input The response object.
   */
  static readCookieValue(
    name: string,
    input: IHttpResponse | HttpResponse | IErrorResponse | ErrorResponse | undefined
  ): string | undefined {
    const headers = this.readHeaders(input)
    if (!headers) {
      return undefined
    }
    const parsed = new Headers(headers)
    const header = parsed.get('set-cookie')
    if (!header) {
      return undefined
    }
    const cookies = CookieParser.parseRelaxed(header)
    const cookie = cookies.find((i) => i.name === name)
    if (cookie) {
      return cookie.value
    }
    return undefined
  }

  static readHeaders(obj: DataInput): string {
    if (!obj) {
      return ''
    }
    const typedInstance = obj as HttpRequest | SentRequest
    if (typedInstance.headersModel && typeof typedInstance.headersModel.toRawString === 'function') {
      return typedInstance.headersModel.toString()
    }
    const typedSchema = obj as IHttpRequest | ISentRequest
    if (Array.isArray(typedSchema.headersModel)) {
      const headers = Headers.fromPropertyArray(typedSchema.headersModel)
      return headers.toString()
    }
    return obj.headers || ''
  }

  /**
   * Reads value from the body.
   * @param value The read body value as string
   * @param mime The body mime
   * @param path THe path to read.
   * @returns The extracted value.
   */
  static async extractBody(value: string, mime: string, path: string): Promise<string | undefined> {
    let reader: DataReader | undefined
    if (mime.includes('json')) {
      // reader = new JsonReader();
      reader = new JmespathReader()
    } else if (mime.includes('xml')) {
      reader = new XmlReader()
    } else if (mime.includes('x-www-form-urlencoded')) {
      reader = new UrlEncodedReader()
    }
    if (!reader) {
      return undefined
    }
    await reader.writePayload(value)
    return reader.getValue(path) as Promise<string>
  }

  /**
   * Returns the value for path for given source object
   *
   * @param url An url to parse.
   * @param path Path to the data
   * @return Value for the path.
   */
  static getDataUrl(url: string, path: string[]): string | number | undefined {
    if (!path || path.length === 0 || !url) {
      return url
    }
    let value: URL | undefined
    try {
      value = new URL(url)
    } catch {
      //
    }
    if (!value) {
      return undefined
    }
    switch (path[0]) {
      case 'host':
        return value.host
      case 'protocol':
        return value.protocol
      case 'path':
        return value.pathname
      case 'query':
        return DataExtractor.readUrlQueryValue(value, path[1]) || undefined
      case 'hash':
        return DataExtractor.readUrlHashValue(value, path[1]) || undefined
      default:
        throw new Error(`Unknown path in the URL: ${path}`)
    }
  }

  /**
   * Reads value of the URL query parameters.
   *
   * The `?` at the beginning of the query string is removed.
   *
   * @param url The URL object instance
   * @param param Param name to return. If not set then it returns  whole query string value.
   * @return Full query string value if `param` is not set or parameter
   * value. This function does not returns `null` values.
   */
  static readUrlQueryValue(url: URL, param?: string): string | null {
    if (!param) {
      let v = url.search || ''
      if (v[0] === '?') {
        v = v.substring(1)
      }
      return v
    }
    let value = url.searchParams.get(param)
    if (!value && value !== '') {
      value = null
    }
    return value
  }

  /**
   * Reads value of the URL hash.
   *
   * The `#` at the beginning of the hash string is removed.
   *
   * If the `param` argument is set then it treats hahs value as a query
   * parameters string and parses it to get the value.
   *
   * @param url The URL object instance
   * @param param Param name to return. If not set then it returns whole hash string value.
   * @return Hash parameter or whole hash value.
   */
  static readUrlHashValue(url: URL, param?: string): string | undefined {
    let value: string | undefined = (url.hash || '').substring(1)
    if (!param) {
      return value
    }
    const obj = new URLSearchParams(value)
    value = obj.get(param) || undefined
    if (!value && value !== '') {
      value = undefined
    }
    return value
  }

  /**
   * Returns a value for the headers.
   *
   * @param source HTTP headers string
   * @param path The path to the object
   * @returns The value for the path.
   */
  static getDataHeaders(source: string, path: string[]): string | undefined {
    if (!path || !path.length || !path[0]) {
      return source
    }
    const headers = new Headers(source)
    const lowerName = path[0].toLowerCase()
    for (const [name, value] of headers) {
      if (name.toLowerCase() === lowerName) {
        return value
      }
    }
    return undefined
  }
}
