/**
 * Text Chunking Utilities
 *
 * Implements hybrid chunking strategy for RAG:
 * - Small documents (≤2500 tokens): Store as single chunk
 * - Large documents (>2500 tokens): Split into overlapping chunks
 *
 * Uses token-based chunking with overlap to preserve context across chunks
 */

import { estimateTokenCount } from './pdf-processor.ts';

/**
 * Configuration for chunking strategy
 */
export interface ChunkConfig {
  /** Maximum tokens for single-chunk threshold */
  maxTokensForSingleChunk: number;
  /** Target tokens per chunk (for large documents) */
  chunkSizeTokens: number;
  /** Overlap between chunks (in tokens) */
  overlapTokens: number;
  /** Minimum chunk size (avoid tiny chunks) */
  minChunkSizeTokens: number;
}

/**
 * Default chunking configuration
 * Optimized for 3072-dimension embeddings
 */
export const DEFAULT_CHUNK_CONFIG: ChunkConfig = {
  maxTokensForSingleChunk: 2500, // Safe margin for 3072-dim vectors
  chunkSizeTokens: 500, // Conservative chunk size
  overlapTokens: 100, // 20% overlap
  minChunkSizeTokens: 50, // Avoid very small chunks
};

/**
 * Result of chunking operation
 */
export interface ChunkResult {
  chunks: string[];
  strategy: 'single' | 'multi';
  totalTokens: number;
  chunkTokens: number[];
}

/**
 * Determine if text should be chunked
 *
 * @param text - Text to analyze
 * @param config - Chunking configuration
 * @returns true if text exceeds single-chunk threshold
 */
export function shouldChunk(
  text: string,
  config: ChunkConfig = DEFAULT_CHUNK_CONFIG
): boolean {
  const tokenCount = estimateTokenCount(text);
  return tokenCount > config.maxTokensForSingleChunk;
}

/**
 * Chunk text using hybrid strategy
 *
 * Strategy:
 * - If text fits in single chunk → return as-is
 * - If text is large → split into overlapping chunks
 *
 * @param text - Text to chunk
 * @param config - Chunking configuration
 * @returns Chunk result with strategy and token counts
 */
export function chunkText(
  text: string,
  config: ChunkConfig = DEFAULT_CHUNK_CONFIG
): ChunkResult {
  const totalTokens = estimateTokenCount(text);

  // Strategy 1: Single chunk (small documents)
  if (totalTokens <= config.maxTokensForSingleChunk) {
    return {
      chunks: [text],
      strategy: 'single',
      totalTokens,
      chunkTokens: [totalTokens],
    };
  }

  // Strategy 2: Multi-chunk with overlap (large documents)
  const chunks = chunkTextWithOverlap(text, config);

  return {
    chunks,
    strategy: 'multi',
    totalTokens,
    chunkTokens: chunks.map(estimateTokenCount),
  };
}

/**
 * Split text into overlapping chunks
 *
 * Uses character-based splitting with token estimation
 * to approximate token-based chunking
 *
 * @param text - Text to split
 * @param config - Chunking configuration
 * @returns Array of text chunks
 */
function chunkTextWithOverlap(
  text: string,
  config: ChunkConfig
): string[] {
  const chunks: string[] = [];

  // Estimate characters per token (average ~4 chars/token)
  const charsPerToken = 4;
  const chunkSizeChars = config.chunkSizeTokens * charsPerToken;
  const overlapChars = config.overlapTokens * charsPerToken;

  let startIndex = 0;

  while (startIndex < text.length) {
    // Calculate chunk end position
    let endIndex = startIndex + chunkSizeChars;

    // If this is the last chunk, take everything
    if (endIndex >= text.length) {
      endIndex = text.length;
    } else {
      // Try to break at sentence boundary near the end
      endIndex = findSentenceBoundary(
        text,
        endIndex,
        Math.min(endIndex + 200, text.length) // Look ahead max 200 chars
      );
    }

    // Extract chunk
    const chunk = text.substring(startIndex, endIndex).trim();

    // Only add non-empty chunks that meet minimum size
    if (chunk.length > 0 && estimateTokenCount(chunk) >= config.minChunkSizeTokens) {
      chunks.push(chunk);
    }

    // Move to next chunk with overlap
    // If this was the last chunk, break
    if (endIndex >= text.length) {
      break;
    }

    startIndex = endIndex - overlapChars;

    // Ensure we're making progress (avoid infinite loop)
    if (startIndex < chunks.length * chunkSizeChars * 0.5) {
      startIndex = endIndex;
    }
  }

  // If no chunks were created (shouldn't happen), return original text
  if (chunks.length === 0) {
    return [text];
  }

  return chunks;
}

/**
 * Find nearest sentence boundary after target position
 *
 * Looks for sentence-ending punctuation (. ! ?) followed by space or newline
 * Falls back to word boundaries to avoid splitting words
 *
 * @param text - Full text
 * @param targetPos - Target position to find boundary near
 * @param maxPos - Maximum position to search
 * @returns Position of sentence boundary
 */
function findSentenceBoundary(
  text: string,
  targetPos: number,
  maxPos: number
): number {
  // Look for sentence endings: . ! ? followed by whitespace
  const sentenceEnders = /[.!?]+[\s\n]/g;

  // Start from target position
  const searchText = text.substring(targetPos, maxPos);

  sentenceEnders.lastIndex = 0;
  const match = sentenceEnders.exec(searchText);

  if (match && match.index !== undefined) {
    // Found a sentence boundary
    return targetPos + match.index + match[0].length;
  }

  // No sentence boundary found, try paragraph break
  const paragraphBreak = searchText.indexOf('\n\n');
  if (paragraphBreak !== -1) {
    return targetPos + paragraphBreak + 2;
  }

  // No paragraph break found, try single newline
  const lineBreak = searchText.indexOf('\n');
  if (lineBreak !== -1) {
    return targetPos + lineBreak + 1;
  }

  // No natural boundary found, find nearest word boundary
  // to avoid splitting words mid-word
  return findWordBoundary(text, targetPos, maxPos);
}

/**
 * Find nearest word boundary (whitespace) after target position
 *
 * Ensures chunks don't split in the middle of words
 *
 * @param text - Full text
 * @param targetPos - Target position to find boundary near
 * @param maxPos - Maximum position to search
 * @returns Position of word boundary
 */
function findWordBoundary(
  text: string,
  targetPos: number,
  maxPos: number
): number {
  // Look for whitespace characters
  const searchText = text.substring(targetPos, maxPos);

  // Find first whitespace after target position
  const whitespaceMatch = searchText.match(/\s/);
  if (whitespaceMatch && whitespaceMatch.index !== undefined) {
    return targetPos + whitespaceMatch.index + 1;
  }

  // No whitespace found in search range
  // Look backwards from targetPos to find last word boundary before it
  // This ensures we don't split a word
  let pos = targetPos;
  while (pos > 0 && !/\s/.test(text[pos - 1])) {
    pos--;
  }

  // If we found a word boundary before targetPos, use it
  if (pos > 0 && pos < targetPos) {
    return pos;
  }

  // Last resort: use target position (shouldn't happen often)
  return targetPos;
}

/**
 * Split text by paragraphs
 *
 * Alternative chunking strategy that preserves paragraph structure
 *
 * @param text - Text to split
 * @param maxTokensPerChunk - Maximum tokens per chunk
 * @returns Array of paragraph-based chunks
 */
export function chunkByParagraphs(
  text: string,
  maxTokensPerChunk: number = 500
): string[] {
  // Split by double newlines (paragraph breaks)
  const paragraphs = text.split(/\n\n+/).map(p => p.trim()).filter(p => p.length > 0);

  const chunks: string[] = [];
  let currentChunk = '';

  for (const paragraph of paragraphs) {
    const paragraphTokens = estimateTokenCount(paragraph);

    // If single paragraph exceeds max, it becomes its own chunk
    if (paragraphTokens > maxTokensPerChunk) {
      // Save current chunk if not empty
      if (currentChunk) {
        chunks.push(currentChunk.trim());
        currentChunk = '';
      }
      // Add large paragraph as its own chunk
      chunks.push(paragraph);
      continue;
    }

    // Check if adding this paragraph would exceed limit
    const combinedTokens = estimateTokenCount(currentChunk + '\n\n' + paragraph);

    if (combinedTokens > maxTokensPerChunk && currentChunk.length > 0) {
      // Save current chunk and start new one
      chunks.push(currentChunk.trim());
      currentChunk = paragraph;
    } else {
      // Add to current chunk
      currentChunk += (currentChunk ? '\n\n' : '') + paragraph;
    }
  }

  // Add final chunk
  if (currentChunk) {
    chunks.push(currentChunk.trim());
  }

  return chunks.length > 0 ? chunks : [text];
}

/**
 * Get chunking statistics for text
 *
 * Useful for debugging and optimization
 *
 * @param text - Text to analyze
 * @param config - Chunking configuration
 * @returns Statistics about how text would be chunked
 */
export function getChunkingStats(
  text: string,
  config: ChunkConfig = DEFAULT_CHUNK_CONFIG
): {
  totalCharacters: number;
  totalTokens: number;
  estimatedChunks: number;
  strategy: 'single' | 'multi';
  averageChunkTokens: number;
} {
  const totalTokens = estimateTokenCount(text);
  const totalCharacters = text.length;

  if (totalTokens <= config.maxTokensForSingleChunk) {
    return {
      totalCharacters,
      totalTokens,
      estimatedChunks: 1,
      strategy: 'single',
      averageChunkTokens: totalTokens,
    };
  }

  // Estimate number of chunks needed
  const effectiveChunkSize = config.chunkSizeTokens - config.overlapTokens;
  const estimatedChunks = Math.ceil(totalTokens / effectiveChunkSize);

  return {
    totalCharacters,
    totalTokens,
    estimatedChunks,
    strategy: 'multi',
    averageChunkTokens: Math.ceil(totalTokens / estimatedChunks),
  };
}

/**
 * Validate chunk configuration
 *
 * @param config - Configuration to validate
 * @returns Validation result with any errors
 */
export function validateChunkConfig(config: ChunkConfig): {
  valid: boolean;
  errors: string[];
} {
  const errors: string[] = [];

  if (config.maxTokensForSingleChunk < 100) {
    errors.push('maxTokensForSingleChunk must be at least 100');
  }

  if (config.chunkSizeTokens < config.minChunkSizeTokens) {
    errors.push('chunkSizeTokens must be greater than minChunkSizeTokens');
  }

  if (config.overlapTokens >= config.chunkSizeTokens) {
    errors.push('overlapTokens must be less than chunkSizeTokens');
  }

  if (config.overlapTokens < 0) {
    errors.push('overlapTokens must be non-negative');
  }

  if (config.minChunkSizeTokens < 10) {
    errors.push('minChunkSizeTokens should be at least 10');
  }

  return {
    valid: errors.length === 0,
    errors,
  };
}
