/**
 * File watcher service using chokidar.
 * Watches project directories for changes, debounces events,
 * and emits fs_changed messages with sequence tracking.
 */
import { relative } from "node:path";
import { watch } from "chokidar";
import { MAX_CHANGES_PER_EVENT, FS_DEBOUNCE_MIN_MS } from "@pocketcoder/shared/constants";
import { incrementSeq } from "../db/repositories/fs-state.js";
import { logger } from "../utils/logger.js";
/** Chokidar event names mapped to FsChange kinds. */
const EVENT_KIND_MAP = {
    add: "create",
    addDir: "create",
    change: "modify",
    unlink: "delete",
    unlinkDir: "delete",
};
/**
 * Ignored patterns from PRD § 6 - File Watcher (chokidar).
 * Chokidar v4 uses exact string matching for string matchers,
 * so we use RegExp for glob-like patterns.
 */
const IGNORED_PATTERNS = [
    /(?:^|[/\\])node_modules(?:[/\\]|$)/,
    /(?:^|[/\\])\.git(?:[/\\]|$)/,
    /(?:^|[/\\])dist(?:[/\\]|$)/,
    /(?:^|[/\\])build(?:[/\\]|$)/,
    /(?:^|[/\\])\.next(?:[/\\]|$)/,
    /(?:^|[/\\])\.turbo(?:[/\\]|$)/,
    /(?:^|[/\\])\.cache(?:[/\\]|$)/,
    /(?:^|[/\\])coverage(?:[/\\]|$)/,
    /\.log$/,
    /(?:^|[/\\])(?:pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$/,
];
export class FileWatcher {
    watcher = null;
    pendingChanges = [];
    debounceTimer = null;
    projectId;
    projectPath;
    onFsChanged;
    debounceMs;
    constructor(options) {
        this.projectId = options.projectId;
        this.projectPath = options.projectPath;
        this.onFsChanged = options.onFsChanged;
        this.debounceMs = options.debounceMs ?? FS_DEBOUNCE_MIN_MS;
    }
    /** Start watching the project directory. */
    start() {
        if (this.watcher)
            return;
        this.watcher = watch(this.projectPath, {
            ignored: IGNORED_PATTERNS,
            persistent: true,
            ignoreInitial: true,
        });
        this.watcher.on("all", (event, filePath) => {
            const kind = EVENT_KIND_MAP[event];
            if (!kind)
                return;
            const relativePath = relative(this.projectPath, filePath);
            // Skip empty paths (the root directory itself)
            if (!relativePath)
                return;
            this.pendingChanges.push({ kind, path: relativePath });
            this.scheduleFlush();
        });
        this.watcher.on("error", (err) => {
            const message = err instanceof Error ? err.message : String(err);
            logger.error(`File watcher error for project ${this.projectId}: ${message}`);
        });
        logger.info(`File watcher started for project ${this.projectId} at ${this.projectPath}`);
    }
    /** Stop watching and flush any pending changes. */
    async stop() {
        if (this.debounceTimer) {
            clearTimeout(this.debounceTimer);
            this.debounceTimer = null;
        }
        // Flush remaining changes before closing
        if (this.pendingChanges.length > 0) {
            this.flush();
        }
        if (this.watcher) {
            await this.watcher.close();
            this.watcher = null;
        }
        logger.info(`File watcher stopped for project ${this.projectId}`);
    }
    /** Whether the watcher is currently active. */
    get isWatching() {
        return this.watcher !== null && !this.watcher.closed;
    }
    /** Schedule a debounced flush of pending changes. */
    scheduleFlush() {
        if (this.debounceTimer) {
            clearTimeout(this.debounceTimer);
        }
        this.debounceTimer = setTimeout(() => {
            this.debounceTimer = null;
            this.flush();
        }, this.debounceMs);
    }
    /** Flush pending changes as an fs_changed event. */
    flush() {
        if (this.pendingChanges.length === 0)
            return;
        const changes = this.pendingChanges;
        this.pendingChanges = [];
        const changesTruncated = changes.length > MAX_CHANGES_PER_EVENT;
        const truncatedChanges = changesTruncated ? changes.slice(0, MAX_CHANGES_PER_EVENT) : changes;
        const seq = incrementSeq(this.projectId);
        const event = {
            type: "fs_changed",
            sessionId: this.projectId,
            payload: {
                seq,
                changes: truncatedChanges,
                ...(changesTruncated ? { changesTruncated: true } : {}),
            },
        };
        try {
            this.onFsChanged(event);
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            logger.error(`Error in fs_changed callback for project ${this.projectId}: ${msg}`);
        }
    }
}
//# sourceMappingURL=file-watcher.js.map