/**
 * Context Extractor
 *
 * Extracts only relevant regions of file content around findings,
 * reducing token usage by ~40-60% for large files while preserving
 * imports and sufficient context for AI validation.
 */

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

export interface LineRange {
  /** 1-based inclusive start line */
  start: number
  /** 1-based inclusive end line */
  end: number
}

export interface ExtractedContext {
  /** Formatted content with line numbers and omission markers */
  content: string
  /** Whether the full file was sent (no omissions) */
  isFullFile: boolean
  /** Number of lines included in the output */
  linesSent: number
  /** Total lines in the original file */
  totalLines: number
}

export interface ExtractionConfig {
  /** Lines of context around each finding (default: 30) */
  padding: number
  /** Always include first N lines for imports (default: 15) */
  importLines: number
  /** Send full file if merged ranges cover >60% of lines (default: 0.6) */
  fullFileThreshold: number
  /** Files with this many lines or fewer always get full content (default: 100) */
  smallFileThreshold: number
}

const DEFAULT_CONFIG: ExtractionConfig = {
  padding: 30,
  importLines: 15,
  fullFileThreshold: 0.6,
  smallFileThreshold: 100,
}

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

/**
 * Extract relevant context regions from file content around finding locations.
 *
 * Algorithm:
 * 1. Small files (<=smallFileThreshold lines) -> send full content
 * 2. Build ranges: imports (1-importLines) + padding around each finding
 * 3. Merge overlapping/adjacent ranges
 * 4. If merged coverage > fullFileThreshold of file -> send full content
 * 5. Format with omission markers between gaps
 *
 * @param fileContent - The full file content as a string
 * @param findingLineNumbers - 1-based line numbers of findings
 * @param config - Optional extraction configuration overrides
 * @returns Extracted context with metadata
 */
export function extractRelevantContext(
  fileContent: string,
  findingLineNumbers: number[],
  config?: Partial<ExtractionConfig>
): ExtractedContext {
  const cfg = { ...DEFAULT_CONFIG, ...config }
  const lines = fileContent.split('\n')
  const totalLines = lines.length

  // Small files: send full content
  if (totalLines <= cfg.smallFileThreshold) {
    return {
      content: formatFullFile(lines),
      isFullFile: true,
      linesSent: totalLines,
      totalLines,
    }
  }

  // Empty findings: send imports only (shouldn't happen but handle gracefully)
  if (findingLineNumbers.length === 0) {
    return {
      content: formatFullFile(lines),
      isFullFile: true,
      linesSent: totalLines,
      totalLines,
    }
  }

  // Build ranges: imports + padding around each finding
  const ranges: LineRange[] = []

  // Always include imports
  if (cfg.importLines > 0) {
    ranges.push({
      start: 1,
      end: Math.min(cfg.importLines, totalLines),
    })
  }

  // Add padding around each finding
  for (const lineNum of findingLineNumbers) {
    ranges.push({
      start: Math.max(1, lineNum - cfg.padding),
      end: Math.min(totalLines, lineNum + cfg.padding),
    })
  }

  // Merge overlapping/adjacent ranges
  const merged = mergeRanges(ranges)

  // Check coverage: if >60% of file is covered, send full
  const coveredLines = merged.reduce((sum, r) => sum + (r.end - r.start + 1), 0)
  if (coveredLines / totalLines > cfg.fullFileThreshold) {
    return {
      content: formatFullFile(lines),
      isFullFile: true,
      linesSent: totalLines,
      totalLines,
    }
  }

  // Format with omission markers
  const content = formatScopedContent(lines, merged, totalLines)

  return {
    content,
    isFullFile: false,
    linesSent: coveredLines,
    totalLines,
  }
}

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

/**
 * Merge overlapping or adjacent line ranges.
 * Sorts by start line, then merges any ranges that overlap or are adjacent.
 */
export function mergeRanges(ranges: LineRange[]): LineRange[] {
  if (ranges.length === 0) return []

  // Sort by start line
  const sorted = [...ranges].sort((a, b) => a.start - b.start)

  const merged: LineRange[] = [{ ...sorted[0] }]

  for (let i = 1; i < sorted.length; i++) {
    const current = sorted[i]
    const last = merged[merged.length - 1]

    // Overlapping or adjacent (start <= last.end + 1)
    if (current.start <= last.end + 1) {
      last.end = Math.max(last.end, current.end)
    } else {
      merged.push({ ...current })
    }
  }

  return merged
}

/**
 * Format all lines with line numbers.
 */
function formatFullFile(lines: string[]): string {
  return lines
    .map((line, i) => `${String(i + 1).padStart(4, ' ')} | ${line}`)
    .join('\n')
}

/**
 * Format scoped content with line numbers and omission markers between gaps.
 */
function formatScopedContent(
  lines: string[],
  ranges: LineRange[],
  totalLines: number
): string {
  const parts: string[] = []

  for (let i = 0; i < ranges.length; i++) {
    const range = ranges[i]

    // Add omission marker before this range (if there's a gap)
    if (i === 0 && range.start > 1) {
      parts.push(`[... ${range.start - 1} lines omitted (lines 1-${range.start - 1}) ...]`)
    } else if (i > 0) {
      const prevEnd = ranges[i - 1].end
      const gapStart = prevEnd + 1
      const gapEnd = range.start - 1
      if (gapStart <= gapEnd) {
        parts.push(`[... ${gapEnd - gapStart + 1} lines omitted (lines ${gapStart}-${gapEnd}) ...]`)
      }
    }

    // Add the range lines with line numbers
    for (let lineIdx = range.start - 1; lineIdx < range.end && lineIdx < lines.length; lineIdx++) {
      parts.push(`${String(lineIdx + 1).padStart(4, ' ')} | ${lines[lineIdx]}`)
    }
  }

  // Add trailing omission marker if needed
  const lastRange = ranges[ranges.length - 1]
  if (lastRange.end < totalLines) {
    const gapStart = lastRange.end + 1
    parts.push(`[... ${totalLines - lastRange.end} lines omitted (lines ${gapStart}-${totalLines}) ...]`)
  }

  return parts.join('\n')
}
