/**
 * File Watcher for Dev Server Hot Reload
 *
 * Watches source files and triggers server restart on changes.
 * Handles both backend (API, agents, lib) and generates restart signals.
 */
import { watch, statSync, readdirSync, lstatSync } from 'node:fs';
import { resolve, relative } from 'node:path';
import { createAgentTemplates } from './templates';
/**
 * Create a file watcher manager for hot reload
 */
export function createFileWatcher(options) {
    const { rootDir, logger, onRestart, additionalPaths = [] } = options;
    const watchers = [];
    let paused = false;
    let buildCooldownTimer = null;
    // Directories to ignore - these are NEVER traversed into
    // This prevents EMFILE errors from symlink cycles in node_modules
    const ignoreDirs = new Set([
        '.agentuity',
        '.agents',
        '.claude',
        '.code',
        '.opencode',
        'node_modules',
        '.git',
        '.github',
        'dist',
        'build',
        '.next',
        '.turbo',
        '.npm',
        '.dist',
        '.vscode',
        '.idea',
        '.DS_Store',
        '.playwright',
        '.bun',
        'src/generated',
    ]);
    // Paths to ignore for file change events (but may still be traversed)
    const ignorePaths = [
        '.agentuity',
        '.agents',
        '.claude',
        '.code',
        '.opencode',
        'node_modules',
        '.git',
        '.github',
        'dist',
        'build',
        '.next',
        '.turbo',
        '.npm',
        '.dist',
        '.vscode',
        '.idea',
        '.bun',
        '.DS_Store',
        '.playwright',
        'src/web', // Vite handles frontend with HMR - no backend restart needed
        'src/generated', // Generated files shouldn't trigger rebuilds
    ];
    // File extensions to ignore - non-code files that shouldn't trigger reload
    const ignoreExtensions = new Set([
        '.md',
        '.mdx',
        '.txt',
        '.log',
        '.lock',
        '.yaml',
        '.yml',
        '.toml',
        '.csv',
        '.svg',
        '.png',
        '.jpg',
        '.jpeg',
        '.gif',
        '.webp',
        '.ico',
        '.woff',
        '.woff2',
        '.ttf',
        '.eot',
        '.mp4',
        '.mp3',
        '.wav',
        '.ogg',
        '.webm',
        '.pdf',
        '.zip',
        '.tar',
        '.gz',
        '.map',
    ]);
    /**
     * Check if a path should be ignored
     */
    function shouldIgnorePath(changedFile, watchDir) {
        if (!changedFile)
            return false;
        const absPath = resolve(watchDir, changedFile);
        // Check against ignore list - match both relative path and absolute path
        for (const ignorePath of ignorePaths) {
            // Check relative path from watchDir
            if (changedFile === ignorePath ||
                changedFile.startsWith(`${ignorePath}/`) ||
                changedFile.startsWith(`${ignorePath}\\`)) {
                logger.trace('File change ignored (%s): %s', ignorePath, changedFile);
                return true;
            }
            // Check if absolute path contains the ignore pattern
            const ignoreAbsPath = resolve(rootDir, ignorePath);
            if (absPath === ignoreAbsPath ||
                absPath.startsWith(`${ignoreAbsPath}/`) ||
                absPath.startsWith(`${ignoreAbsPath}\\`)) {
                logger.trace('File change ignored (%s): %s', ignorePath, changedFile);
                return true;
            }
            // Also check if changedFile path includes the ignore pattern anywhere
            // This handles cases like "some/path/.agentuity/file.js"
            const normalizedChanged = changedFile.replace(/\\/g, '/');
            const normalizedIgnore = ignorePath.replace(/\\/g, '/');
            if (normalizedChanged.includes(`/${normalizedIgnore}/`) ||
                normalizedChanged.includes(`/${normalizedIgnore}`)) {
                logger.trace('File change ignored (%s in path): %s', ignorePath, changedFile);
                return true;
            }
        }
        // Ignore temp files from editors
        if (changedFile.match(/\.(tmp|swp|swo|swx)$|~$/)) {
            logger.trace('File change ignored (temp file): %s', changedFile);
            return true;
        }
        // Ignore files with non-code extensions
        const extIndex = changedFile.lastIndexOf('.');
        if (extIndex !== -1) {
            const ext = changedFile.slice(extIndex).toLowerCase();
            if (ignoreExtensions.has(ext)) {
                logger.trace('File change ignored (%s): %s', ext, changedFile);
                return true;
            }
        }
        // Ignore hidden files (except .env)
        if (changedFile.startsWith('.') && !changedFile.startsWith('.env')) {
            logger.trace('File change ignored (hidden file): %s', changedFile);
            return true;
        }
        return false;
    }
    /**
     * Handle file change event
     */
    function handleFileChange(eventType, changedFile, watchDir) {
        if (paused) {
            logger.trace('File change ignored (watcher paused): %s', changedFile);
            return;
        }
        if (shouldIgnorePath(changedFile, watchDir)) {
            return;
        }
        // During build cooldown, ignore changes (they're likely build outputs)
        if (buildCooldownTimer) {
            logger.trace('File change ignored (build cooldown): %s', changedFile);
            return;
        }
        // Get absolute path for checking
        const absPath = changedFile ? resolve(watchDir, changedFile) : watchDir;
        // Check if this is a directory
        let isDirectory = false;
        try {
            const stats = statSync(absPath);
            isDirectory = stats.isDirectory();
        }
        catch {
            // File doesn't exist (was deleted) - not a directory
            isDirectory = false;
        }
        // Check if an empty directory was created in src/agent/
        // This helps with developer experience by auto-scaffolding template files
        if (changedFile && eventType === 'rename' && isDirectory) {
            try {
                // Normalize the path for comparison (use forward slashes)
                const normalizedPath = changedFile.replace(/\\/g, '/');
                // Check if it's empty
                const contents = readdirSync(absPath);
                if (contents.length === 0) {
                    if (normalizedPath.startsWith('src/agent/') ||
                        normalizedPath.includes('/src/agent/')) {
                        logger.debug('Agent directory created: %s', changedFile);
                        createAgentTemplates(absPath);
                    }
                }
            }
            catch (error) {
                // File might have been deleted or doesn't exist yet - this is normal
                logger.trace('Unable to check directory for template creation (%s): %s', changedFile, error);
            }
        }
        // Ignore directory-only change events (mtime updates when files inside change)
        // We only care about actual file changes - the watcher will report those directly
        if (isDirectory && eventType === 'change') {
            logger.trace('File change ignored (directory mtime update): %s', changedFile);
            return;
        }
        logger.debug('File changed (%s): %s', eventType, changedFile || watchDir);
        onRestart();
    }
    /**
     * Recursively collect all directories to watch, skipping ignored directories.
     * This prevents EMFILE errors from symlink cycles in node_modules.
     */
    function collectWatchDirs(dir, visited = new Set()) {
        const dirs = [dir];
        try {
            // Use lstat to check for symlinks - get the real path to detect cycles
            const stat = lstatSync(dir);
            // Skip symlinks to prevent following circular symlinks
            if (stat.isSymbolicLink()) {
                logger.trace('Skipping symlink: %s', dir);
                return [];
            }
            // Track visited inodes to detect cycles
            const key = `${stat.dev}:${stat.ino}`;
            if (visited.has(key)) {
                logger.trace('Skipping already visited directory (cycle detected): %s', dir);
                return [];
            }
            visited.add(key);
            const entries = readdirSync(dir, { withFileTypes: true });
            for (const entry of entries) {
                if (!entry.isDirectory())
                    continue;
                const name = entry.name;
                // Skip ignored directories entirely - this is the key fix
                if (ignoreDirs.has(name)) {
                    logger.trace('Skipping ignored directory: %s', resolve(dir, name));
                    continue;
                }
                // Skip hidden directories (except specific ones like .env folders)
                if (name.startsWith('.')) {
                    logger.trace('Skipping hidden directory: %s', resolve(dir, name));
                    continue;
                }
                const fullPath = resolve(dir, name);
                dirs.push(...collectWatchDirs(fullPath, visited));
            }
        }
        catch (error) {
            logger.trace('Error reading directory %s: %s', dir, error);
        }
        return dirs;
    }
    /**
     * Start watching files
     */
    function start() {
        logger.debug('Starting file watchers for hot reload...');
        // Collect all directories to watch, excluding node_modules and other ignored dirs
        const allDirs = collectWatchDirs(rootDir);
        // Add additional paths
        if (additionalPaths && additionalPaths.length > 0) {
            for (const additionalPath of additionalPaths) {
                const fullPath = resolve(rootDir, additionalPath);
                allDirs.push(...collectWatchDirs(fullPath));
            }
        }
        // De-duplicate directories
        const uniqueDirs = [...new Set(allDirs)];
        logger.debug('Collected %d directories to watch', uniqueDirs.length);
        // Watch each directory non-recursively
        for (const watchPath of uniqueDirs) {
            try {
                // Use non-recursive watch to avoid traversing into node_modules
                const watcher = watch(watchPath, { recursive: false }, (eventType, changedFile) => {
                    // Construct relative path from rootDir for consistent handling
                    const relPath = changedFile
                        ? relative(rootDir, resolve(watchPath, changedFile))
                        : relative(rootDir, watchPath);
                    handleFileChange(eventType, relPath || changedFile, rootDir);
                });
                watchers.push(watcher);
            }
            catch (error) {
                logger.trace('Failed to start watcher for %s: %s', watchPath, error);
            }
        }
        logger.debug('File watchers started (%d directories)', watchers.length);
    }
    /**
     * Stop all watchers
     */
    function stop() {
        logger.debug('Stopping file watchers...');
        for (const watcher of watchers) {
            try {
                watcher.close();
            }
            catch (error) {
                logger.trace('Error closing watcher: %s', error);
            }
        }
        watchers.length = 0;
        if (buildCooldownTimer) {
            clearTimeout(buildCooldownTimer);
            buildCooldownTimer = null;
        }
        logger.debug('File watchers stopped');
    }
    /**
     * Temporarily pause watching (e.g., during build)
     */
    function pause() {
        paused = true;
        logger.trace('File watchers paused');
        // Set cooldown timer to ignore changes for a bit after build
        if (buildCooldownTimer) {
            clearTimeout(buildCooldownTimer);
        }
        buildCooldownTimer = setTimeout(() => {
            buildCooldownTimer = null;
            logger.trace('Build cooldown expired');
        }, 500); // 500ms cooldown
    }
    /**
     * Resume watching
     */
    function resume() {
        paused = false;
        // Clear any pending build cooldown timer so file changes are detected immediately.
        // Without this, events arriving during the remaining cooldown window after a fast
        // typecheck failure would be silently dropped.
        if (buildCooldownTimer) {
            clearTimeout(buildCooldownTimer);
            buildCooldownTimer = null;
        }
        logger.trace('File watchers resumed');
    }
    return {
        start,
        stop,
        pause,
        resume,
    };
}
//# sourceMappingURL=file-watcher.js.map