import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
import { fileURLToPath } from 'url';
import archiver from 'archiver';
import { createWriteStream } from 'fs';
import { PLATFORM_CONFIGS } from './platform-configs.js';
import { minifyJS, minifyPatchCode } from '../utils/minify.js';
/**
 * 平台 Vite 插件
 * 注入平台特定代码和处理输出格式
 */
export function vitePlatformPlugin(options) {
    return {
        name: 'vite-plugin-platform',
        enforce: 'post',
        transformIndexHtml: {
            order: 'post',
            async handler(html) {
                // 使用平台适配器修改 HTML（现在是异步的）
                let output = await options.adapter.modifyHTML(html, []);
                // MRAID resize patch 仅对 ZIP 格式在此注入
                // HTML 格式已在 plugin-playcanvas.ts 的 getEnginePatchScripts 中注入（避免重复）
                if (options.mraidSupport && options.outputFormat === 'zip') {
                    output = await injectMraidResizePatch(output);
                }
                return output;
            },
        },
        async closeBundle() {
            // 防御性检查：如果 Vite 构建失败，outputDir 可能不存在，跳过后续操作避免二次错误
            const outputDirExists = await fs.access(options.outputDir).then(() => true).catch(() => false);
            if (!outputDirExists) {
                console.warn(`[vite-plugin-platform] 输出目录不存在，跳过 closeBundle: ${options.outputDir}`);
                return;
            }
            // 如果需要 ZIP 格式，在构建完成后打包
            const platformConfig = PLATFORM_CONFIGS[options.platform];
            // 仅多文件输出需要复制基础资源
            if (options.outputFormat === 'zip') {
                await copyBaseBuildAssets(options.baseBuildDir, options.outputDir);
                // 处理 __settings__.js 中的 PRELOAD_MODULES（移除 ammo 模块或将 WASM 内联）
                await processPreloadModulesForZip(options.outputDir, options);
                if (options.mraidSupport) {
                    await applyMraidSupport(options.outputDir);
                }
                if (options.externFiles?.enabled) {
                    await applyExternFiles(options.outputDir, options.externFiles);
                }
                // Google Ads 等渠道只允许特定文件类型（.css/.js/.html/.gif/.png/.jpeg/.svg）
                // 将不受支持的文件（如 .glb/.wasm/.bin/.ttf 等）内联为 base64 到 config.json 中
                if (options.inlineUnsupportedAssets) {
                    await inlineUnsupportedAssetsIntoConfig(options.outputDir);
                }
                // 对 ZIP 中的 JS 文件进行 Terser 压缩混淆
                if (options.minifyJSInZip) {
                    await minifyJSFilesInDir(options.outputDir);
                }
            }
            if (options.outputFormat === 'html') {
                await keepSingleHtml(options.outputDir, platformConfig.outputFileName);
            }
            if (options.outputFormat === 'zip') {
                // 等待 ZIP 创建完成
                await createZipOutput(options.outputDir, platformConfig.outputFileName);
            }
        },
    };
}
/**
 * 创建 ZIP 输出
 */
function createZipOutput(outDir, htmlFileName) {
    return new Promise(async (resolve, reject) => {
        const zipPath = path.join(outDir, 'playable.zip');
        const output = createWriteStream(zipPath);
        const archive = archiver('zip', { zlib: { level: 9 } });
        output.on('close', () => {
            console.log(`✅ ZIP 文件已创建: ${zipPath} (${archive.pointer()} bytes)`);
            resolve();
        });
        archive.on('error', (err) => {
            reject(err);
        });
        archive.pipe(output);
        // 添加输出目录全部内容（排除 zip 本身）
        archive.glob('**/*', {
            cwd: outDir,
            ignore: ['playable.zip'],
        });
        archive.finalize();
    });
}
async function keepSingleHtml(outDir, htmlFileName) {
    let entries = [];
    try {
        entries = await fs.readdir(outDir, { withFileTypes: true });
    }
    catch (error) {
        return;
    }
    // 如果目标文件名不是 index.html，先重命名
    if (htmlFileName !== 'index.html') {
        const indexPath = path.join(outDir, 'index.html');
        const targetPath = path.join(outDir, htmlFileName);
        try {
            await fs.rename(indexPath, targetPath);
            console.log(`[Platform] 已重命名 index.html -> ${htmlFileName}`);
        }
        catch (error) {
            // index.html 可能不存在，忽略错误
        }
    }
    // 删除除目标 HTML 外的所有文件
    await Promise.all(entries.map(async (entry) => {
        // 保留目标 HTML 文件
        if (entry.name === htmlFileName || entry.name === 'index.html') {
            // 如果已经重命名了，index.html 应该不存在了
            // 这里额外检查一下，避免重复保留
            if (entry.name === 'index.html' && htmlFileName !== 'index.html') {
                // index.html 已经被重命名，应该删除（但实际上已经不存在了）
                return;
            }
            return;
        }
        const targetPath = path.join(outDir, entry.name);
        await fs.rm(targetPath, { recursive: true, force: true });
    }));
}
async function applyExternFiles(outDir, externFiles) {
    const folderName = externFiles.folderName?.trim() || `playcraft-assets-${crypto.randomUUID()}`;
    const externalPrefix = externFiles.externalUrlPrefix?.trim().replace(/\/+$/g, '');
    const assetPrefix = externalPrefix ? `${externalPrefix}/${folderName}/` : `${folderName}/`;
    const folderPath = path.join(outDir, folderName);
    await fs.mkdir(folderPath, { recursive: true });
    const entries = await fs.readdir(outDir, { withFileTypes: true });
    for (const entry of entries) {
        if (entry.name === 'index.html' || entry.name === folderName) {
            continue;
        }
        const srcPath = path.join(outDir, entry.name);
        const destPath = path.join(folderPath, entry.name);
        await fs.rename(srcPath, destPath);
    }
    // config.json 也移入资产文件夹，最外层只保留 index.html + 资产文件夹
    // CONFIG_FILENAME 需要加上资产文件夹前缀，引擎通过 CONFIG_FILENAME 加载 config
    const configFilename = `${folderName}/config.json`;
    await rewriteIndexHtml(outDir, assetPrefix, configFilename);
    await rewriteSettings(folderPath, assetPrefix, configFilename);
    await rewriteConfig(path.join(folderPath), folderPath, folderName);
}
async function rewriteIndexHtml(outDir, assetPrefix, configFilename) {
    const indexPath = path.join(outDir, 'index.html');
    let html = await fs.readFile(indexPath, 'utf-8');
    // === 1. 重写 HTML 标签中的外部资源引用 ===
    html = html.replace(/(<script[^>]*src=["'])([^"']+)(["'][^>]*><\/script>)/gi, (match, start, src, end) => {
        if (src.startsWith('http') || src.startsWith('data:')) {
            return match;
        }
        return `${start}${assetPrefix}${src}${end}`;
    });
    html = html.replace(/(<link[^>]*href=["'])([^"']+)(["'][^>]*>)/gi, (match, start, href, end) => {
        if (href.startsWith('http') || href.startsWith('data:')) {
            return match;
        }
        return `${start}${assetPrefix}${href}${end}`;
    });
    // === 2. 重写内联脚本中的路径（支持未压缩和压缩/混淆后的格式） ===
    // 2.1 CONFIG_FILENAME / config.json（config 保留在根目录，不加 prefix，避免引擎双重路径）
    html = html.replace(/(\bconst\s+CONFIG_FILENAME\s*=\s*["'])config\.json(["'])/g, `$1${configFilename}$2`);
    html = html.replace(/(\.configure\s*\(\s*["'])config\.json(["'])/g, `$1${configFilename}$2`);
    // 2.2 ASSET_PREFIX
    // 未压缩格式: const ASSET_PREFIX = ""
    html = html.replace(/(\bconst\s+ASSET_PREFIX\s*=\s*["'])(["'])/g, `$1${assetPrefix}$2`);
    // 压缩后格式: window.ASSET_PREFIX="" 或 window.ASSET_PREFIX = ""
    html = html.replace(/(window\.ASSET_PREFIX\s*=\s*["'])(["'])/g, `$1${assetPrefix}$2`);
    // 压缩后赋值格式: assetPrefix="" 或 .assetPrefix=""
    html = html.replace(/(\.assetPrefix\s*=\s*)(["'])(["'])/g, `$1$2${assetPrefix}$3`);
    // 压缩后: createOptions.assetPrefix=ASSET_PREFIX??"" → 如果 ASSET_PREFIX 被内联为 ""
    // 则变成 createOptions.assetPrefix="" 或 createOptions.assetPrefix=""
    // 2.3 SCRIPT_PREFIX
    // 未压缩格式: const SCRIPT_PREFIX = ""
    html = html.replace(/(\bconst\s+SCRIPT_PREFIX\s*=\s*["'])(["'])/g, `$1${assetPrefix}$2`);
    // 压缩后格式: window.SCRIPT_PREFIX=""
    html = html.replace(/(window\.SCRIPT_PREFIX\s*=\s*["'])(["'])/g, `$1${assetPrefix}$2`);
    // 压缩后: .scriptPrefix=""
    html = html.replace(/(\.scriptPrefix\s*=\s*)(["'])(["'])/g, `$1$2${assetPrefix}$3`);
    // 2.4 loadScene 中的场景路径
    // 场景路径不需要加 prefix，因为 PlayCanvas 引擎会用 ASSET_PREFIX + scenes[].url 来加载
    // 但如果 ASSET_PREFIX 已经被正确设置，场景路径会自动正确
    // === 3. 重写 __settings__.js 中的 window 变量（如果存在于内联脚本中） ===
    // Classic 模式下，__settings__.js 的内容可能被内联到 HTML 中
    // 格式可能是 window.ASSET_PREFIX = ""; 或压缩后 window.ASSET_PREFIX="", (逗号分隔)
    // 注意: window.ASSET_PREFIX="" 已在上面第 2.2 节处理过
    html = html.replace(/(window\.CONFIG_FILENAME\s*=\s*["'])config\.json(["'])/g, `$1${configFilename}$2`);
    await fs.writeFile(indexPath, html);
}
async function rewriteSettings(folderPath, assetPrefix, configFilename) {
    const settingsPath = path.join(folderPath, '__settings__.js');
    try {
        let code = await fs.readFile(settingsPath, 'utf-8');
        code = code.replace(/window\.ASSET_PREFIX\s*=\s*"[^"]*";/, `window.ASSET_PREFIX = "${assetPrefix}";`);
        code = code.replace(/window\.SCRIPT_PREFIX\s*=\s*"[^"]*";/, `window.SCRIPT_PREFIX = "${assetPrefix}";`);
        code = code.replace(/window\.CONFIG_FILENAME\s*=\s*"([^"]+)";/, (match, value) => {
            if (value.startsWith('data:') || value.startsWith('http')) {
                return match;
            }
            return `window.CONFIG_FILENAME = "${configFilename}";`;
        });
        // SCENE_PATH 保持为纯文件名（如 226067097.json），引擎会做 ASSET_PREFIX + SCENE_PATH
        // 若在此处加 prefix 会导致 playcraft-assets-xxx/playcraft-assets-xxx/xxx.json 双重路径
        // code = code.replace(...) 保持 SCENE_PATH 原值，不修改
        // 重写 PRELOAD_MODULES 中的文件路径，添加 assetPrefix
        const modulesMatch = code.match(/window\.PRELOAD_MODULES\s*=\s*(\[[\s\S]*?\]);/);
        if (modulesMatch) {
            try {
                const modules = new Function(`return ${modulesMatch[1]}`)();
                for (const module of modules) {
                    if (module.glueUrl && !module.glueUrl.startsWith('data:') && !module.glueUrl.startsWith('http')) {
                        module.glueUrl = assetPrefix + module.glueUrl;
                    }
                    if (module.wasmUrl && !module.wasmUrl.startsWith('data:') && !module.wasmUrl.startsWith('http')) {
                        module.wasmUrl = assetPrefix + module.wasmUrl;
                    }
                    if (module.fallbackUrl && !module.fallbackUrl.startsWith('data:') && !module.fallbackUrl.startsWith('http')) {
                        module.fallbackUrl = assetPrefix + module.fallbackUrl;
                    }
                }
                const newModulesStr = JSON.stringify(modules);
                code = code.replace(/window\.PRELOAD_MODULES\s*=\s*\[[\s\S]*?\];/, `window.PRELOAD_MODULES = ${newModulesStr};`);
            }
            catch (error) {
                console.warn(`[Platform] rewriteSettings: 无法处理 PRELOAD_MODULES 路径: ${error}`);
            }
        }
        await fs.writeFile(settingsPath, code);
    }
    catch (error) {
        // 忽略缺失
    }
}
async function rewriteConfig(configDir, _folderPath, _folderName) {
    // config.json 已移入资产文件夹，在此处可对其进行后处理
    const configPath = path.join(configDir, 'config.json');
    try {
        await fs.access(configPath);
    }
    catch (error) {
        // 忽略缺失
    }
}
/**
 * 处理 ZIP 输出中 __settings__.js 的 PRELOAD_MODULES
 *
 * 问题：ZIP 模式下 __settings__.js 中的 PRELOAD_MODULES 引用了 ammo.wasm.js 等文件，
 * 但这些文件可能：
 *   1. 被 ammoReplacement 替换（此时 ammo 文件不再需要）
 *   2. 被 inlineUnsupportedAssets 内联到 config.json 后删除（.wasm 文件）
 *   3. 在 externFiles 模式下路径发生变化
 *
 * 解决方案：
 *   - 如果使用了 ammoReplacement，移除 PRELOAD_MODULES 中的 ammo 模块
 *   - 如果使用了 inlineUnsupportedAssets，将 WASM 相关 URL 内联为 base64 或使用 JS fallback
 */
async function processPreloadModulesForZip(outDir, options) {
    // 1. 处理 __settings__.js 中的 PRELOAD_MODULES
    const settingsPath = path.join(outDir, '__settings__.js');
    let settingsCode;
    try {
        settingsCode = await fs.readFile(settingsPath, 'utf-8');
    }
    catch {
        // __settings__.js 不在根目录，可能还没被 externFiles 处理，先跳过
        return;
    }
    const modulesMatch = settingsCode.match(/window\.PRELOAD_MODULES\s*=\s*(\[[\s\S]*?\]);/);
    if (!modulesMatch) {
        return;
    }
    try {
        let modules = new Function(`return ${modulesMatch[1]}`)();
        let modified = false;
        if (options.ammoReplacement) {
            // 移除 ammo 相关模块
            const beforeCount = modules.length;
            modules = modules.filter((module) => !isAmmoModule(module));
            const removed = beforeCount - modules.length;
            if (removed > 0) {
                console.log(`[Platform] ZIP: 已移除 ${removed} 个 Ammo 预加载模块（已替换为 ${options.ammoReplacement}）`);
                modified = true;
            }
            // 同时从 config.json 中移除 ammo 相关 assets
            await stripAmmoAssetsFromConfig(outDir);
        }
        else if (options.inlineUnsupportedAssets) {
            // 将 WASM 模块的 URL 转为 data URL 或使用 JS fallback
            for (const module of modules) {
                // 优先使用 fallback JS 版本
                if (module.fallbackUrl && !module.fallbackUrl.startsWith('data:')) {
                    const fallbackPath = path.join(outDir, module.fallbackUrl);
                    try {
                        const fallbackCode = await fs.readFile(fallbackPath, 'utf-8');
                        module.fallbackUrl = `data:text/javascript;base64,${Buffer.from(fallbackCode).toString('base64')}`;
                        // 清空 WASM 相关 URL，强制使用 fallback
                        module.glueUrl = '';
                        module.wasmUrl = '';
                        modified = true;
                        console.log(`[Platform] ZIP: 已将模块 ${module.moduleName} 的 fallback 内联为 base64`);
                    }
                    catch {
                        console.warn(`[Platform] ZIP: 无法读取 fallback 文件: ${module.fallbackUrl}`);
                    }
                }
                // 如果没有 fallback 或 fallback 读取失败，处理 glueUrl 和 wasmUrl
                if (module.glueUrl && !module.glueUrl.startsWith('data:')) {
                    const gluePath = path.join(outDir, module.glueUrl);
                    try {
                        const glueCode = await fs.readFile(gluePath, 'utf-8');
                        module.glueUrl = `data:text/javascript;base64,${Buffer.from(glueCode).toString('base64')}`;
                        modified = true;
                    }
                    catch {
                        console.warn(`[Platform] ZIP: 无法读取 glue 文件: ${module.glueUrl}`);
                    }
                }
                if (module.wasmUrl && !module.wasmUrl.startsWith('data:')) {
                    const wasmPath = path.join(outDir, module.wasmUrl);
                    try {
                        const wasmBinary = await fs.readFile(wasmPath);
                        module.wasmUrl = `data:application/wasm;base64,${wasmBinary.toString('base64')}`;
                        modified = true;
                    }
                    catch {
                        console.warn(`[Platform] ZIP: 无法读取 WASM 文件: ${module.wasmUrl}`);
                    }
                }
            }
        }
        if (modified) {
            // 写回修改后的 PRELOAD_MODULES
            const newModulesStr = JSON.stringify(modules);
            settingsCode = settingsCode.replace(/window\.PRELOAD_MODULES\s*=\s*\[[\s\S]*?\];/, `window.PRELOAD_MODULES = ${newModulesStr};`);
            await fs.writeFile(settingsPath, settingsCode);
        }
    }
    catch (error) {
        console.warn(`[Platform] ZIP: 无法处理 PRELOAD_MODULES: ${error}`);
    }
}
/**
 * 从 config.json 中移除 ammo 相关的 wasm/script assets
 */
async function stripAmmoAssetsFromConfig(outDir) {
    const configPath = await findConfigJson(outDir);
    if (!configPath)
        return;
    try {
        const raw = await fs.readFile(configPath, 'utf-8');
        const configJson = JSON.parse(raw);
        if (!configJson.assets)
            return;
        const assets = configJson.assets;
        const ammoAssetIds = new Set();
        const ammoRelatedIds = new Set();
        // 找出所有 ammo 相关的 wasm assets 及其关联的 glue/fallback script
        for (const [id, asset] of Object.entries(assets)) {
            const a = asset;
            if (a.type === 'wasm' && a.data?.moduleName?.toLowerCase().includes('ammo')) {
                ammoAssetIds.add(id);
                if (a.data.glueScriptId != null)
                    ammoRelatedIds.add(String(a.data.glueScriptId));
                if (a.data.fallbackScriptId != null)
                    ammoRelatedIds.add(String(a.data.fallbackScriptId));
            }
        }
        // 也检查 file.url 中包含 ammo 的 assets
        for (const [id, asset] of Object.entries(assets)) {
            const a = asset;
            const url = a.file?.url?.toLowerCase() || '';
            const name = (a.name || '').toLowerCase();
            if (url.includes('ammo') || name.includes('ammo')) {
                ammoRelatedIds.add(id);
            }
        }
        const allAmmoIds = new Set([...ammoAssetIds, ...ammoRelatedIds]);
        if (allAmmoIds.size === 0)
            return;
        for (const id of allAmmoIds) {
            delete assets[id];
        }
        configJson.assets = assets;
        await fs.writeFile(configPath, JSON.stringify(configJson));
        console.log(`[Platform] ZIP: 已从 config.json 中移除 ${allAmmoIds.size} 个 Ammo 相关 assets`);
    }
    catch (error) {
        console.warn(`[Platform] ZIP: 无法处理 config.json 中的 Ammo assets: ${error}`);
    }
}
function isAmmoModule(module) {
    const moduleName = module.moduleName?.toLowerCase() ?? '';
    const glueUrl = module.glueUrl?.toLowerCase() ?? '';
    const wasmUrl = module.wasmUrl?.toLowerCase() ?? '';
    const fallbackUrl = module.fallbackUrl?.toLowerCase() ?? '';
    return (moduleName.includes('ammo') ||
        glueUrl.includes('ammo') ||
        wasmUrl.includes('ammo') ||
        fallbackUrl.includes('ammo'));
}
async function applyMraidSupport(outDir) {
    // Note: We no longer patch fillMode to 'NONE' because it causes canvas scaling issues
    // The MRAID resize patch handles the sizing correctly with any fillMode
    // await patchConfigFillMode(outDir);  // Removed: causes canvas scaling issues
    await patchStylesForMraid(outDir);
}
async function patchConfigFillMode(outDir) {
    const configPath = path.join(outDir, 'config.json');
    try {
        const raw = await fs.readFile(configPath, 'utf-8');
        const json = JSON.parse(raw);
        json.application_properties = {
            ...(json.application_properties || {}),
            fillMode: 'NONE',
        };
        await fs.writeFile(configPath, JSON.stringify(json));
    }
    catch (error) {
        // 忽略缺失
    }
}
async function patchStylesForMraid(outDir) {
    const cssPath = path.join(outDir, 'styles.css');
    try {
        let css = await fs.readFile(cssPath, 'utf-8');
        if (!css.includes('fill-mode-NONE')) {
            css += '\n#application-canvas.fill-mode-NONE { margin: 0; width: 100%; height: 100%; }\n';
            await fs.writeFile(cssPath, css);
        }
    }
    catch (error) {
        // 忽略缺失
    }
}
async function injectMraidResizePatch(html) {
    const patchCode = await readPatchFile('one-page-mraid-resize-canvas.js');
    const minified = await minifyPatchCode(patchCode, 'mraid-resize-platform');
    return html.replace('</head>', `<script>${minified}</script>\n</head>`);
}
async function readPatchFile(name) {
    const currentDir = path.dirname(fileURLToPath(import.meta.url));
    const patchPath = path.resolve(currentDir, '../../templates/patches', name);
    return await fs.readFile(patchPath, 'utf-8');
}
async function copyBaseBuildAssets(baseBuildDir, outDir) {
    // 计算 outDir 相对于 baseBuildDir 的名称（如果 outDir 是 baseBuildDir 的子目录）
    const outDirRelative = path.relative(baseBuildDir, outDir);
    const outDirName = outDirRelative.split(path.sep)[0]; // 获取第一级目录名
    const entries = await fs.readdir(baseBuildDir, { withFileTypes: true });
    for (const entry of entries) {
        // 跳过 index.html
        if (entry.name === 'index.html') {
            continue;
        }
        // 跳过输出目录本身（避免无限递归）
        if (entry.name === outDirName) {
            continue;
        }
        // 跳过以 __ 开头的临时目录（如 __dist__, __esm-temp__, __esm-work__ 等）
        // 注意：不能跳过 __ 开头的文件，因为 Classic 模式下
        // __start__.js, __settings__.js, __modules__.js, __loading__.js, __game-scripts.js 都是必需的
        if (entry.isDirectory() && entry.name.startsWith('__')) {
            continue;
        }
        const srcPath = path.join(baseBuildDir, entry.name);
        const destPath = path.join(outDir, entry.name);
        if (entry.isDirectory()) {
            await copyDirectory(srcPath, destPath);
        }
        else {
            await fs.copyFile(srcPath, destPath);
        }
    }
}
async function copyDirectory(src, dest) {
    await fs.mkdir(dest, { recursive: true });
    const entries = await fs.readdir(src, { withFileTypes: true });
    for (const entry of entries) {
        const srcPath = path.join(src, entry.name);
        const destPath = path.join(dest, entry.name);
        if (entry.isDirectory()) {
            await copyDirectory(srcPath, destPath);
        }
        else {
            await fs.copyFile(srcPath, destPath);
        }
    }
}
/**
 * 遍历目录中的所有 .js 文件，使用 Terser 进行压缩混淆
 * 跳过已经压缩过的 .min.js 文件
 */
async function minifyJSFilesInDir(dir) {
    const jsFiles = await collectJSFiles(dir);
    if (jsFiles.length === 0)
        return;
    console.log(`\n🔒 正在压缩混淆 ${jsFiles.length} 个 JS 文件...`);
    let totalSaved = 0;
    for (const filePath of jsFiles) {
        try {
            const originalCode = await fs.readFile(filePath, 'utf-8');
            const originalSize = Buffer.byteLength(originalCode, 'utf-8');
            const isESM = filePath.endsWith('.mjs');
            const minified = await minifyJS(originalCode, {
                removeComments: true,
                mangle: true,
                compress: true,
                module: isESM,
            });
            const minifiedSize = Buffer.byteLength(minified, 'utf-8');
            const saved = originalSize - minifiedSize;
            totalSaved += saved;
            await fs.writeFile(filePath, minified);
            const relativePath = path.relative(dir, filePath);
            const ratio = originalSize > 0 ? ((saved / originalSize) * 100).toFixed(1) : '0';
            console.log(`  ✓ ${relativePath}: ${(originalSize / 1024).toFixed(1)}KB → ${(minifiedSize / 1024).toFixed(1)}KB (-${ratio}%)`);
        }
        catch (error) {
            console.warn(`  ⚠ ${path.relative(dir, filePath)}: 压缩失败，保留原文件`);
        }
    }
    console.log(`✅ JS 压缩混淆完成，共节省 ${(totalSaved / 1024).toFixed(1)}KB`);
}
// 已经是压缩过的引擎文件，跳过 Terser 处理（节省时间）
const SKIP_MINIFY_PATTERNS = [
    'playcanvas-engine-umd.js',
    'playcanvas-engine-umd.mjs',
    /playcanvas.*\.min\./,
];
function shouldSkipMinify(fileName) {
    return SKIP_MINIFY_PATTERNS.some(p => typeof p === 'string' ? fileName === p : p.test(fileName));
}
async function collectJSFiles(dir) {
    const results = [];
    const entries = await fs.readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
        const fullPath = path.join(dir, entry.name);
        if (entry.isDirectory()) {
            const subFiles = await collectJSFiles(fullPath);
            results.push(...subFiles);
        }
        else if ((entry.name.endsWith('.js') || entry.name.endsWith('.mjs')) &&
            !entry.name.endsWith('.min.js') &&
            !entry.name.endsWith('.min.mjs') &&
            !shouldSkipMinify(entry.name)) {
            results.push(fullPath);
        }
    }
    return results;
}
// Google Ads ZIP 支持的文件扩展名白名单
const GOOGLE_SUPPORTED_EXTENSIONS = new Set([
    '.css', '.js', '.html', '.htm',
    '.gif', '.png', '.jpg', '.jpeg', '.svg',
]);
function isGoogleSupportedFile(filePath) {
    const ext = path.extname(filePath).toLowerCase();
    return GOOGLE_SUPPORTED_EXTENSIONS.has(ext);
}
function guessMimeTypeForInline(filePath) {
    const ext = path.extname(filePath).toLowerCase();
    const mimeMap = {
        '.glb': 'model/gltf-binary',
        '.gltf': 'model/gltf+json',
        '.bin': 'application/octet-stream',
        '.wasm': 'application/wasm',
        '.ttf': 'font/ttf',
        '.otf': 'font/otf',
        '.woff': 'font/woff',
        '.woff2': 'font/woff2',
        '.mp3': 'audio/mpeg',
        '.wav': 'audio/wav',
        '.ogg': 'audio/ogg',
        '.mp4': 'video/mp4',
        '.webp': 'image/webp',
        '.json': 'application/json',
        '.txt': 'text/plain',
    };
    return mimeMap[ext] || 'application/octet-stream';
}
/**
 * 将 ZIP 中不受 Google Ads 支持的文件内联为 base64 data URL 到 config.json 中，
 * 然后从输出目录中删除这些文件。
 *
 * Google Ads HTML5 验证器只允许：.css, .js, .html, .gif, .png, .jpeg, .svg
 * 不支持的类型（如 .glb, .wasm, .bin, .ttf 等）会导致验证失败。
 */
async function inlineUnsupportedAssetsIntoConfig(outDir) {
    // 1. 找到 config.json（可能在 outDir 或 externFiles 子目录中）
    const configPath = await findConfigJson(outDir);
    if (!configPath) {
        console.warn('[Platform] inlineUnsupportedAssets: 未找到 config.json，跳过');
        return;
    }
    let configJson;
    try {
        const raw = await fs.readFile(configPath, 'utf-8');
        configJson = JSON.parse(raw);
    }
    catch (error) {
        console.warn(`[Platform] inlineUnsupportedAssets: 无法读取 config.json: ${error}`);
        return;
    }
    if (!configJson?.assets) {
        return;
    }
    const configDir = path.dirname(configPath);
    const assets = configJson.assets;
    const inlinedFiles = [];
    const filesToDelete = [];
    for (const [assetId, asset] of Object.entries(assets)) {
        const file = asset?.file;
        if (!file?.url || typeof file.url !== 'string')
            continue;
        const url = file.url;
        if (url.startsWith('data:') || url.startsWith('http://') || url.startsWith('https://'))
            continue;
        const cleanUrl = url.split('?')[0];
        // 检查此文件是否为 Google 不支持的类型
        if (isGoogleSupportedFile(cleanUrl))
            continue;
        // 尝试从输出目录读取文件
        // 资源路径可能相对于 config.json 所在目录，也可能相对于 outDir，或子目录（externFiles 场景）
        let fullPath = path.join(configDir, cleanUrl);
        try {
            await fs.access(fullPath);
        }
        catch {
            fullPath = path.join(outDir, cleanUrl);
            try {
                await fs.access(fullPath);
            }
            catch {
                // externFiles 场景：config 在根目录，资产在 playcraft-assets-xxx/ 内
                let found = false;
                const entries = await fs.readdir(outDir, { withFileTypes: true });
                for (const entry of entries) {
                    if (entry.isDirectory()) {
                        const candidate = path.join(outDir, entry.name, cleanUrl);
                        try {
                            await fs.access(candidate);
                            fullPath = candidate;
                            found = true;
                            break;
                        }
                        catch {
                            /* 继续尝试 */
                        }
                    }
                }
                if (!found) {
                    console.warn(`[Platform] inlineUnsupportedAssets: 文件不存在: ${cleanUrl}`);
                    continue;
                }
            }
        }
        try {
            const buffer = await fs.readFile(fullPath);
            const mime = guessMimeTypeForInline(cleanUrl);
            const dataUrl = `data:${mime};base64,${buffer.toString('base64')}`;
            file.url = dataUrl;
            file.size = buffer.length;
            if ('hash' in file)
                delete file.hash;
            if ('variants' in file)
                delete file.variants;
            inlinedFiles.push(`${asset.name || assetId} (${cleanUrl}, ${(buffer.length / 1024).toFixed(1)}KB)`);
            filesToDelete.push(fullPath);
        }
        catch (error) {
            console.warn(`[Platform] inlineUnsupportedAssets: 无法读取文件 ${cleanUrl}: ${error}`);
        }
    }
    if (inlinedFiles.length === 0) {
        console.log('[Platform] inlineUnsupportedAssets: 所有文件类型均受支持，无需内联');
        return;
    }
    // 2. 写回 config.json
    await fs.writeFile(configPath, JSON.stringify(configJson));
    console.log(`\n✅ 已将 ${inlinedFiles.length} 个不受支持的文件内联为 base64:`);
    inlinedFiles.forEach(info => console.log(`  - ${info}`));
    // 3. 删除已内联的原始文件
    for (const filePath of filesToDelete) {
        try {
            await fs.unlink(filePath);
        }
        catch {
            // 忽略删除失败
        }
    }
    // 4. 清理空目录
    await cleanEmptyDirs(outDir);
}
async function findConfigJson(outDir) {
    // 直接在 outDir 中查找
    const directPath = path.join(outDir, 'config.json');
    try {
        await fs.access(directPath);
        return directPath;
    }
    catch { }
    // 在子目录中查找（externFiles 场景）
    const entries = await fs.readdir(outDir, { withFileTypes: true });
    for (const entry of entries) {
        if (entry.isDirectory()) {
            const subPath = path.join(outDir, entry.name, 'config.json');
            try {
                await fs.access(subPath);
                return subPath;
            }
            catch { }
        }
    }
    return null;
}
async function cleanEmptyDirs(dir) {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    let isEmpty = true;
    for (const entry of entries) {
        const fullPath = path.join(dir, entry.name);
        if (entry.isDirectory()) {
            const subEmpty = await cleanEmptyDirs(fullPath);
            if (subEmpty) {
                try {
                    await fs.rmdir(fullPath);
                }
                catch { }
            }
            else {
                isEmpty = false;
            }
        }
        else {
            isEmpty = false;
        }
    }
    return isEmpty;
}
