"use strict";
/**
 * Editor Monitor (Feature #2 — Windows side)
 *
 * Standalone daemon that watches the user's active editor (VS Code, Cursor,
 * etc.) and pushes its state to the Fivo Cell bridge. On Windows, it uses
 * PowerShell to query the foreground window title (which contains the
 * active file path in VS Code/Cursor). On macOS/Linux, it falls back to
 * `xdotool`/AppleScript.
 *
 * Two modes:
 *   1) Active-window polling (default) — every N seconds, check what's open
 *   2) File watcher — when a file in the project is touched, push state
 *
 * Usage:
 *   cell watch-editor start [--project NAME] [--interval 3000] [--port 9877]
 *   cell watch-editor stop
 *   cell watch-editor status
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.EditorMonitor = exports.BridgeClient = void 0;
exports.getActiveWindow = getActiveWindow;
exports.parseTitleToState = parseTitleToState;
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const net = __importStar(require("net"));
const child_process_1 = require("child_process");
// ─── Platform-specific active window query ────────────────────────────────
function getActiveWindowWindows() {
    return new Promise((resolve) => {
        const psScript = `
      Add-Type -TypeDefinition @"
        using System;
        using System.Runtime.InteropServices;
        using System.Text;
        public class Win {
          [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
          [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
          [DllImport("user32.dll", SetLastError=true)] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
        }
"@
      $h = [Win]::GetForegroundWindow()
      if ($h -eq [IntPtr]::Zero) { exit 0 }
      $sb = New-Object Text.StringBuilder 512
      [Win]::GetWindowText($h, $sb, 512) | Out-Null
      $pid_out = 0
      [Win]::GetWindowThreadProcessId($h, [ref]$pid_out) | Out-Null
      $proc = Get-Process -Id $pid_out -ErrorAction SilentlyContinue
      $procName = if ($proc) { $proc.ProcessName } else { "" }
      Write-Output ($sb.ToString() + "|" + $procName + "|" + $pid_out)
    `;
        (0, child_process_1.execFile)('powershell', ['-NoProfile', '-Command', psScript], { timeout: 3000 }, (err, stdout) => {
            if (err || !stdout) {
                resolve(null);
                return;
            }
            const parts = stdout.toString().trim().split('|');
            if (parts.length < 3) {
                resolve(null);
                return;
            }
            resolve({
                title: parts[0],
                processName: parts[1],
                pid: parseInt(parts[2], 10) || 0,
            });
        });
    });
}
function getActiveWindowMac() {
    return new Promise((resolve) => {
        (0, child_process_1.execFile)('osascript', ['-e',
            'tell application "System Events" to get {name of first application process whose frontmost is true, name of front window of first application process whose frontmost is true}',
        ], { timeout: 2000 }, (err, stdout) => {
            if (err || !stdout) {
                resolve(null);
                return;
            }
            const s = stdout.toString().trim();
            // Output: "processName, windowName" — AppleScript returns comma-separated
            const idx = s.indexOf(',');
            if (idx < 0) {
                resolve({ title: s, processName: s, pid: 0 });
                return;
            }
            resolve({
                title: s.slice(idx + 1).trim(),
                processName: s.slice(0, idx).trim(),
                pid: 0,
            });
        });
    });
}
function getActiveWindowLinux() {
    return new Promise((resolve) => {
        (0, child_process_1.execFile)('xdotool', ['getactivewindow', 'getwindowname'], { timeout: 2000 }, (err, stdout) => {
            if (err || !stdout) {
                resolve(null);
                return;
            }
            (0, child_process_1.execFile)('xdotool', ['getactivewindow', 'getwindowpid'], { timeout: 1000 }, (err2, stdout2) => {
                const pid = parseInt((stdout2 || '').toString().trim(), 10) || 0;
                resolve({
                    title: stdout.toString().trim(),
                    processName: '',
                    pid,
                });
            });
        });
    });
}
function getActiveWindow() {
    const p = os.platform();
    if (p === 'win32')
        return getActiveWindowWindows();
    if (p === 'darwin')
        return getActiveWindowMac();
    return getActiveWindowLinux();
}
// ─── Window title → editor state parser ──────────────────────────────────
const EDITOR_PROCESSES = {
    'Code': 'vscode',
    'Cursor': 'cursor',
    'Cursor Helper': 'cursor',
    'Code - Insiders': 'vscode-insiders',
    'VSCodium': 'vscodium',
    'windsurf': 'windsurf',
    'WebStorm': 'webstorm',
    'IntelliJ': 'intellij',
    'Sublime_text': 'sublime',
    'notepad++': 'notepad-plus-plus',
    'notepad': 'notepad',
    'OpenCode': 'opencode',
    'antigravity': 'antigravity',
    'Antigravity IDE': 'antigravity',
};
function detectEditor(processName) {
    if (!processName)
        return 'unknown';
    // Check longer keys first so "Code - Insiders" matches "vscode-insiders"
    // before falling through to "vscode" from the "Code" key.
    const entries = Object.entries(EDITOR_PROCESSES)
        .sort((a, b) => b[0].length - a[0].length);
    for (const [key, value] of entries) {
        if (processName.toLowerCase().includes(key.toLowerCase()))
            return value;
    }
    return processName.toLowerCase();
}
const LANGUAGE_BY_EXT = {
    '.ts': 'typescript', '.tsx': 'tsx', '.js': 'javascript', '.jsx': 'jsx',
    '.py': 'python', '.rb': 'ruby', '.go': 'go', '.rs': 'rust', '.java': 'java',
    '.kt': 'kotlin', '.swift': 'swift', '.c': 'c', '.cpp': 'cpp', '.h': 'c-header',
    '.cs': 'csharp', '.php': 'php', '.html': 'html', '.css': 'css', '.scss': 'scss',
    '.json': 'json', '.yaml': 'yaml', '.yml': 'yaml', '.md': 'markdown',
    '.sql': 'sql', '.sh': 'bash', '.ps1': 'powershell', '.vue': 'vue', '.svelte': 'svelte',
};
function detectLanguage(filePath) {
    const ext = path.extname(filePath).toLowerCase();
    return LANGUAGE_BY_EXT[ext];
}
// Patterns for various editors' window titles:
//   VS Code: "filename.ts - project - Visual Studio Code"
//           "● filename.ts - project - Visual Studio Code" (unsaved)
//   Cursor: "filename.ts - project — Cursor" or "filename.ts — Cursor"
//   Windsurf: "filename.ts - project - Windsurf"
const TITLE_PATTERNS = [
    { editor: 'vscode', regex: /^([●◐○]?\s*)(.+?)\s+-\s+(.+?)\s+-\s+Visual Studio Code(?:\s*\[.*\])?$/, group: 2 },
    { editor: 'cursor', regex: /^([●◐○]?\s*)(.+?)\s+—\s+Cursor(?:\s*$|\s*\[)/, group: 2 },
    { editor: 'cursor', regex: /^([●◐○]?\s*)(.+?)\s+-\s+(.+?)\s+—\s+Cursor$/, group: 2 },
    { editor: 'windsurf', regex: /^([●◐○]?\s*)(.+?)\s+-\s+(.+?)\s+-\s+Windsurf$/, group: 2 },
];
function parseTitleToState(title, processName, project, fileBaseDir) {
    const editorName = detectEditor(processName);
    const dirtyMarkers = ['●', '◐', '○', '*'];
    const isDirty = dirtyMarkers.some(m => title.includes(m));
    const cleaned = title.replace(/[●◐○]/g, '').trim();
    for (const p of TITLE_PATTERNS) {
        const match = cleaned.match(p.regex);
        if (match) {
            const fileName = match[p.group].trim();
            if (!fileName || fileName.includes(' ')) {
                // VS Code/Cursor use the FULL path in the title for non-workspaces
                // or just the basename for workspaces. Try to resolve.
            }
            // Try to find the file in the project base dir
            let filePath = fileName;
            if (fileBaseDir && !path.isAbsolute(fileName)) {
                const candidate = path.join(fileBaseDir, fileName);
                if (fs.existsSync(candidate)) {
                    filePath = candidate;
                }
                else {
                    // Try a recursive search (capped)
                    filePath = findFileInDir(fileBaseDir, fileName, 3) || fileName;
                }
            }
            return {
                editorName,
                filePath,
                line: 1, // We can't get line/column from window title alone
                column: 1,
                isDirty,
                project,
                language: detectLanguage(filePath),
            };
        }
    }
    // Fallback: if the process is an editor and the title looks like a file path
    // (has an extension or contains a directory separator), treat it as the file
    if (editorName !== 'unknown' && cleaned) {
        const looksLikeFile = /\.[a-zA-Z0-9]{1,8}$/.test(cleaned)
            || cleaned.includes(path.sep)
            || cleaned.includes('/');
        if (looksLikeFile) {
            return {
                editorName,
                filePath: cleaned,
                line: 1,
                column: 1,
                isDirty,
                project,
                language: detectLanguage(cleaned),
            };
        }
    }
    return null;
}
function findFileInDir(dir, fileName, depth) {
    if (depth <= 0)
        return null;
    let entries;
    try {
        entries = fs.readdirSync(dir);
    }
    catch {
        return null;
    }
    for (const entry of entries) {
        if (entry === 'node_modules' || entry === '.git' || entry.startsWith('.'))
            continue;
        const full = path.join(dir, entry);
        let stat;
        try {
            stat = fs.statSync(full);
        }
        catch {
            continue;
        }
        if (stat.isFile() && entry === fileName)
            return full;
        if (stat.isDirectory()) {
            const found = findFileInDir(full, fileName, depth - 1);
            if (found)
                return found;
        }
    }
    return null;
}
// ─── TCP client to push state to the bridge ──────────────────────────────
class BridgeClient {
    socket = null;
    buffer = '';
    connected = false;
    retryTimer = null;
    connectResolvers = [];
    host;
    port;
    constructor(host = '127.0.0.1', port = 9877) {
        this.host = host;
        this.port = port;
    }
    connect() {
        if (this.socket)
            return;
        this.socket = new net.Socket();
        this.socket.on('error', () => {
            this.cleanup();
            this.scheduleRetry();
        });
        this.socket.on('close', () => {
            this.connected = false;
            this.scheduleRetry();
        });
        this.socket.on('data', (chunk) => {
            this.buffer += chunk.toString('utf8');
            // We don't really care about responses for editor_state — just drain.
            const idx = this.buffer.indexOf('\n');
            if (idx >= 0)
                this.buffer = this.buffer.slice(idx + 1);
        });
        this.socket.connect(this.port, this.host, () => {
            this.connected = true;
            const toResolve = this.connectResolvers.splice(0);
            for (const r of toResolve)
                r();
        });
    }
    whenConnected(timeoutMs = 5000) {
        if (this.connected)
            return Promise.resolve(true);
        return new Promise((resolve) => {
            const timer = setTimeout(() => {
                const idx = this.connectResolvers.indexOf(onConnect);
                if (idx >= 0)
                    this.connectResolvers.splice(idx, 1);
                resolve(this.connected);
            }, timeoutMs);
            const onConnect = () => { clearTimeout(timer); resolve(true); };
            this.connectResolvers.push(onConnect);
        });
    }
    cleanup() {
        if (this.socket) {
            try {
                this.socket.destroy();
            }
            catch { /* ignore */ }
            this.socket = null;
        }
        this.connected = false;
    }
    scheduleRetry() {
        if (this.retryTimer)
            return;
        this.retryTimer = setTimeout(() => {
            this.retryTimer = null;
            this.connect();
        }, 2000);
    }
    pushState(state) {
        if (!this.connected || !this.socket)
            return false;
        try {
            this.socket.write(JSON.stringify({
                type: 'editor_state',
                state: { ...state, timestamp: new Date().toISOString() },
            }) + '\n');
            return true;
        }
        catch {
            return false;
        }
    }
    stop() {
        if (this.retryTimer) {
            clearTimeout(this.retryTimer);
            this.retryTimer = null;
        }
        this.cleanup();
    }
    isConnected() { return this.connected; }
}
exports.BridgeClient = BridgeClient;
class EditorMonitor {
    opts;
    client;
    timer = null;
    fileWatcher = null;
    lastState = null;
    running = false;
    startTime = null;
    pollsCompleted = 0;
    statesDetected = 0;
    statesPushed = 0;
    constructor(opts) {
        this.opts = {
            project: opts.project,
            intervalMs: opts.intervalMs ?? 3000,
            bridgeHost: opts.bridgeHost ?? '127.0.0.1',
            bridgePort: opts.bridgePort ?? 9877,
            fileBaseDir: opts.fileBaseDir ?? process.cwd(),
            watchFiles: opts.watchFiles ?? true,
            onState: opts.onState ?? (() => { }),
            onError: opts.onError ?? (() => { }),
            onSkip: opts.onSkip ?? (() => { }),
        };
        this.client = new BridgeClient(this.opts.bridgeHost, this.opts.bridgePort);
    }
    start() {
        if (this.running)
            return;
        this.running = true;
        this.startTime = new Date().toISOString();
        this.client.connect();
        this.timer = setInterval(() => this.poll().catch(() => { }), this.opts.intervalMs);
        if (this.opts.watchFiles && fs.existsSync(this.opts.fileBaseDir)) {
            try {
                this.fileWatcher = fs.watch(this.opts.fileBaseDir, { recursive: false }, (event, filename) => {
                    if (filename && this.lastState && filename === path.basename(this.lastState.filePath)) {
                        this.opts.onSkip('file changed: ' + filename);
                    }
                });
            }
            catch { /* ignore — directory might not be watchable */ }
        }
        // First poll immediately
        this.poll().catch(() => { });
    }
    stop() {
        this.running = false;
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = null;
        }
        if (this.fileWatcher) {
            try {
                this.fileWatcher.close();
            }
            catch { /* ignore */ }
            this.fileWatcher = null;
        }
        this.client.stop();
    }
    isRunning() { return this.running; }
    getInfo() {
        return {
            running: this.running,
            project: this.opts.project,
            startTime: this.startTime,
            pollsCompleted: this.pollsCompleted,
            statesDetected: this.statesDetected,
            statesPushed: this.statesPushed,
            bridgeConnected: this.client.isConnected(),
            lastState: this.lastState,
        };
    }
    async poll() {
        this.pollsCompleted++;
        try {
            const win = await getActiveWindow();
            if (!win) {
                this.opts.onSkip('no active window');
                return;
            }
            const state = parseTitleToState(win.title, win.processName, this.opts.project, this.opts.fileBaseDir);
            if (!state) {
                this.opts.onSkip(`could not parse title: "${win.title}" (process: ${win.processName})`);
                return;
            }
            this.statesDetected++;
            this.opts.onState(state);
            // Only push if changed
            const isDifferent = !this.lastState
                || this.lastState.filePath !== state.filePath
                || this.lastState.line !== state.line
                || this.lastState.column !== state.column
                || this.lastState.isDirty !== state.isDirty;
            if (isDifferent) {
                // Wait briefly for connection (up to 3s) — don't drop the first push
                await this.client.whenConnected(3000);
                if (this.client.pushState(state))
                    this.statesPushed++;
                this.lastState = state;
            }
        }
        catch (e) {
            this.opts.onError(e instanceof Error ? e : new Error(String(e)));
        }
    }
}
exports.EditorMonitor = EditorMonitor;
//# sourceMappingURL=editor-monitor.js.map