import crypto from 'node:crypto'
import { type Logger, type ILogObj } from '../../../lib/logging/index.js'

export interface DigestChallenge {
  realm: string
  nonce: string
  qop?: string
  algorithm?: string
  opaque?: string
  stale?: string
  domain?: string
}

export interface DigestResponse {
  username: string
  realm: string
  nonce: string
  uri: string
  response: string
  qop?: string
  nc?: string
  cnonce?: string
  algorithm?: string
  opaque?: string
}

/**
 * Handles HTTP Digest authentication according to RFC 2617
 */
export class DigestAuthHandler {
  private logger: Logger<ILogObj>
  private username: string
  private password: string
  private nc = 1
  private cnonce: string

  constructor(username: string, password: string, logger: Logger<ILogObj>) {
    this.username = username
    this.password = password
    this.logger = logger
    this.cnonce = this.generateCnonce()
  }

  /**
   * Parse a WWW-Authenticate or Proxy-Authenticate header
   */
  parseChallenge(authenticateHeader: string): DigestChallenge {
    const challenge: DigestChallenge = { realm: '', nonce: '' }

    // Extract the method (Digest)
    const methodMatch = authenticateHeader.match(/^(\w+)/)
    if (!methodMatch || methodMatch[1].toLowerCase() !== 'digest') {
      throw new Error('Not a Digest authentication challenge')
    }

    // Parse key-value pairs (both quoted and unquoted)
    const paramRegex = /(\w+)=(?:"([^"]*)"|([^,\s]+))/g
    let match
    while ((match = paramRegex.exec(authenticateHeader)) !== null) {
      const [, key, quotedValue, unquotedValue] = match
      const value = quotedValue !== undefined ? quotedValue : unquotedValue

      switch (key.toLowerCase()) {
        case 'realm':
          challenge.realm = value
          break
        case 'nonce':
          challenge.nonce = value
          break
        case 'qop':
          challenge.qop = value
          break
        case 'algorithm':
          challenge.algorithm = value
          break
        case 'opaque':
          challenge.opaque = value
          break
        case 'stale':
          challenge.stale = value
          break
        case 'domain':
          challenge.domain = value
          break
      }
    }

    // Validate required fields
    if (!challenge.realm || !challenge.nonce) {
      throw new Error('Invalid Digest challenge: missing realm or nonce')
    }

    this.logger.debug('Parsed Digest challenge:', challenge)
    return challenge
  }

  /**
   * Generate a Digest authentication response
   */
  generateResponse(challenge: DigestChallenge, method: string, uri: string): string {
    const algorithm = challenge.algorithm?.toLowerCase() || 'md5'
    const qop = challenge.qop?.toLowerCase()

    // Generate HA1 = MD5(username:realm:password)
    const ha1 = this.calculateHA1(challenge.realm, algorithm)

    // Generate HA2 = MD5(method:uri)
    const ha2 = this.calculateHA2(method, uri, algorithm)

    let response: string

    if (qop) {
      // With quality of protection
      const nc = this.formatNC(this.nc)
      const cnonce = this.cnonce

      if (qop === 'auth') {
        // response = MD5(HA1:nonce:nc:cnonce:qop:HA2)
        const responseInput = `${ha1}:${challenge.nonce}:${nc}:${cnonce}:${qop}:${ha2}`
        response = crypto.createHash(algorithm).update(responseInput).digest('hex')
      } else {
        throw new Error(`Unsupported qop value: ${qop}`)
      }
    } else {
      // Without quality of protection
      // response = MD5(HA1:nonce:HA2)
      const responseInput = `${ha1}:${challenge.nonce}:${ha2}`
      response = crypto.createHash(algorithm).update(responseInput).digest('hex')
    }

    // Build the response header
    const responseParams: DigestResponse = {
      username: this.username,
      realm: challenge.realm,
      nonce: challenge.nonce,
      uri,
      response,
    }

    if (challenge.algorithm) {
      responseParams.algorithm = challenge.algorithm
    }

    if (challenge.opaque) {
      responseParams.opaque = challenge.opaque
    }

    if (qop) {
      responseParams.qop = qop
      responseParams.nc = this.formatNC(this.nc)
      responseParams.cnonce = this.cnonce
    }

    const responseHeader = this.buildResponseHeader(responseParams)
    this.logger.debug('Generated Digest response:', responseHeader)

    // Increment nonce count for next request
    this.nc++

    return responseHeader
  }

  /**
   * Calculate HA1 = MD5(username:realm:password)
   */
  private calculateHA1(realm: string, algorithm: string): string {
    const input = `${this.username}:${realm}:${this.password}`
    return crypto.createHash(algorithm).update(input).digest('hex')
  }

  /**
   * Calculate HA2 = MD5(method:uri)
   */
  private calculateHA2(method: string, uri: string, algorithm: string): string {
    const input = `${method}:${uri}`
    return crypto.createHash(algorithm).update(input).digest('hex')
  }

  /**
   * Generate a client nonce
   */
  private generateCnonce(): string {
    return crypto.randomBytes(16).toString('hex')
  }

  /**
   * Format nonce count as 8-digit hex string
   */
  private formatNC(nc: number): string {
    return nc.toString(16).padStart(8, '0')
  }

  /**
   * Build the Authorization header string
   */
  private buildResponseHeader(params: DigestResponse): string {
    const parts: string[] = ['Digest']

    // Add required parameters
    parts.push(`username="${params.username}"`)
    parts.push(`realm="${params.realm}"`)
    parts.push(`nonce="${params.nonce}"`)
    parts.push(`uri="${params.uri}"`)
    parts.push(`response="${params.response}"`)

    // Add optional parameters
    if (params.algorithm) {
      parts.push(`algorithm="${params.algorithm}"`)
    }

    if (params.opaque) {
      parts.push(`opaque="${params.opaque}"`)
    }

    if (params.qop) {
      parts.push(`qop="${params.qop}"`)
      parts.push(`nc="${params.nc}"`)
      parts.push(`cnonce="${params.cnonce}"`)
    }

    return parts.join(', ')
  }

  /**
   * Reset the nonce count (useful for new sessions)
   */
  reset(): void {
    this.nc = 1
    this.cnonce = this.generateCnonce()
  }

  /**
   * Check if a challenge is stale
   */
  isStale(challenge: DigestChallenge): boolean {
    return challenge.stale === 'true'
  }
}
