/**
 * Cross-File Taint Analysis — Detects taint flows that cross file boundaries.
 *
 * Scenario 1: Source in file A, sink in imported function (file B).
 *   File A has req.params.id (tainted), calls getUser(id) imported from B.
 *   B's getUser has param 'id' reaching a sql_query sink.
 *
 * Scenario 2: Imported function returns user data.
 *   File A calls getUserInput() from B which returns req.body.
 *   File A then passes the return value to a local sink.
 *
 * Constraint: One level of imports only. No transitive chasing (A->B->C).
 */

import type { ScanFile } from '../shared/types'
import type { ModuleGraph } from './module-graph'
import type { FunctionProfileMap, ExportedFunctionProfile } from './function-classifier'
import type {
  FileTaintAnalysis,
  UserInputSource,
  TaintSink,
  SinkType,
} from './taint-types'
import { SINK_PATTERNS, escapeRegex } from './sink-patterns'

// ============================================================================
// Types
// ============================================================================

export interface CrossFileTaintPath {
  sourceFile: string
  source: UserInputSource
  sinkFile: string
  sink: TaintSink
  importLink: {
    fromFile: string
    toFile: string
    symbolName: string
    paramName?: string
    callLine: number
  }
  sanitised: boolean
  confidence: 'high' | 'medium' | 'low'
}

// ============================================================================
// Main Analysis
// ============================================================================

/**
 * Analyze cross-file taint paths across the project.
 *
 * For each file with taint sources, check if tainted variables flow into
 * calls to imported functions that reach dangerous sinks.
 */
export function analyzeCrossFileTaint(
  files: ScanFile[],
  graph: ModuleGraph,
  functionProfiles: FunctionProfileMap,
  fileTaintAnalyses: Map<string, FileTaintAnalysis>
): CrossFileTaintPath[] {
  const paths: CrossFileTaintPath[] = []
  const fileContentMap = new Map(files.map(f => [f.path, f.content]))

  for (const [filePath, analysis] of fileTaintAnalyses) {
    if (analysis.sources.length === 0) continue

    const node = graph.nodes.get(filePath)
    if (!node) continue

    // Skip files involved in circular imports (unsafe to analyze)
    if (graph.circularImports.has(filePath)) continue

    const content = fileContentMap.get(filePath)
    if (!content) continue

    // Scenario 1: Source in file A, sink in imported function (file B)
    const scenario1Paths = analyzeScenario1(
      filePath,
      content,
      analysis,
      node,
      graph,
      functionProfiles
    )
    paths.push(...scenario1Paths)

    // Scenario 2: Imported function returns user data, used in local sink
    const scenario2Paths = analyzeScenario2(
      filePath,
      content,
      analysis,
      node,
      graph,
      functionProfiles
    )
    paths.push(...scenario2Paths)
  }

  return paths
}

// ============================================================================
// Scenario 1: Source in A, sink in imported function B
// ============================================================================

function analyzeScenario1(
  filePath: string,
  content: string,
  analysis: FileTaintAnalysis,
  node: ReturnType<ModuleGraph['nodes']['get']> & {},
  graph: ModuleGraph,
  functionProfiles: FunctionProfileMap
): CrossFileTaintPath[] {
  const paths: CrossFileTaintPath[] = []
  const lines = content.split('\n')

  // Build a map of imported function names → their profiles
  const importedFunctions = new Map<string, { profile: ExportedFunctionProfile; importPath: string }>()

  for (const imp of node.imports) {
    if (!imp.resolvedPath) continue
    const profiles = functionProfiles.get(imp.resolvedPath)
    if (!profiles) continue

    for (const name of imp.importedNames) {
      if (name === 'default') continue
      const profile = profiles.find(p => p.name === name)
      if (profile && profile.reachesSinks.length > 0) {
        importedFunctions.set(name, { profile, importPath: imp.resolvedPath })
      }
    }
  }

  if (importedFunctions.size === 0) return paths

  // Get tainted variable names and their sources
  const taintedVarNames = new Map<string, UserInputSource>()
  for (const src of analysis.sources) {
    taintedVarNames.set(src.variable, src)
  }
  for (const tv of analysis.taintedVariables) {
    if (!tv.sanitised) {
      taintedVarNames.set(tv.name, tv.source)
    }
  }

  // Pre-compile call patterns for imported functions (avoid per-line RegExp construction)
  const compiledCallPatterns = new Map<string, RegExp>()
  for (const funcName of importedFunctions.keys()) {
    compiledCallPatterns.set(funcName, new RegExp(`\\b${escapeRegex(funcName)}\\s*\\(`))
  }

  // Pre-compile var patterns for tainted variables
  const compiledVarPatterns = new Map<string, RegExp>()
  for (const varName of taintedVarNames.keys()) {
    compiledVarPatterns.set(varName, new RegExp(`\\b${escapeRegex(varName)}\\b`))
  }

  // Scan for calls to imported functions with tainted arguments
  const seen = new Set<string>()

  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

    for (const [funcName, { profile, importPath }] of importedFunctions) {
      // Check if this line calls the imported function
      if (!compiledCallPatterns.get(funcName)!.test(line)) continue

      // Extract call arguments
      const args = extractCallArguments(line, funcName)

      // Check if any tainted variable is passed as an argument
      for (const arg of args) {
        const argTrimmed = arg.trim()

        // Check if this argument is a tainted variable
        for (const [varName, source] of taintedVarNames) {
          if (!compiledVarPatterns.get(varName)!.test(argTrimmed)) continue

          // Check if the corresponding param is dangerous
          const argIndex = args.indexOf(arg)
          const paramName = profile.paramNames[argIndex]
          const isDangerousParam = paramName
            ? profile.dangerousParams.includes(paramName)
            : profile.paramNames.length === 0 && profile.dangerousParams.length > 0

          if (!isDangerousParam && profile.dangerousParams.length > 0) continue

          // Build cross-file taint path
          const key = `${filePath}:${source.variable}:${importPath}:${funcName}:${lineNumber}`
          if (seen.has(key)) continue
          seen.add(key)

          const sinkType: SinkType = profile.reachesSinks[0]
          paths.push({
            sourceFile: filePath,
            source,
            sinkFile: profile.filePath,
            sink: {
              line: profile.line,
              expression: `${funcName}(...)`,
              sinkType,
            },
            importLink: {
              fromFile: filePath,
              toFile: importPath,
              symbolName: funcName,
              paramName,
              callLine: lineNumber,
            },
            sanitised: profile.hasSanitisation,
            confidence: 'medium',  // cross-file paths are medium confidence by default
          })
        }
      }
    }
  }

  return paths
}

// ============================================================================
// Scenario 2: Imported function returns user data, used in local sink
// ============================================================================

function analyzeScenario2(
  filePath: string,
  content: string,
  _analysis: FileTaintAnalysis,
  node: ReturnType<ModuleGraph['nodes']['get']> & {},
  _graph: ModuleGraph,
  functionProfiles: FunctionProfileMap
): CrossFileTaintPath[] {
  const paths: CrossFileTaintPath[] = []
  const lines = content.split('\n')

  // Find imported functions that return user data
  const userDataFunctions = new Map<string, { profile: ExportedFunctionProfile; importPath: string }>()

  for (const imp of node.imports) {
    if (!imp.resolvedPath) continue
    const profiles = functionProfiles.get(imp.resolvedPath)
    if (!profiles) continue

    for (const name of imp.importedNames) {
      if (name === 'default') continue
      const profile = profiles.find(p => p.name === name)
      if (profile && profile.returnsUserData) {
        userDataFunctions.set(name, { profile, importPath: imp.resolvedPath })
      }
    }
  }

  if (userDataFunctions.size === 0) return paths

  // Pre-compile assignment patterns for user-data functions
  const compiledAssignPatterns = new Map<string, RegExp>()
  for (const funcName of userDataFunctions.keys()) {
    compiledAssignPatterns.set(funcName, new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:await\\s+)?${escapeRegex(funcName)}\\s*\\(`))
  }

  // Track variables assigned from user-data-returning functions
  const taintedByImport = new Map<string, { source: ExportedFunctionProfile; callLine: number; importPath: string; funcName: string }>()
  // Cache compiled var patterns for tainted-by-import variables (grows as new vars are discovered)
  const compiledImportVarPatterns = new Map<string, RegExp>()

  const seen = new Set<string>()

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

    if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue

    // Check for assignments from user-data functions
    for (const [funcName, { profile, importPath }] of userDataFunctions) {
      const assignMatch = line.match(compiledAssignPatterns.get(funcName)!)
      if (assignMatch) {
        const varName = assignMatch[1]
        taintedByImport.set(varName, { source: profile, callLine: lineNumber, importPath, funcName })
        // Pre-compile pattern for newly discovered tainted variable
        if (!compiledImportVarPatterns.has(varName)) {
          compiledImportVarPatterns.set(varName, new RegExp(`\\b${escapeRegex(varName)}\\b`))
        }
      }
    }

    // Check if any tainted-by-import variable reaches a local sink
    for (const sp of SINK_PATTERNS) {
      if (!sp.pattern.test(line)) continue

      for (const [varName, info] of taintedByImport) {
        if (!compiledImportVarPatterns.get(varName)!.test(line)) continue

        const key = `${filePath}:${varName}:${info.importPath}:${info.funcName}:${lineNumber}`
        if (seen.has(key)) continue
        seen.add(key)

        paths.push({
          sourceFile: info.importPath,
          source: {
            line: info.source.line,
            expression: `${info.funcName}() return value`,
            variable: varName,
            sourceType: info.source.returnSourceType ?? 'http_body',
            confidence: 'medium',
          },
          sinkFile: filePath,
          sink: {
            line: lineNumber,
            expression: trimmed,
            sinkType: sp.sinkType,
          },
          importLink: {
            fromFile: filePath,
            toFile: info.importPath,
            symbolName: info.funcName,
            callLine: info.callLine,
          },
          sanitised: false,
          confidence: 'low',  // scenario 2 is lower confidence
        })
      }
    }
  }

  return paths
}

// ============================================================================
// Helpers
// ============================================================================

function extractCallArguments(line: string, funcName: string): string[] {
  const startIdx = line.indexOf(funcName)
  if (startIdx === -1) return []

  const parenStart = line.indexOf('(', startIdx + funcName.length)
  if (parenStart === -1) return []

  let depth = 0
  let current = ''
  const args: string[] = []

  for (let i = parenStart; i < line.length; i++) {
    const ch = line[i]
    if (ch === '(') {
      if (depth > 0) current += ch
      depth++
    } else if (ch === ')') {
      depth--
      if (depth === 0) {
        if (current.trim()) args.push(current.trim())
        break
      }
      current += ch
    } else if (ch === ',' && depth === 1) {
      if (current.trim()) args.push(current.trim())
      current = ''
    } else if (depth > 0) {
      current += ch
    }
  }

  return args
}

