/**
 * HTML Patcher — AG workbench HTML patching + product.json checksum update.
 *
 * Extracted from extension.ts to reduce file size.
 * Handles:
 * - Injecting the approval observer script into AG's workbench HTML files
 * - Patching CSP to allow inline scripts
 * - Managing .orig backups and .patch-lock
 * - Updating product.json checksums for vscode-file:// protocol
 */

import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { generateApprovalObserverScript } from './observer-script';

// ─── Types ───

interface HtmlFileSpec {
    name: string;
    requiredMarker: string;
    requiredScript: string;
}

// ─── Public API ───

/**
 * Set up the approval observer by patching AG's workbench HTML files
 * with an inline script that monitors DOM for approval buttons.
 */
export async function setupApprovalObserver(
    sdk: any,
    bridgePort: number,
    logToFile: (msg: string) => void,
): Promise<void> {
    if (!sdk) { logToFile('[OBSERVER] no SDK'); return; }
    try {
        const integration = sdk.integration;
        if (!integration) { logToFile('[OBSERVER] sdk.integration unavailable'); return; }

        // 1. Register a TOP_BAR button so build() works
        try {
            integration.register({
                id: 'gravity_bridge_status',
                point: 'topBar',
                icon: '🌉',
                tooltip: 'Gravity Bridge Active',
            });
        } catch { /* already registered */ }

        // 2. Write renderer script with HTTP fetch() approach
        const observerJS = generateApprovalObserverScript(bridgePort);
        const patcher = (integration as any)._patcher;
        logToFile(`[OBSERVER-DEBUG] patcher type: ${typeof patcher}, has getScriptPath: ${patcher && typeof patcher.getScriptPath === 'function'}`);
        if (patcher && typeof patcher.getScriptPath === 'function') {
            let baseScript = '';
            try { baseScript = integration.build(); } catch { baseScript = ''; }
            // Strip old Gravity Bridge observer IIFE from baseScript.
            // integration.build() caches the previous session's script, so without
            // stripping, the old observer (e.g. v12) runs alongside the new one (v13),
            // and the old one wins because it executes first.
            if (baseScript.includes('Gravity Bridge v')) {
                const oldVer = baseScript.match(/Gravity Bridge v(\d+)/);
                const newVer = observerJS.match(/Gravity Bridge v(\d+)/);
                logToFile(`[OBSERVER] baseScript contains old observer ${oldVer?.[0] || '?'}, new is ${newVer?.[0] || '?'} — stripping old`);
                // Remove the old observer IIFE: starts with "// ── Gravity Bridge" comment, ends at the last "})();" before EOF or next section
                baseScript = baseScript.replace(/\n?\/\/\s*[─═].*Gravity Bridge[\s\S]*$/, '');
            }
            const combinedScript = baseScript + '\n' + observerJS;
            const scriptPath = patcher.getScriptPath();
            fs.writeFileSync(scriptPath, combinedScript, 'utf8');
            logToFile(`[OBSERVER] script written → ${scriptPath} (port=${bridgePort})`);
            if (!integration.isInstalled()) {
                patcher.install(combinedScript);
                logToFile('[OBSERVER] patcher.install() called (needs reload)');
            }

            // Patch BOTH HTML files with inline script injection.
            const scriptDir = path.dirname(scriptPath);
            _patchHtmlFiles(scriptDir, combinedScript, logToFile);
        }

        // 3. Update product.json checksums
        updateProductChecksums(sdk, logToFile);

        try { integration.enableAutoRepair(); } catch { }
        setInterval(() => { try { integration.signalActive(); } catch { } }, 30_000);

        logToFile(`[OBSERVER] setup complete (HTTP bridge on port ${bridgePort})`);
        console.log(`Gravity Bridge: ✅ Approval observer installed (port ${bridgePort})`);
    } catch (err: any) {
        logToFile(`[OBSERVER] setup error: ${err.message}`);
    }
}

/**
 * Update product.json checksums so vscode-file:// serves our patched files.
 * Without valid checksums, Electron serves the ORIGINAL cached version.
 */
export function updateProductChecksums(sdk: any, logToFile: (msg: string) => void): void {
    try {
        // Find product.json (2 levels up from workbench dir: resources/app/product.json)
        const patcher = (sdk?.integration as any)?._patcher;
        if (!patcher || typeof patcher.getWorkbenchDir !== 'function') {
            logToFile('[CHECKSUM] no patcher/workbenchDir — skipping');
            return;
        }
        const workbenchDir = patcher.getWorkbenchDir();
        const appDir = path.resolve(workbenchDir, '..', '..', '..', '..', '..');
        const productJsonPath = path.join(appDir, 'product.json');

        if (!fs.existsSync(productJsonPath)) {
            logToFile(`[CHECKSUM] product.json not found at ${productJsonPath}`);
            return;
        }

        // Read product.json (may have BOM)
        let raw = fs.readFileSync(productJsonPath, 'utf8');
        if (raw.charCodeAt(0) === 0xFEFF) raw = raw.substring(1);
        const product = JSON.parse(raw);

        if (!product.checksums) {
            logToFile('[CHECKSUM] no checksums section in product.json');
            return;
        }

        // Files we may modify or create (relative key in product.json → absolute path)
        const filesToCheck: Record<string, string> = {
            'vs/code/electron-browser/workbench/workbench.html': path.join(workbenchDir, 'workbench.html'),
            'vs/code/electron-browser/workbench/workbench-jetski-agent.html': path.join(workbenchDir, 'workbench-jetski-agent.html'),
            'vs/code/electron-browser/workbench/ag-sdk-variet-gravity-bridge.js': path.join(workbenchDir, 'ag-sdk-variet-gravity-bridge.js'),
        };

        let updated = false;
        for (const [key, filePath] of Object.entries(filesToCheck)) {
            if (!fs.existsSync(filePath)) continue;

            const fileBytes = fs.readFileSync(filePath);
            const hash = crypto.createHash('sha256').update(fileBytes).digest('base64').replace(/=+$/, '');

            if (product.checksums[key] && product.checksums[key] !== hash) {
                logToFile(`[CHECKSUM] updating ${key}: ${product.checksums[key].substring(0, 12)}... → ${hash.substring(0, 12)}...`);
                product.checksums[key] = hash;
                updated = true;
            } else if (!product.checksums[key]) {
                logToFile(`[CHECKSUM] adding ${key}: → ${hash.substring(0, 12)}...`);
                product.checksums[key] = hash;
                updated = true;
            }
        }

        if (updated) {
            fs.writeFileSync(productJsonPath, JSON.stringify(product, null, '\t'), 'utf8');
            logToFile('[CHECKSUM] product.json updated ✅');
        } else {
            logToFile('[CHECKSUM] all checksums already match ✅');
        }
    } catch (e: any) {
        logToFile(`[CHECKSUM] error: ${e.message}`);
    }
}

// ─── Private ───

/**
 * Patch both AG workbench HTML files with inline observer script.
 * CRITICAL: vscode-file:// does NOT serve custom .js files (silent 404),
 * so we MUST inline the script directly into BOTH HTML files.
 */
function _patchHtmlFiles(scriptDir: string, combinedScript: string, logToFile: (msg: string) => void): void {
    // Each HTML file has DIFFERENT CSS/JS entry points — they are NOT interchangeable
    const htmlFileSpecs: HtmlFileSpec[] = [
        {
            name: 'workbench.html',
            requiredMarker: 'workbench.desktop.main.css',
            requiredScript: 'workbench.js',
        },
        {
            name: 'workbench-jetski-agent.html',
            requiredMarker: 'jetskiMain.tailwind.css',
            requiredScript: 'jetskiAgent.js',
        },
    ];

    // ── File lock to prevent multi-instance HTML patching race ──
    const lockFile = path.join(scriptDir, '.patch-lock');
    let lockAcquired = false;
    try {
        if (fs.existsSync(lockFile)) {
            const lockAge = Date.now() - fs.statSync(lockFile).mtimeMs;
            if (lockAge < 30_000) {
                logToFile(`[OBSERVER] another instance is patching (lock age=${Math.round(lockAge / 1000)}s) — skipping`);
                return;
            }
            logToFile(`[OBSERVER] stale lock (age=${Math.round(lockAge / 1000)}s) — force-acquiring`);
        }
        fs.writeFileSync(lockFile, JSON.stringify({ pid: process.pid, ts: Date.now() }), 'utf-8');
        lockAcquired = true;
    } catch (lockErr: any) {
        logToFile(`[OBSERVER] lock acquire error: ${lockErr.message} — proceeding anyway`);
    }

    for (const spec of htmlFileSpecs) {
        const htmlPath = path.join(scriptDir, spec.name);
        const backupPath = htmlPath + '.orig';
        try {
            if (!fs.existsSync(htmlPath)) {
                logToFile(`[OBSERVER] ${spec.name} not found — skipping`);
                continue;
            }
            let html = fs.readFileSync(htmlPath, 'utf8');

            // ── BACKUP: Save original before first-ever patch ──
            if (!fs.existsSync(backupPath)
                && html.length >= 500
                && html.includes('<!DOCTYPE html>')
                && html.includes(spec.requiredMarker)) {
                fs.writeFileSync(backupPath, html, 'utf8');
                logToFile(`[OBSERVER] ${spec.name} backed up to .orig (${html.length} bytes)`);
            }

            // ── SAFETY: Refuse to patch if file is corrupt, empty, or wrong type ──
            const isCorrupt = html.length < 500 || !html.includes('<!DOCTYPE html>');
            const isWrongType = !isCorrupt && !html.includes(spec.requiredMarker);

            if (isCorrupt || isWrongType) {
                const reason = isCorrupt
                    ? `corrupt/empty (${html.length} bytes)`
                    : `wrong type (missing ${spec.requiredMarker})`;
                logToFile(`[OBSERVER] ${spec.name} detected ${reason}`);

                // Try to restore from backup
                if (fs.existsSync(backupPath)) {
                    const backup = fs.readFileSync(backupPath, 'utf8');
                    if (backup.length >= 500
                        && backup.includes('<!DOCTYPE html>')
                        && backup.includes(spec.requiredMarker)) {
                        fs.writeFileSync(htmlPath, backup, 'utf8');
                        html = backup;
                        logToFile(`[OBSERVER] ${spec.name} RESTORED from .orig backup (${backup.length} bytes) ✅`);
                    } else {
                        logToFile(`[OBSERVER] ${spec.name} .orig backup also invalid — SKIPPING`);
                        continue;
                    }
                } else {
                    logToFile(`[OBSERVER] ${spec.name} no .orig backup available — SKIPPING to prevent further damage`);
                    continue;
                }
            }

            // CRITICAL: Patch CSP to allow inline scripts.
            if (html.includes('script-src') && !html.match(/script-src[^;]*'unsafe-inline'/)) {
                html = html.replace(
                    /(script-src\s[^;]*?)('self')/,
                    "$1$2\n\t\t\t\t\t'unsafe-inline'"
                );
                logToFile(`[OBSERVER] ${spec.name} CSP patched: added 'unsafe-inline' to script-src`);
            }

            // CRITICAL: Patch CSP connect-src to allow HTTP bridge requests from the webview
            // In Tailwind UI, connect-src is either missing (defaults to 'none') or strict.
            if (!html.includes('connect-src') && html.includes('default-src')) {
                html = html.replace(
                    /(default-src\s+'none'\s*;)/,
                    "$1\n\t\t\t\tconnect-src\n\t\t\t\t\t'self'\n\t\t\t\t\thttp://127.0.0.1:*\n\t\t\t\t\thttps://127.0.0.1:*\n\t\t\t\t\twss://127.0.0.1:*\n\t\t\t\t;"
                );
                logToFile(`[OBSERVER] ${spec.name} CSP patched: injected connect-src for localhost API`);
            } else if (html.includes('connect-src') && !html.match(/connect-src[^;]*127\.0\.0\.1/)) {
                html = html.replace(
                    /(connect-src\s[^;]*?)('self'|vscode-remote-resource:|[a-z-]+:)/i,
                    "$1$2\n\t\t\t\t\thttp://127.0.0.1:*\n\t\t\t\t\thttps://127.0.0.1:*"
                );
                logToFile(`[OBSERVER] ${spec.name} CSP patched: added localhost to existing connect-src`);
            }

            // Remove old external script tag if present (legacy, cannot be served)
            const extMarkerStart = '<!-- AG SDK [variet-gravity-bridge] -->';
            const extMarkerEnd = '<!-- /AG SDK [variet-gravity-bridge] -->';
            if (html.includes(extMarkerStart)) {
                const extRe = new RegExp(
                    '\\n?' + extMarkerStart.replace(/[[\]]/g, '\\$&') +
                    '[\\s\\S]*?' +
                    extMarkerEnd.replace(/[[\]]/g, '\\$&') + '\\n?'
                );
                html = html.replace(extRe, '');
                logToFile(`[OBSERVER] removed external script tag from ${spec.name}`);
            }

            // Insert or update inline script — MUST be BEFORE </body> for Electron execution
            const inlineMarkerStart = '<!-- AG SDK INLINE [variet-gravity-bridge] -->';
            const inlineMarkerEnd = '<!-- /AG SDK INLINE [variet-gravity-bridge] -->';
            const inlineBlock = `${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}`;

            if (html.includes(inlineMarkerStart)) {
                // Remove existing block (may be in wrong position, e.g. after </body>)
                const re = new RegExp(
                    '\\n?' + inlineMarkerStart.replace(/[[\]]/g, '\\$&') +
                    '[\\s\\S]*?' +
                    inlineMarkerEnd.replace(/[[\]]/g, '\\$&') + '\\n?'
                );
                html = html.replace(re, '');
                // Remove duplicate </html> if present (from previous bad insertions)
                html = html.replace(/(<\/html>\s*){2,}/gi, '</html>\n');
                logToFile(`[OBSERVER] ${spec.name} removed old inline script block`);
            }
            // Insert BEFORE </body> (not </html>) to ensure Electron executes it
            // CRITICAL: Escape $ in inlineBlock to prevent String.replace() special patterns
            // ($' = text after match, $& = matched text, etc.) which corrupt the JS code.
            const safeInlineBlock = inlineBlock.replace(/\$/g, '$$$$');
            if (html.includes('</body>')) {
                html = html.replace('</body>',
                    `\n${safeInlineBlock}\n</body>`);
                logToFile(`[OBSERVER] ${spec.name} inline script INSERTED before </body>`);
            } else {
                // Fallback: insert before </html>
                html = html.replace('</html>',
                    `\n${safeInlineBlock}\n</html>`);
                logToFile(`[OBSERVER] ${spec.name} inline script INSERTED before </html> (fallback)`);
            }
            // SAFETY: Final validation before write
            if (html.length < 500 || !html.includes('<!DOCTYPE html>') || !html.includes(spec.requiredMarker)) {
                logToFile(`[OBSERVER] ${spec.name} WOULD BE CORRUPT after patching (${html.length} bytes) — ABORTING write`);
                continue;
            }
            fs.writeFileSync(htmlPath, html, 'utf8');
        } catch (e: any) {
            logToFile(`[OBSERVER] ${spec.name} patch error: ${e.message}`);
        }
    }

    // Release patch lock
    if (lockAcquired) {
        try { fs.unlinkSync(lockFile); } catch { }
        logToFile('[OBSERVER] patch lock released');
    }
}
