/**
 * Command Handler — Discord → AG command processing.
 *
 * Extracted from extension.ts to reduce file size.
 * Handles commands from both:
 * - File-based bridge (legacy/fallback): fs.watch + polling
 * - WebSocket Hub (primary): direct callbacks
 */

import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';

// ─── Context interface (shared state from extension.ts) ───

export interface CommandHandlerContext {
    bridgePath: string;
    projectName: string;
    sdk: any;
    /** LSBridge instance for direct LS RPC calls (cancelCascade, etc.) */
    ls: any;
    autoApproveEnabled: boolean;
    logToFile: (msg: string) => void;
    /** Called when auto-approve is toggled; extension.ts updates its own state */
    onAutoApproveChanged: (enabled: boolean) => void;
    /** Track recently sent Discord→AG texts to avoid echo relay */
    recentDiscordSentTexts: Map<string, number>;
    /** Get the active cascade/session ID from step-probe polling state */
    getActiveSessionId: () => string;
}

// ─── File-based command watcher ───

let commandsWatcher: fs.FSWatcher | null = null;

/**
 * Watch the commands directory for Discord→AG command files.
 * Uses fs.watch + polling fallback (fs.watch unreliable on Windows).
 */
export function watchCommandsDir(ctx: CommandHandlerContext) {
    const cmdDir = path.join(ctx.bridgePath, 'commands');

    // Process existing files
    const processAllCommands = () => {
        try {
            for (const f of fs.readdirSync(cmdDir)) {
                if (f.endsWith('.json')) {
                    _processCommandFile(path.join(cmdDir, f), ctx);
                }
            }
        } catch { }
    };

    processAllCommands();

    // Watch for new files (may not fire reliably on Windows)
    try {
        commandsWatcher = fs.watch(cmdDir, (event, filename) => {
            if (filename && filename.endsWith('.json') && event === 'rename') {
                const fp = path.join(cmdDir, filename);
                if (fs.existsSync(fp)) {
                    setTimeout(() => _processCommandFile(fp, ctx), 200);
                }
            }
        });
    } catch { }

    // Polling fallback: fs.watch on Windows can silently fail
    setInterval(() => {
        processAllCommands();
    }, 3000);
}

/** Dispose the file watcher on deactivate */
export function disposeCommandsWatcher() {
    if (commandsWatcher) {
        commandsWatcher.close();
        commandsWatcher = null;
    }
}

// ─── WebSocket command handler ───

/**
 * Handle a command received via WebSocket Hub.
 * Same logic as file-based processing but without file I/O.
 */
export function handleWSCommand(ctx: CommandHandlerContext, data: { text?: string; action?: string; project_name?: string }) {
    const text = data.text || '';
    if (!text) return;

    // Project filtering (WS already routes by project, but double-check)
    if (data.project_name && data.project_name !== ctx.projectName) {
        ctx.logToFile(`[WS-CMD] Ignoring command for ${data.project_name} (we are ${ctx.projectName})`);
        return;
    }

    if (text === '!stop') {
        ctx.logToFile('[WS-CMD] !stop — cancelling AG task');
        _cancelCurrentCascade(ctx);
        return;
    }

    if (text.startsWith('!auto')) {
        const parts = text.split(' ');
        const enabled = parts[1] !== 'off';
        ctx.onAutoApproveChanged(enabled);
        ctx.logToFile(`[WS-CMD] auto_approve=${enabled}`);
        return;
    }

    // General text → send as user message to AG
    ctx.logToFile(`[WS-CMD] Sending text to AG: ${text.substring(0, 80)}`);
    // Mark for echo-dedup: step-probe will skip relaying this back to Discord
    ctx.recentDiscordSentTexts.set(text.trim(), Date.now());
    if (ctx.sdk) {
        try {
            ctx.sdk.cascade.sendPrompt(text);
        } catch (e: any) {
            ctx.logToFile(`[WS-CMD] SDK sendPrompt error: ${e.message}`);
        }
    }
}

// ─── Private ───

/**
 * Cancel the currently active cascade via CancelCascadeInvocation RPC.
 * This is the same mechanism AG's native red ■ stop button uses.
 */
async function _cancelCurrentCascade(ctx: CommandHandlerContext) {
    // 1. Get the active cascade ID from extension state (step-probe polling)
    //    NOTE: ctx.sdk.titles.getActiveCascadeId() is renderer-only (DOM scraping)
    //    and always returns undefined from extension host. Use activeSessionId instead.
    const cascadeId = ctx.getActiveSessionId();
    if (!cascadeId) {
        ctx.logToFile('[STOP] No active cascade — no session tracked yet');
        return;
    }

    ctx.logToFile(`[STOP] Cancelling cascade: ${cascadeId.substring(0, 12)}...`);

    // 2. Use LSBridge.cancelCascade() → CancelCascadeInvocation RPC
    if (ctx.ls) {
        try {
            await ctx.ls.cancelCascade(cascadeId);
            ctx.logToFile(`[STOP] ✅ CancelCascadeInvocation sent for ${cascadeId.substring(0, 12)}`);
            return;
        } catch (e: any) {
            ctx.logToFile(`[STOP] LSBridge cancelCascade failed: ${e.message}`);
        }
    }

    // 3. Fallback: try rawRPC directly via sdk.ls
    if (ctx.sdk?.ls?.rawRPC) {
        try {
            await ctx.sdk.ls.rawRPC('CancelCascadeInvocation', { cascadeId });
            ctx.logToFile(`[STOP] ✅ rawRPC CancelCascadeInvocation sent for ${cascadeId.substring(0, 12)}`);
        } catch (e: any) {
            ctx.logToFile(`[STOP] rawRPC fallback also failed: ${e.message}`);
        }
    }
}

function _processCommandFile(filePath: string, ctx: CommandHandlerContext) {
    try {
        const content = fs.readFileSync(filePath, 'utf-8');
        const cmd = JSON.parse(content);

        // Skip already consumed commands
        if (cmd.consumed) {
            try { fs.unlinkSync(filePath); } catch { }
            return;
        }

        // Ignore commands for other projects
        if (cmd.project_name && cmd.project_name !== ctx.projectName) {
            console.log(`Gravity Bridge: skipping command for "${cmd.project_name}" (we are "${ctx.projectName}")`);
            return;
        }

        // Bot writes 'text' field, not 'message'
        const text = cmd.text || cmd.message || '';
        const action = cmd.action || '';

        console.log(`Gravity Bridge: command — text="${text}" action="${action}"`);

        if (action === 'approve' && ctx.sdk) {
            ctx.sdk.cascade.acceptStep().catch((e: any) =>
                console.log(`Gravity Bridge: approve error: ${e.message}`)
            );
        } else if (action === 'reject' && ctx.sdk) {
            ctx.sdk.cascade.rejectStep().catch((e: any) =>
                console.log(`Gravity Bridge: reject error: ${e.message}`)
            );
        } else if (action === 'approve_terminal' && ctx.sdk) {
            ctx.sdk.cascade.acceptTerminalCommand().catch((e: any) =>
                console.log(`Gravity Bridge: approve_terminal error: ${e.message}`)
            );
        } else if (text === '!stop') {
            // Cancel current operation — use CancelCascadeInvocation RPC (same as AG's red ■ button)
            _cancelCurrentCascade(ctx);
        } else if (text.startsWith('!auto')) {
            // Auto-approve mode toggle
            let enabled: boolean;
            if (text === '!auto on') {
                enabled = true;
            } else if (text === '!auto off') {
                enabled = false;
            } else {
                // Toggle if no explicit on/off
                enabled = !ctx.autoApproveEnabled;
            }
            ctx.onAutoApproveChanged(enabled);
            ctx.logToFile(`[AUTO] auto-approve toggled → ${enabled}`);
        } else if (text) {
            // Send message to Antigravity — use VS Code command (most reliable)
            ctx.recentDiscordSentTexts.set(text.trim(), Date.now());
            vscode.commands.executeCommand('antigravity.sendPromptToAgentPanel', text)
                .then(() => console.log(`Gravity Bridge: ✅ sent "${text.substring(0, 50)}" via sendPromptToAgentPanel`),
                    (e: any) => console.log(`Gravity Bridge: sendPrompt failed: ${e.message}`));
        }

        // Remove processed command file
        try { fs.unlinkSync(filePath); } catch { }
    } catch (e: any) {
        console.log(`Gravity Bridge: command processing error: ${e.message}`);
    }
}
