/**
 * Sink Matcher — Matches tainted variables to dangerous sinks.
 *
 * Scans each line for known dangerous sink patterns, then checks if
 * any tainted variable name appears on that line. Builds complete
 * taint paths from source → propagation chain → sink.
 */

import type { TaintedVariable, TaintPath, TaintSink, SinkType } from './taint-types'
import type { SanitiserInfo } from './taint-types'
import { isSanitiserCall } from './sanitiser-detection'
import { SINK_PATTERNS, escapeRegex } from './sink-patterns'

// ============================================================================
// Helper Functions
// ============================================================================

/**
 * Check if a tainted variable appears on a line (using word boundaries).
 */
function taintedVarAppearsOnLine(
  line: string,
  taintedVars: TaintedVariable[]
): TaintedVariable | null {
  for (const tv of taintedVars) {
    const regex = new RegExp(`\\b${escapeRegex(tv.name)}\\b`)
    if (regex.test(line)) {
      return tv
    }
  }
  return null
}

/**
 * Build a taint chain from a tainted variable back to its source.
 * Follows the propagation chain through assignments.
 */
function buildChain(
  target: TaintedVariable,
  allVars: TaintedVariable[]
): TaintedVariable[] {
  const chain: TaintedVariable[] = [target]
  let current = target
  const visited = new Set<string>()
  visited.add(current.name)

  // Walk back through the propagation chain
  while (current.source) {
    const sourceVar = allVars.find(v =>
      v.name === current.source.variable &&
      v.taintedAt < current.taintedAt &&
      !visited.has(v.name)
    )
    if (!sourceVar) break
    visited.add(sourceVar.name)
    chain.unshift(sourceVar)
    current = sourceVar
  }

  return chain
}

/**
 * Compute confidence for a taint path based on source confidence and chain length.
 */
function computePathConfidence(
  chain: TaintedVariable[],
  sourceConfidence: 'high' | 'medium' | 'low'
): 'high' | 'medium' | 'low' {
  const chainLen = chain.length

  if (sourceConfidence === 'high' && chainLen <= 3) return 'high'
  if (sourceConfidence === 'low' || chainLen > 5) return 'low'
  return 'medium'
}

/**
 * Check if the line is a parameterised query (where the tainted var
 * is in the parameter array, not interpolated into the query string).
 */
function isParameterisedQueryUsage(line: string, varName: string): boolean {
  // Pattern: .query('...', [varName]) — var is in the parameter array
  const paramArrayPattern = new RegExp(
    `\\.query\\s*\\([^,]+,\\s*\\[[^\\]]*\\b${escapeRegex(varName)}\\b[^\\]]*\\]`
  )
  if (paramArrayPattern.test(line)) return true

  // Pattern: .where('column', varName) — Knex parameterised
  const knexWherePattern = new RegExp(
    `\\.where\\s*\\(\\s*['"][^'"]+['"]\\s*,\\s*\\b${escapeRegex(varName)}\\b`
  )
  if (knexWherePattern.test(line)) return true

  return false
}

// ============================================================================
// Main Matching Function
// ============================================================================

/**
 * Match tainted variables to dangerous sinks and build taint paths.
 *
 * For each line that matches a sink pattern, checks if any tainted variable
 * appears on that line. Builds a complete TaintPath with source, chain, and sink.
 */
export function matchSinks(
  content: string,
  _filePath: string,
  taintedVariables: TaintedVariable[],
  sanitisers: SanitiserInfo[]
): TaintPath[] {
  if (taintedVariables.length === 0) return []

  const lines = content.split('\n')
  const taintPaths: TaintPath[] = []
  const seen = new Set<string>()

  // Get only unsanitised tainted variables (and all for chain building)
  const activeTaintedVars = taintedVariables.filter(tv => !tv.sanitised)

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i]
    const lineNumber = i + 1
    const trimmed = line.trim()

    // Skip comments
    if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
      continue
    }

    // Check each sink pattern
    for (const sp of SINK_PATTERNS) {
      if (!sp.pattern.test(line)) continue

      // Check if any tainted variable appears on this line
      const taintedVar = taintedVarAppearsOnLine(line, activeTaintedVars)
      if (!taintedVar) continue

      // Dedup: one taint path per source-sink-line combination
      const key = `${taintedVar.source.variable}:${sp.sinkType}:${lineNumber}`
      if (seen.has(key)) continue
      seen.add(key)

      // Build the sink
      const sink: TaintSink = {
        line: lineNumber,
        expression: trimmed,
        sinkType: sp.sinkType,
      }

      // Build the chain from source to this variable
      const chain = buildChain(taintedVar, taintedVariables)

      // Check if sanitised at the sink line itself
      const sinkSanitResult = isSanitiserCall(line, sanitisers)
      const isSanitised = sinkSanitResult.sanitised || taintedVar.sanitised

      // Special case: parameterised SQL queries
      const isParamQuery = sp.sinkType === 'sql_query' && isParameterisedQueryUsage(line, taintedVar.name)

      const sanitised = isSanitised || isParamQuery

      // Compute confidence
      const confidence = computePathConfidence(chain, taintedVar.source.confidence)

      taintPaths.push({
        source: taintedVar.source,
        chain,
        sink,
        sanitised,
        confidence,
      })
    }
  }

  return taintPaths
}
