import fetch from 'cross-fetch'
import { debug, debugMode } from '../debug.js'
import type { AbsoluteUrl } from '../types/common.js'
import type { Agent as HttpAgent } from 'node:http'
import type { Agent as HttpsAgent } from 'node:https'

export type HttpHeaderKey = 'content-type' | 'cookie' | 'user-agent'
export type HttpHeaders = Partial<Record<HttpHeaderKey, string>>
export type HttpMethodLowerCased = 'get' | 'post' | 'put' | 'delete'
export type HttpMethod = HttpMethodLowerCased | Uppercase<HttpMethodLowerCased>

export interface CustomFetchOptions {
  method?: HttpMethod
  headers?: HttpHeaders
  timeout?: number
  agent?: HttpAgent | HttpsAgent
  body?: string
}

export type HttpRequestAgent = HttpAgent | HttpsAgent

export async function customFetch (url: AbsoluteUrl, { timeout, ...options }: CustomFetchOptions = {}) {
  options.headers ??= {}
  options.headers['accept-encoding'] = 'gzip,deflate'
  if (debugMode) {
    const { method = 'get', headers, body } = options
    debug('request', method.toUpperCase(), url, {
      headers: obfuscateHeaders(headers),
      body: obfuscateBody({ url, body }),
    })
  }
  try {
    return await fetchWithTimeout(url, options, timeout)
  } catch (err) {
    if (err.type === 'aborted') {
      const rephrasedErr = new Error('request timeout')
      rephrasedErr.cause = err
      throw rephrasedErr
    } else {
      throw err
    }
  }
}

// Based on https://stackoverflow.com/questions/46946380/fetch-api-request-timeout#57888548
function fetchWithTimeout (url: AbsoluteUrl, options: CustomFetchOptions, timeoutMs = 120_000) {
  const controller = new AbortController()
  const promise = fetch(url, {
    keepalive: true,
    signal: controller.signal,
    credentials: 'include',
    mode: 'cors',
    ...options,
  })
  const timeout = setTimeout(() => controller.abort(), timeoutMs)
  return promise.finally(() => clearTimeout(timeout))
}

function obfuscateHeaders (headers: HttpHeaders) {
  const obfuscatedHeadersEntries = Object.entries(headers).map(([ name, value ]) => [ name.toLowerCase(), value ])
  const obfuscatedHeaders = Object.fromEntries(obfuscatedHeadersEntries)
  if (obfuscatedHeaders.authorization) {
    obfuscatedHeaders.authorization = obfuscatedHeaders.authorization.replace(/"[^"]+"/g, '"***"')
  }
  if (obfuscatedHeaders.cookie) {
    obfuscatedHeaders.cookie = obfuscateParams(obfuscatedHeaders.cookie)
  }
  return obfuscatedHeaders
}

function obfuscateBody ({ url, body = '' }: { url: string, body?: string }) {
  const { searchParams } = new URL(url)
  if (searchParams.get('action') === 'login') {
    return obfuscateParams(body)
  } else {
    return body.replace(/token=[^=\s;&]+([=\s;&]?)/g, 'token=***$1')
  }
}

function obfuscateParams (urlEncodedStr: string) {
  return urlEncodedStr.replace(/=[^=\s;&]+([=\s;&]?)/g, '=***$1')
}
