import * as path from "path"
// @ts-ignore-next-line
import pdf from "pdf-parse/lib/pdf-parse"
import mammoth from "mammoth"
import fs from "fs/promises"
import { isBinaryFile } from "isbinaryfile"
import { extractTextFromXLSX } from "./extract-text-from-xlsx"
import { countFileLines } from "./line-counter"
import { readLines } from "./read-lines"

async function extractTextFromPDF(filePath: string): Promise<string> {
	const dataBuffer = await fs.readFile(filePath)
	const data = await pdf(dataBuffer)
	return addLineNumbers(data.text)
}

async function extractTextFromDOCX(filePath: string): Promise<string> {
	const result = await mammoth.extractRawText({ path: filePath })
	return addLineNumbers(result.value)
}

async function extractTextFromIPYNB(filePath: string): Promise<string> {
	const data = await fs.readFile(filePath, "utf8")
	const notebook = JSON.parse(data)
	let extractedText = ""

	for (const cell of notebook.cells) {
		if ((cell.cell_type === "markdown" || cell.cell_type === "code") && cell.source) {
			extractedText += cell.source.join("\n") + "\n"
		}
	}

	return addLineNumbers(extractedText)
}

/**
 * Map of supported binary file formats to their extraction functions
 */
const SUPPORTED_BINARY_FORMATS = {
	".pdf": extractTextFromPDF,
	".docx": extractTextFromDOCX,
	".ipynb": extractTextFromIPYNB,
	".xlsx": extractTextFromXLSX,
} as const

/**
 * Returns the list of supported binary file formats that can be processed by extractTextFromFile
 */
export function getSupportedBinaryFormats(): string[] {
	return Object.keys(SUPPORTED_BINARY_FORMATS)
}

/**
 * Extracts text content from a file, with support for various formats including PDF, DOCX, XLSX, and plain text.
 * For large text files, can limit the number of lines read to prevent context exhaustion.
 *
 * @param filePath - Path to the file to extract text from
 * @param maxReadFileLine - Maximum number of lines to read from text files.
 *                          Use UNLIMITED_LINES (-1) or undefined for no limit.
 *                          Must be a positive integer or UNLIMITED_LINES.
 * @returns Promise resolving to the extracted text content with line numbers
 * @throws {Error} If file not found, unsupported format, or invalid parameters
 */
export async function extractTextFromFile(filePath: string, maxReadFileLine?: number): Promise<string> {
	// Validate maxReadFileLine parameter
	if (maxReadFileLine !== undefined && maxReadFileLine !== -1) {
		if (!Number.isInteger(maxReadFileLine) || maxReadFileLine < 1) {
			throw new Error(
				`Invalid maxReadFileLine: ${maxReadFileLine}. Must be a positive integer or -1 for unlimited.`,
			)
		}
	}

	try {
		await fs.access(filePath)
	} catch (error) {
		throw new Error(`File not found: ${filePath}`)
	}

	const fileExtension = path.extname(filePath).toLowerCase()

	// Check if we have a specific extractor for this format
	const extractor = SUPPORTED_BINARY_FORMATS[fileExtension as keyof typeof SUPPORTED_BINARY_FORMATS]
	if (extractor) {
		return extractor(filePath)
	}

	// Handle other files
	const isBinary = await isBinaryFile(filePath).catch(() => false)

	if (!isBinary) {
		// Check if we need to apply line limit
		if (maxReadFileLine !== undefined && maxReadFileLine !== -1) {
			const totalLines = await countFileLines(filePath)
			if (totalLines > maxReadFileLine) {
				// Read only up to maxReadFileLine (endLine is 0-based and inclusive)
				const content = await readLines(filePath, maxReadFileLine - 1, 0)
				const numberedContent = addLineNumbers(content)
				return (
					numberedContent +
					`\n\n[File truncated: showing ${maxReadFileLine} of ${totalLines} total lines. The file is too large and may exhaust the context window if read in full.]`
				)
			}
		}
		// Read the entire file if no limit or file is within limit
		return addLineNumbers(await fs.readFile(filePath, "utf8"))
	} else {
		throw new Error(`Cannot read text for file type: ${fileExtension}`)
	}
}

export function addLineNumbers(content: string, startLine: number = 1): string {
	// If content is empty, return empty string - empty files should not have line numbers
	// If content is empty but startLine > 1, return "startLine | " because we know the file is not empty
	// but the content is empty at that line offset
	if (content === "") {
		return startLine === 1 ? "" : `${startLine} | \n`
	}

	// Split into lines and handle trailing line feeds (\n)
	const lines = content.split("\n")
	const lastLineEmpty = lines[lines.length - 1] === ""
	if (lastLineEmpty) {
		lines.pop()
	}

	const maxLineNumberWidth = String(startLine + lines.length - 1).length
	const numberedContent = lines
		.map((line, index) => {
			const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, " ")
			return `${lineNumber} | ${line}`
		})
		.join("\n")

	return numberedContent + "\n"
}
// Checks if every line in the content has line numbers prefixed (e.g., "1 | content" or "123 | content")
// Line numbers must be followed by a single pipe character (not double pipes)
export function everyLineHasLineNumbers(content: string): boolean {
	const lines = content.split(/\r?\n/) // Handles both CRLF (carriage return (\r) + line feed (\n)) and LF (line feed (\n)) line endings
	return lines.length > 0 && lines.every((line) => /^\s*\d+\s+\|(?!\|)/.test(line))
}

/**
 * Strips line numbers from content while preserving the actual content.
 *
 * @param content The content to process
 * @param aggressive When false (default): Only strips lines with clear number patterns like "123 | content"
 *                   When true: Uses a more lenient pattern that also matches lines with just a pipe character,
 *                   which can be useful when LLMs don't perfectly format the line numbers in diffs
 * @returns The content with line numbers removed
 */
export function stripLineNumbers(content: string, aggressive: boolean = false): string {
	// Split into lines to handle each line individually
	const lines = content.split(/\r?\n/)

	// Process each line
	const processedLines = lines.map((line) => {
		// Match line number pattern and capture everything after the pipe
		const match = aggressive ? line.match(/^\s*(?:\d+\s)?\|\s(.*)$/) : line.match(/^\s*\d+\s+\|(?!\|)\s?(.*)$/)
		return match ? match[1] : line
	})

	// Join back with original line endings (carriage return (\r) + line feed (\n) or just line feed (\n))
	const lineEnding = content.includes("\r\n") ? "\r\n" : "\n"
	let result = processedLines.join(lineEnding)

	// Preserve trailing newline if present in original content
	if (content.endsWith(lineEnding)) {
		if (!result.endsWith(lineEnding)) {
			result += lineEnding
		}
	}

	return result
}

/**
 * Truncates multi-line output while preserving context from both the beginning and end.
 * When truncation is needed, it keeps 20% of the lines from the start and 80% from the end,
 * with a clear indicator of how many lines were omitted in between.
 *
 * IMPORTANT: Character limit takes precedence over line limit. This is because:
 * 1. Character limit provides a hard cap on memory usage and context window consumption
 * 2. A single line with millions of characters could bypass line limits and cause issues
 * 3. Character limit ensures consistent behavior regardless of line structure
 *
 * When both limits are specified:
 * - If content exceeds character limit, character-based truncation is applied (regardless of line count)
 * - If content is within character limit but exceeds line limit, line-based truncation is applied
 * - This prevents edge cases where extremely long lines could consume excessive resources
 *
 * @param content The multi-line string to truncate
 * @param lineLimit Optional maximum number of lines to keep. If not provided or 0, no line limit is applied
 * @param characterLimit Optional maximum number of characters to keep. If not provided or 0, no character limit is applied
 * @returns The truncated string with an indicator of omitted content, or the original content if no truncation needed
 *
 * @example
 * // With 10 line limit on 25 lines of content:
 * // - Keeps first 2 lines (20% of 10)
 * // - Keeps last 8 lines (80% of 10)
 * // - Adds "[...15 lines omitted...]" in between
 *
 * @example
 * // With character limit on long single line:
 * // - Keeps first 20% of characters
 * // - Keeps last 80% of characters
 * // - Adds "[...X characters omitted...]" in between
 *
 * @example
 * // Character limit takes precedence:
 * // content = "A".repeat(50000) + "\n" + "B".repeat(50000) // 2 lines, 100,002 chars
 * // truncateOutput(content, 10, 40000) // Uses character limit, not line limit
 * // Result: First ~8000 chars + "[...60002 characters omitted...]" + Last ~32000 chars
 */
export function truncateOutput(content: string, lineLimit?: number, characterLimit?: number): string {
	// If no limits are specified, return original content
	if (!lineLimit && !characterLimit) {
		return content
	}

	// Character limit takes priority over line limit
	if (characterLimit && content.length > characterLimit) {
		const beforeLimit = Math.floor(characterLimit * 0.2) // 20% of characters before
		const afterLimit = characterLimit - beforeLimit // remaining 80% after

		const startSection = content.slice(0, beforeLimit)
		const endSection = content.slice(-afterLimit)
		const omittedChars = content.length - characterLimit

		return startSection + `\n[...${omittedChars} characters omitted...]\n` + endSection
	}

	// If character limit is not exceeded or not specified, check line limit
	if (!lineLimit) {
		return content
	}

	// Count total lines
	let totalLines = 0
	let pos = -1
	while ((pos = content.indexOf("\n", pos + 1)) !== -1) {
		totalLines++
	}
	totalLines++ // Account for last line without line feed (\n)

	if (totalLines <= lineLimit) {
		return content
	}

	const beforeLimit = Math.floor(lineLimit * 0.2) // 20% of lines before
	const afterLimit = lineLimit - beforeLimit // remaining 80% after

	// Find start section end position
	let startEndPos = -1
	let lineCount = 0
	pos = 0
	while (lineCount < beforeLimit && (pos = content.indexOf("\n", pos)) !== -1) {
		startEndPos = pos
		lineCount++
		pos++
	}

	// Find end section start position
	let endStartPos = content.length
	lineCount = 0
	pos = content.length
	while (lineCount < afterLimit && (pos = content.lastIndexOf("\n", pos - 1)) !== -1) {
		endStartPos = pos + 1 // Start after the line feed (\n)
		lineCount++
	}

	const omittedLines = totalLines - lineLimit
	const startSection = content.slice(0, startEndPos + 1)
	const endSection = content.slice(endStartPos)
	return startSection + `\n[...${omittedLines} lines omitted...]\n\n` + endSection
}

/**
 * Applies run-length encoding to compress repeated lines in text.
 * Only compresses when the compression description is shorter than the repeated content.
 *
 * @param content The text content to compress
 * @returns The compressed text with run-length encoding applied
 */
export function applyRunLengthEncoding(content: string): string {
	if (!content) {
		return content
	}

	let result = ""
	let pos = 0
	let repeatCount = 0
	let prevLine = null

	while (pos < content.length) {
		const nextNewlineIdx = content.indexOf("\n", pos) // Find next line feed (\n) index
		const currentLine = nextNewlineIdx === -1 ? content.slice(pos) : content.slice(pos, nextNewlineIdx + 1)

		if (prevLine === null) {
			prevLine = currentLine
		} else if (currentLine === prevLine) {
			repeatCount++
		} else {
			if (repeatCount > 0) {
				const compressionDesc = `<previous line repeated ${repeatCount} additional times>\n`
				if (compressionDesc.length < prevLine.length * (repeatCount + 1)) {
					result += prevLine + compressionDesc
				} else {
					for (let i = 0; i <= repeatCount; i++) {
						result += prevLine
					}
				}
				repeatCount = 0
			} else {
				result += prevLine
			}
			prevLine = currentLine
		}

		pos = nextNewlineIdx === -1 ? content.length : nextNewlineIdx + 1
	}

	if (repeatCount > 0 && prevLine !== null) {
		const compressionDesc = `<previous line repeated ${repeatCount} additional times>\n`
		if (compressionDesc.length < prevLine.length * repeatCount) {
			result += prevLine + compressionDesc
		} else {
			for (let i = 0; i <= repeatCount; i++) {
				result += prevLine
			}
		}
	} else if (prevLine !== null) {
		result += prevLine
	}

	return result
}

/**
 * Processes carriage returns (\r) in terminal output to simulate how a real terminal would display content.
 * This function is optimized for performance by using in-place string operations and avoiding memory-intensive
 * operations like split/join.
 *
 * Key features:
 * 1. Processes output line-by-line to maximize chunk processing
 * 2. Uses string indexes and substring operations instead of arrays
 * 3. Single-pass traversal of the entire input
 * 4. Special handling for multi-byte characters (like emoji) to prevent corruption
 * 5. Replacement of partially overwritten multi-byte characters with spaces
 *
 * @param input The terminal output to process
 * @returns The processed terminal output with carriage returns (\r) handled
 */
export function processCarriageReturns(input: string): string {
	// Quick check: if no carriage returns (\r), return the original input
	if (input.indexOf("\r") === -1) return input

	let output = ""
	let i = 0
	const len = input.length

	// Single-pass traversal of the entire input
	while (i < len) {
		// Find current line's end position (line feed (\n) or end of text)
		let lineEnd = input.indexOf("\n", i)
		if (lineEnd === -1) lineEnd = len

		// Check if current line contains carriage returns (\r)
		let crPos = input.indexOf("\r", i)
		if (crPos === -1 || crPos >= lineEnd) {
			// No carriage returns (\r) in this line, copy entire line
			output += input.substring(i, lineEnd)
		} else {
			// Line has carriage returns (\r), handle overwrite logic
			let curLine = input.substring(i, crPos)
			curLine = processLineWithCarriageReturns(input, curLine, crPos, lineEnd)
			output += curLine
		}

		// 'curLine' now holds the processed content of the line *without* its original terminating line feed (\n) character.
		// 'lineEnd' points to the position of that line feed (\n) in the original input, or to the end of the input string if no line feed (\n) was found.
		// This check explicitly adds the line feed (\n) character back *only if* one was originally present at this position (lineEnd < len).
		// This ensures we preserve the original structure, correctly handling inputs both with and without a final line feed (\n),
		// rather than incorrectly injecting a line feed (\n) if the original input didn't end with one.
		if (lineEnd < len) output += "\n"

		// Move to next line
		i = lineEnd + 1
	}

	return output
}

/**
 * Processes backspace characters (\b) in terminal output using index operations.
 * Uses indexOf to efficiently locate and handle backspaces.
 *
 * Technically terminal only moves the cursor and overwrites in-place,
 * but we assume \b is destructive as an optimization which is acceptable
 * for all progress spinner cases and most terminal output cases.
 *
 * @param input The terminal output to process
 * @returns The processed output with backspaces handled
 */
export function processBackspaces(input: string): string {
	let output = ""
	let pos = 0
	let bsPos = input.indexOf("\b")

	while (bsPos !== -1) {
		// Fast path: exclude char before backspace
		output += input.substring(pos, bsPos - 1)

		// Move past backspace
		pos = bsPos + 1

		// Count consecutive backspaces
		let count = 0
		while (input[pos] === "\b") {
			count++
			pos++
		}

		// Trim output mathematically for consecutive backspaces
		if (count > 0 && output.length > 0) {
			output = output.substring(0, Math.max(0, output.length - count))
		}

		// Find next backspace
		bsPos = input.indexOf("\b", pos)
	}

	// Add remaining content
	if (pos < input.length) {
		output += input.substring(pos)
	}

	return output
}

/**
 * Helper function to process a single line with carriage returns.
 * Handles the overwrite logic for a line that contains one or more carriage returns (\r).
 *
 * @param input The original input string
 * @param initialLine The line content up to the first carriage return
 * @param initialCrPos The position of the first carriage return in the line
 * @param lineEnd The position where the line ends
 * @returns The processed line with carriage returns handled
 */
function processLineWithCarriageReturns(
	input: string,
	initialLine: string,
	initialCrPos: number,
	lineEnd: number,
): string {
	let curLine = initialLine
	let crPos = initialCrPos

	while (crPos < lineEnd) {
		// Find next carriage return (\r) or line end (line feed (\n))
		let nextCrPos = input.indexOf("\r", crPos + 1)
		if (nextCrPos === -1 || nextCrPos >= lineEnd) nextCrPos = lineEnd

		// Extract segment after carriage return (\r)
		let segment = input.substring(crPos + 1, nextCrPos)

		// Skip empty segments
		if (segment !== "") {
			// Determine how to handle overwrite
			if (segment.length >= curLine.length) {
				// Complete overwrite
				curLine = segment
			} else {
				// Partial overwrite - need to check for multi-byte character boundary issues
				const potentialPartialChar = curLine.charAt(segment.length)
				const segmentLastCharCode = segment.length > 0 ? segment.charCodeAt(segment.length - 1) : 0
				const partialCharCode = potentialPartialChar.charCodeAt(0)

				// Simplified condition for multi-byte character detection
				if (
					(segmentLastCharCode >= 0xd800 && segmentLastCharCode <= 0xdbff) || // High surrogate at end of segment
					(partialCharCode >= 0xdc00 && partialCharCode <= 0xdfff) || // Low surrogate at overwrite position
					(curLine.length > segment.length + 1 && partialCharCode >= 0xd800 && partialCharCode <= 0xdbff) // High surrogate followed by another character
				) {
					// If a partially overwritten multi-byte character is detected, replace with space
					const remainPart = curLine.substring(segment.length + 1)
					curLine = segment + " " + remainPart
				} else {
					// Normal partial overwrite
					curLine = segment + curLine.substring(segment.length)
				}
			}
		}

		crPos = nextCrPos
	}

	return curLine
}
