/**
 * HTTP Bridge Server — Extension Host ↔ Renderer communication.
 *
 * Extracted from extension.ts to reduce file size.
 * Provides an HTTP server that the AG renderer (DOM observer script) uses to:
 * - Report detected approval buttons (POST /pending)
 * - Poll for Discord responses (GET /response/:rid)
 * - Receive click triggers (GET /trigger-click)
 * - Deep DOM inspection (GET/POST /deep-inspect*)
 */

import * as fs from 'fs';
import * as path from 'path';
import { WSBridgeClient } from './ws-client';

let lastFilePermissionTime = 0;

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

export interface HttpBridgeContext {
    bridgePath: string;
    projectName: string;
    activeSessionId: string;
    wsBridge: WSBridgeClient | null;
    sessionStalled: boolean;
    lastPendingStepIndex: number;
    logToFile: (msg: string) => void;
    writeChatSnapshot?: (text: string) => void;
}

// ─── Module-level state ───

let observerHttpServer: any = null;
const pendingResponses = new Map<string, { approved: boolean } | null>();

// Click trigger: extension sets this, renderer polls and clicks button
let clickTrigger: { action: 'approve' | 'reject'; timestamp: number } | null = null;

// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
let deepInspectRequested = false;
let deepInspectResult: any = null;
let deepInspectWaiters: Array<(data: any) => void> = [];

// ─── Public API ───

/** Set click trigger (called from step-probe when approval needed) */
export function setClickTrigger(action: 'approve' | 'reject') {
    clickTrigger = { action, timestamp: Date.now() };
}

/** Get the HTTP bridge server instance */
export function getHttpServer(): any {
    return observerHttpServer;
}

/** Derive a deterministic port from project name (range 10000-60000) */
export function getDeterministicPort(name: string): number {
    let hash = 0;
    for (let i = 0; i < name.length; i++) {
        hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
    }
    return 10000 + (Math.abs(hash) % 50000);
}

/**
 * Start the HTTP bridge server.
 * Returns the port number (0 if failed).
 */
export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<number> {
    return new Promise((resolve) => {
        try {
            const http = require('http');
            const server = http.createServer((req: any, res: any) => {
                // CORS headers for renderer fetch()
                res.setHeader('Access-Control-Allow-Origin', '*');
                res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
                res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
                if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }

                const url = new URL(req.url, `http://127.0.0.1`);

                // DIAGNOSTIC: log ALL requests (except noisy polling endpoints)
                if (!['/trigger-click', '/deep-inspect-trigger'].includes(url.pathname)) {
                    ctx.logToFile(`[HTTP-REQ] ${req.method} ${url.pathname}`);
                }

                // POST /pending — renderer reports a detected approval button
                if (req.method === 'POST' && url.pathname === '/pending') {
                    _handlePending(req, res, ctx);
                    return;
                }

                // GET /response/:rid — renderer polls for Discord approval
                if (req.method === 'GET' && url.pathname.startsWith('/response/')) {
                    _handleGetResponse(req, res, url, ctx);
                    return;
                }

                // GET /trigger-click — renderer polls to check if extension wants a click
                if (req.method === 'GET' && url.pathname === '/trigger-click') {
                    _handleTriggerClick(res, ctx);
                    return;
                }

                // GET /deep-inspect — trigger deep DOM inspection from renderer
                if (req.method === 'GET' && url.pathname === '/deep-inspect') {
                    _handleDeepInspect(res, ctx);
                    return;
                }

                // GET /deep-inspect-trigger — renderer polls this
                if (req.method === 'GET' && url.pathname === '/deep-inspect-trigger') {
                    _handleDeepInspectTrigger(res);
                    return;
                }

                // POST /chat — renderer posts chat snapshots directly
                if (req.method === 'POST' && url.pathname === '/chat') {
                    _handleChatSnapshot(req, res, ctx);
                    return;
                }

                // POST /deep-inspect-result — renderer posts inspection results here
                if (req.method === 'POST' && url.pathname === '/deep-inspect-result') {
                    _handleDeepInspectResult(req, res, ctx);
                    return;
                }

                // POST /log — renderer relays important diagnostic logs
                if (req.method === 'POST' && url.pathname === '/log') {
                    let logBody = '';
                    req.setEncoding('utf8');
                    req.on('data', (c: string) => logBody += c);
                    req.on('end', () => {
                        try {
                            const logData = JSON.parse(logBody);
                            ctx.logToFile(`[OBSERVER-LOG] ${logData.msg || logBody.substring(0, 500)}`);
                        } catch { ctx.logToFile(`[OBSERVER-LOG] ${logBody.substring(0, 500)}`); }
                        res.writeHead(200); res.end('ok');
                    });
                    return;
                }

                if (req.method === 'POST' && url.pathname === '/dump-html') {
                    let dumpBody = '';
                    req.setEncoding('utf8');
                    req.on('data', (c: string) => dumpBody += c);
                    req.on('end', () => {
                        try {
                            // Save indexed dump for history + latest as dump_html.json
                            let idx = 1;
                            try { const parsed = JSON.parse(dumpBody); idx = parsed.dumpIndex || idx; } catch {}
                            fs.writeFileSync(path.join(ctx.bridgePath, `dump_html_${idx}.json`), dumpBody, 'utf-8');
                            fs.writeFileSync(path.join(ctx.bridgePath, 'dump_html.json'), dumpBody, 'utf-8');
                            ctx.logToFile(`[HTTP] DOM dump #${idx} saved (${dumpBody.length} bytes)`);
                        } catch (e) { }
                        res.writeHead(200); res.end('ok');
                    });
                    return;
                }

                if (req.method === 'POST' && url.pathname === '/test-rpc') {
                    let rpcBody = '';
                    req.setEncoding('utf8');
                    req.on('data', (c: string) => rpcBody += c);
                    req.on('end', async () => {
                        try {
                            const params = JSON.parse(rpcBody);
                            const result = await sdk.ls.rawRPC(params.method, params.args || {});
                            res.writeHead(200, { 'Content-Type': 'application/json' });
                            res.end(typeof result === 'string' ? result : JSON.stringify(result));
                        } catch (e: any) {
                            res.writeHead(500); res.end(e.message);
                        }
                    });
                    return;
                }

                // GET /status — diagnostic endpoint
                if (req.method === 'GET' && url.pathname === '/status') {
                    const { getStepProbeContext } = require('./step-probe');
                    const probeCtx = getStepProbeContext();
                    const status = {
                        projectName: ctx.projectName,
                        activeSessionId: probeCtx.activeSessionId || ctx.activeSessionId,
                        lastPendingStepIndex: probeCtx.lastPendingStepIndex,
                        sessionStalled: probeCtx.sessionStalled,
                        wsConnected: ctx.wsBridge?.isConnected() ?? false,
                        clickTrigger: clickTrigger ? { ...clickTrigger, ageMs: Date.now() - clickTrigger.timestamp } : null,
                        uptime: Math.round(process.uptime()),
                        timestamp: new Date().toISOString(),
                    };
                    res.writeHead(200, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify(status, null, 2));
                    return;
                }

                // GET /ping — health check
                if (url.pathname === '/ping') {
                    res.writeHead(200); res.end('pong');
                    return;
                }

                res.writeHead(404); res.end('not found');
            });

            // Listen on deterministic port (derived from projectName), fallback to random
            let detPort = getDeterministicPort(ctx.projectName);
            const tryListen = (targetPort: number) => {
                server.listen(targetPort, '127.0.0.1', () => {
                    const port = server.address().port;
                    observerHttpServer = server;
                    ctx.logToFile(`[HTTP] bridge server started on port ${port}`);

                    // Write port to shared ports JSON (multi-bridge support)
                    const patcher = (sdk.integration as any)?._patcher;
                    if (patcher && typeof patcher.getWorkbenchDir === 'function') {
                        const workbenchDir = patcher.getWorkbenchDir();
                        const portsFile = path.join(workbenchDir, 'ag-bridge-ports.json');
                        let portsData: Record<string, number> = {};
                        try {
                            if (fs.existsSync(portsFile)) {
                                portsData = JSON.parse(fs.readFileSync(portsFile, 'utf-8'));
                            }
                        } catch { }
                        portsData[ctx.projectName] = port;
                        fs.writeFileSync(portsFile, JSON.stringify(portsData), 'utf-8');
                        ctx.logToFile(`[HTTP] ports JSON updated → ${portsFile} (${ctx.projectName}=${port})`);
                    }

                    resolve(port);
                });
            };

            server.on('error', (e: any) => {
                if (e.code === 'EADDRINUSE' && detPort > 0) {
                    ctx.logToFile(`[HTTP] deterministic port ${detPort} in use, trying random...`);
                    detPort = 0;
                    const server2 = require('http').createServer(server._events.request);
                    observerHttpServer = server2;
                    server2.on('error', (e2: any) => {
                        ctx.logToFile(`[HTTP] random port also failed: ${e2.message}`);
                        resolve(0);
                    });
                    server2.listen(0, '127.0.0.1', () => {
                        const port = server2.address().port;
                        ctx.logToFile(`[HTTP] bridge server started on RANDOM port ${port}`);
                        resolve(port);
                    });
                    return;
                }
                ctx.logToFile(`[HTTP] server error: ${e.message}`);
                resolve(0);
            });

            tryListen(detPort);
        } catch (e: any) {
            ctx.logToFile(`[HTTP] server failed: ${e.message}`);
            resolve(0);
        }
    });
}

// ─── Route Handlers (private) ───

function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
    let body = '';
    req.setEncoding('utf8');
    req.on('data', (c: string) => body += c);
    req.on('end', () => {
        try {
            const data = JSON.parse(body);

            // ── v12: Command enrichment FIRST — extract actual command from description ──
            // Must run before filters so "Always run" with useful description isn't filtered out
            const rawCmd = (data.command || '').trim();
            // v15: Strip Material icon names from description BEFORE enrichment
            // DOM textContent concatenates icon text (e.g. "content_copy") without separators
            const ICON_STRIP_RE = /\b(chevron_right|chevron_left|arrow_drop_down|arrow_drop_up|arrow_right|arrow_left|arrow_forward|arrow_back|expand_more|expand_less|more_horiz|more_vert|content_copy|content_paste|check_circle|check|keyboard_arrow_up|keyboard_arrow_down|keyboard_arrow_left|keyboard_arrow_right|slow_motion_video|open_in_new|alternate_email)\b/g;
            const rawDesc = (data.description || '').replace(ICON_STRIP_RE, '').replace(/\s{2,}/g, ' ').trim();
            const GENERIC_BTN_RE = /^(?:Always\s*)?(?:Run|Allow|Accept|Approve)$/i;
            let enrichedCmd = rawCmd;
            let enrichedDesc = rawDesc;

            // v19: "Always run" auto-approve — MUST run BEFORE any filter can reject it
            // Detects from rawCmd OR from buttons array (Observer may detect sibling first)
            let alwaysRunDetected = /^Always\s+run$/i.test(rawCmd);
            let alwaysRunBtnIndex = alwaysRunDetected ? 0 : -1;
            if (!alwaysRunDetected && Array.isArray(data.buttons)) {
                for (let bi = 0; bi < data.buttons.length; bi++) {
                    if (/^Always\s+run$/i.test((data.buttons[bi].text || '').trim())) {
                        alwaysRunDetected = true;
                        alwaysRunBtnIndex = bi;
                        break;
                    }
                }
            }
            if (alwaysRunDetected) {
                // Try enrichment for better Discord display text
                let displayCmd = rawCmd;
                ctx.logToFile(`[HTTP] AUTO-APPROVE raw: cmd="${rawCmd}" desc="${rawDesc.substring(0, 120)}" buttons=${JSON.stringify((data.buttons || []).map((b: any) => b.text)).substring(0, 200)}`);
                if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 3 && rawDesc !== rawCmd) {
                    const promptMatch = rawDesc.match(/[>»]\s*(.+)/);
                    if (promptMatch && promptMatch[1].trim().length > 3) {
                        displayCmd = promptMatch[1].trim().substring(0, 200);
                    } else {
                        // v19: Fallback — use longest line from description as command text
                        const descLines = rawDesc.split(/\n/).map((l: string) => l.trim()).filter((l: string) => l.length > 3);
                        if (descLines.length > 0) {
                            descLines.sort((a: string, b: string) => b.length - a.length);
                            displayCmd = descLines[0].substring(0, 200);
                        }
                    }
                }
                // v20: Final fallback — read latest Step Probe pending file for actual command
                if (displayCmd === rawCmd && GENERIC_BTN_RE.test(displayCmd)) {
                    try {
                        const pendingDir = path.join(ctx.bridgePath, 'pending');
                        if (fs.existsSync(pendingDir)) {
                            const pFiles = fs.readdirSync(pendingDir)
                                .filter((f: string) => f.endsWith('.json'))
                                .map((f: string) => ({ name: f, time: fs.statSync(path.join(pendingDir, f)).mtimeMs }))
                                .sort((a: any, b: any) => b.time - a.time);
                            if (pFiles.length > 0 && (Date.now() - pFiles[0].time) < 30_000) {
                                const pData = JSON.parse(fs.readFileSync(path.join(pendingDir, pFiles[0].name), 'utf-8'));
                                if (pData.command && pData.command.length > 3 && !GENERIC_BTN_RE.test(pData.command)) {
                                    displayCmd = pData.command.substring(0, 200);
                                    ctx.logToFile(`[HTTP] AUTO-APPROVE enriched from pending file: "${displayCmd.substring(0, 80)}"`);
                                } else if (pData.description && pData.description.length > 5) {
                                    displayCmd = pData.description.substring(0, 200);
                                    ctx.logToFile(`[HTTP] AUTO-APPROVE enriched from pending desc: "${displayCmd.substring(0, 80)}"`);
                                }
                            }
                        }
                    } catch (e: any) { ctx.logToFile(`[HTTP] AUTO-APPROVE pending lookup error: ${e.message}`); }
                }
                const rid = data.request_id || Date.now().toString();
                ctx.logToFile(`[HTTP] AUTO-APPROVE "Always run" (btnIdx=${alwaysRunBtnIndex}): cmd="${displayCmd.substring(0, 80)}"`);
                // Write response file so observer's pollResponseGroup picks it up and clicks the button
                const responseDir = path.join(ctx.bridgePath, 'response');
                if (!fs.existsSync(responseDir)) {
                    fs.mkdirSync(responseDir, { recursive: true });
                }
                const respPayload = {
                    request_id: rid,
                    approved: true,
                    button_index: alwaysRunBtnIndex >= 0 ? alwaysRunBtnIndex : 0,
                    step_type: data.step_type || 'command',
                    project_name: ctx.projectName,
                };
                fs.writeFileSync(
                    path.join(responseDir, `${rid}.json`),
                    JSON.stringify(respPayload),
                    'utf-8'
                );
                // Notify Discord (non-interactive "자동 승인" embed)
                if (ctx.wsBridge && ctx.wsBridge.isConnected()) {
                    ctx.wsBridge.sendPending({
                        request_id: rid,
                        command: displayCmd,
                        description: rawDesc ? `[${rawCmd}] ${rawDesc}` : rawCmd,
                        step_type: data.step_type || 'command',
                        status: 'auto_approved',
                        buttons: data.buttons,
                        project_name: ctx.projectName,
                    });
                }
                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ ok: true, request_id: rid, auto_approved: true }));
                return;
            }
            if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 10 && rawDesc !== rawCmd) {
                // Extract the actual command from description (often includes terminal prompt)
                // Pattern: "…\project_name > actual_command"
                const promptMatch = rawDesc.match(/[>»]\s*(.+)/);
                if (promptMatch && promptMatch[1].trim().length > 3) {
                    const extracted = promptMatch[1].trim();
                    // v16: Validate extracted text is not just a prompt fragment or path
                    const PROMPT_ONLY_RE = /^.*[>»$#]\s*$/;
                    const TERMINAL_PROMPT_RE = /^[^\n]*\\[^\\>]+\s*[>»]\s*$/;
                    if (!PROMPT_ONLY_RE.test(extracted) && !TERMINAL_PROMPT_RE.test(extracted)) {
                        enrichedCmd = extracted.substring(0, 200);
                        enrichedDesc = `[${rawCmd}] ${rawDesc}`;
                        ctx.logToFile(`[HTTP] command enriched: "${rawCmd}" → "${enrichedCmd.substring(0, 60)}"`);
                    } else {
                        // Prompt-only extraction — filter
                        ctx.logToFile(`[HTTP] enrichment skipped (prompt-only): "${rawCmd}" desc="${rawDesc.substring(0, 60)}"`);
                        ctx.logToFile(`[HTTP] filtered generic+prompt-only: "${rawCmd}"`);
                        res.writeHead(200, { 'Content-Type': 'application/json' });
                        res.end(JSON.stringify({ ok: false, filtered: true, reason: 'generic_btn_prompt_only' }));
                        return;
                    }
                } else {
                    // v16: No prompt marker (> » $ #) found in description — this is terminal OUTPUT, not a command
                    // Observer extracted stdout text from code block (e.g. "No extension.log found", "Log found: ...")
                    ctx.logToFile(`[HTTP] filtered terminal output (no prompt marker): "${rawDesc.substring(0, 60)}"`);
                    res.writeHead(200, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify({ ok: false, filtered: true, reason: 'terminal_output' }));
                    return;
                }
            } else if (GENERIC_BTN_RE.test(rawCmd) && (rawDesc.length <= 10 || rawDesc === rawCmd)) {
                // v13: Generic button with no useful description (observer prompt-only context)
                ctx.logToFile(`[HTTP] filtered generic button no-context: "${rawCmd}" desc="${rawDesc.substring(0, 30)}"`);
                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ ok: false, filtered: true, reason: 'generic_btn_no_context' }));
                return;
            }

            // ── Server-side false positive filter (uses enriched cmd) ──
            const cmd = enrichedCmd;
            const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it|Dismiss)$/i;
            if (FALSE_POSITIVE_RE.test(cmd)) {
                ctx.logToFile(`[HTTP] filtered false positive: "${cmd}"`);
                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ ok: false, filtered: true }));
                return;
            }
            // v14: Server-side junk content filter — CSS, source code, icon glue
            // This is the last line of defense regardless of observer version
            const JUNK_CONTENT_RE = /(!important|::selection|background-color:|var\(--|font-size:|border-[a-z]+:|padding:|margin:|display:\s|===|!==|\|\||\.\btest\(|\.\bmatch\(|\.\breplace\(|_RE[.\s]|\brawDesc\b|\brawCmd\b|\benrichedCmd\b|\bquerySelector\b|\.code-block|\.code-line|\.line-content|\{\s*--|integration\.build)/;
            // v15: ICON_GLUE_RE now also catches standalone icon names (no trailing [a-zA-Z] required)
            const ICON_GLUE_RE = /\b(alternate_email|content_copy|content_paste|check_circle|chevron_right|chevron_left|keyboard_arrow|arrow_drop_down|arrow_drop_up|more_horiz|more_vert|expand_more|expand_less)\b/;
            // v15: Terminal prompt pattern — catches bare prompts like "…\project >" or "PS C:\path>"
            const BARE_PROMPT_RE = /^[^\n]{0,60}[>»$#]\s*$/;
            if (JUNK_CONTENT_RE.test(cmd) || ICON_GLUE_RE.test(cmd)) {
                ctx.logToFile(`[HTTP] filtered junk content: "${cmd.substring(0, 80)}"`);
                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ ok: false, filtered: true, reason: 'junk_content' }));
                return;
            }
            // v15: Final bare prompt filter — catches any enriched cmd that's just a terminal prompt
            if (BARE_PROMPT_RE.test(cmd) && cmd.length < 80) {
                ctx.logToFile(`[HTTP] filtered bare prompt: "${cmd.substring(0, 80)}"`);
                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ ok: false, filtered: true, reason: 'bare_prompt' }));
                return;
            }
            // "Run" button → step_probe handles these with full command detail
            // Only filter when step_probe IS actively tracking AND cmd is still generic button text
            if (/^(?:Always\s*)?Run\b/i.test(cmd)) {
                if (ctx.activeSessionId && (!ctx.sessionStalled || ctx.lastPendingStepIndex >= 0)) {
                    ctx.logToFile(`[HTTP] filtered "Run" — ${!ctx.sessionStalled ? 'not stalled' : 'step_probe pending exists'} (session=${ctx.activeSessionId.substring(0, 8)})`);
                    res.writeHead(200, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify({ ok: false, filtered: true }));
                    return;
                }
                // v9: When step_probe has no active session, let DOM observer handle approval
                ctx.logToFile(`[HTTP] allowing "Run" — step_probe has no active session`);
            }

            const rid = data.request_id || Date.now().toString();
            const pending: Record<string, any> = {
                ...data,
                request_id: rid,
                command: enrichedCmd,
                description: enrichedDesc,
                conversation_id: ctx.activeSessionId || '',
                timestamp: Date.now() / 1000,
                status: 'pending',
                project_name: ctx.projectName,
                auto_detected: true,
                source: 'dom_observer',
                step_type: data.step_type,
                buttons: data.buttons,
                step_index: ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : undefined,
            };
            // v19: "Always run" auto-approve was already handled above (before filter chain)
            // No need for duplicate check here.
            // File permission: inject multi-choice buttons
            const cmdLower = enrichedCmd.toLowerCase();
            if (cmdLower.includes('allow') && !pending.buttons) {
                // Dedup: skip if another file_permission pending was created within 10s
                const nowMs = Date.now();
                if (nowMs - lastFilePermissionTime < 10000) {
                    ctx.logToFile(`[HTTP] filtered duplicate file_permission (${nowMs - lastFilePermissionTime}ms old)`);
                    res.writeHead(200, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify({ ok: false, filtered: true, reason: 'dedup_file_permission' }));
                    return;
                }
                lastFilePermissionTime = nowMs;

                pending.buttons = [
                    { text: 'Allow Once', index: 0 },
                    { text: 'Allow This Conversation', index: 1 },
                    { text: 'Deny', index: 2 },
                ];
                pending.step_type = 'file_permission';
                // Clean description: remove button labels from text
                const cleanDesc = enrichedDesc.replace(/Deny|Allow Once|Allow This Conversation/gi, '').trim();
                pending.command = `파일 접근 권한${cleanDesc ? ': ' + cleanDesc : ''}`;
            }
            // WS dispatch
            if (ctx.wsBridge && ctx.wsBridge.isConnected()) {
                ctx.wsBridge.sendPending({
                    request_id: rid,
                    command: pending.command || data.command || '',
                    description: pending.description || data.description || '',
                    step_type: pending.step_type,
                    status: 'pending',
                    buttons: pending.buttons,
                    project_name: ctx.projectName,
                });
                ctx.logToFile(`[HTTP-WS] pending sent via WS: ${rid}`);
            }
            ctx.logToFile(`[HTTP] pending created: ${rid} cmd="${pending.command || data.command}" btns=${(pending.buttons || data.buttons || []).length} ctx="${(pending.description || data.description || '').substring(0, 80)}"`);

            if (data._debug_trail) {
                ctx.logToFile(`[HTTP-DIAG] trail: ${data._debug_trail.substring(0, 500)}`);
            }
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ ok: true, request_id: rid }));
        } catch (e: any) {
            ctx.logToFile(`[HTTP] pending error: ${e.message}`);
            res.writeHead(400); res.end(JSON.stringify({ error: e.message }));
        }
    });
}

function _handleGetResponse(_req: any, res: any, url: URL, ctx: HttpBridgeContext) {
    const rid = url.pathname.split('/')[2];
    const respFile = path.join(ctx.bridgePath, 'response', `${rid}.json`);
    if (fs.existsSync(respFile)) {
        try {
            const data = JSON.parse(fs.readFileSync(respFile, 'utf8'));
            ctx.logToFile(`[HTTP] response served to renderer: ${rid} approved=${data.approved}`);
            // Delay deletion: processResponseFile (response watcher) may need to read it too.
            // The watcher fires with 300ms delay, so 2s is safe.
            setTimeout(() => {
                try { fs.unlinkSync(respFile); } catch { }
            }, 2000);
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify(data));
        } catch {
            res.writeHead(200); res.end(JSON.stringify({ waiting: true }));
        }
    } else {
        res.writeHead(200); res.end(JSON.stringify({ waiting: true }));
    }
}

function _handleTriggerClick(res: any, ctx: HttpBridgeContext) {
    if (clickTrigger && (Date.now() - clickTrigger.timestamp) < 30000) {
        const trigger = clickTrigger;
        clickTrigger = null; // consume once
        ctx.logToFile(`[HTTP] trigger-click consumed: ${trigger.action}`);
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ action: trigger.action }));
    } else {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ action: null }));
    }
}

function _handleDeepInspect(res: any, ctx: HttpBridgeContext) {
    deepInspectRequested = true;
    ctx.logToFile('[HTTP] deep-inspect triggered — waiting for renderer result...');
    // Wait up to 10s for renderer to POST result
    const timeout = setTimeout(() => {
        deepInspectWaiters = deepInspectWaiters.filter(w => w !== waiter);
        if (deepInspectResult) {
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify(deepInspectResult));
        } else {
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ status: 'timeout', message: 'Renderer did not respond in 10s. Is the v3 script loaded?' }));
        }
    }, 10000);
    const waiter = (data: any) => {
        clearTimeout(timeout);
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(data));
    };
    deepInspectWaiters.push(waiter);
}

function _handleDeepInspectTrigger(res: any) {
    const requested = deepInspectRequested;
    deepInspectRequested = false;
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ inspect: requested }));
}

function _handleDeepInspectResult(req: any, res: any, ctx: HttpBridgeContext) {
    let body = '';
    req.setEncoding('utf8');
    req.on('data', (c: string) => body += c);
    req.on('end', () => {
        try {
            const data = JSON.parse(body);
            deepInspectResult = data;
            ctx.logToFile(`[HTTP] deep-inspect result received (${body.length} bytes)`);
            const inspectFile = path.join(ctx.bridgePath, 'deep-inspect-result.json');
            fs.writeFileSync(inspectFile, JSON.stringify(data, null, 2));
            const waiters = [...deepInspectWaiters];
            deepInspectWaiters = [];
            waiters.forEach(w => w(data));
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ ok: true }));
        } catch (e: any) {
            ctx.logToFile(`[HTTP] deep-inspect-result parse error: ${e.message}`);
            res.writeHead(400); res.end(JSON.stringify({ error: e.message }));
        }
    });
}

function _handleChatSnapshot(req: any, res: any, ctx: HttpBridgeContext) {
    let body = '';
    req.setEncoding('utf8');
    req.on('data', (c: string) => body += c);
    req.on('end', () => {
        try {
            const data = JSON.parse(body);
            if (data.text && typeof ctx.writeChatSnapshot === 'function') {
                const isUser = data.role === 'user';
                const prefix = isUser ? '🧑‍💻 **[DOM 추출] 사용자 요청**' : '💬 **[DOM 추출] AI 응답**';
                ctx.writeChatSnapshot(`${prefix}\n\n${data.text}`);
                ctx.logToFile(`[HTTP] chat snapshot written (${data.text.length} chars, role: ${data.role || 'bot'})`);
            }
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ ok: true }));
        } catch (e: any) {
            ctx.logToFile(`[HTTP] chat parse error: ${e.message}`);
            res.writeHead(400); res.end(JSON.stringify({ error: e.message }));
        }
    });
}
