'use strict'

const { InvalidArgumentError, SocketError } = require('../core/errors')
const { AsyncResource } = require('node:async_hooks')
const assert = require('node:assert')
const util = require('../core/util')
const { kHTTP2Stream } = require('../core/symbols')
const { addSignal, removeSignal } = require('./abort-signal')

class UpgradeHandler extends AsyncResource {
  constructor (opts, callback) {
    if (!opts || typeof opts !== 'object') {
      throw new InvalidArgumentError('invalid opts')
    }

    if (typeof callback !== 'function') {
      throw new InvalidArgumentError('invalid callback')
    }

    const { signal, opaque, responseHeaders } = opts

    if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
      throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
    }

    super('UNDICI_UPGRADE')

    this.responseHeaders = responseHeaders || null
    this.opaque = opaque || null
    this.callback = callback
    this.abort = null
    this.context = null

    addSignal(this, signal)
  }

  onRequestStart (controller, context) {
    if (this.reason) {
      controller.abort(this.reason)
      return
    }

    assert(this.callback)

    this.abort = (reason) => controller.abort(reason)
    this.context = context
  }

  onResponseStart () {
    throw new SocketError('bad upgrade', null)
  }

  onRequestUpgrade (controller, statusCode, headers, socket) {
    const expectedStatusCode = socket[kHTTP2Stream] === true ? 200 : 101

    if (statusCode !== expectedStatusCode) {
      const socketInfo = socket[kHTTP2Stream] === true ? null : util.getSocketInfo(socket)
      controller.abort(new SocketError('bad upgrade', socketInfo))
      return
    }

    const { callback, opaque, context } = this

    removeSignal(this)

    this.callback = null

    const rawHeaders = controller?.rawHeaders
    const responseHeaders = this.responseHeaders === 'raw'
      ? util.parseRawHeaders(rawHeaders)
      : headers

    this.runInAsyncScope(callback, null, null, {
      headers: responseHeaders,
      socket,
      opaque,
      context
    })
  }

  onResponseError (_controller, err) {
    const { callback, opaque } = this

    removeSignal(this)

    if (callback) {
      this.callback = null
      queueMicrotask(() => {
        this.runInAsyncScope(callback, null, err, { opaque })
      })
    }
  }
}

function upgrade (opts, callback) {
  if (callback === undefined) {
    return new Promise((resolve, reject) => {
      upgrade.call(this, opts, (err, data) => {
        return err ? reject(err) : resolve(data)
      })
    })
  }

  try {
    const upgradeHandler = new UpgradeHandler(opts, callback)
    const upgradeOpts = {
      ...opts,
      method: opts.method || 'GET',
      upgrade: opts.protocol || 'Websocket'
    }
    this.dispatch(upgradeOpts, upgradeHandler)
  } catch (err) {
    if (typeof callback !== 'function') {
      throw err
    }
    const opaque = opts?.opaque
    queueMicrotask(() => callback(err, { opaque }))
  }
}

module.exports = upgrade
