/**
 * Workspace Stream — Real-time bidirectional file sync between CLI and V3.
 *
 * Architecture:
 *   1. WorkspaceWatcher — chokidar-based file watcher that detects local changes
 *      and pushes them to V3 via WebSocket.
 *   2. MutationApplier — receives file_mutation events from V3's SSE stream
 *      and applies them to the local workspace IN REAL TIME (not at completion).
 *   3. WorkspaceWSClient — WebSocket client connected to V3's /ws/workspace
 *      for bidirectional file sync outside the SSE stream.
 *
 * Event types (V3 → CLI via SSE):
 *   {"type": "file_mutation", "path": "rel/path", "content": "...", "action": "write"|"edit", "tool": "write_file"|"edit_file"}
 *
 * Event types (CLI → V3 via /ws/workspace):
 *   {"type": "bind", "context_id": "...", "workspace_root": "..."}
 *   {"type": "file_sync", "path": "rel/path", "content": "...", "action": "write"|"delete"}
 *   {"type": "file_batch", "files": [{path, content}, ...]}
 */
import * as chokidar from 'chokidar';
import fs from 'fs';
import path from 'path';
import WebSocket from 'ws';
import { Logger } from './logger.js';
import { joinV3WorkspacePath, normalizeV3WorkspaceRelativePath } from './v3-workspace-path.js';
const logger = new Logger();
logger.setVerbose(!!process.env.VIGTHORIA_DEBUG);
// Files/dirs to ignore in the watcher
const IGNORE_PATTERNS = [
    '**/node_modules/**',
    '**/.git/**',
    '**/__pycache__/**',
    '**/.venv/**',
    '**/dist/**',
    '**/build/**',
    '**/.next/**',
    '**/.cache/**',
    '**/.vigthoria/**',
    '**/coverage/**',
    '**/*.pyc',
    // CRITICAL: Windows AppData & system directories (prevents EPERM errors)
    '**/Anwendungsdaten/**', // German AppData
    '**/AppData/**', // English AppData
    '**/Local Settings/**', // Legacy Windows
    '**/Application Data/**', // Legacy Windows
    '**/$RECYCLE.BIN/**', // Windows Recycle Bin
    '**/System Volume Information/**',
    '**/Temp/**',
    '**/tmp/**',
    '**/OneDrive/**', // OneDrive cache
    '**/MicrosoftEdgeBackups/**',
];
const BINARY_EXTENSIONS = new Set([
    '.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.webp', '.bmp',
    '.mp3', '.mp4', '.wav', '.ogg', '.webm', '.avi',
    '.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
    '.woff', '.woff2', '.ttf', '.eot', '.otf',
    '.pdf', '.doc', '.docx', '.xls', '.xlsx',
    '.pyc', '.pyo', '.so', '.dll', '.exe', '.bin',
    '.db', '.sqlite', '.sqlite3',
]);
const MAX_SYNC_FILE_BYTES = 500 * 1024; // 500 KB
// ── Mutation Applier ─────────────────────────────────────────
/**
 * Tracks files that V3 has mutated so the watcher doesn't echo them back.
 */
const _v3MutatedFiles = new Set();
const _v3MuteTimeout = 2000; // 2s mute window after V3 writes a file
/**
 * Apply a file_mutation event from V3's SSE stream to the local workspace.
 * Call this from the SSE event handler for real-time file application.
 */
export function applyFileMutation(event, workspaceRoot) {
    if (event.type !== 'file_mutation')
        return false;
    if (!event.path || !workspaceRoot)
        return false;
    const relativePath = normalizeV3WorkspaceRelativePath(event.path, workspaceRoot);
    if (!relativePath) {
        logger.warn(`Refusing to apply mutation with invalid path: ${event.path}`);
        return false;
    }
    const absPath = joinV3WorkspacePath(workspaceRoot, relativePath);
    const resolvedRoot = path.resolve(workspaceRoot);
    // Safety: ensure the resolved path is within the workspace
    if (!absPath.startsWith(resolvedRoot + path.sep) && absPath !== resolvedRoot) {
        logger.warn(`Refusing to apply mutation outside workspace: ${event.path}`);
        return false;
    }
    try {
        if (event.action === 'delete') {
            if (fs.existsSync(absPath)) {
                fs.unlinkSync(absPath);
                logger.debug(`Deleted: ${event.path}`);
            }
        }
        else if (typeof event.content === 'string') {
            fs.mkdirSync(path.dirname(absPath), { recursive: true });
            fs.writeFileSync(absPath, event.content, 'utf8');
            // Mute the watcher for this file to prevent echo
            _v3MutatedFiles.add(absPath);
            setTimeout(() => _v3MutatedFiles.delete(absPath), _v3MuteTimeout);
            logger.debug(`Applied: ${event.path} (${event.action})`);
        }
        return true;
    }
    catch (err) {
        logger.error(`Failed to apply mutation for ${event.path}: ${err}`);
        return false;
    }
}
export class WorkspaceWatcher {
    watcher = null;
    workspaceRoot;
    onFileChange;
    _ready = false;
    constructor(options) {
        this.workspaceRoot = path.resolve(options.workspaceRoot);
        this.onFileChange = options.onFileChange;
    }
    start() {
        if (this.watcher)
            return;
        try {
            this.watcher = chokidar.watch(this.workspaceRoot, {
                ignored: IGNORE_PATTERNS,
                persistent: false,
                ignoreInitial: true,
                awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 },
                // Windows-specific settings to prevent EPERM on system directories
                usePolling: false,
                interval: 100,
                binaryInterval: 300,
                depth: 50, // Limit traversal depth
            });
            this.watcher
                .on('ready', () => {
                this._ready = true;
                logger.debug('Workspace watcher ready');
            })
                .on('add', (filePath) => this._handleChange(filePath, 'write'))
                .on('change', (filePath) => this._handleChange(filePath, 'write'))
                .on('unlink', (filePath) => this._handleChange(filePath, 'delete'))
                // CRITICAL: Handle permission errors gracefully (Windows AppData EPERM)
                .on('error', (error) => {
                if (error?.code === 'EPERM' || error?.errno === -4048) {
                    logger.warn(`Workspace watcher permission error (ignored): ${error.message}`);
                    // Continue without watcher; don't crash
                }
                else {
                    logger.error(`Workspace watcher error: ${error?.message || error}`);
                }
            });
        }
        catch (err) {
            // Handle initialization errors (e.g., EPERM during chokidar.watch setup)
            if (err?.code === 'EPERM' || err?.errno === -4048 || err?.message?.includes('EPERM')) {
                logger.warn(`Failed to initialize workspace watcher (EPERM): ${err.message}`);
                logger.info('Continuing without file watcher; local file changes will not sync in real-time');
                this.watcher = null;
            }
            else {
                logger.error(`Failed to initialize workspace watcher: ${err.message}`);
                throw err;
            }
        }
    }
    stop() {
        if (this.watcher) {
            // close() returns a Promise; fire-and-forget so handles are released
            this.watcher.close().catch(() => { });
            this.watcher = null;
            this._ready = false;
        }
    }
    get isReady() {
        return this._ready;
    }
    _handleChange(filePath, action) {
        // Skip if this file was just written by V3 (echo prevention)
        if (_v3MutatedFiles.has(filePath))
            return;
        const ext = path.extname(filePath).toLowerCase();
        if (BINARY_EXTENSIONS.has(ext))
            return;
        const relativePath = path.relative(this.workspaceRoot, filePath);
        if (relativePath.startsWith('..'))
            return; // safety
        if (action === 'delete') {
            this.onFileChange?.(relativePath, null, 'delete');
            return;
        }
        try {
            const stat = fs.statSync(filePath);
            if (stat.size > MAX_SYNC_FILE_BYTES)
                return;
            const content = fs.readFileSync(filePath, 'utf8');
            this.onFileChange?.(relativePath, content, 'write');
        }
        catch {
            // File might have been deleted between event and read
        }
    }
}
export class WorkspaceWSClient {
    ws = null;
    opts;
    reconnectTimer = null;
    _connected = false;
    _queue = [];
    _maxQueue = 200;
    constructor(opts) {
        this.opts = opts;
    }
    connect() {
        if (this.ws)
            return;
        const url = `${this.opts.serverUrl}/ws/workspace`;
        this.ws = new WebSocket(url);
        this.ws.on('open', () => {
            this._connected = true;
            this._send({
                type: 'auth',
                token: this.opts.token,
            });
            // Bind to workspace
            this._send({
                type: 'bind',
                context_id: this.opts.contextId,
                workspace_root: this.opts.workspaceRoot,
            });
            // Flush queued messages
            while (this._queue.length > 0) {
                const msg = this._queue.shift();
                this._send(msg);
            }
            logger.debug('WS workspace connected');
        });
        this.ws.on('message', (data) => {
            try {
                const event = JSON.parse(data.toString());
                if (event.type === 'file_mutation') {
                    this.opts.onMutation?.(event);
                }
            }
            catch {
                // ignore parse errors
            }
        });
        this.ws.on('close', () => {
            this._connected = false;
            this.ws = null;
            // Auto-reconnect after 3s
            this.reconnectTimer = setTimeout(() => this.connect(), 3000);
        });
        this.ws.on('error', () => {
            // error handler to prevent crash, close event handles reconnect
        });
    }
    disconnect() {
        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer);
            this.reconnectTimer = null;
        }
        if (this.ws) {
            this.ws.close();
            this.ws = null;
        }
        this._connected = false;
    }
    /**
     * Push a local file change to V3's workspace.
     */
    syncFile(relativePath, content, action) {
        const msg = {
            type: 'file_sync',
            path: relativePath,
            content: content || '',
            action,
        };
        if (this._connected) {
            this._send(msg);
        }
        else if (this._queue.length < this._maxQueue) {
            this._queue.push(msg);
        }
    }
    /**
     * Push multiple files at once.
     */
    syncBatch(files) {
        const msg = { type: 'file_batch', files };
        if (this._connected) {
            this._send(msg);
        }
        else if (this._queue.length < this._maxQueue) {
            this._queue.push(msg);
        }
    }
    get isConnected() {
        return this._connected;
    }
    _send(msg) {
        try {
            this.ws?.send(JSON.stringify(msg));
        }
        catch {
            // connection lost
        }
    }
}
