import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { createPlatformAdapter } from './platforms/index.js';
import { minifyPatchCode } from './utils/minify.js';
/**
 * Playable Ads 打包器 - 将多文件版本转换为单HTML
 *
 * 职责：
 * 1. 读取阶段1的多文件构建产物
 * 2. 将所有文件内联到单个HTML
 * 3. 应用渠道特定配置和限制
 * 4. 优化文件大小
 */
export class PlayableBuilder {
    constructor(baseBuildDir, options) {
        this.baseBuildDir = baseBuildDir;
        this.options = options;
        this.platformAdapter = createPlatformAdapter(options);
        this.sizeReport = {
            engine: 0,
            assets: {},
            total: 0,
            limit: this.platformAdapter.getSizeLimit(),
        };
    }
    /**
     * 执行打包
     */
    async build() {
        // 1. 验证输入是有效的基础构建
        await this.validateBaseBuild();
        // 2. 转换为单HTML
        const html = await this.convertToSingleHTML();
        // 3. 应用渠道配置（现在是异步的）
        const finalHtml = await this.platformAdapter.modifyHTML(html, []);
        // 4. 验证大小限制
        const htmlSize = Buffer.from(finalHtml, 'utf-8').length;
        this.sizeReport.total = htmlSize;
        this.sizeReport.assets['index.html'] = htmlSize;
        if (htmlSize > this.sizeReport.limit) {
            console.warn(`⚠️  警告: 文件大小 ${(htmlSize / 1024 / 1024).toFixed(2)} MB ` +
                `超过限制 ${(this.sizeReport.limit / 1024 / 1024).toFixed(2)} MB`);
        }
        // 5. 输出文件
        const outputDir = this.options.outputDir || './dist';
        await fs.mkdir(outputDir, { recursive: true });
        const outputPath = path.join(outputDir, 'index.html');
        await fs.writeFile(outputPath, finalHtml, 'utf-8');
        return outputPath;
    }
    /**
     * 验证基础构建
     */
    async validateBaseBuild() {
        const requiredFiles = [
            'index.html',
            'config.json',
            '__start__.js',
        ];
        const missingFiles = [];
        for (const file of requiredFiles) {
            try {
                await fs.access(path.join(this.baseBuildDir, file));
            }
            catch (error) {
                missingFiles.push(file);
            }
        }
        if (missingFiles.length > 0) {
            throw new Error(`基础构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
                `请确保输入目录包含完整的多文件构建产物。`);
        }
    }
    /**
     * 转换为单HTML
     */
    async convertToSingleHTML() {
        // 1. 读取 index.html 模板
        let html = await fs.readFile(path.join(this.baseBuildDir, 'index.html'), 'utf-8');
        // 2. 内联 CSS 文件
        html = await this.inlineCSS(html);
        // 3. 内联 manifest.json
        html = await this.inlineManifest(html);
        // 4. 内联 PlayCanvas Engine
        html = await this.inlineEngine(html);
        // 4.1 内联物理库（替换 Ammo）
        if (this.options.ammoReplacement) {
            html = await this.injectPhysicsLibrary(html);
        }
        // 4.2 注入 Scene applySettings 补丁，避免 settings.physics 缺失导致 gravity 报错
        html = await this.injectScenePhysicsPatch(html);
        // 5. 内联 __settings__.js，并转换资源URL为data URLs
        html = await this.inlineSettings(html);
        // 6. 内联 __modules__.js
        html = await this.inlineModules(html);
        // 7. 内联 __game-scripts.js（如果存在）
        html = await this.inlineGameScripts(html);
        // 8. 内联 __start__.js
        html = await this.inlineStart(html);
        // 9. 内联 __loading__.js（如果存在）
        html = await this.inlineLoading(html);
        return html;
    }
    /**
     * 内联 CSS 文件
     */
    async inlineCSS(html) {
        // 匹配 <link rel="stylesheet" href="...">
        const cssPattern = /<link[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+)["'][^>]*>/gi;
        const matches = Array.from(html.matchAll(cssPattern));
        for (const match of matches) {
            const cssPath = match[1];
            if (cssPath.startsWith('data:') || cssPath.startsWith('http://') || cssPath.startsWith('https://')) {
                continue; // 跳过已经是 data URL 或外部 URL
            }
            const fullCssPath = path.join(this.baseBuildDir, cssPath);
            try {
                await fs.access(fullCssPath);
                const cssContent = await fs.readFile(fullCssPath, 'utf-8');
                // 替换 link 标签为 style 标签
                html = html.replace(match[0], `<style>${cssContent}</style>`);
            }
            catch (error) {
                console.warn(`警告: CSS 文件不存在: ${cssPath}，移除引用`);
                // 移除不存在的 CSS 引用
                html = html.replace(match[0], '');
            }
        }
        return html;
    }
    /**
     * 内联 manifest.json
     */
    async inlineManifest(html) {
        // 匹配 <link rel="manifest" href="...">
        const manifestPattern = /<link[^>]*rel=["']manifest["'][^>]*href=["']([^"']+)["'][^>]*>/gi;
        const matches = Array.from(html.matchAll(manifestPattern));
        for (const match of matches) {
            const manifestPath = match[1];
            if (manifestPath.startsWith('data:') || manifestPath.startsWith('http://') || manifestPath.startsWith('https://')) {
                continue; // 跳过已经是 data URL 或外部 URL
            }
            const fullManifestPath = path.join(this.baseBuildDir, manifestPath);
            try {
                await fs.access(fullManifestPath);
                const manifestContent = await fs.readFile(fullManifestPath, 'utf-8');
                // 将 manifest 转换为 data URL 并内联到 meta 标签
                const manifestDataUrl = `data:application/manifest+json;base64,${Buffer.from(manifestContent).toString('base64')}`;
                html = html.replace(match[0], `<link rel="manifest" href="${manifestDataUrl}">`);
            }
            catch (error) {
                console.warn(`警告: manifest 文件不存在: ${manifestPath}，移除引用`);
                // 移除不存在的 manifest 引用
                html = html.replace(match[0], '');
            }
        }
        return html;
    }
    /**
     * 内联 PlayCanvas Engine
     */
    async inlineEngine(html) {
        // 查找可能的引擎文件名
        const engineNames = [
            'playcanvas-stable.min.js',
            'playcanvas.min.js',
            '__lib__.js',
        ];
        for (const engineName of engineNames) {
            const enginePath = path.join(this.baseBuildDir, engineName);
            try {
                await fs.access(enginePath);
                const engineCode = await fs.readFile(enginePath, 'utf-8');
                this.sizeReport.engine = Buffer.from(engineCode, 'utf-8').length;
                // 替换 script 标签
                const scriptPattern = new RegExp(`<script[^>]*src=["']${engineName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["'][^>]*></script>`, 'i');
                html = html.replace(scriptPattern, `<script>${engineCode}</script>`);
                return html;
            }
            catch (error) {
                // 继续尝试下一个
            }
        }
        throw new Error('未找到 PlayCanvas Engine 文件');
    }
    /**
     * 内联 __settings__.js 并转换资源URL
     */
    async inlineSettings(html) {
        const settingsPath = path.join(this.baseBuildDir, '__settings__.js');
        try {
            await fs.access(settingsPath);
        }
        catch (error) {
            // __settings__.js 不存在，尝试从 config.json 生成
            return await this.generateAndInlineSettings(html);
        }
        let settingsCode = await fs.readFile(settingsPath, 'utf-8');
        // 转换 CONFIG_FILENAME 为 data URL
        settingsCode = await this.convertConfigFilename(settingsCode);
        // 转换 SCENE_PATH 为 data URL
        settingsCode = await this.convertScenePath(settingsCode);
        // 转换 PRELOAD_MODULES 中的资源URL
        settingsCode = await this.convertPreloadModules(settingsCode);
        // 替换 script 标签
        const scriptPattern = /<script[^>]*src=["']__settings__\.js["'][^>]*><\/script>/i;
        html = html.replace(scriptPattern, `<script>${settingsCode}</script>`);
        return html;
    }
    /**
     * 生成并内联 settings（如果 __settings__.js 不存在）
     */
    async generateAndInlineSettings(html) {
        const configPath = path.join(this.baseBuildDir, 'config.json');
        const configContent = await fs.readFile(configPath, 'utf-8');
        let configJson = JSON.parse(configContent);
        if (this.options.ammoReplacement) {
            configJson = this.stripAmmoAssets(configJson);
        }
        // 生成 config data URL
        const configDataUrl = `data:application/json;base64,${Buffer.from(JSON.stringify(configJson)).toString('base64')}`;
        // 生成 scene data URL
        let sceneDataUrl = '';
        if (configJson.scenes && configJson.scenes.length > 0) {
            const sceneUrl = configJson.scenes[0].url;
            if (sceneUrl && !sceneUrl.startsWith('data:')) {
                const scenePath = path.join(this.baseBuildDir, sceneUrl);
                try {
                    const sceneContent = await fs.readFile(scenePath, 'utf-8');
                    sceneDataUrl = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
                }
                catch (error) {
                    console.warn(`警告: 场景文件不存在: ${sceneUrl}`);
                }
            }
            else {
                sceneDataUrl = sceneUrl;
            }
        }
        const appProps = configJson.application_properties || {};
        const scripts = appProps.scripts || [];
        const preloadModules = this.extractPreloadModules(configJson);
        // 保留原始的 powerPreference 设置，默认为 high-performance
        const powerPreference = appProps.powerPreference || 'high-performance';
        const settingsCode = `
window.ASSET_PREFIX = "";
window.SCRIPT_PREFIX = "";
window.SCENE_PATH = "${sceneDataUrl}";
window.CONTEXT_OPTIONS = {
    'antialias': ${appProps.antiAlias !== false},
    'alpha': ${appProps.transparentCanvas === true},
    'preserveDrawingBuffer': ${appProps.preserveDrawingBuffer === true},
    'deviceTypes': ['webgl2', 'webgl1'],
    'powerPreference': "${powerPreference}"
};
window.SCRIPTS = [${scripts.join(', ')}];
window.CONFIG_FILENAME = "${configDataUrl}";
window.INPUT_SETTINGS = {
    useKeyboard: ${appProps.useKeyboard !== false},
    useMouse: ${appProps.useMouse !== false},
    useGamepads: ${appProps.useGamepads === true},
    useTouch: ${appProps.useTouch !== false}
};
pc.script.legacy = ${appProps.useLegacyScripts === true};
window.PRELOAD_MODULES = ${JSON.stringify(preloadModules)};
`;
        // 在 </head> 之前插入
        html = html.replace('</head>', `<script>${settingsCode}</script>\n</head>`);
        return html;
    }
    /**
     * 转换 CONFIG_FILENAME 为 data URL
     */
    async convertConfigFilename(settingsCode) {
        const configMatch = settingsCode.match(/window\.CONFIG_FILENAME\s*=\s*"([^"]+)"/);
        if (!configMatch) {
            return settingsCode;
        }
        const configPath = configMatch[1];
        if (configPath.startsWith('data:')) {
            return settingsCode; // 已经是 data URL
        }
        const fullConfigPath = path.join(this.baseBuildDir, configPath);
        try {
            const configContent = await fs.readFile(fullConfigPath, 'utf-8');
            let configJson = JSON.parse(configContent);
            if (this.options.ammoReplacement) {
                configJson = this.stripAmmoAssets(configJson);
            }
            const configDataUrl = `data:application/json;base64,${Buffer.from(JSON.stringify(configJson)).toString('base64')}`;
            return settingsCode.replace(/window\.CONFIG_FILENAME\s*=\s*"[^"]+"/, `window.CONFIG_FILENAME = "${configDataUrl}"`);
        }
        catch (error) {
            console.warn(`警告: 无法读取配置文件: ${configPath}`);
            return settingsCode;
        }
    }
    /**
     * 转换 SCENE_PATH 为 data URL
     */
    async convertScenePath(settingsCode) {
        const sceneMatch = settingsCode.match(/window\.SCENE_PATH\s*=\s*"([^"]+)"/);
        if (!sceneMatch) {
            return settingsCode;
        }
        const scenePath = sceneMatch[1];
        if (scenePath.startsWith('data:') || !scenePath) {
            return settingsCode; // 已经是 data URL 或为空
        }
        const fullScenePath = path.join(this.baseBuildDir, scenePath);
        try {
            const sceneContent = await fs.readFile(fullScenePath, 'utf-8');
            const sceneDataUrl = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
            return settingsCode.replace(/window\.SCENE_PATH\s*=\s*"[^"]+"/, `window.SCENE_PATH = "${sceneDataUrl}"`);
        }
        catch (error) {
            console.warn(`警告: 无法读取场景文件: ${scenePath}`);
            return settingsCode;
        }
    }
    /**
     * 转换 PRELOAD_MODULES 中的资源URL
     * 对于 Playable Ads，优先使用 fallback JS 版本（跳过 WASM）
     */
    async convertPreloadModules(settingsCode) {
        // 匹配 PRELOAD_MODULES 数组（支持单引号和双引号）
        const modulesMatch = settingsCode.match(/window\.PRELOAD_MODULES\s*=\s*(\[[\s\S]*?\]);/);
        if (!modulesMatch) {
            return settingsCode;
        }
        try {
            // 使用 Function 构造函数安全地解析 JavaScript 对象字面量
            // 这样可以处理单引号、双引号等格式
            const modulesStr = modulesMatch[1];
            let modules = new Function(`return ${modulesStr}`)();
            if (this.options.ammoReplacement) {
                const beforeCount = modules.length;
                modules = modules.filter((module) => !this.isAmmoModule(module));
                const removed = beforeCount - modules.length;
                if (removed > 0) {
                    console.warn(`警告: 已移除 ${removed} 个 Ammo 预加载模块，替换为 ${this.options.ammoReplacement}`);
                }
            }
            // 转换每个模块的URL为data URL
            for (const module of modules) {
                // 对于 Playable Ads，优先使用 fallback JS 版本，跳过 WASM
                // 因为很多平台不支持 WASM，且 WASM 文件很大
                if (module.fallbackUrl && !module.fallbackUrl.startsWith('data:')) {
                    const fallbackPath = path.join(this.baseBuildDir, 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 = '';
                    }
                    catch (error) {
                        console.warn(`警告: 无法读取 fallback 文件: ${module.fallbackUrl}`);
                        // 如果 fallback 读取失败，尝试读取 WASM 相关文件
                        if (module.glueUrl && !module.glueUrl.startsWith('data:')) {
                            const gluePath = path.join(this.baseBuildDir, module.glueUrl);
                            try {
                                const glueCode = await fs.readFile(gluePath, 'utf-8');
                                module.glueUrl = `data:text/javascript;base64,${Buffer.from(glueCode).toString('base64')}`;
                            }
                            catch (error) {
                                console.warn(`警告: 无法读取 glue 文件: ${module.glueUrl}`);
                            }
                        }
                        if (module.wasmUrl && !module.wasmUrl.startsWith('data:')) {
                            const wasmPath = path.join(this.baseBuildDir, module.wasmUrl);
                            try {
                                const wasmBinary = await fs.readFile(wasmPath);
                                module.wasmUrl = `data:application/wasm;base64,${wasmBinary.toString('base64')}`;
                            }
                            catch (error) {
                                console.warn(`警告: 无法读取 WASM 文件: ${module.wasmUrl}`);
                            }
                        }
                    }
                }
                else {
                    // 如果没有 fallback，尝试转换 WASM 相关文件
                    if (module.glueUrl && !module.glueUrl.startsWith('data:')) {
                        const gluePath = path.join(this.baseBuildDir, module.glueUrl);
                        try {
                            const glueCode = await fs.readFile(gluePath, 'utf-8');
                            module.glueUrl = `data:text/javascript;base64,${Buffer.from(glueCode).toString('base64')}`;
                        }
                        catch (error) {
                            console.warn(`警告: 无法读取 glue 文件: ${module.glueUrl}`);
                        }
                    }
                    if (module.wasmUrl && !module.wasmUrl.startsWith('data:')) {
                        const wasmPath = path.join(this.baseBuildDir, module.wasmUrl);
                        try {
                            const wasmBinary = await fs.readFile(wasmPath);
                            module.wasmUrl = `data:application/wasm;base64,${wasmBinary.toString('base64')}`;
                        }
                        catch (error) {
                            console.warn(`警告: 无法读取 WASM 文件: ${module.wasmUrl}`);
                        }
                    }
                }
            }
            // 重新生成 PRELOAD_MODULES 配置（使用双引号 JSON 格式）
            const newModulesStr = JSON.stringify(modules, null, 4);
            return settingsCode.replace(/window\.PRELOAD_MODULES\s*=\s*\[[\s\S]*?\];/, `window.PRELOAD_MODULES = ${newModulesStr};`);
        }
        catch (error) {
            console.warn(`警告: 无法解析 PRELOAD_MODULES: ${error instanceof Error ? error.message : String(error)}`);
            return settingsCode;
        }
    }
    /**
     * 从 config.json 提取 PRELOAD_MODULES
     */
    extractPreloadModules(configJson) {
        const modules = [];
        if (configJson.assets) {
            for (const [id, asset] of Object.entries(configJson.assets)) {
                const assetData = asset;
                if (assetData.type === 'wasm' && assetData.data) {
                    const moduleData = assetData.data;
                    const fallbackAssetId = moduleData.fallbackScriptId;
                    let fallbackUrl = '';
                    if (fallbackAssetId && configJson.assets[fallbackAssetId]) {
                        const fallbackAsset = configJson.assets[fallbackAssetId];
                        fallbackUrl = fallbackAsset.file?.url || '';
                    }
                    if (moduleData.moduleName && fallbackUrl) {
                        modules.push({
                            moduleName: moduleData.moduleName,
                            glueUrl: '',
                            wasmUrl: '',
                            fallbackUrl: fallbackUrl,
                            preload: true,
                        });
                    }
                }
            }
        }
        return modules;
    }
    stripAmmoAssets(configJson) {
        const next = { ...configJson };
        if (!next.assets) {
            return next;
        }
        const assets = { ...next.assets };
        for (const [id, asset] of Object.entries(assets)) {
            const assetData = asset;
            const moduleName = assetData?.data?.moduleName?.toLowerCase?.() || '';
            const fileUrl = assetData?.file?.url?.toLowerCase?.() || '';
            const fileName = assetData?.file?.filename?.toLowerCase?.() || '';
            const assetName = assetData?.name?.toLowerCase?.() || '';
            if (moduleName.includes('ammo') ||
                fileUrl.includes('ammo') ||
                fileName.includes('ammo') ||
                assetName.includes('ammo')) {
                delete assets[id];
            }
        }
        next.assets = assets;
        return next;
    }
    async injectPhysicsLibrary(html) {
        const library = this.options.ammoReplacement;
        if (!library) {
            return html;
        }
        const filename = library === 'p2' ? 'p2.min.js' : 'cannon.min.js';
        const code = await this.readPatchFile(filename);
        if (!code) {
            return html;
        }
        const tag = `<script>${code}</script>`;
        if (html.includes('</head>')) {
            return html.replace('</head>', `${tag}\n</head>`);
        }
        return `${tag}\n${html}`;
    }
    async injectScenePhysicsPatch(html) {
        const patchCode = await this.readPatchFile('scene-physics-defaults.js');
        if (!patchCode)
            return html;
        const minified = await minifyPatchCode(patchCode, 'scene-physics-defaults.js');
        const tag = `<script>${minified}</script>`;
        if (html.includes('</head>')) {
            return html.replace('</head>', `${tag}\n</head>`);
        }
        return `${tag}\n${html}`;
    }
    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 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');
    }
    /**
     * 内联 __modules__.js
     */
    async inlineModules(html) {
        const modulesPath = path.join(this.baseBuildDir, '__modules__.js');
        try {
            await fs.access(modulesPath);
        }
        catch (error) {
            // __modules__.js 不存在，跳过
            return html;
        }
        const modulesCode = await fs.readFile(modulesPath, 'utf-8');
        // 替换 script 标签
        const scriptPattern = /<script[^>]*src=["']__modules__\.js["'][^>]*><\/script>/i;
        html = html.replace(scriptPattern, `<script>${modulesCode}</script>`);
        // 如果 HTML 中没有找到 script 标签，在 <body> 开头添加
        if (!scriptPattern.test(html)) {
            html = html.replace('<body>', `<body>\n<script>${modulesCode}</script>\n`);
        }
        return html;
    }
    /**
     * 内联 __game-scripts.js
     * 游戏脚本需要在内联到HTML中，以便PlayCanvas可以加载它们
     */
    async inlineGameScripts(html) {
        const gameScriptsPath = path.join(this.baseBuildDir, '__game-scripts.js');
        try {
            await fs.access(gameScriptsPath);
        }
        catch (error) {
            // __game-scripts.js 不存在，跳过
            return html;
        }
        const gameScriptsCode = await fs.readFile(gameScriptsPath, 'utf-8');
        // 在 </head> 之前或第一个 <script> 标签之后插入游戏脚本
        // 确保在 PlayCanvas Engine 加载之后，但在 __start__.js 之前
        if (html.includes('</head>')) {
            html = html.replace('</head>', `<script>${gameScriptsCode}</script>\n</head>`);
        }
        else {
            // 如果没有 </head>，在第一个 <body> 标签之后插入
            html = html.replace('<body>', `<body>\n<script>${gameScriptsCode}</script>\n`);
        }
        return html;
    }
    /**
     * 内联 __start__.js
     */
    async inlineStart(html) {
        const startPath = path.join(this.baseBuildDir, '__start__.js');
        const startCode = await fs.readFile(startPath, 'utf-8');
        // 替换 script 标签
        const scriptPattern = /<script[^>]*src=["']__start__\.js["'][^>]*><\/script>/i;
        if (scriptPattern.test(html)) {
            html = html.replace(scriptPattern, `<script>${startCode}</script>`);
        }
        else {
            // 如果 HTML 中没有找到 script 标签，在 </body> 之前插入
            html = html.replace('</body>', `<script>${startCode}</script>\n</body>`);
        }
        return html;
    }
    /**
     * 内联 __loading__.js
     */
    async inlineLoading(html) {
        const loadingPath = path.join(this.baseBuildDir, '__loading__.js');
        const scriptPattern = /<script[^>]*src=["']__loading__\.js["'][^>]*><\/script>/i;
        try {
            await fs.access(loadingPath);
            // 文件存在，内联它
            const loadingCode = await fs.readFile(loadingPath, 'utf-8');
            if (scriptPattern.test(html)) {
                html = html.replace(scriptPattern, `<script>${loadingCode}</script>`);
            }
            else {
                // 如果 HTML 中没有找到 script 标签，在 </body> 之前添加
                html = html.replace('</body>', `<script>${loadingCode}</script>\n</body>`);
            }
        }
        catch (error) {
            // __loading__.js 不存在，移除 script 标签
            if (scriptPattern.test(html)) {
                html = html.replace(scriptPattern, '');
            }
        }
        return html;
    }
    /**
     * 获取大小报告
     */
    getSizeReport() {
        return this.sizeReport;
    }
}
