/**
 * Pipeline Orchestrator — Thin orchestrator calling stages in sequence.
 *
 * Pipeline flow:
 *  1. resolveScanModeConfig()
 *  2. Filter locale files
 *  3. buildProjectModel(files)
 *  4. runDetectors(input)
 *  5. aggregateNoisyFindings(findings)
 *  6. enrichWithMetadata(findings, ctx)
 *  7. scoreFindings(findings, depth)
 *  8. Route by confidence (surface/validate/suppress)
 *  9. capValidationCandidatesPerFile()
 * 10. validateFindingsWithAI()
 * 11. Combine surfaced + validated
 * 12. postprocessFindings(input)
 * 13. buildScanResult(input)
 */

import type {
  ScanFile,
  ScanResult,
  Vulnerability,
  ScanProgress,
  ProgressCallback,
} from '../shared/types'
import type { ScanOptions } from './config'
import { resolveScanModeConfig } from './config'
import { isLocaleFile } from '../parse/file-classifier'
import { buildProjectModel } from '../model'
import { runDetectors } from '../detect'
import { aggregateNoisyFindings } from '../postprocess/aggregation'
import { enrichWithMetadata } from '../report/enrichment'
import { scoreFindings, type ScoringDiagnostics } from '../score'
import type { ScoredFinding } from '../score'
import type { TaintAnalysisStats, RouteDiscoveryStats } from '../model'
import { capValidationCandidatesPerFile } from '../postprocess/validation-cap'
import { validateFindingsWithAI, type ValidationStats } from '../validate'
import { postprocessFindings } from '../postprocess'
import { buildScanResult } from '../report/build-result'
import { FilterPipeline } from '../postprocess/filtering/pipeline'
import { sortBySeverity, computeSeverityCounts, computeCategoryCounts } from '../report/summary'
import { deduplicateVulnerabilities } from '../postprocess/dedup'
import { resolveContradictions } from '../postprocess/contradictions'

// Re-export ScanOptions for external consumers
export { type ScanOptions } from './config'

/**
 * Run a complete security scan on the provided files
 *
 * Supports two scan modes:
 * - full: Complete scan with AI validation on all files (initial onboarding, deep audits)
 * - incremental: Focused scan on changed files only (CI/CD, fast feedback)
 */
export async function runScan(
  files: ScanFile[],
  repoInfo: { name: string; url: string; branch: string },
  options: ScanOptions = {},
  onProgress?: ProgressCallback
): Promise<ScanResult> {
  const startTime = Date.now()
  const allVulnerabilities: Vulnerability[] = []

  // Filter audit pipeline (zero overhead when disabled)
  const filterPipeline = new FilterPipeline(options.includeFilterAudit ?? false)
  const fid = (v: Pick<Vulnerability, 'filePath' | 'lineNumber' | 'category'>) => `${v.filePath}:${v.lineNumber}:${v.category}`

  // Resolve scan mode configuration
  const scanModeConfig = resolveScanModeConfig(options)
  const isIncremental = scanModeConfig.mode === 'incremental'
  const depth = scanModeConfig.scanDepth || 'local'
  const quiet = options.quiet ?? false
  const cancellationToken = options.cancellationToken

  // Conditional logging helper - suppresses output in quiet mode (interactive CLI)
  const log = (message: string) => {
    if (!quiet) {
      console.log(message)
    }
  }

  // Helper to check cancellation and throw if cancelled
  const checkCancelled = () => {
    if (cancellationToken?.cancelled) {
      throw new Error(`Scan cancelled: ${cancellationToken.reason || 'user requested'}`)
    }
  }

  log(`[Scanner] repo=${repoInfo.name} mode=${scanModeConfig.mode} depth=${depth} files=${files.length}`)
  if (isIncremental && scanModeConfig.changedFiles) {
    log(`[Scanner] repo=${repoInfo.name} incremental_files=${scanModeConfig.changedFiles.length}`)
  }

  // Filter out locale/i18n files - they contain translations, not code
  const localeFileCount = files.filter(f => isLocaleFile(f.path)).length
  if (localeFileCount > 0) {
    files = files.filter(f => !isLocaleFile(f.path))
    log(`[Scanner] repo=${repoInfo.name} skipped_locale_files=${localeFileCount} remaining_files=${files.length}`)
  }

  // Report progress helper
  const reportProgress = (
    status: ScanProgress['status'],
    message: string,
    vulnerabilitiesFound: number = allVulnerabilities.length
  ) => {
    if (onProgress) {
      onProgress({
        status,
        message,
        filesProcessed: files.length,
        totalFiles: files.length,
        vulnerabilitiesFound,
      })
    }
  }

  // For incremental scans, filter to only changed files for AI-heavy operations
  const filesForAI = isIncremental && scanModeConfig.changedFiles
    ? files.filter(f => scanModeConfig.changedFiles!.some(cf => f.path.endsWith(cf) || f.path.includes(cf)))
    : files

  // Declare variables that need to be accessible in catch block
  let middlewareConfig: ReturnType<typeof buildProjectModel>['middlewareConfig'] | undefined
  let capturedValidationStats: ValidationStats | undefined

  try {
    checkCancelled()

    // ===== Build Project Model =====
    const modelStart = Date.now()
    const model = buildProjectModel(files)
    const modelDuration = Date.now() - modelStart
    middlewareConfig = model.middlewareConfig

    log(`[Model] repo=${repoInfo.name} build_duration=${modelDuration}ms taint_duration=${model.taintStats.duration}ms`)

    if (model.middlewareConfig.hasAuthMiddleware) {
      log(`[Scanner] repo=${repoInfo.name} auth_middleware=${model.middlewareConfig.authType || 'unknown'} file=${model.middlewareConfig.middlewareFile}`)
    }

    const filesWithImportedAuth = Array.from(model.fileAuthImports.values()).filter(f => f.usesImportedAuth).length
    if (filesWithImportedAuth > 0) {
      log(`[Scanner] repo=${repoInfo.name} files_with_imported_auth=${filesWithImportedAuth}`)
    }

    // Log route discovery stats
    const rs = model.routeStats
    if (rs.totalRoutes > 0) {
      log(`[Routes] repo=${repoInfo.name} total=${rs.totalRoutes} authed=${rs.authedRoutes} public=${rs.publicRoutes} unprotected=${rs.unprotectedRoutes} frameworks=${rs.frameworksDetected.join(',')} duration=${rs.duration}ms`)
    }

    // Log taint analysis stats
    const ts = model.taintStats
    log(`[Taint] repo=${repoInfo.name} files_analyzed=${ts.filesAnalyzed} files_with_sources=${ts.filesWithSources} files_with_taint_paths=${ts.filesWithTaintPaths} duration=${ts.duration}ms`)
    if (ts.totalSources > 0) {
      log(`[Taint] repo=${repoInfo.name} sources=${ts.totalSources} tainted_vars=${ts.totalTaintedVars} taint_paths=${ts.totalTaintPaths} (unsanitised=${ts.unsanitisedPaths} sanitised=${ts.sanitisedPaths}) sanitisers=${ts.totalSanitisers}`)
      const sourceBreakdown = Object.entries(ts.sourceTypeBreakdown).map(([k, v]) => `${k}:${v}`).join(',')
      if (sourceBreakdown) log(`[Taint] repo=${repoInfo.name} source_types={${sourceBreakdown}}`)
      const sinkBreakdown = Object.entries(ts.sinkTypeBreakdown).map(([k, v]) => `${k}:${v}`).join(',')
      if (sinkBreakdown) log(`[Taint] repo=${repoInfo.name} sink_types={${sinkBreakdown}}`)
    }

    checkCancelled()

    // ===== Run Detectors (Layer 1 + Layer 2) =====
    const detectorOutput = await runDetectors({
      files,
      middlewareConfig: model.middlewareConfig,
      fileAuthImports: model.fileAuthImports,
      detectorContext: model.detectorContext,
      onProgress,
      cancellationToken,
      filterPipeline,
      repoName: repoInfo.name,
      quiet,
    })

    const phaseTiming: { layer1?: number; layer2?: number; aiValidation?: number; layer3?: number } = {
      ...detectorOutput.phaseTiming,
    }

    // ===== Aggregate Noisy Findings =====
    const beforeAggregationCount = detectorOutput.findings.length
    const aggregatedFindings = aggregateNoisyFindings(detectorOutput.findings)
    if (filterPipeline.isEnabled) {
      const afterIds = new Set(aggregatedFindings.map(fid))
      for (const v of detectorOutput.findings) {
        if (!afterIds.has(fid(v))) {
          filterPipeline.record(fid(v), { stage: 'noisy_aggregation', action: 'aggregated', reason: 'Aggregated noisy finding (3+ similar per file)' })
        }
      }
    }

    // ===== Enrich With Metadata =====
    const enrichedFindings = enrichWithMetadata(aggregatedFindings, model.contextEngine.projectContext)

    // ===== Confidence Scoring =====
    const { scored: scoredFindings, diagnostics: scoringDiagnostics } = scoreFindings(enrichedFindings, depth, model.contextEngine)

    // ===== Confidence-based Routing =====
    const toSurface = scoredFindings.filter(f => f.confidence_score.routing === 'surface')
    const toValidate = scoredFindings.filter(f => f.confidence_score.routing === 'validate')
    const suppressed = scoredFindings.filter(f => f.confidence_score.routing === 'suppress')

    log(`[Confidence] repo=${repoInfo.name} depth=${depth} routing: surface=${toSurface.length} validate=${toValidate.length} suppress=${suppressed.length}`)

    // Log scoring diagnostics
    const dist = scoringDiagnostics.scoreDistribution
    log(`[Scoring] repo=${repoInfo.name} score_distribution: [0-0.2]=${dist['0.0-0.2']} [0.2-0.4]=${dist['0.2-0.4']} [0.4-0.6]=${dist['0.4-0.6']} [0.6-0.8]=${dist['0.6-0.8']} [0.8-1.0]=${dist['0.8-1.0']}`)
    const ruleHits = Object.entries(scoringDiagnostics.ruleHitCounts)
      .sort(([, a], [, b]) => b - a)
      .map(([name, count]) => `${name}:${count}`)
      .join(',')
    if (ruleHits) log(`[Scoring] repo=${repoInfo.name} rule_hits={${ruleHits}}`)
    const taintHits = Object.entries(scoringDiagnostics.taintRuleHits)
      .map(([name, count]) => `${name}:${count}`)
      .join(',')
    if (taintHits) log(`[Scoring] repo=${repoInfo.name} taint_rule_hits={${taintHits}}`)
    const catScores = Object.entries(scoringDiagnostics.categoryAvgScores)
      .sort(([, a], [, b]) => b.avg - a.avg)
      .map(([cat, d]) => `${cat}:${d.avg.toFixed(2)}(n=${d.count})`)
      .join(',')
    if (catScores) log(`[Scoring] repo=${repoInfo.name} category_avg_scores={${catScores}}`)

    // Record suppressed findings in filter pipeline
    if (filterPipeline.isEnabled) {
      for (const v of suppressed) {
        const adjustmentSummary = v.confidence_score.adjustments
          .map(a => `${a.factor}(${a.delta > 0 ? '+' : ''}${a.delta.toFixed(2)})`)
          .join(', ')
        filterPipeline.record(fid(v), {
          stage: 'confidence_scoring',
          action: 'dismissed',
          reason: `Suppressed by confidence score (${v.confidence_score.score.toFixed(2)}): ${adjustmentSummary || 'base too low'}`,
        })
      }
    }

    // ===== Per-File Validation Cap =====
    const maxValidationFiles = scanModeConfig.maxAIValidationFiles || 10
    const cappedValidation = capValidationCandidatesPerFile(toValidate, maxValidationFiles)

    checkCancelled()

    // ===== AI Validation =====
    let validatedFindings: Vulnerability[] = cappedValidation
    const shouldValidate = options.enableAI !== false && !scanModeConfig.skipAIValidation && cappedValidation.length > 0

    if (shouldValidate) {
      checkCancelled()
      const aiValidationStart = Date.now()
      reportProgress('validating', 'AI validating findings (entropy, secrets, AI patterns)...', cappedValidation.length)

      // For incremental scans, only validate findings in changed files
      const findingsToValidate = isIncremental && scanModeConfig.changedFiles
        ? cappedValidation.filter(v => scanModeConfig.changedFiles!.some(cf => v.filePath.endsWith(cf) || v.filePath.includes(cf)))
        : cappedValidation

      if (findingsToValidate.length > 0) {
        const validationResult = await validateFindingsWithAI(
          findingsToValidate,
          filesForAI,
          model.contextEngine,
          onProgress ? (progress) => {
            onProgress({
              status: 'validating',
              message: progress.status,
              filesProcessed: progress.filesProcessed,
              totalFiles: progress.totalFiles,
              vulnerabilitiesFound: allVulnerabilities.length,
            })
          } : undefined
        )
        validatedFindings = validationResult.vulnerabilities
        const { stats: validationStats } = validationResult
        capturedValidationStats = validationStats
        phaseTiming.aiValidation = Date.now() - aiValidationStart

        if (filterPipeline.isEnabled) {
          const validatedIds = new Set(validatedFindings.map(fid))
          for (const v of findingsToValidate) {
            if (!validatedIds.has(fid(v))) {
              filterPipeline.record(fid(v), { stage: 'ai_validation', action: 'dismissed', reason: 'Dismissed by AI validation' })
            }
          }
          for (const v of validatedFindings) {
            if (v.validationNotes?.toLowerCase().includes('downgrad')) {
              filterPipeline.record(fid(v), { stage: 'ai_validation', action: 'downgraded', reason: v.validationNotes || 'Downgraded by AI validation', originalSeverity: undefined, newSeverity: v.severity })
            }
          }
        }
        log(`[AI Validation] repo=${repoInfo.name} depth=${depth} duration=${phaseTiming.aiValidation}ms candidates=${findingsToValidate.length} kept=${validationStats.confirmedFindings} rejected=${validationStats.dismissedFindings} downgraded=${validationStats.downgradedFindings}`)
        log(`[AI Validation] cost_estimate: input_tokens=${validationStats.estimatedInputTokens} output_tokens=${validationStats.estimatedOutputTokens} cost=$${validationStats.estimatedCost.toFixed(4)} api_calls=${validationStats.apiCalls}`)

        // Per-category AI validation breakdown
        const categorySubmitted: Record<string, number> = {}
        const categoryKept: Record<string, number> = {}
        const categoryRejected: Record<string, number> = {}
        const categoryDowngraded: Record<string, number> = {}
        for (const f of findingsToValidate) {
          categorySubmitted[f.category] = (categorySubmitted[f.category] || 0) + 1
        }
        for (const f of validatedFindings) {
          if (f.validationStatus === 'confirmed') {
            categoryKept[f.category] = (categoryKept[f.category] || 0) + 1
          } else if (f.validationStatus === 'downgraded') {
            categoryDowngraded[f.category] = (categoryDowngraded[f.category] || 0) + 1
          }
        }
        for (const [cat, submitted] of Object.entries(categorySubmitted)) {
          const kept = (categoryKept[cat] || 0) + (categoryDowngraded[cat] || 0)
          categoryRejected[cat] = submitted - kept
        }
        const rejBreakdown = Object.entries(categoryRejected).filter(([, v]) => v > 0).sort(([, a], [, b]) => b - a).map(([k, v]) => `${k}:${v}`).join(',')
        if (rejBreakdown) log(`[AI Validation] repo=${repoInfo.name} rejected_by_category={${rejBreakdown}}`)
        const keptBreakdown = Object.entries(categoryKept).filter(([, v]) => v > 0).sort(([, a], [, b]) => b - a).map(([k, v]) => `${k}:${v}`).join(',')
        if (keptBreakdown) log(`[AI Validation] repo=${repoInfo.name} confirmed_by_category={${keptBreakdown}}`)
        const dgBreakdown = Object.entries(categoryDowngraded).filter(([, v]) => v > 0).sort(([, a], [, b]) => b - a).map(([k, v]) => `${k}:${v}`).join(',')
        if (dgBreakdown) log(`[AI Validation] repo=${repoInfo.name} downgraded_by_category={${dgBreakdown}}`)

        // Add back findings that weren't validated (not in changed files)
        const notValidated = cappedValidation.filter(v => !findingsToValidate.includes(v)).map(v => ({
          ...v,
          validatedByAI: false,
          validationStatus: 'not_validated' as const,
          validationNotes: 'Skipped validation (not in changed files for incremental scan)',
        }))
        validatedFindings.push(...notValidated)
      }
    } else if (scanModeConfig.skipAIValidation) {
      log(`[AI Validation] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config findings_requiring_validation=${cappedValidation.length}`)
      validatedFindings = []
    }

    // ===== Combine surfaced + validated =====
    allVulnerabilities.push(...validatedFindings, ...toSurface)

    // Layer 3: AI Semantic Analysis (removed — deep mode unused in production)
    if (scanModeConfig.skipLayer3) {
      log(`[Layer3] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`)
    }

    // Log phase timing summary
    const allTimings: Record<string, number | undefined> = {
      model: modelDuration,
      ...phaseTiming,
    }
    const phaseTimingStr = Object.entries(allTimings)
      .filter(([, ms]) => ms !== undefined)
      .map(([phase, ms]) => `${phase}=${ms}ms`)
      .join(' ')
    if (phaseTimingStr) {
      log(`[Scanner] repo=${repoInfo.name} phase_timing: ${phaseTimingStr}`)
    }

    // ===== Postprocess Findings =====
    const postprocessed = postprocessFindings({
      findings: allVulnerabilities,
      files,
      middlewareConfig: model.middlewareConfig,
      projectPath: options.projectPath,
      filterPipeline,
      showSuppressed: options.showSuppressed,
    })

    // Log suppression stats if any were suppressed
    if (postprocessed.suppressionResult.suppressed.length > 0 || postprocessed.suppressionResult.expiredSuppressions > 0) {
      log(`[Suppression] repo=${repoInfo.name} suppressed=${postprocessed.suppressionResult.suppressed.length} (inline=${postprocessed.suppressionResult.stats.inlineSuppressed} config_finding=${postprocessed.suppressionResult.stats.configFindingSuppressed} config_rule=${postprocessed.suppressionResult.stats.configRuleSuppressed}) expired=${postprocessed.suppressionResult.expiredSuppressions}`)
    }

    reportProgress('complete', 'Scan complete!', postprocessed.findings.length)

    // ===== Pipeline Funnel Summary =====
    const totalDuration = Date.now() - startTime
    const funnelParts = [
      `raw=${beforeAggregationCount}`,
      `aggregated=${aggregatedFindings.length}`,
      `scored=${scoredFindings.length}`,
      `surface=${toSurface.length}`,
      `validate=${toValidate.length}`,
      `suppress=${suppressed.length}`,
    ]
    if (shouldValidate && capturedValidationStats) {
      funnelParts.push(`ai_kept=${capturedValidationStats.confirmedFindings + capturedValidationStats.downgradedFindings}`)
      funnelParts.push(`ai_rejected=${capturedValidationStats.dismissedFindings}`)
    }
    funnelParts.push(`postprocessed=${postprocessed.findings.length}`)
    log(`[Funnel] repo=${repoInfo.name} ${funnelParts.join(' → ')}`)
    log(`[Summary] repo=${repoInfo.name} total_duration=${totalDuration}ms files=${files.length} final_findings=${postprocessed.findings.length}`)

    // ===== Build Final Result =====
    return buildScanResult({
      repoInfo,
      files,
      findings: postprocessed.findings,
      suppressionResult: postprocessed.suppressionResult,
      startTime,
      validationStats: capturedValidationStats,
      showSuppressed: options.showSuppressed,
      filterPipeline,
    })
  } catch (error) {
    if (cancellationToken?.cancelled) {
      // Return partial results on cancellation
      reportProgress('failed', 'Scan cancelled')

      // Compute partial results
      const uniqueVulnerabilities = deduplicateVulnerabilities(allVulnerabilities)
      const resolvedVulnerabilities = resolveContradictions(uniqueVulnerabilities, middlewareConfig)
      const sortedVulnerabilities = sortBySeverity(resolvedVulnerabilities)
      const severityCounts = computeSeverityCounts(sortedVulnerabilities)
      const categoryCounts = computeCategoryCounts(sortedVulnerabilities)

      return {
        repoName: repoInfo.name,
        repoUrl: repoInfo.url,
        branch: repoInfo.branch,
        filesScanned: files.length,
        filesSkipped: 0,
        vulnerabilities: sortedVulnerabilities,
        severityCounts,
        categoryCounts,
        hasBlockingIssues: false, // Don't block on partial results
        scanDuration: Date.now() - startTime,
        timestamp: new Date().toISOString(),
        validationStats: capturedValidationStats,
        cancelled: true,
        cancelReason: cancellationToken.reason,
      }
    }

    reportProgress('failed', `Scan failed: ${error}`)
    throw error
  }
}
