import type http from 'http'
import { IProperty, Property } from '../../models/Property.js'

interface RawValue {
  name: string
  value: string
  enabled: boolean
}

/**
 * Normalizes name of a header.
 *
 * @param name
 * @return Normalized name
 */
function normalizeName(name: string): string {
  if (typeof name !== 'string') {
    name = String(name)
  }
  return name.toLowerCase()
}

/**
 * Normalizes value of a header.
 * @param value
 * @return Normalized name
 */
function normalizeValue(value: string): string {
  if (typeof value !== 'string') {
    value = String(value)
  }
  return value
}

/**
 * A generator for list of headers from a string.
 *
 * ```javascript
 * for (let [name, value] of headersStringToList('a:b')) {
 *  ...
 * }
 * ```
 * @param string Headers string to parse
 */
function* headersStringToList(string: string): Generator<RawValue> {
  if (!string || string.trim() === '') {
    return []
  }
  const headers = string.split(/\n(?=[^ \t]+)/gim)
  for (let i = 0, len = headers.length; i < len; i++) {
    let line = headers[i].trim()
    if (line === '') {
      continue
    }
    const disabled = line.startsWith('#')
    if (disabled) {
      line = line.replace('#', '').trim()
    }

    const sepPosition = line.indexOf(':')
    if (sepPosition === -1) {
      yield { name: line, value: '', enabled: !disabled }
    } else {
      const name = line.substring(0, sepPosition)
      const value = line.substring(sepPosition + 1).trim()
      yield { name, value, enabled: !disabled }
    }
  }
}

/**
 * The same interface as Web platform's Headers but without
 * CORS restrictions.
 */
export class Headers {
  /**
   * The keys are canonical keys and the values are the input values.
   */
  _map: Record<string, RawValue> = {}

  /**
   * @param headers The headers to parse.
   */
  constructor(headers?: string | Record<string, string> | Headers | http.IncomingHttpHeaders) {
    if (headers instanceof Headers) {
      headers.forEach((value, name) => this.append(name, value))
    } else if (typeof headers === 'string') {
      for (const item of headersStringToList(headers)) {
        this.append(item.name, item.value, item.enabled)
      }
    } else if (headers) {
      Object.keys(headers).forEach((name) => this.append(name, headers[name]))
    }
  }

  /**
   * Adds value to existing header or creates new header
   */
  append(name: string, value: string | string[] | undefined, enabled = true): void {
    if (Array.isArray(value)) {
      value.forEach((v) => this.append(name, v))
      return
    }
    const normalizedName = normalizeName(name)
    value = value ? normalizeValue(value) : ''
    let item = this._map[normalizedName]
    if (item) {
      const oldValue = item.value
      item.value = oldValue ? `${oldValue},${value}` : value
    } else {
      item = {
        name,
        value,
        enabled,
      }
    }
    this._map[normalizedName] = item
  }

  /**
   * Removes a header from the list of headers.
   * @param name The header name
   */
  delete(name: string): void {
    // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
    delete this._map[normalizeName(name)]
  }

  /**
   * Returns the current value of the header
   * @param name Header name
   */
  get(name: string): string | undefined {
    name = normalizeName(name)
    if (!this.has(name)) {
      return undefined
    }
    if (!this._map[name].enabled) {
      return undefined
    }
    return this._map[name].value
  }

  /**
   * Checks if the header exists.
   */
  has(name: string): boolean {
    const normalized = normalizeName(name)
    return Object.prototype.hasOwnProperty.call(this._map, normalized) && this._map[normalized].enabled
  }

  /**
   * Creates a new header. If the header exist it replaces the value.
   */
  set(name: string, value: string, enabled = true): void {
    const normalizedName = normalizeName(name)
    this._map[normalizedName] = {
      value: normalizeValue(value),
      name,
      enabled,
    }
  }

  /**
   * Iterates over each header.
   */
  forEach(
    callback: (value: string, name: string, enabled: boolean, headers: Headers) => void,
    thisArg?: unknown
  ): void {
    const keys = Object.keys(this._map)
    keys.forEach((key) => {
      const item = this._map[key]
      callback.call(thisArg, item.value, item.name, item.enabled, this)
    })
  }

  /**
   * Calls a defined callback function on each element of the headers, and returns an array that contains the results.
   *
   * @param callbackfn A function that accepts up to two arguments.
   *  The map method calls the callbackfn function one time for each header.
   * @param thisArg An object to which the `this` keyword can refer in the callbackfn function.
   *  If thisArg is omitted, undefined is used as the this value.
   */
  map<U>(callbackfn: (value: string, name: string, enabled: boolean) => U, thisArg?: unknown): U[] {
    const keys = Object.keys(this._map)
    const results: U[] = []
    for (const name of keys) {
      const item = this._map[name]
      const cbReturn = callbackfn.call(thisArg, item.value, item.name, item.enabled)
      results.push(cbReturn)
    }
    return results
  }

  /**
   * @return The headers HTTP string
   */
  toString(): string {
    return this._generateString()
  }

  /**
   * @returns The HTTP headers string with `#` marks when the header is disabled
   */
  toRawString(): string {
    return this._generateString(true)
  }

  /**
   * @param list The Property list to create the Headers from.
   */
  static fromPropertyArray(list: IProperty[]): Headers {
    const result = new Headers()
    list.forEach((prop) => {
      result.append(prop.name, String(prop.value), prop.enabled)
    })
    return result
  }

  /**
   * Transforms current headers list to property list.
   */
  toPropertyArray(): IProperty[] {
    return this.map((value, name, enabled) => {
      const item = Property.String(name, value, enabled)
      return item.toJSON()
    })
  }

  /**
   * Generates a map of header that can be applied to the `Request` object
   */
  toJSON(): Record<string, string> {
    const result: Record<string, string> = {}
    for (const [name, value] of this.entries()) {
      result[name] = value
    }
    return result
  }

  protected _generateString(withDisabled = false): string {
    const result: string[] = []
    const keys = Object.keys(this._map)
    for (const name of keys) {
      const item = this._map[name]
      let line = ''
      if (!item.enabled) {
        if (withDisabled) {
          line += '# '
        } else {
          continue
        }
      }
      line += `${item.name}: `
      if (item.value) {
        line += item.value
      }
      result.push(line)
    }
    return result.join('\n')
  }

  /**
   * Iterates over keys.
   */
  *keys(): IterableIterator<string> {
    const keys = Object.keys(this._map)
    for (const name of keys) {
      const item = this._map[name]
      if (item.enabled) {
        yield this._map[name].name
      }
    }
  }

  /**
   * Iterates over values.
   */
  *values(): IterableIterator<string> {
    const keys = Object.keys(this._map)
    for (const name of keys) {
      const item = this._map[name]
      if (item.enabled) {
        yield this._map[name].value
      }
    }
  }

  /**
   * Iterates over headers.
   */
  *entries(): IterableIterator<string[]> {
    const keys = Object.keys(this._map)
    for (const name of keys) {
      const item = this._map[name]
      if (item.enabled) {
        yield [this._map[name].name, this._map[name].value]
      }
    }
  }

  /**
   * Iterates over headers.
   */
  *[Symbol.iterator](): IterableIterator<string[]> {
    const keys = Object.keys(this._map)
    for (const name of keys) {
      const item = this._map[name]
      if (item.enabled) {
        yield [this._map[name].name, this._map[name].value]
      }
    }
  }
}
