/**
 * File Watcher Service
 *
 * Provides robust cross-platform file watching using Chokidar library.
 * Handles file watching with debouncing, error recovery, and graceful fallbacks.
 */
import * as chokidar from "chokidar";
import { EventEmitter } from "events";
export class FileWatcherService extends EventEmitter {
    constructor(logger, config) {
        super();
        this.watchers = new Map();
        this.globalWatcher = null;
        this.logger = logger;
        this.defaultConfig = {
            stabilityThreshold: 300,
            pollInterval: 100,
            maxRetries: 3,
            fallbackPolling: false,
            ignoreTempFiles: true,
            ...config,
        };
    }
    /**
     * Start watching a file
     * Maps to FR-010: Handle file deletion, creation, and modification
     */
    async watchFile(path, callback) {
        try {
            if (this.watchers.has(path)) {
                // Add callback to existing watcher
                const entry = this.watchers.get(path);
                entry.callbacks.add(callback);
                return;
            }
            // Create new watcher entry
            const entry = {
                path,
                watcher: null,
                isActive: false,
                lastEvent: Date.now(),
                errorCount: 0,
                lastError: undefined,
                callbacks: new Set([callback]),
                config: { ...this.defaultConfig },
            };
            this.watchers.set(path, entry);
            await this.initializeWatcher(entry);
        }
        catch (error) {
            this.logger?.error(`FileWatcher: Failed to watch file ${path}: ${error.message}`);
            throw error;
        }
    }
    /**
     * Stop watching a file
     * Resource cleanup
     */
    async unwatchFile(path) {
        const entry = this.watchers.get(path);
        if (!entry)
            return;
        try {
            if (entry.watcher) {
                entry.watcher.unwatch(path);
            }
            this.watchers.delete(path);
        }
        catch (error) {
            this.logger?.warn(`FileWatcher: Error unwatching file ${path}: ${error.message}`);
        }
    }
    /**
     * Get watcher status
     * Maps to FR-012: Handle watcher initialization failures
     */
    getWatcherStatus(path) {
        const entry = this.watchers.get(path);
        if (!entry)
            return null;
        return {
            isActive: entry.isActive,
            path: entry.path,
            method: entry.errorCount > 0
                ? "failed"
                : entry.config.fallbackPolling
                    ? "polling"
                    : "native",
            errorCount: entry.errorCount,
            lastError: entry.lastError,
            lastEvent: entry.lastEvent > 0
                ? {
                    type: "change",
                    path: entry.path,
                    timestamp: entry.lastEvent,
                }
                : undefined,
        };
    }
    /**
     * Get all watcher statuses
     * For monitoring and debugging
     */
    getAllWatcherStatuses() {
        return Array.from(this.watchers.keys())
            .map((path) => this.getWatcherStatus(path))
            .filter((status) => status !== null);
    }
    /**
     * Cleanup all watchers
     */
    async cleanup() {
        const paths = Array.from(this.watchers.keys());
        for (const path of paths) {
            await this.unwatchFile(path);
        }
        if (this.globalWatcher) {
            const watcher = this.globalWatcher;
            this.globalWatcher = null;
            await watcher.close();
        }
    }
    async initializeWatcher(entry) {
        try {
            // Initialize global watcher if needed
            if (!this.globalWatcher) {
                this.globalWatcher = chokidar.watch([], {
                    persistent: true,
                    ignoreInitial: true,
                    awaitWriteFinish: {
                        stabilityThreshold: entry.config.stabilityThreshold,
                        pollInterval: entry.config.pollInterval,
                    },
                    usePolling: entry.config.fallbackPolling,
                    interval: entry.config.pollInterval,
                });
                // Unref the global watcher to allow natural exit if it's the only thing left
                // (chokidar watchers keep the process alive by default)
                // Note: some platforms might need additional handling
                const watcher = this.globalWatcher;
                if (watcher.unref) {
                    watcher.unref();
                }
                this.setupGlobalWatcherEvents();
            }
            // Add path to global watcher
            this.globalWatcher.add(entry.path);
            entry.watcher = this.globalWatcher;
            entry.isActive = true;
            entry.errorCount = 0;
        }
        catch (error) {
            entry.errorCount++;
            entry.isActive = false;
            entry.lastError = error.message;
            this.logger?.error(`FileWatcher: Failed to initialize watcher for ${entry.path}: ${error.message}`);
            // Try fallback polling if not already using it
            if (!entry.config.fallbackPolling &&
                entry.errorCount < entry.config.maxRetries) {
                entry.config.fallbackPolling = true;
                await this.initializeWatcher(entry);
            }
            else {
                throw error;
            }
        }
    }
    setupGlobalWatcherEvents() {
        if (!this.globalWatcher)
            return;
        this.globalWatcher.on("change", (filePath, stats) => {
            this.handleFileEvent("change", filePath, stats);
        });
        this.globalWatcher.on("add", (filePath, stats) => {
            this.handleFileEvent("create", filePath, stats);
        });
        this.globalWatcher.on("unlink", (filePath) => {
            this.handleFileEvent("delete", filePath);
        });
        this.globalWatcher.on("addDir", (dirPath) => {
            this.handleFileEvent("create", dirPath);
        });
        this.globalWatcher.on("unlinkDir", (dirPath) => {
            this.handleFileEvent("delete", dirPath);
        });
        this.globalWatcher.on("error", (err) => {
            const error = err instanceof Error ? err : new Error(String(err));
            this.logger?.error(`FileWatcher: File watcher error: ${error.message}`);
            this.emit("watcherError", error);
        });
    }
    handleFileEvent(type, filePath, stats) {
        const event = {
            type,
            path: filePath,
            timestamp: Date.now(),
            size: stats?.size,
        };
        // Notify all watchers that match the path or are parents of the path
        for (const [watchedPath, entry] of this.watchers.entries()) {
            if (filePath === watchedPath ||
                filePath.startsWith(watchedPath + "/") ||
                // Handle cases where the watched path might be a file and we get an event for it
                // (already covered by filePath === watchedPath)
                false) {
                entry.lastEvent = event.timestamp;
                // Notify all callbacks for this watcher
                for (const callback of entry.callbacks) {
                    try {
                        callback(event);
                    }
                    catch (error) {
                        this.logger?.error(`FileWatcher: Error in file watch callback for ${watchedPath} (event on ${filePath}): ${error.message}`);
                    }
                }
            }
        }
    }
}
