import JavaScriptObfuscator from 'javascript-obfuscator';
import { minify } from 'terser';
/**
 * JS 代码混淆保护插件
 *
 * 使用 javascript-obfuscator 对 HTML 中所有内联 <script> 标签的 JS 代码
 * 进行深度混淆，包括：
 * - 变量名混淆
 * - 字符串混淆（字面量转为十六进制/unicode 编码）
 * - 控制流平坦化
 * - 死代码注入
 *
 * 混淆后的代码在浏览器中无法直接阅读理解。
 *
 * 注意：这个插件必须在 vite-plugin-singlefile 之后执行，
 * 确保所有资源已内联到 HTML 中。
 */
export function viteCompressJSPlugin(options) {
    if (!options.enabled) {
        return null;
    }
    return {
        name: 'vite-plugin-compress-js',
        enforce: 'post',
        async generateBundle(_outputOptions, bundle) {
            for (const [fileName, asset] of Object.entries(bundle)) {
                if (!fileName.endsWith('.html'))
                    continue;
                if (asset.type !== 'asset')
                    continue;
                const html = typeof asset.source === 'string'
                    ? asset.source
                    : new TextDecoder().decode(asset.source);
                console.log('[CompressJS] 开始混淆 HTML 中的 JS 代码...');
                const result = await obfuscateAllInlineScripts(html);
                if (result.obfuscatedCount > 0) {
                    asset.source = result.html;
                    console.log(`[CompressJS] 混淆完成: ${result.obfuscatedCount} 个脚本块`);
                    console.log(`[CompressJS] 原始大小: ${formatBytes(result.originalSize)}`);
                    console.log(`[CompressJS] 混淆后大小: ${formatBytes(result.obfuscatedSize)}`);
                }
                else {
                    console.log('[CompressJS] 未找到需要混淆的脚本块');
                }
            }
        },
    };
}
/**
 * 检测一个 <script> 标签内容是否是 lz4.js 运行时
 */
function isLz4Runtime(content) {
    return content.includes('window.lz4') && content.includes('window.Buffer') && content.includes('lz4js');
}
/**
 * 检测一个 <script> 标签内容是否是 LZ4 自解压 bootstrap（由 compressEngine 生成）
 */
function isLz4Bootstrap(content) {
    return content.includes('lz4.decompress') && content.includes('new Buffer(') && content.includes('textContent');
}
/**
 * 混淆 HTML 中所有内联 <script> 标签的 JS 代码
 */
async function obfuscateAllInlineScripts(html) {
    // 阶段 1：收集所有需要混淆的脚本块及其位置
    const scriptRegex = /<script(?:\s[^>]*)?>([\s\S]*?)<\/script>/gi;
    const entries = [];
    let match;
    while ((match = scriptRegex.exec(html)) !== null) {
        const fullMatch = match[0];
        const content = match[1];
        const tagAttributes = fullMatch.substring(0, fullMatch.indexOf('>'));
        let skip = false;
        // 跳过有 src 属性的外部脚本
        if (/\bsrc\s*=/i.test(tagAttributes))
            skip = true;
        // 跳过 type 不是 javascript 的
        if (!skip) {
            const typeMatch = tagAttributes.match(/\btype\s*=\s*["']([^"']+)["']/i);
            if (typeMatch) {
                const type = typeMatch[1].toLowerCase();
                if (type !== 'text/javascript' && type !== 'application/javascript' && type !== '') {
                    skip = true;
                }
            }
        }
        // 跳过空脚本
        if (!skip && !content.trim())
            skip = true;
        // 跳过 lz4.js 运行时和 bootstrap
        if (!skip && (isLz4Runtime(content) || isLz4Bootstrap(content)))
            skip = true;
        // 跳过太短的脚本
        if (!skip && content.trim().length < 100)
            skip = true;
        entries.push({
            fullMatch,
            content,
            index: match.index,
            skip,
            replacement: fullMatch,
        });
    }
    // 阶段 2：混淆 + 二次压缩（async）
    let obfuscatedCount = 0;
    let originalSize = 0;
    let obfuscatedSize = 0;
    for (const entry of entries) {
        if (entry.skip)
            continue;
        try {
            const originalLen = Buffer.from(entry.content).length;
            originalSize += originalLen;
            console.log(`[CompressJS] 混淆脚本 (${formatBytes(originalLen)})...`);
            // javascript-obfuscator：标识符混淆（关闭 stringArray 避免体积膨胀和运行时冲突）
            const obfuscatedRaw = JavaScriptObfuscator.obfuscate(entry.content, {
                compact: true,
                controlFlowFlattening: false,
                deadCodeInjection: false,
                identifierNamesGenerator: 'hexadecimal',
                stringArray: false,
                splitStrings: false,
                numbersToExpressions: false,
                transformObjectKeys: false,
                unicodeEscapeSequence: false,
                disableConsoleOutput: false,
                selfDefending: false,
                target: 'browser',
                // ========== 保留名单 ==========
                reservedNames: [
                    '^pc$', '^app$', '^entity$', '^pcBootstrap$',
                    '^ASSET_PREFIX$', '^SCRIPT_PREFIX$', '^SCENE_PATH$',
                    '^CONTEXT_OPTIONS$', '^SCRIPTS$', '^INPUT_SETTINGS$',
                    '^PRELOAD_MODULES$', '^CONFIG_FILENAME$',
                    '^PlayCraftCTA$', '^jump2AppStore$', '^loadModules$',
                    '^__esmScriptClasses$', '^__esmScriptSchemas$',
                    '^__deferredESMScripts$', '^__pendingScriptRegistrations$',
                    '^Application$', '^Entity$', '^ScriptHandler$',
                    '^ScriptType$', '^Http$', '^Asset$', '^AssetRegistry$',
                    '^GraphNode$', '^Vec2$', '^Vec3$', '^Vec4$', '^Quat$',
                    '^Mat4$', '^Color$', '^Curve$', '^CurveSet$',
                    '^BoundingBox$', '^BoundingSphere$', '^Ray$',
                    '^Texture$', '^Material$', '^StandardMaterial$',
                    '^Mesh$', '^MeshInstance$', '^Model$',
                    '^AnimComponent$', '^RigidBodyComponent$',
                    '^CollisionComponent$', '^ElementComponent$',
                    '^SpriteComponent$', '^ParticleSystemComponent$',
                    '^ScreenComponent$', '^LayoutGroupComponent$',
                    '^ScrollViewComponent$', '^ButtonComponent$',
                    '^CameraComponent$', '^LightComponent$',
                    '^RenderComponent$', '^SoundComponent$',
                    '^ScriptComponent$',
                    '^GameplaySystem$', '^GameRule$',
                    '^FbPlayableAd$', '^mraid$', '^dapi$', '^ExitApi$',
                    '^BGY_MRAID$', '^snapchatCta$', '^openAppStore$',
                    '^install$', '^TJ_API$', '^NUC$', '^smxTracking$',
                ],
            }).getObfuscatedCode();
            // Terser 二次压缩：只做 compress，不做 mangle
            let obfuscated = obfuscatedRaw;
            try {
                const terserResult = await minify(obfuscatedRaw, {
                    compress: {
                        passes: 2,
                        drop_console: true,
                        drop_debugger: true,
                    },
                    mangle: false,
                    format: {
                        comments: false,
                    },
                });
                if (terserResult.code) {
                    obfuscated = terserResult.code;
                    console.log(`[CompressJS] 二次压缩: ${formatBytes(Buffer.from(obfuscatedRaw).length)} → ${formatBytes(Buffer.from(obfuscated).length)}`);
                }
            }
            catch (e) {
                console.warn(`[CompressJS] 二次压缩失败，使用混淆后原始代码: ${e.message}`);
            }
            const obfuscatedLen = Buffer.from(obfuscated).length;
            obfuscatedSize += obfuscatedLen;
            obfuscatedCount++;
            const tagOpen = entry.fullMatch.substring(0, entry.fullMatch.indexOf('>') + 1);
            entry.replacement = `${tagOpen}${obfuscated}</script>`;
        }
        catch (error) {
            console.warn(`[CompressJS] 混淆脚本失败: ${error.message}`);
        }
    }
    // 阶段 3：从后往前替换，避免偏移量变化
    let newHtml = html;
    for (let i = entries.length - 1; i >= 0; i--) {
        const entry = entries[i];
        if (entry.skip)
            continue;
        newHtml = newHtml.substring(0, entry.index) + entry.replacement + newHtml.substring(entry.index + entry.fullMatch.length);
    }
    return {
        html: newHtml,
        obfuscatedCount,
        originalSize,
        obfuscatedSize,
    };
}
function formatBytes(bytes) {
    if (bytes === 0)
        return '0 B';
    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
}
