/**
 * Config File Watcher
 *
 * Watches configuration files for changes and emits events
 * when modifications are detected.
 */
import * as fs from 'fs';
import * as path from 'path';
import { EventEmitter } from 'events';
import { DEFAULT_WATCHER_CONFIG } from './types.js';
import { createSnapshot } from './diff.js';
/**
 * Debounce function for file path changes
 */
function debounceFilePath(fn, delay) {
    let timeoutId = null;
    return {
        call: (filePath) => {
            if (timeoutId) {
                clearTimeout(timeoutId);
            }
            timeoutId = setTimeout(() => {
                fn(filePath);
                timeoutId = null;
            }, delay);
        },
        cancel: () => {
            if (timeoutId) {
                clearTimeout(timeoutId);
                timeoutId = null;
            }
        },
    };
}
/**
 * Check if a path matches any ignore pattern
 */
function matchesIgnorePattern(filePath, patterns) {
    const fileName = path.basename(filePath);
    for (const pattern of patterns) {
        // Simple glob matching
        if (pattern.startsWith('*.')) {
            const ext = pattern.slice(1);
            if (fileName.endsWith(ext))
                return true;
        }
        else if (pattern.endsWith('/**')) {
            const dir = pattern.slice(0, -3);
            if (filePath.includes(dir))
                return true;
        }
        else if (pattern === fileName) {
            return true;
        }
    }
    return false;
}
/**
 * Config file watcher
 */
export class ConfigWatcher extends EventEmitter {
    config;
    watchers = new Map();
    snapshots = new Map();
    debouncedHandlers = new Map();
    running = false;
    constructor(config = {}) {
        super();
        this.config = { ...DEFAULT_WATCHER_CONFIG, ...config };
    }
    /**
     * Start watching files
     */
    async start() {
        if (this.running) {
            return;
        }
        this.running = true;
        for (const watchPath of this.config.paths) {
            await this.watchPath(watchPath);
        }
        this.emit('ready');
    }
    /**
     * Stop watching files
     */
    stop() {
        this.running = false;
        // Cancel all debounced handlers
        for (const handler of this.debouncedHandlers.values()) {
            handler.cancel();
        }
        this.debouncedHandlers.clear();
        // Close all watchers
        for (const watcher of this.watchers.values()) {
            watcher.close();
        }
        this.watchers.clear();
        // Clear snapshots
        this.snapshots.clear();
    }
    /**
     * Add a path to watch
     */
    async addPath(watchPath) {
        if (!this.config.paths.includes(watchPath)) {
            this.config.paths.push(watchPath);
        }
        if (this.running) {
            await this.watchPath(watchPath);
        }
    }
    /**
     * Remove a path from watching
     */
    removePath(watchPath) {
        const index = this.config.paths.indexOf(watchPath);
        if (index >= 0) {
            this.config.paths.splice(index, 1);
        }
        const watcher = this.watchers.get(watchPath);
        if (watcher) {
            watcher.close();
            this.watchers.delete(watchPath);
        }
        const handler = this.debouncedHandlers.get(watchPath);
        if (handler) {
            handler.cancel();
            this.debouncedHandlers.delete(watchPath);
        }
        this.snapshots.delete(watchPath);
    }
    /**
     * Get current snapshot for a path
     */
    getSnapshot(filePath) {
        return this.snapshots.get(filePath);
    }
    /**
     * Force reload a file
     */
    async reload(filePath) {
        return await this.loadAndSnapshot(filePath);
    }
    /**
     * Watch a specific path
     */
    async watchPath(watchPath) {
        try {
            const resolvedPath = path.resolve(watchPath);
            // Check if path exists
            if (!fs.existsSync(resolvedPath)) {
                // Watch parent directory for file creation
                const parentDir = path.dirname(resolvedPath);
                if (fs.existsSync(parentDir)) {
                    this.watchDirectory(parentDir, resolvedPath);
                }
                return;
            }
            const stat = fs.statSync(resolvedPath);
            if (stat.isDirectory()) {
                this.watchDirectory(resolvedPath);
            }
            else {
                await this.watchFile(resolvedPath);
            }
        }
        catch (error) {
            this.emit('error', error instanceof Error ? error : new Error(String(error)));
        }
    }
    /**
     * Watch a directory
     */
    watchDirectory(dirPath, targetFile) {
        if (this.watchers.has(dirPath)) {
            return;
        }
        try {
            const watcher = fs.watch(dirPath, { recursive: this.config.recursive }, (eventType, filename) => {
                if (!filename)
                    return;
                const fullPath = path.join(dirPath, filename);
                // If watching for a specific file, check if it matches
                if (targetFile && fullPath !== targetFile) {
                    return;
                }
                // Check ignore patterns
                if (matchesIgnorePattern(fullPath, this.config.ignorePatterns)) {
                    return;
                }
                // Only process JSON files
                if (!filename.endsWith('.json')) {
                    return;
                }
                this.handleChange(fullPath);
            });
            this.watchers.set(dirPath, watcher);
        }
        catch (error) {
            this.emit('error', error instanceof Error ? error : new Error(String(error)));
        }
    }
    /**
     * Watch a specific file
     */
    async watchFile(filePath) {
        if (this.watchers.has(filePath)) {
            return;
        }
        // Load initial snapshot
        await this.loadAndSnapshot(filePath);
        try {
            const watcher = fs.watch(filePath, (eventType) => {
                if (eventType === 'change') {
                    this.handleChange(filePath);
                }
            });
            this.watchers.set(filePath, watcher);
        }
        catch (error) {
            this.emit('error', error instanceof Error ? error : new Error(String(error)));
        }
    }
    /**
     * Handle a file change event
     */
    handleChange(filePath) {
        // Get or create debounced handler for this file
        let handler = this.debouncedHandlers.get(filePath);
        if (!handler) {
            handler = debounceFilePath((p) => this.processChange(p), this.config.debounceMs);
            this.debouncedHandlers.set(filePath, handler);
        }
        handler.call(filePath);
    }
    /**
     * Process a file change after debouncing
     */
    async processChange(filePath) {
        const oldSnapshot = this.snapshots.get(filePath);
        const newSnapshot = await this.loadAndSnapshot(filePath);
        if (!newSnapshot) {
            return;
        }
        // Check if content actually changed
        if (oldSnapshot && oldSnapshot.hash === newSnapshot.hash) {
            return;
        }
        this.emit('change', filePath, newSnapshot);
    }
    /**
     * Load a file and create a snapshot
     */
    async loadAndSnapshot(filePath) {
        try {
            if (!fs.existsSync(filePath)) {
                return null;
            }
            const content = fs.readFileSync(filePath, 'utf-8');
            const data = JSON.parse(content);
            const snapshot = createSnapshot(data);
            this.snapshots.set(filePath, snapshot);
            return snapshot;
        }
        catch (error) {
            this.emit('error', error instanceof Error ? error : new Error(String(error)));
            return null;
        }
    }
    /**
     * Get watched paths
     */
    getWatchedPaths() {
        return [...this.config.paths];
    }
    /**
     * Check if watcher is running
     */
    isRunning() {
        return this.running;
    }
}
// Singleton instance
let watcherInstance = null;
/**
 * Get the singleton config watcher
 */
export function getConfigWatcher(config) {
    if (!watcherInstance) {
        watcherInstance = new ConfigWatcher(config);
    }
    return watcherInstance;
}
/**
 * Reset the singleton watcher
 */
export function resetConfigWatcher() {
    if (watcherInstance) {
        watcherInstance.stop();
        watcherInstance = null;
    }
}
//# sourceMappingURL=watcher.js.map