import net from 'net'
import { URL } from 'url'
import tls from 'tls'
import { EventEmitter } from 'events'
import { Logger, type ILogObj } from 'tslog'
import { IRequestLog } from '../../models/RequestLog.js'
import { IHttpRequest, HttpRequest } from '../../models/HttpRequest.js'
import { IRequestBaseConfig } from '../../models/RequestConfig.js'
import { IRequestAuthorization } from '../../models/RequestAuthorization.js'

import { HttpCertificate } from '../../models/ClientCertificate.js'
import { SentRequest } from '../../models/SentRequest.js'
import { Response } from '../../models/Response.js'
import { ErrorResponse } from '../../models/ErrorResponse.js'
import { RequestsSize } from '../../models/RequestsSize.js'
import { HttpResponse } from '../../models/HttpResponse.js'
import { ResponseRedirect } from '../../models/ResponseRedirect.js'
import { RequestLog } from '../../models/RequestLog.js'
import { RequestTime } from '../../models/RequestTime.js'
import { SerializableError } from '../../models/SerializableError.js'
import { ResponseAuthorization } from '../../models/ResponseAuthorization.js'
import { Headers } from '../../lib/headers/Headers.js'
import { getPort } from './RequestUtils.js'

import { HttpResponseParser, ParserCallbacks, RequestState } from './parsers/index.js'
import { ConnectionManager } from './connections/index.js'
import { AuthManager, type ProxyAuthCredentials } from './auth/index.js'
import { MessageBuilder } from './message/index.js'
import { HttpEngineErrorFactory } from './errors/index.js'
import { isMethodWithoutBody, HEADER_PROXY_AUTHORIZATION } from './constants.js'
import { decompress } from './compression/index.js'
import { checkServerIdentity } from './certificates/index.js'
import { readUrl, getHostHeader, getRedirectLocation, isRedirectLoop } from './url/index.js'
import { processRedirectCookies } from './cookies/index.js'
import { computeStats } from './statistics/index.js'
import { processResponse } from './response/index.js'
import { getCodeMessage } from './HttpErrorCodes.js'
import { createLogger } from '../../lib/logging/index.js'
import * as PayloadSupport from './PayloadSupport.js'
import { addContentLength } from './RequestUtils.js'
import { PayloadSerializer } from '../../lib/transformers/PayloadSerializer.js'
import { ProxyAuthenticationError } from './errors/HttpEngineErrors.js'

export interface HttpEngineOptions extends IRequestBaseConfig {
  /**
   * The authorization configuration to apply to the request.
   */
  authorization?: IRequestAuthorization[]
  /**
   * Logger object.
   */
  logger?: Logger<ILogObj>
  /**
   * A certificate to use with the request.
   */
  certificates?: HttpCertificate[]
}

export interface RequestStats {
  firstReceiveTime?: number
  lastReceivedTime?: number
  messageStart?: number
  sentTime?: number
  connectionTime?: number
  lookupTime?: number
  connectedTime?: number
  secureStartTime?: number
  secureConnectedTime?: number
  startTime?: number
  responseTime?: number
  receivingTime?: number
}

export interface ResponseErrorInit {
  code?: number | string
  message?: string
}

export interface BeforeRedirectDetail {
  location: string
  returnValue: boolean
}

export interface HeadersReceivedDetail {
  value: string
  returnValue: boolean
}

export interface IRequestAuthState {
  method: 'ntlm'
  state: number
  headers?: string
  challengeHeader?: string
}

export const mainPromiseSymbol = Symbol('mainPromise')

/**
 * API Client's HTTP engine with refactored parsing and connection handling.
 * An HTTP 1.1 engine working directly on the socket. It communicates with the remote machine and
 * collects stats about the request and response.
 */
export class CoreEngine extends EventEmitter {
  request: HttpRequest
  opts: HttpEngineOptions
  logger: Logger<ILogObj>

  /**
   * The current sent request
   */
  sentRequest: SentRequest

  redirects: ResponseRedirect[] = []
  /**
   * When true the request has been aborted.
   */
  aborted = false
  /**
   * Parsed value of the request URL.
   */
  uri: URL

  socket?: net.Socket
  /**
   * Host header can be different than registered URL because of
   * `hosts` rules.
   * If a rule changes host value of the URL the original URL host value
   * is used when generating the request and not overwritten one.
   * This way virtual hosts can be tested using hosts.
   */
  hostHeader: string | undefined

  protected hostTestReg = /^\s*host\s*:/im
  /**
   * Set when the request is redirected.
   */
  redirecting = false

  /**
   * The response headers.
   * The object may be empty when the response is not set.
   */
  currentHeaders = new Headers()

  /**
   * The response object build during the execution.
   */
  currentResponse?: Response

  /**
   * Keeps the raw body in a temporary buffer while processing the response.
   */
  _rawBody?: Buffer

  stats: RequestStats = {}

  auth?: IRequestAuthState

  protected mainResolver?: (log: IRequestLog) => void
  protected mainRejecter?: (err: SerializableError) => void;
  [mainPromiseSymbol]?: Promise<IRequestLog>

  protected _signal?: AbortSignal

  // CoreEngine specific properties
  private parser: HttpResponseParser
  private connectionManager: ConnectionManager
  private authManager: AuthManager
  private messageBuilder: MessageBuilder

  hasProxy: boolean
  isProxyTunnel: boolean
  isProxySsl: boolean

  /**
   * @return True if following redirects is allowed.
   */
  get followRedirects(): boolean {
    const { opts } = this
    if (typeof opts.followRedirects === 'boolean') {
      return opts.followRedirects
    }
    return true
  }

  /**
   * The request timeout.
   */
  get timeout(): number {
    const { opts } = this
    if (typeof opts.timeout === 'number') {
      return opts.timeout
    }
    return 0
  }

  /**
   * The abort signal to set on this request.
   * Aborts the request when the signal fires.
   */
  get signal(): AbortSignal | undefined {
    return this._signal
  }

  set signal(value: AbortSignal | undefined) {
    const old = this._signal
    if (old === value) {
      return
    }
    this._signal = value
    if (old) {
      old.removeEventListener('abort', this._abortHandler)
    }
    if (value) {
      value.addEventListener('abort', this._abortHandler)
    }
  }

  constructor(request: IHttpRequest, opts: HttpEngineOptions = {}) {
    super()
    this.request = new HttpRequest({ ...request })
    this.opts = opts
    this.logger = this.setupLogger(opts)
    this.sentRequest = new SentRequest({ ...request, startTime: Date.now() })
    this.uri = this.readUrl(request.url)
    this.hostHeader = this.getHostHeader(request.url)

    // CoreEngine specific initialization
    this.hasProxy = !!this.opts.proxy
    this.isProxyTunnel = this.hasProxy && this.request.url.startsWith('https:')
    this.isProxySsl = !!this.opts.proxy && this.opts.proxy.startsWith('https:')

    // Initialize the HTTP response parser
    this.parser = new HttpResponseParser(this.logger, this.createParserCallbacks())

    // Initialize the connection manager
    this.connectionManager = new ConnectionManager(this.logger, this.stats)

    // Initialize the auth manager
    const proxyCredentials: ProxyAuthCredentials = {
      proxyUsername: this.opts.proxyUsername,
      proxyPassword: this.opts.proxyPassword,
      proxyAuthorization: this.opts.proxyAuthorization,
    }

    this.authManager = new AuthManager({
      logger: this.logger,
      proxyCredentials,
    })

    // Initialize the message builder
    this.messageBuilder = new MessageBuilder({
      logger: this.logger,
      request: this.request as IHttpRequest,
      hasProxy: this.hasProxy,
      isProxyTunnel: this.isProxyTunnel,
      hostHeader: this.hostHeader,
    })

    this._abortHandler = this._abortHandler.bind(this)
    if (opts.signal) {
      this.signal = opts.signal
    }
  }

  /**
   * Creates a logger object to log debug output.
   */
  setupLogger(opts: HttpEngineOptions = {}): Logger<ILogObj> {
    if (opts.logger) {
      return opts.logger
    }
    return createLogger()
  }

  /**
   * Updates the `uri` property from current request URL
   */
  readUrl(value: string): URL {
    return readUrl(value, { hosts: this.opts.hosts })
  }

  /**
   * Get host header for the request
   */
  getHostHeader(url: string): string | undefined {
    return getHostHeader(url, { hosts: this.opts.hosts })
  }

  /**
   * Aborts current request.
   * It emits `error` event
   */
  abort(): void {
    this.logger.debug('Aborting the request...')
    this.aborted = true
    if (!this.socket) {
      return
    }
    this.socket.removeAllListeners()
    if (this.socket.destroyed) {
      this.socket = undefined
      return
    }
    this.socket.pause()
    this.socket.destroy()
    this.socket = undefined
  }

  /**
   * Handler for the `abort` event on the `AbortSignal`.
   */
  protected _abortHandler(): void {
    const e = new SerializableError('Request aborted', 3)
    this._errorRequest(e)
    this.abort()
  }

  /**
   * Create parser callbacks to handle parsed data
   */
  private createParserCallbacks(): ParserCallbacks {
    return {
      onStatusParsed: (status: number, statusText: string) => {
        this.logger.debug('onStatusParsed called', status, statusText)
        const response = Response.fromValues(status, statusText)
        response.loadingTime = 0
        this.currentResponse = response
      },
      onHeadersParsed: (headers: Headers) => {
        this.logger.debug('onHeadersParsed called')
        this.currentHeaders = headers
        if (this.currentResponse) {
          this.currentResponse.headers = headers.toString()
        }
      },
      onBodyComplete: (body: Buffer) => {
        this.logger.debug('onBodyComplete called')
        this._rawBody = body
        this._reportResponse().catch((error) => {
          this.logger.error('Error in _reportResponse:', error)
          this._errorRequest({
            message: `Response processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
          })
        })
      },
      onError: (error: Error) => {
        this.logger.debug('onError called', error)
        this._errorRequest({
          message: error.message || 'Unknown error occurred',
        })
      },
      onAbort: () => {
        this.logger.debug('onAbort called')
        this.abort()
      },
      emit: (event: string, detail?: unknown) => {
        this.logger.debug('emit called', event, detail)
        this.emit(event, detail)
      },
    }
  }

  /**
   * Sends the request
   */
  async send(): Promise<IRequestLog> {
    const promise = this.wrapExecution()
    this.sendRequest()
    return promise
  }

  private async sendRequest(): Promise<void> {
    try {
      // Use the connection manager to establish the connection
      const socket = await this.establishConnection()
      if (!socket || this.aborted) {
        return
      }

      const message = await this.prepareMessage()
      await this.writeMessage(message)
    } catch (cause) {
      if (cause instanceof ProxyAuthenticationError) {
        this.currentHeaders = new Headers(cause.response.headers)
        const currentResponse = Response.fromValues(
          cause.response.statusCode || 401,
          cause.response.statusMessage,
          this.currentHeaders.toString()
        )
        currentResponse.loadingTime = 0
        this.currentResponse = currentResponse
        if (cause.head.length) {
          this._rawBody = cause.head
          currentResponse.payload = PayloadSerializer.stringifyBuffer(cause.head)
        }
        this._publishResponse()
        return
      }
      const e = cause as SerializableError
      const err = new SerializableError(e.message, { cause: e })
      if (e.code || e.code === 0) {
        err.code = e.code as string
      }
      this.abort()
      this._errorRequest({
        message: err.message,
        code: err.code,
      })
    }
  }

  /**
   * Establish connection using the connection manager
   */
  private async establishConnection(): Promise<net.Socket> {
    const port = getPort(this.uri.port, this.uri.protocol)
    const host = this.uri.hostname
    const protocol = this.uri.protocol

    const socket = await this.connectionManager.createConnection(host, port, protocol, this.opts)
    const { timeout } = this
    if (timeout > 0) {
      socket.setTimeout(timeout)
    }
    this.socket = socket
    this._addSocketListeners(socket)
    socket.resume()
    return socket
  }

  /**
   * Decompresses received body if `content-encoding` header is set.
   */
  async decompress(body?: Buffer): Promise<Buffer | undefined> {
    return decompress(body, {
      aborted: this.aborted,
      headers: this.currentHeaders,
    })
  }

  /**
   * Prepares headers list to be send to the remote machine.
   * If `defaultHeaders` option is set then it adds `user-agent` and `accept`
   * headers.
   */
  prepareHeaders(headers: Headers, withPayload = false): void {
    if (this.opts.defaultHeaders) {
      if (!headers.has('user-agent')) {
        if (this.opts.defaultUserAgent) {
          headers.set('user-agent', this.opts.defaultUserAgent)
        } else {
          headers.set('user-agent', 'api client')
        }
      }
      if (!headers.has('accept')) {
        if (this.opts.defaultAccept) {
          headers.set('accept', this.opts.defaultAccept)
        } else {
          headers.set('accept', '*/*')
        }
      }
    }
    if (withPayload) {
      if (!headers.has('content-type') && this.request.payloadOptions) {
        if (this.request.payloadOptions.mime) {
          headers.set('content-type', this.request.payloadOptions.mime)
        }
      }
    }
  }

  /**
   * Cleans the state after finished.
   */
  _cleanUp(): void {
    this.redirects = []
    this.currentHeaders = new Headers()
    this.currentResponse = undefined
    this._rawBody = undefined
    this.redirecting = false
    this.stats = {}
    this.mainRejecter = undefined
    this.mainResolver = undefined
    this._clearSocketEventListeners()
    this.parser.reset()
  }

  /**
   * Cleans up the state for redirect.
   */
  _cleanUpRedirect(): void {
    this.currentHeaders = new Headers()
    this.currentResponse = undefined
    this._rawBody = undefined
    this.stats = {}
    this._clearSocketEventListeners()
    this.parser.reset()
  }

  /**
   * Reports response when redirected.
   */
  _reportRedirect(status: number): boolean {
    const { request, currentHeaders } = this
    if (!currentHeaders) {
      return false
    }
    const rerUrl = currentHeaders.get('location')
    // https://github.com/jarrodek/socket-fetch/issues/13
    const redirectOptions = this.getRedirectOptions(status, request.method, rerUrl)
    if (!redirectOptions.redirect) {
      return false
    }
    this.redirecting = true
    setTimeout(() => this._redirectRequest(redirectOptions))
    return true
  }

  /**
   * Get redirect options based on status and method
   */
  private getRedirectOptions(
    status: number,
    method?: string,
    location?: string
  ): { redirect: boolean; location?: string; forceGet?: boolean } {
    if (!location) {
      return { redirect: false }
    }

    // 301, 302, 303, 307, 308 are redirect status codes
    if (status >= 300 && status < 400) {
      const forceGet = status === 301 || status === 302 || status === 303
      return { redirect: true, location, forceGet }
    }

    return { redirect: false }
  }

  /**
   * Creates a response and adds it to the redirects list and redirects
   * the request to the new location.
   */
  async _redirectRequest(options: { redirect: boolean; location?: string; forceGet?: boolean }): Promise<void> {
    if (this.followRedirects === false) {
      this._publishResponse()
      return
    }
    const location = options.location && this.getRedirectLocation(options.location, this.request.url)
    if (!location) {
      this._errorRequest({ code: 302 })
      return
    }

    // check if this is infinite loop
    if (this.isRedirectLoop(location, this.redirects)) {
      this._errorRequest({ code: 310 })
      return
    }

    const detail: BeforeRedirectDetail = {
      location,
      returnValue: true,
    }
    this.emit('beforeredirect', detail)
    if (!detail.returnValue) {
      this._publishResponse()
      return
    }
    try {
      const responseCookies = this.currentHeaders.get('set-cookie')
      const response = await this._createRedirectResponse(location)
      this.redirects.push(response)
      this._cleanUpRedirect()
      if (responseCookies) {
        this._processRedirectCookies(responseCookies, location)
      }
      this.redirecting = false

      this.request.url = location
      this.sentRequest.url = location
      if (options.forceGet) {
        this.request.method = 'GET'
      }
      this.uri = this.readUrl(location)
      this.hostHeader = this.getHostHeader(location)
      // No idea why but without setTimeout the program loses it's
      // scope after calling the function.
      setTimeout(() => this.send())
    } catch (e) {
      const error = e as Error
      this._errorRequest({
        message: (error && error.message) || 'Unknown error occurred',
      })
    }
  }

  /**
   * Get redirect location
   */
  private getRedirectLocation(location: string, currentUrl: string): string {
    return getRedirectLocation(location, currentUrl)
  }

  /**
   * Check if redirect is a loop
   */
  private isRedirectLoop(location: string, redirects: ResponseRedirect[]): boolean {
    return isRedirectLoop(location, redirects)
  }

  /**
   * @param location The redirect location.
   * @return Redirect response object
   */
  async _createRedirectResponse(location: string): Promise<ResponseRedirect> {
    const { currentResponse = new Response() } = this

    const response = HttpResponse.fromValues(
      currentResponse.status,
      currentResponse.statusText,
      currentResponse.headers
    )
    if (currentResponse.payload) {
      response.payload = currentResponse.payload
    }

    const body = await this.decompress(this._rawBody)
    if (body) {
      await response.writePayload(body)
      currentResponse.payload = response.payload
    }

    const redirect = ResponseRedirect.fromValues(
      location,
      response.toJSON(),
      this.stats.startTime,
      this.stats.responseTime
    )
    redirect.timings = this._computeStats(this.stats)

    return redirect
  }

  /**
   * Creates a response object
   */
  async _createResponse(): Promise<Response> {
    if (this.aborted) {
      throw new Error(`Request aborted.`)
    }
    const { currentResponse } = this
    if (!currentResponse) {
      throw new Error(`Tried to create a response but no response data is set.`)
    }
    const { status } = currentResponse
    if (status === undefined) {
      throw new Error(`The response status is empty.
It means that the successful connection wasn't made.
Check your request parameters.`)
    }
    const body = await this.decompress(this._rawBody)
    const response = Response.fromValues(status, currentResponse.statusText, currentResponse.headers)
    response.timings = this._computeStats(this.stats)
    response.loadingTime = response.timings.total()
    if (body) {
      await response.writePayload(body)
      currentResponse.payload = response.payload
    }
    if (status === 401) {
      this.logger.silly('Setting auth on the response...')
      response.auth = this._getAuth()
    }
    return response
  }

  /**
   * Finishes the response with error message.
   */
  _errorRequest(opts: ResponseErrorInit): void {
    const { currentResponse } = this
    this.aborted = true
    let message
    if (opts.code && !opts.message) {
      message = getCodeMessage(opts.code)
    } else if (opts.message) {
      message = opts.message
    }
    message = message || 'Unknown error occurred'
    const error = new SerializableError(message, opts.code)
    const log = RequestLog.fromRequest(this.sentRequest.toJSON())
    const response = ErrorResponse.fromError(error)
    log.response = response
    if (currentResponse && currentResponse.status) {
      response.status = currentResponse.status
      response.statusText = currentResponse.statusText
      response.headers = currentResponse.headers
      response.payload = currentResponse.payload
    }
    this.finalizeRequest(log)
    this._cleanUp()
  }

  /**
   * Generates authorization info object from response.
   */
  _getAuth(): ResponseAuthorization {
    this.logger.debug('Getting auth from response headers')
    let auth = this.currentHeaders.get('www-authenticate')
    const result = new ResponseAuthorization()
    if (auth) {
      this.logger.silly('Auth header found', auth)
      auth = auth.toLowerCase()
      if (auth.indexOf('ntlm') !== -1) {
        this.logger.silly('Detected NTLM authorization')
        result.method = 'ntlm'
      } else if (auth.indexOf('basic') !== -1) {
        result.method = 'basic'
        this.logger.silly('Detected basic authorization')
      } else if (auth.indexOf('digest') !== -1) {
        result.method = 'digest'
        this.logger.silly('Detected digest authorization')
      }
    }
    return result
  }

  /**
   * Creates HAR 1.2 timings object from stats.
   */
  _computeStats(stats: RequestStats): RequestTime {
    return computeStats(stats)
  }

  /**
   * Handles cookie exchange when redirecting the request.
   */
  _processRedirectCookies(responseCookies: string, location: string): void {
    this.request.headers = processRedirectCookies(
      responseCookies,
      location,
      this.request.url,
      this.request.toHeadersString()
    )
  }

  /**
   * Checks certificate identity using TLS api.
   */
  _checkServerIdentity(host: string, cert: tls.PeerCertificate): Error | undefined {
    return checkServerIdentity(host, cert)
  }

  /**
   * Clears event listeners of the socket object,
   */
  _clearSocketEventListeners(): void {
    if (!this.socket) {
      return
    }
    this.socket.removeAllListeners('error')
    this.socket.removeAllListeners('timeout')
    this.socket.removeAllListeners('end')
  }

  /**
   * Called with the `send()` function to initialize the main promise returned by the send function.
   * The send function returns a promise that is resolved when the request finish.
   */
  protected wrapExecution(): Promise<IRequestLog> {
    let promise: Promise<IRequestLog>
    if (this[mainPromiseSymbol]) {
      promise = this[mainPromiseSymbol] as Promise<IRequestLog>
    } else {
      promise = new Promise((resolve, reject) => {
        this.mainResolver = resolve
        this.mainRejecter = reject
      })
      this[mainPromiseSymbol] = promise
    }
    return promise
  }

  /**
   * Called by the request finalizer or error finalized to report the response.
   */
  protected finalizeRequest(log: RequestLog | SerializableError): void {
    const { mainRejecter, mainResolver } = this
    if (!mainRejecter || !mainResolver) {
      return
    }

    if (log instanceof SerializableError) {
      mainRejecter(log)
    } else {
      mainResolver(log.toJSON())
    }
    this.mainRejecter = undefined
    this.mainResolver = undefined
    this[mainPromiseSymbol] = undefined
  }

  /**
   * Prepares an HTTP message from API Client's request object.
   */
  async prepareMessage(): Promise<Buffer> {
    let payload = this.request.payload
    if (isMethodWithoutBody(this.request.method)) {
      payload = undefined
    }
    const headers = new Headers(this.request.toHeadersString())
    this.prepareHeaders(headers, !!payload)
    const auth = this.hasProxy && !this.isProxyTunnel ? this._proxyAuthHeader() : undefined
    if (auth) {
      headers.set(HEADER_PROXY_AUTHORIZATION, auth)
    }
    const buffer = PayloadSupport.payloadToBuffer(headers, payload)
    if (buffer) {
      addContentLength(this.request.method || 'GET', buffer, headers)
    }

    this._handleAuthorization(headers)
    this.sentRequest.headers = headers.toString()

    // Update message builder with current host header
    this.messageBuilder.updateHostHeader(this.hostHeader || '')

    const message = this.messageBuilder.buildMessage(headers, buffer)
    if (this.auth) {
      // This restores altered by authorization original headers
      // so it can be safe to use when redirecting
      if (this.auth.headers) {
        this.request.headers = this.auth.headers
        delete this.auth.headers
      }
    }
    return message
  }

  /**
   * Sends a data to a socket.
   */
  writeMessage(buffer: Buffer): Promise<void> {
    this.logger.debug(`Writing the message to the socket...`)
    let msg = buffer.toString()
    const limit = this.opts.sentMessageLimit
    if (limit && limit > 0 && msg.length >= limit) {
      msg = msg.substring(0, limit)
      msg += ' ...'
    }
    this.sentRequest.httpMessage = msg
    const startTime = Date.now()
    this.stats.startTime = startTime
    this.sentRequest.startTime = startTime

    this.stats.messageStart = Date.now()
    return new Promise((resolve) => {
      this.socket?.write(buffer, () => {
        this.logger.debug(`The message has been sent.`)
        this.stats.sentTime = Date.now()
        try {
          this.emit('loadstart')
        } catch {
          //
        }
        resolve()
      })
    })
  }

  /**
   * Alters authorization header depending on the `auth` object
   */
  _handleAuthorization(headers: Headers): void {
    const { authorization } = this.opts
    this.authManager.handleRequestAuthorization(headers, authorization, this.uri.host)

    // Sync the auth state from AuthManager to parent class for compatibility
    const authState = this.authManager.getAuthState()
    if (authState) {
      this.auth = authState
    }
  }

  /**
   * Generate proxy authorization header
   */
  _proxyAuthHeader(): string | undefined {
    return this.authManager.generateProxyAuthHeader()
  }

  /**
   * Add event listeners to existing socket.
   */
  _addSocketListeners(socket: net.Socket): net.Socket {
    let received = false
    socket.on('data', (data) => {
      this.logger.debug(`Received server data from the socket...`)
      if (!received) {
        const now = Date.now()
        this.stats.firstReceiveTime = now
        this.emit('firstbyte')
        received = true
      }
      data = Buffer.from(data)
      try {
        // Use the new parser instead of the old parsing methods
        this.parser.processData(data)
        this.logger.debug('Parser state:', this.parser.getState())
      } catch (e) {
        this.logger.error('Error in _addSocketListeners:', e)
        const err = e as Error
        this._errorRequest({
          message: err.message || 'Unknown error occurred',
        })
        return
      }
    })
    socket.once('timeout', () => {
      this.logger.debug(`Socket timeout occurred after ${this.timeout}ms.`)
      this.parser.abort()
      this._errorRequest(HttpEngineErrorFactory.connectionTimeout(this.timeout))
      socket.destroy()
    })
    socket.on('end', () => {
      const endTime = Date.now()
      this.logger.debug(`Server connection ended.`)
      socket.removeAllListeners('timeout')
      socket.removeAllListeners('error')
      socket.removeAllListeners('data')
      this.stats.lastReceivedTime = endTime
      this.sentRequest.endTime = endTime
      // console.log(`Socket ended at ${endTime} in state`, this.parser.getState())
      if (this.parser.getState() !== RequestState.Done) {
        if (!this.currentResponse) {
          const message = `Connection closed without receiving any data.`
          this.logger.error(message)
          const e = HttpEngineErrorFactory.connectionError(message, 100)
          this._errorRequest(e)
        } else {
          this.emit('loadend')
          this._publishResponse()
        }
      }
    })
    socket.once('error', (err) => {
      this.logger.error(`Socket error occurred: ${err.message}`)
      socket.removeAllListeners('timeout')
      this._errorRequest(err)
    })
    return socket
  }

  /**
   * Reports response after processing it.
   */
  async _reportResponse(): Promise<void> {
    this._clearSocketEventListeners()
    const { aborted, currentResponse } = this
    if (aborted || !currentResponse) {
      return
    }
    const { status } = currentResponse

    const endTime = Date.now()
    this.stats.lastReceivedTime = endTime
    this.sentRequest.endTime = endTime

    const result = processResponse({
      status,
      currentResponse,
      currentHeaders: this.currentHeaders,
      followRedirects: this.followRedirects,
      auth: this.auth,
      aborted: this.aborted,
    })

    switch (result.action) {
      case 'redirect':
        if (this._reportRedirect(status)) {
          this.closeClient()
          return
        }
        break
      case 'auth':
        if (result.data?.method === 'ntlm') {
          await this.handleNtlmResponse()
          return
        }
        break
      case 'publish':
        this.closeClient()
        this.emit('loadend')
        this._publishResponse()
        break
      case 'abort':
        // Already aborted, do nothing
        break
    }
  }

  /**
   * Generate response object and publish it to the listeners.
   */
  async _publishResponse(): Promise<void> {
    if (this.aborted) {
      return
    }
    try {
      const response = await this._createResponse()
      const result = RequestLog.fromRequestResponse(this.sentRequest.toJSON(), response.toJSON())
      if (this.redirects.length) {
        result.redirects = this.redirects
      }
      result.size = new RequestsSize()
      if (this.sentRequest.httpMessage) {
        result.size.request = Buffer.from(this.sentRequest.httpMessage).length
      }
      if (response.payload) {
        if (typeof response.payload === 'string') {
          result.size.response = response.payload.length
        } else {
          result.size.response = response.payload.data.length
        }
      }
      this.finalizeRequest(result)
    } catch (e) {
      const error = e as Error
      this.logger.error('Error in _publishResponse:', error)
      this._errorRequest({
        message: (error && error.message) || 'Unknown error occurred',
      })
    }
    this.abort()
    this._cleanUp()
  }

  /**
   * closes the connection, if any
   */
  closeClient(): void {
    if (this.socket && !this.socket.destroyed) {
      this.logger.debug('Closing the socket connection...')
      this.socket.destroy()
    }
  }

  /**
   * Handles the response with NTLM authorization
   */
  async handleNtlmResponse(): Promise<void> {
    this.logger.debug('Handling NTLM response...')
    this.logger.debug('Current headers:', this.currentHeaders?.toString())
    this.logger.debug('Auth state:', this.authManager.getAuthState())

    const shouldRetry = this.authManager.handleNtlmResponse(this.currentHeaders)
    this.logger.debug('Should retry NTLM:', shouldRetry)

    if (shouldRetry) {
      // Update the parent auth state to match AuthManager state
      this.auth = this.authManager.getAuthState()
      this._cleanUpRedirect()

      try {
        const message = await this.prepareMessage()
        await this.writeMessage(message)
        // Don't return here - let the socket listeners handle the response
      } catch (error) {
        this.logger.error('Failed to retry NTLM authentication:', error)
        this._errorRequest({
          message: `NTLM retry failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
        })
      }
      return
    }

    this.logger.debug('NTLM authentication completed')
    // Clear both auth states
    this.authManager.clearAuthState()
    delete this.auth
    this.emit('loadend')
    this._publishResponse()
  }
}
