/**
 * This is implemented as an mjs file explicitly since it
 * needs to be loaded as a worker thread. It can't be
 * TypeScript for unit testing reasons. It also can't be
 * TS in production since Node 20 does not have type stripping.
 */

import { isMainThread, workerData } from 'node:worker_threads'
import { fetch, EnvHttpProxyAgent, getGlobalDispatcher } from 'undici'

if (isMainThread) {
  throw new Error('This script should only be run as a worker thread')
}

/**
 * The core Error type used when this worker fails.
 * It is the only error to exit the message bus.
 * Any other errors are causes of this one.
 */
class FetchWorkerError extends Error {
  constructor(context) {
    super('FetchWorkerError')
    this.name = 'FetchWorkerError'
    this.context = context
  }
}

/**
 * A specific error used when we encounter an error that
 * can not be serialized.
 */
class OverrideError extends Error {
  constructor(message, sourceName) {
    super(message)
    this.name = 'OverrideError'
    this.sourceName = sourceName
  }
}

/**
 * Attempt to sanitize a URL by redacting sensitive information.
 * If the URL cannot be parsed, it is returned as-is.
 *
 * @internal
 *
 * @param {string} url - The URL to sanitize.
 *
 * @returns {string} The sanitized URL with sensitive information redacted.
 */
const sanitizeUrl = url => {
  try {
    const parsedUrl = new URL(url)

    if (parsedUrl.username !== '') {
      parsedUrl.username = 'REDACTED'
    }
    if (parsedUrl.password !== '') {
      parsedUrl.password = 'REDACTED'
    }

    return parsedUrl.toString()
  } catch {
    return url
  }
}

/**
 * Tested source in ./shouldUseEnvProxy.ts
 * This has to be duplicated since we can't import here and
 * it is ran in a worker thread. Once Node 22+ is our
 * minimum, we can convert this file to TS since that has
 * erasable types.
 */
export const shouldUseEnvProxy = function () {
  const envVars = [
    'AXE_WATCHER_USE_ENV_PROXY',
    'AXE_USE_ENV_PROXY',
    'NODE_USE_ENV_PROXY'
  ]

  for (const envVar of envVars) {
    const value = (
      process.env[envVar.toLowerCase()] ?? process.env[envVar]
    )?.trim()

    if (value === '0') {
      return false
    }

    if (value === '1') {
      return true
    }
  }

  return false
}

/**
 * @typedef {Object} WorkerData
 * @property {string} url - The URL to fetch.
 * @property {Object} options - The fetch options.
 * @property {number} timeout - The timeout in milliseconds.
 * @property {MessagePort} port - The message port for communication.
 */
const { url, options, timeout, port } = workerData

try {
  let dispatcher = getGlobalDispatcher()

  if (shouldUseEnvProxy()) {
    dispatcher = new EnvHttpProxyAgent()
  }

  const response = await fetch(url, {
    ...options,
    dispatcher,
    signal: AbortSignal.timeout(timeout)
  })

  const contentType = response.headers.get('Content-Type')
  const isResponseJson = contentType?.startsWith('application/json') ?? false
  const body = isResponseJson ? await response.json() : await response.text()

  const responseData = {
    status: response.status,
    statusText: response.statusText,
    // Headers can't be serialized directly
    headers: Object.fromEntries(response.headers.entries()),
    body,
    ok: response.ok
  }

  port.postMessage({
    data: responseData
  })
} catch (error) {
  const HTTP_PROXY = process.env.http_proxy || process.env.HTTP_PROXY
  const HTTPS_PROXY = process.env.https_proxy || process.env.HTTPS_PROXY
  const NO_PROXY = process.env.no_proxy || process.env.NO_PROXY

  const context = {
    url: sanitizeUrl(url),
    env: {
      HTTP_PROXY: HTTP_PROXY ? sanitizeUrl(HTTP_PROXY) : undefined,
      HTTPS_PROXY: HTTPS_PROXY ? sanitizeUrl(HTTPS_PROXY) : undefined,
      NO_PROXY,
      NODE_EXTRA_CA_CERTS: process.env.NODE_EXTRA_CA_CERTS,
      NODE_TLS_REJECT_UNAUTHORIZED: process.env.NODE_TLS_REJECT_UNAUTHORIZED
    }
  }

  const err = new FetchWorkerError(context)
  err.cause = error

  if (error instanceof DOMException) {
    if (error.name === 'TimeoutError') {
      const timeoutError = new OverrideError(
        `The operation was aborted due to timeout (${timeout}ms)`,
        'TimeoutError'
      )
      err.cause = timeoutError
    }
  }

  port.postMessage({
    error: err
  })
} finally {
  port.close()
}
