// engine/hooks/file-watcher.ts — File system watcher with normalized events (SPEC-129)
import { EventEmitter } from 'node:events';
import { readdirSync, watch } from 'node:fs';
import { stat } from 'node:fs/promises';
import { normalize, join } from 'node:path';
// ---------------------------------------------------------------------------
// Glob matching (simple, no external deps)
// ---------------------------------------------------------------------------
/**
 * Convert a glob pattern to a RegExp. Supports:
 * - `**` matches any path segment (including nested)
 * - `*` matches any characters except path separator
 * - `?` matches a single character
 */
export function globToRegex(pattern) {
    let regex = '';
    let i = 0;
    while (i < pattern.length) {
        const ch = pattern.charAt(i);
        if (ch === '*' && pattern.charAt(i + 1) === '*') {
            // ** matches everything including /
            regex += '.*';
            i += 2;
            // skip trailing /
            if (pattern.charAt(i) === '/') {
                i++;
            }
        }
        else if (ch === '*') {
            // * matches everything except /
            regex += '[^/]*';
            i++;
        }
        else if (ch === '?') {
            regex += '[^/]';
            i++;
        }
        else if (ch === '.') {
            regex += '\\.';
            i++;
        }
        else {
            regex += ch;
            i++;
        }
    }
    return new RegExp(`^${regex}$`);
}
/**
 * Test whether `filePath` matches a glob pattern.
 */
export function matchesGlobPattern(filePath, pattern) {
    const normalized = filePath.replace(/\\/g, '/');
    return globToRegex(pattern).test(normalized);
}
/**
 * Returns true if filePath matches any include pattern
 * and does not match any exclude pattern.
 */
export function matchesPatterns(filePath, include, exclude) {
    // If no include patterns, match everything
    const included = !include || include.length === 0 || include.some((p) => matchesGlobPattern(filePath, p));
    if (!included) {
        return false;
    }
    // If any exclude matches, reject
    if (exclude?.some((p) => matchesGlobPattern(filePath, p))) {
        return false;
    }
    return true;
}
// ---------------------------------------------------------------------------
// Default excludes
// ---------------------------------------------------------------------------
const DEFAULT_EXCLUDES = ['data/**', 'node_modules/**', '.git/**'];
const DEFAULT_MAX_TS_WATCHERS = 128;
function maxTypeScriptWatchers() {
    const raw = Number(process.env.PLANU_MAX_TS_WATCHERS ?? DEFAULT_MAX_TS_WATCHERS);
    return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_MAX_TS_WATCHERS;
}
// ---------------------------------------------------------------------------
// FileWatcher
// ---------------------------------------------------------------------------
import { isNativeActive, startNativeWatcher } from '../core-bridge.js';
export class FileWatcher extends EventEmitter {
    watcher = null;
    watchers = new Set();
    rootDir;
    include;
    exclude;
    maxWatchers = maxTypeScriptWatchers();
    /** Track known files to distinguish create vs modify */
    knownFiles = new Set();
    closed = false;
    constructor(options) {
        super();
        this.rootDir = normalize(options.rootDir);
        this.include = options.include ?? [];
        this.exclude = [...DEFAULT_EXCLUDES, ...(options.exclude ?? [])];
    }
    /**
     * Start watching. Emits 'change' events with WatcherEvent payloads.
     * Emits 'error' on watch failure — never throws.
     */
    start() {
        if (this.closed) {
            return;
        }
        try {
            if (isNativeActive()) {
                console.error('[Planu-RS] Starting high-performance Rust file watcher');
                startNativeWatcher(this.rootDir, (err, filename) => {
                    if (err || !filename || this.closed) {
                        return;
                    }
                    const relPath = filename.replace(this.rootDir, '').replace(/^\/+/, '');
                    void this.handleFsEvent(relPath);
                });
            }
            else {
                this.startTypeScriptWatcher();
            }
        }
        catch (err) {
            this.emitWatcherError(err);
        }
    }
    /**
     * Mark a file as known (pre-existing). Call this for initial file scan.
     */
    addKnownFile(relPath) {
        this.knownFiles.add(relPath.replace(/\\/g, '/'));
    }
    /**
     * Mark multiple files as known.
     */
    addKnownFiles(paths) {
        for (const p of paths) {
            this.knownFiles.add(p.replace(/\\/g, '/'));
        }
    }
    /**
     * Graceful shutdown: release all handles.
     */
    close() {
        this.closed = true;
        for (const watcher of this.watchers) {
            watcher.close();
        }
        this.watchers.clear();
        this.watcher = null;
        this.removeAllListeners();
    }
    // ---------------------------------------------------------------------------
    // Private
    // ---------------------------------------------------------------------------
    startTypeScriptWatcher() {
        this.watchDirectory(this.rootDir, '');
        this.watchSubdirectories(this.rootDir, '');
    }
    watchDirectory(absDir, relDir) {
        const watcher = watch(absDir, {}, (_eventType, filename) => {
            if (!filename || this.closed) {
                return;
            }
            const relPath = join(relDir, filename).replace(/\\/g, '/');
            void this.handleFsEvent(relPath);
        });
        watcher.on('error', (err) => {
            this.emitWatcherError(err);
        });
        this.watchers.add(watcher);
        this.watcher ??= watcher;
    }
    watchSubdirectories(absDir, relDir) {
        if (this.watchers.size >= this.maxWatchers) {
            return;
        }
        let entries;
        try {
            entries = readdirSync(absDir, { withFileTypes: true });
        }
        catch {
            return;
        }
        for (const entry of entries) {
            if (!entry.isDirectory()) {
                continue;
            }
            const childRel = join(relDir, entry.name).replace(/\\/g, '/');
            if (this.exclude.some((p) => matchesGlobPattern(childRel, p))) {
                continue;
            }
            const childAbs = join(absDir, entry.name);
            try {
                this.watchDirectory(childAbs, childRel);
            }
            catch {
                continue;
            }
            if (this.watchers.size >= this.maxWatchers) {
                return;
            }
            this.watchSubdirectories(childAbs, childRel);
        }
    }
    emitWatcherError(err) {
        if (this.listenerCount('error') > 0) {
            this.emit('error', err);
        }
    }
    async handleFsEvent(relPath) {
        try {
            // Check exclude patterns first
            if (this.exclude.some((p) => matchesGlobPattern(relPath, p))) {
                return;
            }
            // Check include patterns
            if (this.include.length > 0 && !this.include.some((p) => matchesGlobPattern(relPath, p))) {
                return;
            }
            const evType = await this.determineEventType(relPath);
            if (!evType) {
                return;
            }
            const event = {
                type: evType,
                path: relPath,
                timestamp: Date.now(),
            };
            this.emit('change', event);
        }
        catch (err) {
            this.emit('error', err);
        }
    }
    async determineEventType(relPath) {
        const fullPath = join(this.rootDir, relPath);
        try {
            await stat(fullPath);
            // File exists
            if (this.knownFiles.has(relPath)) {
                return 'modify';
            }
            this.knownFiles.add(relPath);
            return 'create';
        }
        catch {
            // File doesn't exist — it was deleted
            if (this.knownFiles.has(relPath)) {
                this.knownFiles.delete(relPath);
                return 'delete';
            }
            // Unknown file deleted — still report
            return 'delete';
        }
    }
}
//# sourceMappingURL=file-watcher.js.map