import { Request, Response, NextFunction } from 'express'
import fs from 'node:fs'
import mime from 'mime-types'
import {
  Utils,
  VerifiableCertificate,
  Peer,
  AuthMessage,
  RequestedCertificateSet,
  Transport,
  SessionManager,
  AsyncSessionManager,
  WalletInterface,
  PubKeyHex
} from '@bsv/sdk'
import {
  LogLevel,
  isLogLevelEnabled,
  getLogMethod,
  writeUrlToWriter,
  writeRequestHeadersToWriter,
  writeHeaderPair,
  writeBodyToWriter,
  convertValueToArray,
  makeDebugLogger
} from './authMiddlewareHelpers.js'

export type { LogLevel } from './authMiddlewareHelpers.js'
export { isLogLevelEnabled, getLogMethod } from './authMiddlewareHelpers.js'
export { writeBodyToWriter } from './authMiddlewareHelpers.js'

export interface AuthRequest extends Request {
  auth?: {
    identityKey: PubKeyHex
  }
}

// Developers may optionally provide a handler for incoming certificates.
export interface AuthMiddlewareOptions {
  wallet: WalletInterface
  // Optional session store. Default is in-process synchronous `SessionManager`.
  // Pass an `AsyncSessionManager` (Redis/SQL-backed, etc.) to share state
  // across load-balanced instances; Peer awaits internally so both work.
  sessionManager?: SessionManager | AsyncSessionManager
  allowUnauthenticated?: boolean
  certificatesToRequest?: RequestedCertificateSet
  onCertificatesReceived?: (
    senderPublicKey: string,
    certs: VerifiableCertificate[],
    req: AuthRequest,
    res: Response,
    next: NextFunction
  ) => void

  /**
   * Optional logger (e.g., console). If not provided, logging is disabled.
   */
  logger?: typeof console

  /**
   * Optional logging level. Defaults to no logging if not provided.
   * 'debug' | 'info' | 'warn' | 'error'
   *
   * - debug: Logs *everything*, including low-level details of the auth process.
   * - info: Logs general informational messages about normal operation.
   * - warn: Logs potential issues but not necessarily errors.
   * - error: Logs only critical issues and errors.
   */
  logLevel?: LogLevel
}

/**
 * ResponseWriterWrapper buffers response data until signing is complete.
 * This pattern matches the Go implementation for cleaner response handling.
 */
class ResponseWriterWrapper {
  private statusCode: number = 200
  private headers: Record<string, string> = {}
  private body: number[] = []
  private readonly originalRes: Response
  private flushed: boolean = false

  constructor (res: Response) {
    this.originalRes = res
  }

  status (code: number): this {
    this.statusCode = code
    return this
  }

  set (key: string | Record<string, string>, value?: string): this {
    if (typeof key === 'object' && key !== null) {
      for (const [k, v] of Object.entries(key)) {
        this.headers[k.toLowerCase()] = String(v)
      }
    } else if (typeof key === 'string' && value !== undefined) {
      this.headers[key.toLowerCase()] = String(value)
    }
    return this
  }

  send (data: any): this {
    this.body = convertValueToArray(data, this.headers)
    return this
  }

  json (data: any): this {
    if (!this.headers['content-type']) {
      this.headers['content-type'] = 'application/json'
    }
    this.body = Utils.toArray(JSON.stringify(data), 'utf8')
    return this
  }

  text (data: string): this {
    if (!this.headers['content-type']) {
      this.headers['content-type'] = 'text/plain'
    }
    this.body = Utils.toArray(data, 'utf8')
    return this
  }

  end (): this {
    // No-op for buffering, actual end happens on flush
    return this
  }

  getStatusCode (): number {
    return this.statusCode
  }

  getHeaders (): Record<string, string> {
    return this.headers
  }

  getBody (): number[] {
    return this.body
  }

  getOriginalRes (): Response {
    return this.originalRes
  }

  // Called after peer signs the response
  flush (): void {
    if (this.flushed) return
    this.flushed = true

    this.originalRes.status(this.statusCode)
    for (const [key, value] of Object.entries(this.headers)) {
      this.originalRes.set(key, value)
    }
    if (this.body.length > 0) {
      this.originalRes.send(Buffer.from(new Uint8Array(this.body)))
    } else {
      this.originalRes.end()
    }
  }
}

/**
 * Transport implementation for Express.
 */
export class ExpressTransport implements Transport {
  peer?: Peer
  allowAuthenticated: boolean
  openNonGeneralHandles: Record<string, Array<{ res: Response, next: Function }>> = {}
  openGeneralHandles: Record<string, { next: Function, res: Response }> = {}
  openNextHandlers: Record<string, NextFunction> = {}
  openNextHandlerTimeouts: Record<string, ReturnType<typeof setTimeout>> = {}

  private messageCallback?: (message: AuthMessage) => Promise<void>
  private readonly logger: typeof console | undefined
  private readonly logLevel: LogLevel

  /**
   * Constructs a new ExpressTransport instance.
   *
   * @param {boolean} [allowUnauthenticated=false] - Whether to allow unauthenticated requests passed the auth middleware.
   *   If `true`, requests without authentication will be permitted, and `req.auth.identityKey`
   *   will be set to `"unknown"`. If `false`, unauthenticated requests will result in a `401 Unauthorized` response.
   * @param {typeof console} [logger] - Logger to use (e.g., console). If omitted, logging is disabled.
   * @param {'debug' | 'info' | 'warn' | 'error'} [logLevel] - Log level. If omitted, no logs are output.
   */
  constructor (
    allowUnauthenticated: boolean = false,
    logger?: typeof console,
    logLevel?: LogLevel
  ) {
    this.allowAuthenticated = allowUnauthenticated
    this.logger = logger
    this.logLevel = logLevel || 'error' // Default to 'error' if not provided
  }

  /**
   * Internal logging method, only logs if logger is defined and log level is appropriate.
   *
   * @param level - The log level for this message
   * @param message - The message to log
   * @param data - Optional additional data to log
   */
  private log (
    level: LogLevel,
    message: string,
    data?: any
  ): void {
    if (typeof this.logger !== 'object') return // Logging disabled
    if (isLogLevelEnabled(this.logLevel, level)) {
      const logMethod = getLogMethod(this.logger, level)
      if (data !== undefined) {
        logMethod(`[ExpressTransport] [${level.toUpperCase()}] ${message}`, data)
      } else {
        logMethod(`[ExpressTransport] [${level.toUpperCase()}] ${message}`)
      }
    }
  }

  setPeer (peer: Peer): void {
    this.peer = peer
    this.log('debug', 'Peer set in ExpressTransport', { peer })
  }

  /**
   * Sends an AuthMessage to the connected Peer.
   * This method uses an Express response object to deliver the message to the specified Peer.
   *
   * ### Parameters:
   * @param {AuthMessage} message - The authenticated message to send.
   *
   * ### Returns:
   * @returns {Promise<void>} A promise that resolves once the message has been sent successfully.
   */
  async send (message: AuthMessage): Promise<void> {
    this.log('debug', 'Attempting to send AuthMessage', { message })
    if (message.messageType === 'general') {
      await this.sendGeneralMessage(message)
    } else {
      await this.sendNonGeneralMessage(message)
    }
  }

  /**
   * Handles a general (authenticated application) AuthMessage response.
   */
  private async sendGeneralMessage (message: AuthMessage): Promise<void> {
    const reader = new Utils.Reader(message.payload)
    const requestId = Utils.toBase64(reader.read(32))

    if (typeof this.openGeneralHandles[requestId] !== 'object') {
      this.log('warn', 'No response handle for this requestId', { requestId })
      throw new Error('No response handle for this requestId!')
    }
    let { res, next } = this.openGeneralHandles[requestId]
    delete this.openGeneralHandles[requestId]

    const statusCode = reader.readVarIntNum()
    ;(res as any).__status(statusCode)

    const responseHeaders = this.readResponseHeaders(reader)
    responseHeaders['x-bsv-auth-version'] = message.version
    responseHeaders['x-bsv-auth-identity-key'] = message.identityKey
    responseHeaders['x-bsv-auth-nonce'] = message.nonce!
    responseHeaders['x-bsv-auth-your-nonce'] = message.yourNonce!
    responseHeaders['x-bsv-auth-signature'] = Utils.toHex(message.signature!)
    responseHeaders['x-bsv-auth-request-id'] = requestId

    if (message.requestedCertificates) {
      responseHeaders['x-bsv-auth-requested-certificates'] = JSON.stringify(message.requestedCertificates)
    }

    for (const [k, v] of Object.entries(responseHeaders)) {
      ;(res as any).__set(k, v)
    }

    let responseBody: number[] | undefined
    const responseBodyBytes = reader.readVarIntNum()
    if (responseBodyBytes > 0) {
      responseBody = reader.read(responseBodyBytes)
    }

    res = this.resetRes(res, next)
    this.log('info', 'Sending general AuthMessage response', {
      status: statusCode,
      responseHeaders,
      responseBodyLength: responseBody ? responseBody.length : 0,
      requestId
    })
    if (responseBody) {
      res.send(Buffer.from(new Uint8Array(responseBody)))
    } else {
      res.end()
    }
  }

  /**
   * Reads response headers from a binary reader.
   */
  private readResponseHeaders (reader: Utils.Reader): Record<string, string> {
    const responseHeaders: Record<string, string> = {}
    const nHeaders = reader.readVarIntNum()
    for (let i = 0; i < nHeaders; i++) {
      const nHeaderKeyBytes = reader.readVarIntNum()
      const headerKeyBytes = reader.read(nHeaderKeyBytes)
      const headerKey = Utils.toUTF8(headerKeyBytes)
      const nHeaderValueBytes = reader.readVarIntNum()
      const headerValueBytes = reader.read(nHeaderValueBytes)
      const headerValue = Utils.toUTF8(headerValueBytes)
      responseHeaders[headerKey] = headerValue
    }
    return responseHeaders
  }

  /**
   * Handles a non-general (handshake) AuthMessage response.
   */
  private async sendNonGeneralMessage (message: AuthMessage): Promise<void> {
    const handles = this.openNonGeneralHandles[message.yourNonce!]
    if (!Array.isArray(handles) || handles.length === 0) {
      this.log('warn', 'No open handles to peer for nonce', { yourNonce: message.yourNonce })
      throw new Error('No open handles to this peer!')
    }

    // Since this is an initial response, we can assume there's only one handle per identity
    const { res, next } = handles[0]
    const responseHeaders: Record<string, string> = {
      'x-bsv-auth-version': message.version,
      'x-bsv-auth-message-type': message.messageType,
      'x-bsv-auth-identity-key': message.identityKey,
      'x-bsv-auth-nonce': message.nonce!,
      'x-bsv-auth-your-nonce': message.yourNonce!,
      'x-bsv-auth-signature': Utils.toHex(message.signature!)
    }

    if (typeof message.requestedCertificates === 'object') {
      responseHeaders['x-bsv-auth-requested-certificates'] = JSON.stringify(message.requestedCertificates)
    }
    if ((res as any).__set !== undefined) {
      this.resetRes(res, next)
    }
    for (const [k, v] of Object.entries(responseHeaders)) {
      res.set(k, v)
    }

    this.log('info', 'Sending non-general AuthMessage response', {
      status: 200,
      responseHeaders,
      messagePayload: message
    })
    res.send(message)
    handles.shift()
  }

  /**
   * Stores the callback bound by a Peer
   * @param callback
   */
  async onData (callback: (message: AuthMessage) => Promise<void>): Promise<void> {
    this.log('debug', 'onData callback set')
    this.messageCallback = callback
  }

  /**
   * Handles an incoming request for the Express server.
   *
   * This method processes both general and non-general message types,
   * manages peer-to-peer certificate handling, and modifies the response object
   * to enable custom behaviors like certificate requests and tailored responses.
   *
   * ### Behavior:
   * - For `/.well-known/auth`:
   *   - Handles non-general messages and listens for certificates.
   *   - Calls the `onCertificatesReceived` callback (if provided) when certificates are received.
   * - For general messages:
   *   - Sets up a listener for peer-to-peer general messages.
   *   - Overrides response methods (`send`, `json`, etc.) for custom handling.
   * - Returns a 401 error if mutual authentication fails.
   *
   * ### Parameters:
   * @param {AuthRequest} req - The incoming HTTP request.
   * @param {Response} res - The HTTP response.
   * @param {NextFunction} next - The Express `next` middleware function.
   * @param {Function} [onCertificatesReceived] - Optional callback invoked when certificates are received.
   */
  public async handleIncomingRequest (
    req: AuthRequest,
    res: Response,
    next: NextFunction,
    onCertificatesReceived?: (
      senderPublicKey: string,
      certs: VerifiableCertificate[],
      req: AuthRequest,
      res: Response,
      next: NextFunction
    ) => void
  ): Promise<void> {
    this.log('debug', 'Handling incoming request', {
      path: req.path,
      headers: req.headers,
      method: req.method,
      body: req.body
    })
    try {
      if (!this.peer) {
        this.log('error', 'No Peer set in ExpressTransport! Cannot handle request.')
        throw new Error('You must set a Peer before you can handle incoming requests!')
      }
      if (req.path === '/.well-known/auth') {
        await this.handleWellKnownAuth(req, res, next, onCertificatesReceived)
      } else if (req.headers['x-bsv-auth-request-id']) {
        this.handleGeneralMessage(req, res, next)
      } else {
        this.handleUnauthenticated(req, res, next)
      }
    } catch (error) {
      this.log('error', 'Caught error in handleIncomingRequest', { error })
      next(error)
    }
  }

  /**
   * Handles a request to /.well-known/auth (non-general / handshake messages).
   */
  private async handleWellKnownAuth (
    req: AuthRequest,
    res: Response,
    next: NextFunction,
    onCertificatesReceived?: (
      senderPublicKey: string,
      certs: VerifiableCertificate[],
      req: AuthRequest,
      res: Response,
      next: NextFunction
    ) => void
  ): Promise<void> {
    const message = req.body as AuthMessage
    this.log('debug', 'Received non-general message at /.well-known/auth', { message })

    let requestId = req.headers['x-bsv-auth-request-id'] as string
    if (!requestId) {
      requestId = message.initialNonce!
    }

    if (Array.isArray(this.openNonGeneralHandles[requestId])) {
      this.openNonGeneralHandles[requestId].push({ res, next })
    } else {
      this.openNonGeneralHandles[requestId] = [{ res, next }]
    }

    if (!await this.peer!.sessionManager.hasSession(message.identityKey)) {
      this.registerCertificateListener(req, res, next, requestId, message, onCertificatesReceived)
    }

    if (this.messageCallback) {
      this.log('debug', 'Invoking stored messageCallback for non-general message')
      this.messageCallback(message).catch((err) => {
        this.log('error', 'Error in messageCallback', { error: err.message, err })
        return res.status(500).json({
          status: 'error',
          code: 'ERR_INTERNAL_SERVER_ERROR',
          description: err.message || 'An unknown error occurred.'
        })
      })
    }
  }

  /**
   * Registers a certificate-received listener for a non-general message.
   */
  private registerCertificateListener (
    req: AuthRequest,
    res: Response,
    next: NextFunction,
    requestId: string,
    message: AuthMessage,
    onCertificatesReceived?: (
      senderPublicKey: string,
      certs: VerifiableCertificate[],
      req: AuthRequest,
      res: Response,
      next: NextFunction
    ) => void
  ): void {
    const listenerId = this.peer!.listenForCertificatesReceived(
      (senderPublicKey: string, certs: VerifiableCertificate[]) => {
        try {
          this.log('debug', 'Certificates received event triggered', {
            senderPublicKey,
            certCount: certs?.length,
            requestId
          })
          if (senderPublicKey === req.body.identityKey) {
            this.handleCertificatesForPeer(senderPublicKey, certs, req, res, next, message, onCertificatesReceived)
          }
        } catch (error) {
          this.log('error', 'Error in certificate listener callback', { error })
        } finally {
          const handles = this.openNonGeneralHandles[requestId]
          if (handles && handles.length > 0) {
            handles.shift()
            if (handles.length === 0) {
              delete this.openNonGeneralHandles[requestId]
            }
          }
          this.peer?.stopListeningForCertificatesReceived(listenerId)
        }
      })
    this.log('debug', 'listenForCertificatesReceived registered', { listenerId, requestId })
  }

  /**
   * Processes certificates received from a peer during the handshake.
   */
  private handleCertificatesForPeer (
    senderPublicKey: string,
    certs: VerifiableCertificate[],
    req: AuthRequest,
    res: Response,
    next: NextFunction,
    message: AuthMessage,
    onCertificatesReceived?: (
      senderPublicKey: string,
      certs: VerifiableCertificate[],
      req: AuthRequest,
      res: Response,
      next: NextFunction
    ) => void
  ): void {
    if (!Array.isArray(certs) || certs.length === 0) {
      this.log('warn', 'No certificates provided by peer', { senderPublicKey })
      const handles = this.openNonGeneralHandles[req.headers['x-bsv-auth-request-id'] as string ?? message.initialNonce]
      if (handles && handles.length > 0) {
        handles[0].res.status(400).json({ status: 'No certificates provided' })
      }
      return
    }

    this.log('info', 'Certificates successfully received from peer', { senderPublicKey, certs })
    if (typeof onCertificatesReceived === 'function') {
      onCertificatesReceived(senderPublicKey, certs, req, res, next)
    }

    // Validate that identityKey is an own property of the handler map before
    // invoking, preventing CodeQL js/unvalidated-dynamic-method-call from
    // flagging prototype-chain dispatch on a user-supplied key.
    const identityKey = message.identityKey
    if (typeof identityKey === 'string' && Object.hasOwn(this.openNextHandlers, identityKey)) {
      const nextFn = this.openNextHandlers[identityKey]
      const timeoutHandle = this.openNextHandlerTimeouts[identityKey]
      if (timeoutHandle != null) {
        clearTimeout(timeoutHandle)
        delete this.openNextHandlerTimeouts[identityKey]
      }
      nextFn()
      delete this.openNextHandlers[identityKey]
    }
  }

  /**
   * Handles an authenticated general message (has x-bsv-auth-request-id header).
   */
  private handleGeneralMessage (
    req: AuthRequest,
    res: Response,
    next: NextFunction
  ): void {
    const message = buildAuthMessageFromRequest(req, this.logger, this.logLevel)
    this.log('debug', 'Received general message with x-bsv-auth-request-id', { message })

    // Setup general message listener
    const listenerId = this.peer!.listenForGeneralMessages((senderPublicKey: string, payload: number[]) => {
      try {
        if (senderPublicKey !== req.headers['x-bsv-auth-identity-key']) return
        const requestId = Utils.toBase64(new Utils.Reader(payload).read(32))
        if (requestId === req.headers['x-bsv-auth-request-id']) {
          this.peer?.stopListeningForGeneralMessages(listenerId)
          this.setupAuthenticatedResponse(req, res, next, senderPublicKey, requestId)
        }
      } catch (error) {
        this.log('error', 'Error in listenForGeneralMessages callback', { error })
        next(error)
      }
    })

    this.log('debug', 'listenForGeneralMessages registered', { listenerId })

    if (this.messageCallback) {
      this.log('debug', 'Invoking stored messageCallback for general message')
      this.messageCallback(message).catch((err) => {
        const msg = err instanceof Error ? err.message : String(err)
        const isAuthError = /nonce|signature|session|auth version/i.test(msg)
        this.log('error', 'Error in messageCallback (general message)', { error: msg, isAuthError })
        const statusCode = isAuthError ? 401 : 500
        const code = isAuthError ? 'ERR_AUTH_FAILED' : 'ERR_INTERNAL_SERVER_ERROR'
        const description = isAuthError
          ? (msg || 'Authentication failed.')
          : (msg || 'An unexpected error occurred.')
        return res.status(statusCode).json({ status: 'error', code, description })
      })
    }
  }

  /**
   * Sets up the intercepted response for an authenticated general message.
   */
  private setupAuthenticatedResponse (
    req: AuthRequest,
    res: Response,
    next: NextFunction,
    senderPublicKey: string,
    requestId: string
  ): void {
    this.log('debug', 'General message from the correct identity key', { requestId, senderPublicKey })
    req.auth = { identityKey: senderPublicKey }

    const wrapper = new ResponseWriterWrapper(res)
    let responseSent = false

    const buildAndSendResponse = async (): Promise<void> => {
      if (responseSent) return
      responseSent = true
      try {
        const responsePayload = buildResponsePayload(
          requestId,
          wrapper.getStatusCode(),
          wrapper.getHeaders(),
          wrapper.getBody(),
          req,
          this.logger,
          this.logLevel
        )
        this.openGeneralHandles[requestId] = { res, next }
        this.log('debug', 'Sending general message response', {
          requestId,
          responseStatus: wrapper.getStatusCode(),
          responseHeaders: wrapper.getHeaders(),
          responseBodyLength: wrapper.getBody().length
        })
        await this.peer?.toPeer(responsePayload, req.headers['x-bsv-auth-identity-key'] as string)
      } catch (err) {
        delete this.openGeneralHandles[requestId]
        this.log('error', 'Failed to build and send authenticated response', { error: err })
        try {
          const restored = this.resetRes(res, next)
          restored.status(500).json({
            status: 'error',
            code: 'ERR_RESPONSE_SIGNING_FAILED',
            description: err instanceof Error ? err.message : 'Failed to sign response'
          })
        } catch (_responseAlreadySent) {
          // Response may already be partially sent — nothing to do
        }
      }
    }

    this.hijackResponse(res, next, wrapper, buildAndSendResponse)
    void this.scheduleNextOrCertificateWait(next, senderPublicKey, wrapper, buildAndSendResponse).catch(next)
  }

  /**
   * Overrides the response methods to intercept and buffer the response for signing.
   */
  private hijackResponse (
    res: Response,
    next: NextFunction,
    wrapper: ResponseWriterWrapper,
    buildAndSendResponse: () => Promise<void>
  ): void {
    // Override methods to capture response data
    this.checkRes(res, 'needs to be clear', next)
    ;(res as any).__status = res.status
    res.status = (n) => {
      wrapper.status(n)
      return res
    }

    ;(res as any).__set = res.set
    ;(res as any).set = (keyOrHeaders: string | Record<string, string>, value?: string) => {
      wrapper.set(keyOrHeaders, value)
      return res
    }

    ;(res as any).__send = res.send
    ;(res as any).send = (val: any) => {
      if (typeof val === 'object' && val !== null && !wrapper.getHeaders()['content-type']) {
        wrapper.set('content-type', 'application/json')
      }
      wrapper.send(val)
      buildAndSendResponse()
      return res
    }

    ;(res as any).__json = res.json
    ;(res as any).json = (obj: any) => {
      wrapper.json(obj)
      buildAndSendResponse()
      return res
    }

    ;(res as any).__text = (res as any).text
    ;(res as any).text = (str: string) => {
      wrapper.text(str)
      buildAndSendResponse()
      return res
    }

    ;(res as any).__end = res.end
    ;(res as any).end = () => {
      buildAndSendResponse()
      return res
    }

    ;(res as any).__sendFile = res.sendFile
    ;(res as any).sendFile = (path: string, options?: any, callback?: Function) => {
      fs.readFile(path, (err, data) => {
        if (err) {
          this.log('error', 'Error reading file in sendFile', { error: err.message })
          if (callback) return callback(err)
          wrapper.status(500)
          buildAndSendResponse()
          return
        }

        const mimeType = mime.lookup(path) || 'application/octet-stream'
        wrapper.set('Content-Type', mimeType)
        wrapper.send(Array.from(data))
        buildAndSendResponse()
      })
    }
  }

  /**
   * Either calls next() immediately or stores it pending certificate arrival.
   */
  private async scheduleNextOrCertificateWait (
    next: NextFunction,
    senderPublicKey: string,
    wrapper: ResponseWriterWrapper,
    buildAndSendResponse: () => Promise<void>
  ): Promise<void> {
    const hasSession = await (this.peer?.sessionManager.hasSession(senderPublicKey) ?? false)
    const needsCertificates = this.peer?.certificatesToRequest?.certifiers?.length
    this.log('debug', 'Checking if we need to wait for certificates', {
      senderPublicKey,
      hasSession,
      needsCertificates,
      openNextHandlersKeys: Object.keys(this.openNextHandlers)
    })

    if (!needsCertificates || hasSession) {
      this.log('debug', 'Calling next() immediately - no certificate wait needed', { senderPublicKey, hasSession })
      next()
      return
    }

    this.log('debug', 'Storing next handler to wait for certificates', { senderPublicKey })
    const existingTimeout = this.openNextHandlerTimeouts[senderPublicKey]
    if (existingTimeout != null) {
      clearTimeout(existingTimeout)
      delete this.openNextHandlerTimeouts[senderPublicKey]
    }
    this.openNextHandlers[senderPublicKey] = next

    const CERTIFICATE_TIMEOUT_MS = 30000
    const timeoutHandle = setTimeout(() => {
      if (this.openNextHandlers[senderPublicKey]) {
        this.log('warn', 'Certificate request timed out', { senderPublicKey })
        delete this.openNextHandlers[senderPublicKey]
        delete this.openNextHandlerTimeouts[senderPublicKey]
        wrapper.status(408).json({
          status: 'error',
          code: 'CERTIFICATE_TIMEOUT',
          message: 'Certificate request timed out'
        })
        buildAndSendResponse()
      }
    }, CERTIFICATE_TIMEOUT_MS)
    this.openNextHandlerTimeouts[senderPublicKey] = timeoutHandle
  }

  /**
   * Handles a request with no auth headers.
   */
  private handleUnauthenticated (req: AuthRequest, res: Response, next: NextFunction): void {
    this.log(
      'warn',
      'No Auth headers found on request. Checking allowUnauthenticated setting.',
      { allowAuthenticated: this.allowAuthenticated }
    )
    if (this.allowAuthenticated) {
      req.auth = { identityKey: 'unknown' }
      next()
    } else {
      this.log('warn', 'Mutual-authentication failed. Returning 401.')
      res.status(401).json({
        status: 'error',
        code: 'UNAUTHORIZED',
        message: 'Mutual-authentication failed!'
      })
    }
  }

  private checkRes (res: any, test?: 'needs to be clear' | 'needs to be hijacked', next?: Function): void {
    if (test === 'needs to be clear') {
      if (
        typeof res.__status === 'function' ||
        typeof res.__set === 'function' ||
        typeof res.__json === 'function' ||
        typeof res.__text === 'function' ||
        typeof res.__send === 'function' ||
        typeof res.__end === 'function' ||
        typeof res.__sendFile === 'function'
      ) {
        const e = new Error('Unable to install Auth midddleware on the response object as it is not clear. Are two middleware instances installed?')
        if (typeof next === 'function') {
          next(e)
        }
        throw e
      }
    } else if (
      typeof res.__status !== 'function' ||
      typeof res.__set !== 'function' ||
      typeof res.__json !== 'function' ||
      typeof res.__send !== 'function' ||
      typeof res.__end !== 'function' ||
      typeof res.__sendFile !== 'function'
    ) {
      const e = new Error('Unable to restore response object. Did you tamper with hijacked properties (res.__status, __set, __json, __text, __send, __end, __sendFile) ?')
      if (typeof next === 'function') {
        next(e)
      }
      throw e
    }
  }

  private resetRes (res: Response, next?: Function): Response {
    this.checkRes(res, 'needs to be hijacked', next)
    res.status = (res as any).__status
    res.set = (res as any).__set
    res.json = (res as any).__json
    ;(res as any).text = (res as any).__text
    res.send = (res as any).__send
    res.end = (res as any).__end
    res.sendFile = (res as any).__sendFile
    return res
  }
}

/**
 * Helper: Build AuthMessage from Request
 */
function buildAuthMessageFromRequest (
  req: Request,
  logger?: typeof console,
  logLevel?: LogLevel
): AuthMessage {
  const debugLog = makeDebugLogger(logger, logLevel)
  debugLog('[buildAuthMessageFromRequest] Building message from request...', {
    path: req.path,
    headers: req.headers,
    method: req.method,
    body: req.body
  })

  const writer = new Utils.Writer()
  const requestNonce = req.headers['x-bsv-auth-request-id']
  const requestNonceBytes = requestNonce ? Utils.toArray(requestNonce, 'base64') : []
  writer.write(requestNonceBytes)
  writer.writeVarIntNum(req.method.length)
  writer.write(Utils.toArray(req.method))

  const protocol = req.protocol
  const host = req.get('host')
  const parsedUrl = new URL(`${protocol}://${host}${req.originalUrl}`)

  writeUrlToWriter(parsedUrl, writer)
  writeRequestHeadersToWriter(req, writer)
  writeBodyToWriter(req, writer, logger, logLevel)

  const authMessage = {
    messageType: 'general' as const,
    version: req.headers['x-bsv-auth-version'] as string,
    identityKey: req.headers['x-bsv-auth-identity-key'] as string,
    nonce: req.headers['x-bsv-auth-nonce'] as string,
    yourNonce: req.headers['x-bsv-auth-your-nonce'] as string,
    payload: writer.toArray(),
    signature: req.headers['x-bsv-auth-signature']
      ? Utils.toArray(req.headers['x-bsv-auth-signature'], 'hex')
      : []
  }

  debugLog('[buildAuthMessageFromRequest] AuthMessage built', { authMessage })

  return authMessage
}

/**
 * Helper: Build response payload for sending back to peer
 */
function buildResponsePayload (
  requestId: string,
  responseStatus: number,
  responseHeaders: Record<string, any>,
  responseBody: number[],
  req: Request,
  logger?: typeof console,
  logLevel?: LogLevel
): number[] {
  const debugLog = makeDebugLogger(logger, logLevel)
  debugLog('[buildResponsePayload] Building response payload', {
    requestId,
    responseStatus,
    responseHeaders,
    responseBodyLength: responseBody.length
  })

  const writer = new Utils.Writer()
  writer.write(Utils.toArray(requestId, 'base64'))
  writer.writeVarIntNum(responseStatus)

  // Filter out headers that should NOT be signed:
  // - Include custom headers prefixed with x-bsv (excluding those starting with x-bsv-auth)
  // - Include the authorization header
  const includedHeaders: Array<[string, string]> = []
  Object.entries(responseHeaders).forEach(([key, value]) => {
    const lowerKey = key.toLowerCase()
    if ((lowerKey.startsWith('x-bsv-') || lowerKey === 'authorization') && !lowerKey.startsWith('x-bsv-auth')) {
      includedHeaders.push([lowerKey, value])
    }
  })

  // Sort the headers by key to ensure a consistent order for signing and verification.
  includedHeaders.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))

  writer.writeVarIntNum(includedHeaders.length)
  for (const [headerKey, headerValue] of includedHeaders) {
    writeHeaderPair(writer, headerKey, headerValue)
  }

  if (responseBody.length > 0) {
    writer.writeVarIntNum(responseBody.length)
    writer.write(responseBody)
  } else {
    writer.writeVarIntNum(-1)
  }

  return writer.toArray()
}

/**
 * Creates an Express middleware that handles authentication via BSV-SDK.
 *
 * @param {AuthMiddlewareOptions} options
 * @returns {(req: Request, res: Response, next: NextFunction) => void} Express middleware
 */
export function createAuthMiddleware (options: AuthMiddlewareOptions): (req: AuthRequest, res: Response, next: NextFunction) => void {
  const {
    wallet,
    sessionManager,
    allowUnauthenticated,
    certificatesToRequest,
    onCertificatesReceived,
    logger,
    logLevel
  } = options

  if (!wallet) {
    if (logger && logLevel && isLogLevelEnabled(logLevel, 'error')) {
      getLogMethod(logger, 'error')(
        '[createAuthMiddleware] No wallet provided in AuthMiddlewareOptions.'
      )
    }
    throw new Error('You must configure the auth middleware with a wallet.')
  }

  const transport = new ExpressTransport(allowUnauthenticated ?? false, logger, logLevel)

  const sessionMgr = sessionManager || new SessionManager()

  if (logger && logLevel && isLogLevelEnabled(logLevel, 'info')) {
    getLogMethod(logger, 'info')(
      `[createAuthMiddleware] Creating Peer with provided wallet & transport. Session Manager: ${sessionManager ? 'Custom' : 'Default'
      }`
    )
  }

  const peer = new Peer(wallet, transport, certificatesToRequest, sessionMgr)
  transport.setPeer(peer)

  return (req: AuthRequest, res: Response, next: NextFunction) => {
    if (logger && logLevel && isLogLevelEnabled(logLevel, 'debug')) {
      getLogMethod(logger, 'debug')('[createAuthMiddleware] Incoming request to auth middleware', {
        path: req.path,
        headers: req.headers,
        method: req.method
      })
    }
    void transport.handleIncomingRequest(req, res, next, onCertificatesReceived).catch(next)
  }
}
