/**
 * Prompt Loader - Handles reading prompt files from disk with compression support
 */
import * as fs from 'fs/promises';
import * as fsSync from 'fs';
import * as path from 'path';
import { existsSync } from 'fs';
/**
 * PromptLoader handles file I/O operations for prompt configuration files
 */
export class PromptLoader {
    baseDir;
    static MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
    constructor(baseDir) {
        this.baseDir = baseDir;
    }
    /**
     * Load a single file with optional compression
     */
    async loadFile(filePath, shouldCompress) {
        // Step 1: Validate input
        if (filePath === null || filePath === undefined) {
            return { success: false, content: '', error: 'Invalid file path' };
        }
        // Check for path traversal
        const normalizedPath = path.normalize(filePath);
        const resolvedPath = path.resolve(filePath);
        const resolvedBaseDir = path.resolve(this.baseDir);
        if (normalizedPath.includes('..') ||
            !resolvedPath.startsWith(resolvedBaseDir)) {
            return { success: false, content: '', error: 'Path traversal detected' };
        }
        try {
            // Step 2: Check file size before reading
            // Use lstat to detect symbolic links
            const stats = await fs.lstat(filePath);
            if (stats.isSymbolicLink()) {
                return { success: false, content: '', error: 'Not a regular file' };
            }
            if (!stats.isFile()) {
                return { success: false, content: '', error: 'Not a regular file' };
            }
            if (stats.size > PromptLoader.MAX_FILE_SIZE) {
                return { success: false, content: '', error: 'File too large' };
            }
            // Step 3: Read file content
            let rawContent;
            try {
                rawContent = await fs.readFile(filePath, 'utf8');
            }
            catch (readError) {
                return {
                    success: false,
                    content: '',
                    error: `Failed to read file: ${readError instanceof Error ? readError.message : 'Unknown error'}`,
                };
            }
            // Step 4: Validate UTF-8 encoding
            // Node.js will throw an error if the file is not valid UTF-8 when reading with 'utf8'
            // However, we need to check for specific invalid sequences
            if (this.containsInvalidUtf8(rawContent)) {
                return { success: false, content: '', error: 'Invalid UTF-8 encoding' };
            }
            // Step 5: Apply compression if requested
            const finalContent = shouldCompress
                ? this.compressContent(rawContent)
                : rawContent;
            return { success: true, content: finalContent, error: null };
        }
        catch (error) {
            if (error.code === 'ENOENT') {
                return { success: false, content: '', error: 'File not found' };
            }
            return {
                success: false,
                content: '',
                error: `Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`,
            };
        }
    }
    /**
     * Check if content contains invalid UTF-8 sequences
     */
    containsInvalidUtf8(content) {
        // Check for replacement character which indicates invalid UTF-8
        return content.includes('\ufffd');
    }
    /**
     * Compress content according to prompt configuration rules
     */
    compressContent(content) {
        // Step 1: Handle empty or null content
        if (!content) {
            return '';
        }
        // Step 2: Initialize compression state
        const lines = content.split(/\r?\n/); // Handle both \n and \r\n
        const compressedLines = [];
        let inCodeBlock = false;
        let lastLineWasEmpty = false;
        let codeBlockDelimiter = null;
        // Step 3: Process each line
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];
            // Skip the last empty line if it's from a trailing newline
            if (i === lines.length - 1 && line === '' && content.endsWith('\n')) {
                continue;
            }
            // Check for code block boundaries
            if (line.startsWith('```')) {
                if (!inCodeBlock) {
                    inCodeBlock = true;
                    codeBlockDelimiter = line;
                }
                else if (line === codeBlockDelimiter || line === '```') {
                    inCodeBlock = false;
                    codeBlockDelimiter = null;
                }
                compressedLines.push(line);
                lastLineWasEmpty = false;
                continue;
            }
            if (inCodeBlock) {
                compressedLines.push(line);
                lastLineWasEmpty = false;
                continue;
            }
            // Apply prose compression rules
            let compressedLine = line;
            // Simplify headers
            compressedLine = compressedLine.replace(/^#{2,}\s+(.+)$/, '# $1');
            // Simplify bold list items
            compressedLine = compressedLine.replace(/^(\s*)-\s+\*\*(.+?)\*\*:\s*(.*)$/, '$1- $2: $3');
            // Remove excessive whitespace
            // For list items, preserve leading spaces (indentation)
            if (compressedLine.match(/^\s*[-*+]\s/) ||
                compressedLine.match(/^\s*\d+\.\s/)) {
                const leadingSpaces = compressedLine.match(/^\s*/)?.[0] || '';
                compressedLine =
                    leadingSpaces + compressedLine.trim().replace(/\s+/g, ' ');
            }
            else {
                // For non-list items, remove all leading/trailing whitespace
                compressedLine = compressedLine.trim().replace(/\s+/g, ' ');
            }
            // Handle blank lines
            if (compressedLine === '') {
                if (lastLineWasEmpty) {
                    continue; // Skip multiple blank lines
                }
                else {
                    lastLineWasEmpty = true;
                }
            }
            else {
                lastLineWasEmpty = false;
            }
            compressedLines.push(compressedLine);
        }
        return compressedLines.join('\n');
    }
    /**
     * Load multiple files and return a map of path to content
     */
    async loadAllFiles(baseDir, fileList, shouldCompress) {
        // Step 1: Validate inputs
        if (!baseDir || !fileList) {
            return new Map();
        }
        // Step 2: Initialize result map
        const fileContents = new Map();
        // Step 3: Process each file
        for (const relativePath of fileList) {
            const absolutePath = path.join(baseDir, relativePath);
            const result = await this.loadFile(absolutePath, shouldCompress);
            if (result.success) {
                fileContents.set(relativePath, result.content);
            }
            else {
                // Log warning but continue processing other files
                console.warn(`Failed to load ${relativePath}: ${result.error}`);
            }
        }
        return fileContents;
    }
    /**
     * Detect environment characteristics
     */
    detectEnvironment(workingDirectory) {
        // Step 1: Detect Git repository
        let isGitRepository = false;
        let currentDir = workingDirectory;
        try {
            while (currentDir !== path.dirname(currentDir)) {
                // Not at root
                if (existsSync(path.join(currentDir, '.git'))) {
                    isGitRepository = true;
                    break;
                }
                currentDir = path.dirname(currentDir);
            }
        }
        catch {
            // Permission errors default to false
            isGitRepository = false;
        }
        // Step 2: Detect sandbox environment
        let isSandboxed = false;
        if (process.env.SANDBOX === '1' || process.env.SANDBOX === 'true') {
            isSandboxed = true;
        }
        else if (process.env.CONTAINER === '1' ||
            process.env.CONTAINER === 'true') {
            isSandboxed = true;
        }
        else if (existsSync('/sandbox') || existsSync('/.dockerenv')) {
            isSandboxed = true;
        }
        // Step 3: Detect IDE companion
        let hasIdeCompanion = false;
        if (process.env.IDE_COMPANION === '1' ||
            process.env.IDE_COMPANION === 'true') {
            hasIdeCompanion = true;
        }
        else if (existsSync(path.join(workingDirectory, '.vscode'))) {
            hasIdeCompanion = true;
        }
        else if (existsSync(path.join(workingDirectory, '.idea'))) {
            hasIdeCompanion = true;
        }
        return {
            isGitRepository,
            isSandboxed,
            hasIdeCompanion,
        };
    }
    /**
     * Watch files for changes
     */
    watchFiles(baseDir, callback) {
        // Step 1: Validate inputs
        if (!existsSync(baseDir)) {
            return null;
        }
        if (typeof callback !== 'function') {
            return null;
        }
        // Step 2: Set up file watcher
        let watcher;
        const timeouts = new Map();
        try {
            // Try to use chokidar if available
            // eslint-disable-next-line @typescript-eslint/no-require-imports, no-restricted-syntax
            const chokidar = require('chokidar');
            watcher = chokidar.watch(baseDir, {
                persistent: true,
                recursive: true,
                ignoreInitial: true,
                ignored: (path) => !path.endsWith('.md'),
            });
            // Step 3: Handle file change events with debouncing
            const handleChange = (eventType, filePath) => {
                const relativePath = path.relative(baseDir, filePath);
                // Only process .md files
                if (!relativePath.endsWith('.md')) {
                    return;
                }
                // Debounce events
                if (timeouts.has(relativePath)) {
                    clearTimeout(timeouts.get(relativePath));
                }
                timeouts.set(relativePath, setTimeout(() => {
                    callback(eventType, relativePath);
                    timeouts.delete(relativePath);
                }, 100)); // 100ms debounce
            };
            watcher.on('add', (filePath) => handleChange('add', filePath));
            watcher.on('change', (filePath) => handleChange('change', filePath));
            watcher.on('unlink', (filePath) => handleChange('unlink', filePath));
            // Step 4: Return watcher control object
            return {
                stop: () => {
                    // Clear all pending timeouts
                    for (const timeout of timeouts.values()) {
                        clearTimeout(timeout);
                    }
                    timeouts.clear();
                    // Close the watcher
                    if (watcher && typeof watcher.close === 'function') {
                        watcher.close();
                    }
                },
            };
        }
        catch {
            // If chokidar is not available, use fs.watch as fallback
            try {
                const fsWatcher = fsSync.watch(baseDir, { recursive: true });
                // Note: fs.watch has limitations but works as a fallback
                fsWatcher.on('change', (eventType, filename) => {
                    if (filename) {
                        const filenameStr = typeof filename === 'string' ? filename : filename.toString();
                        if (filenameStr.endsWith('.md')) {
                            // Simple debouncing
                            if (timeouts.has(filenameStr)) {
                                clearTimeout(timeouts.get(filenameStr));
                            }
                            timeouts.set(filenameStr, setTimeout(() => {
                                callback(eventType || 'change', filenameStr);
                                timeouts.delete(filenameStr);
                            }, 100));
                        }
                    }
                });
                return {
                    stop: () => {
                        for (const timeout of timeouts.values()) {
                            clearTimeout(timeout);
                        }
                        timeouts.clear();
                        fsWatcher.close();
                    },
                };
            }
            catch {
                return null;
            }
        }
    }
}
//# sourceMappingURL=prompt-loader.js.map