// engine/hooks/file-watcher.ts — File system watcher with normalized events (SPEC-129)
import { EventEmitter } from 'node:events';
import { watch } from 'node:fs';
import { stat } from 'node:fs/promises';
import { normalize, sep } 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/**'];
// ---------------------------------------------------------------------------
// FileWatcher
// ---------------------------------------------------------------------------
export class FileWatcher extends EventEmitter {
    watcher = null;
    rootDir;
    include;
    exclude;
    /** 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 {
            this.watcher = watch(this.rootDir, { recursive: true }, (_eventType, filename) => {
                if (!filename || this.closed) {
                    return;
                }
                // Normalize path separator
                const relPath = filename.replace(/\\/g, '/');
                void this.handleFsEvent(relPath);
            });
            this.watcher.on('error', (err) => {
                this.emit('error', err);
            });
        }
        catch (err) {
            this.emit('error', 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;
        if (this.watcher) {
            this.watcher.close();
            this.watcher = null;
        }
        this.removeAllListeners();
    }
    // ---------------------------------------------------------------------------
    // Private
    // ---------------------------------------------------------------------------
    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 = [this.rootDir, relPath].join(sep).replace(/\//g, sep);
        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