import fs from 'fs/promises';
import path from 'path';
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
// ESM 兼容：获取当前模块的目录路径
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ESM 兼容：创建 require 函数用于动态加载 JSON
const require = createRequire(import.meta.url);
/**
 * PlayCraft 平台名 → playable-scripts 渠道名映射
 * 大部分一致，少数需要映射
 */
const CHANNEL_MAP = {
    bigo: 'bigoads',
    playcraft: 'preview',
};
/**
 * PlayCraft 不支持的渠道（playable-scripts 不认识的 PlayCraft 平台）
 */
const UNSUPPORTED_CHANNELS = new Set(['remerge', 'mintegral']);
/**
 * 将 PlayCraft 平台名映射为 playable-scripts 渠道名
 */
function mapChannel(playcraftPlatform) {
    return CHANNEL_MAP[playcraftPlatform] ?? playcraftPlatform;
}
/**
 * PlayableScripts 构建工具适配器
 *
 * 职责：作为 PlayCraft 和 @playcraft/devkit 之间的中间层
 * - @playcraft/devkit 是 PlayCraft 平台内置的构建工具（已集成到 @playcraft/cli）
 * - 用户项目只需依赖 @playcraft/adsdk（运行时 SDK），不需要安装构建工具
 * - 通过平台内置的 devkit 调用 playable-scripts build/builds 命令
 * - 将 playcraft.config.json 的参数转换为 playable-scripts CLI 参数
 * - 收集构建产物并标记 skipChannelBuild
 *
 * 注意：产物已是最终格式（单 HTML），无需 ViteBuilder 二次处理
 */
export class PlayableScriptsAdapter {
    constructor(projectDir, options, config) {
        this.projectDir = projectDir;
        this.options = options;
        this.config = config;
        console.log(`[PlayableScriptsAdapter] 初始化: projectDir=${projectDir}`);
        if (config?.channels) {
            console.log(`[PlayableScriptsAdapter] 目标渠道: ${config.channels.join(', ')}`);
        }
    }
    /**
     * 执行 playable-scripts 构建
     *
     * 流程:
     * 1. 合并配置（playcraft.config.json → builds.config.js）
     * 2. 构建 CLI 参数
     * 3. 调用平台内置的 playable-scripts build/builds
     * 4. 收集构建产物
     * 5. 复制到输出目录
     * 6. 写入元数据（skipChannelBuild: true）
     *
     * 注意：不再在用户项目中执行 npm install，
     * devkit 是 PlayCraft 平台内置的构建工具。
     */
    async baseBuild() {
        console.log('[PlayableScriptsAdapter] 开始 playable-scripts 构建（使用平台内置 devkit）');
        // 1. 合并配置
        await this.mergeConfig();
        // 2. 构建 CLI 参数
        const cliArgs = this.buildCLIArgs();
        // 3. 打印构建参数信息
        this.logBuildParams();
        // 4. 清理输出目录（在 devkit 构建前清理，避免旧产物干扰）
        // 判断是否为批量构建模式（多个渠道或有主题）
        const isBatchMode = (this.config?.channels && this.config.channels.length > 1) ||
            (this.config?.themes?.enabled);
        if (isBatchMode) {
            // 批量构建：不预清理，让 devkit 来清理，避免 EBUSY 错误
            console.log('[PlayableScriptsAdapter] 批量构建模式：跳过预清理（由 devkit 负责）...');
        }
        else {
            // 单渠道构建：清理项目目录下的 dist 目录（playable-scripts 实际输出目录）
            const devkitOutputDir = path.join(this.projectDir, this.config?.outputDir ?? 'dist');
            console.log('[PlayableScriptsAdapter] 单渠道构建模式：清理 devkit 输出目录...');
            await this.cleanOutputDir(devkitOutputDir);
        }
        // 5. 执行 playable-scripts 构建（使用平台内置的 devkit）
        console.log('[PlayableScriptsAdapter] 执行 playable-scripts 构建...');
        await this.executePlayableScripts(cliArgs);
        // 6. 收集产物
        console.log('[PlayableScriptsAdapter] 收集构建产物...');
        const outputs = await this.collectOutputs();
        // 7. 复制产物到 PlayCraft outputDir
        const files = await this.copyToOutputDir(outputs);
        // 7. 构建元数据
        const metadata = {
            mode: 'classic',
            engine: 'phaser', // adsdk 项目通常是 Phaser 游戏
            buildTool: 'playable-scripts',
            skipChannelBuild: true, // 产物已是最终格式，跳过 ViteBuilder 阶段2
        };
        // 8. 保存元数据
        await this.saveBuildMetadata(metadata);
        console.log('[PlayableScriptsAdapter] 构建完成');
        return {
            outputDir: this.options.outputDir,
            metadata,
            files,
        };
    }
    // ==================== 1. 合并配置 ====================
    /**
     * 扫描项目中的可用主题
     * 主题目录: src/theme/ 下的子目录（排除 index.ts 等文件）
     */
    async scanThemes() {
        const themeDir = path.join(this.projectDir, 'src', 'theme');
        const themes = [];
        try {
            const entries = await fs.readdir(themeDir, { withFileTypes: true });
            for (const entry of entries) {
                // 只包含目录，且排除常见非主题目录
                if (entry.isDirectory() && !entry.name.startsWith('.') && !entry.name.startsWith('tiles-')) {
                    themes.push(entry.name);
                }
            }
        }
        catch {
            // 目录不存在或无法读取
            console.log('[PlayableScriptsAdapter] 未找到主题目录: src/theme/');
        }
        return themes.sort();
    }
    /**
     * 将 playcraft.config.json 的参数注入到项目的 builds.config.js
     * - storeUrls → googlePlayUrl / appStoreUrl
     * - naming → naming
     * - themes.enabled → 批量构建时自动禁用主题
     * - build.outputDir → 强制与 PlayCraft 输出目录一致
     */
    async mergeConfig() {
        // 判断是否为批量构建模式
        const isBatchMode = (this.config?.channels && this.config.channels.length > 1) ||
            (this.config?.themes?.enabled);
        // 读取或创建 builds.config.js
        const configPath = path.join(this.projectDir, 'builds.config.js');
        let configContent;
        try {
            configContent = await fs.readFile(configPath, 'utf-8');
            console.log('[PlayableScriptsAdapter] 读取现有 builds.config.js');
        }
        catch {
            configContent = 'module.exports = {};';
            console.log('[PlayableScriptsAdapter] builds.config.js 不存在，将创建');
        }
        // 构建注入内容
        const injections = [];
        // 强制覆盖 build.outputDir，确保与 PlayCraft 输出目录一致
        // devkit 把 build.outputDir 当作相对路径，与 projectDir 拼接
        // 所以需要传入相对路径，而不是绝对路径
        const outputDir = this.config?.outputDir ?? 'dist';
        const relativeOutputDir = path.isAbsolute(outputDir)
            ? path.relative(this.projectDir, outputDir) || 'dist'
            : outputDir;
        injections.push(`  build: { outputDir: ${JSON.stringify(relativeOutputDir)} }`);
        console.log(`[PlayableScriptsAdapter] 强制设置输出目录: ${relativeOutputDir}`);
        if (this.config?.naming) {
            injections.push(`  naming: ${JSON.stringify(this.config.naming)}`);
        }
        // 批量构建时处理主题配置
        if (isBatchMode) {
            // 扫描可用主题
            const availableThemes = await this.scanThemes();
            if (this.config?.themes?.whitelist && this.config.themes.whitelist.length > 0) {
                // 用户指定了主题白名单：启用主题切换
                const validThemes = this.config.themes.whitelist.filter(t => availableThemes.includes(t));
                if (validThemes.length > 0) {
                    injections.push(`  themes: { enabled: true, sourceDir: 'src/theme', entryFile: 'src/theme/index.ts' }`);
                    console.log(`[PlayableScriptsAdapter] 批量构建模式：启用主题切换 (${validThemes.length} 个主题)`);
                }
                else {
                    // 指定的主题不存在，禁用主题切换
                    injections.push(`  themes: { enabled: false }`);
                    console.log('[PlayableScriptsAdapter] 批量构建模式：指定主题未找到，使用默认主题');
                }
            }
            else {
                // 用户未指定主题：禁用主题切换，使用当前默认主题
                injections.push(`  themes: { enabled: false }`);
                console.log('[PlayableScriptsAdapter] 批量构建模式：使用当前默认主题');
            }
        }
        // 始终注入配置，确保 build.outputDir 被覆盖
        // 在 module.exports 的对象末尾注入配置
        // 策略：创建一个 wrapper 文件来合并配置
        // 注意：将 module.exports = {...} 转换为 return {...}
        const modifiedContent = configContent
            .replace(/module\.exports\s*=\s*/, 'return ')
            .replace(/;\s*$/, '');
        const wrapperContent = [
            `// PlayCraft 自动生成 - 合并 playcraft.config.json 配置`,
            `const originalConfig = (() => { ${modifiedContent} })();`,
            `module.exports = {`,
            `  ...originalConfig,`,
            ...injections.map(i => `${i},`),
            `};`,
        ].join('\n');
        // 备份原文件
        try {
            await fs.copyFile(configPath, `${configPath}.bak`);
        }
        catch {
            // 原文件不存在，无需备份
        }
        await fs.writeFile(configPath, wrapperContent, 'utf-8');
        console.log('[PlayableScriptsAdapter] 已注入配置到 builds.config.js');
    }
    // ==================== 4. 构建 CLI 参数 ====================
    /**
     * 将 PlayCraft 配置转换为 playable-scripts CLI 参数
     * ★ 始终显式传递 --obfuscate-level 和 --fflate-compression
     */
    buildCLIArgs() {
        const config = this.config ?? {};
        const args = [];
        // 混淆等级：config 指定 > PlayCraft 强制默认 4
        const obfuscateLevel = config.obfuscateLevel ?? PlayableScriptsAdapter.DEFAULTS.obfuscateLevel;
        args.push('--obfuscate-level', `${obfuscateLevel}`);
        // fflate 压缩：config 指定 > PlayCraft 强制默认 true
        const fflate = config.fflateCompression ?? PlayableScriptsAdapter.DEFAULTS.fflateCompression;
        args.push('--fflate-compression', `${fflate}`);
        // 渠道列表
        if (config.channels && config.channels.length > 0) {
            const mappedChannels = config.channels
                .filter(ch => !UNSUPPORTED_CHANNELS.has(ch))
                .map(ch => mapChannel(ch));
            if (mappedChannels.length > 0) {
                args.push('--channels', mappedChannels.join(','));
            }
        }
        // 商店跳转地址
        if (config.storeUrls) {
            if (config.storeUrls.ios) {
                args.push('--app-store-url', config.storeUrls.ios);
            }
            if (config.storeUrls.android) {
                args.push('--google-play-url', config.storeUrls.android);
            }
        }
        // 主题配置
        if (config.themes?.enabled && config.themes.whitelist && config.themes.whitelist.length > 0) {
            args.push('--themes', config.themes.whitelist.join(','));
        }
        // 并发数
        if (config.parallel) {
            args.push('--parallel', `${config.parallel}`);
        }
        // 输出目录 - 统一使用 dist，与 PlayCraft 标准一致
        const outputDir = config.outputDir ?? 'dist';
        args.push('--out-dir', outputDir);
        // ZIP 输出：仅 ZIP-only 渠道、显式 config.zip、或构建请求 format=zip（如 Google / Facebook 选 ZIP）
        const isZipFormat = this.isZipOutput();
        if (isZipFormat) {
            args.push('--zip');
        }
        // 自定义文件名格式 - 根据平台使用 PlayCraft 标准命名
        const platformFilename = this.getPlatformFilename(isZipFormat);
        const filename = config.filename || platformFilename;
        if (filename) {
            args.push('--filename', filename);
        }
        // 所有渠道输出为 ZIP
        if (config.isChannelFold2zip) {
            args.push('--is-channel-fold2zip');
        }
        return args;
    }
    // ==================== 3. 日志输出 ====================
    logBuildParams() {
        const config = this.config ?? {};
        const obfuscateLevel = config.obfuscateLevel ?? PlayableScriptsAdapter.DEFAULTS.obfuscateLevel;
        const fflate = config.fflateCompression ?? PlayableScriptsAdapter.DEFAULTS.fflateCompression;
        const levelLabels = {
            1: 'minimum - 基础',
            2: 'medium - 中等',
            3: 'high - 较高',
            4: 'maximum - 全部拉满',
        };
        console.log(`[PlayCraft] 🔧 使用 @playcraft/devkit 构建工具`);
        console.log(`[PlayCraft] 📋 构建参数:`);
        console.log(`[PlayCraft]   - 混淆等级: ${obfuscateLevel} (${levelLabels[obfuscateLevel] ?? 'unknown'})`);
        console.log(`[PlayCraft]   - fflate 压缩: ${fflate ? '开启 (压缩率 ~66-68%)' : '关闭'}`);
        if (config.channels && config.channels.length > 0) {
            console.log(`[PlayCraft]   - 目标渠道: ${config.channels.join(', ')}`);
        }
        if (config.themes?.enabled) {
            console.log(`[PlayCraft]   - 主题: ${config.themes.whitelist?.join(', ') ?? '全部'}`);
        }
        console.log(`[PlayCraft]   - 输出目录: ${config.outputDir ?? 'dist'}`);
        // 警告：如果用户覆盖了默认值
        if (config.obfuscateLevel !== undefined && config.obfuscateLevel < PlayableScriptsAdapter.DEFAULTS.obfuscateLevel) {
            console.log(`[PlayCraft] ⚠️ 混淆等级已从默认值 ${PlayableScriptsAdapter.DEFAULTS.obfuscateLevel} 降级到 ${config.obfuscateLevel} (来自 playcraft.config.json)`);
            console.log(`[PlayCraft] ⚠️ 注意: 降级混淆会减弱代码保护，仅建议在体积超限时使用`);
        }
        if (config.fflateCompression === false) {
            console.log(`[PlayCraft] ⚠️ fflate 压缩已关闭 (来自 playcraft.config.json)`);
            console.log(`[PlayCraft] ⚠️ 注意: 关闭压缩会增大产物体积，仅建议在调试时使用`);
        }
    }
    /**
     * 自动迁移用户项目中的旧包名引用
     * 将源码中的旧包名替换为新包名
     */
    async migratePackageNames() {
        const srcDir = path.join(this.projectDir, 'src');
        try {
            await fs.access(srcDir);
        }
        catch {
            // src 目录不存在，跳过
            return;
        }
        let hasMigration = false;
        const migrations = PlayableScriptsAdapter.PACKAGE_MIGRATIONS;
        // 递归查找所有 .ts 和 .js 文件
        const files = await this.findSourceFiles(srcDir);
        for (const filePath of files) {
            try {
                const content = await fs.readFile(filePath, 'utf-8');
                let newContent = content;
                let fileMigrated = false;
                for (const [oldPkg, newPkg] of Object.entries(migrations)) {
                    if (newContent.includes(oldPkg)) {
                        // 使用 split/join 替代 replaceAll 以兼容旧版 Node.js
                        newContent = newContent.split(oldPkg).join(newPkg);
                        fileMigrated = true;
                    }
                }
                if (fileMigrated) {
                    await fs.writeFile(filePath, newContent, 'utf-8');
                    console.log(`[PlayableScriptsAdapter] 📝 已迁移: ${path.relative(this.projectDir, filePath)}`);
                    hasMigration = true;
                }
            }
            catch (error) {
                // 读取或写入失败，跳过
                console.log(`[PlayableScriptsAdapter] ⚠️  无法处理文件: ${filePath}`);
            }
        }
        if (hasMigration) {
            console.log('[PlayableScriptsAdapter] ✅ 包名迁移完成');
        }
    }
    /**
     * 递归查找源码文件
     */
    async findSourceFiles(dir) {
        const files = [];
        const entries = await fs.readdir(dir, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = path.join(dir, entry.name);
            if (entry.isDirectory()) {
                // 排除 node_modules 和 .git
                if (entry.name !== 'node_modules' && entry.name !== '.git') {
                    files.push(...await this.findSourceFiles(fullPath));
                }
            }
            else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
                files.push(fullPath);
            }
        }
        return files;
    }
    /**
     * 准备构建环境
     * - 执行包名迁移
     * - 确保项目本地 devkit 和 loader 可用
     */
    async prepareBuildEnvironment() {
        // 1. 执行包名迁移
        await this.migratePackageNames();
        // 2. 确保项目本地存在 devkit 和构建依赖
        const localDevkit = await this.ensureLocalDevkit();
        if (!localDevkit) {
            throw new Error('[PlayableScriptsAdapter] 无法安装项目本地的 @playcraft/devkit。\n' +
                '请检查网络连接或手动运行: npm install --save-dev @playcraft/devkit');
        }
        console.log('[PlayableScriptsAdapter] ✅ 使用项目本地 devkit');
        // 3. 确保构建 loader 存在
        await this.ensureBuildLoaders();
    }
    /**
     * 调用 playable-scripts 命令
     *
     * 流程:
     * 1. 准备构建环境（包名迁移、安装本地 devkit 和 loader）
     * 2. 执行构建（使用项目本地 devkit）
     *
     * 单渠道模式: <local-devkit> build <channel> [options]
     * 批量模式:   <local-devkit> builds [options]
     */
    async executePlayableScripts(cliArgs) {
        // 1. 执行包名迁移
        await this.migratePackageNames();
        // 2. 确保项目本地存在 devkit 和构建依赖
        const localDevkit = await this.ensureLocalDevkit();
        if (!localDevkit) {
            throw new Error('[PlayableScriptsAdapter] 无法安装或找到项目本地的 @playcraft/devkit。\n' +
                '请检查网络连接或手动运行: npm install --save-dev @playcraft/devkit');
        }
        console.log(`[PlayableScriptsAdapter] ✅ 使用项目本地 devkit: ${localDevkit}`);
        // 3. 确保构建 loader 存在
        await this.ensureBuildLoaders();
        const config = this.config ?? {};
        const isBatchMode = (config.channels && config.channels.length > 1) ||
            (config.themes?.enabled);
        // 构建命令参数
        let args;
        let modeLabel;
        if (isBatchMode) {
            // 批量模式: <devkit> builds [options]
            args = ['builds', ...cliArgs];
            // 扫描可用主题
            const availableThemes = await this.scanThemes();
            // 确定要构建的主题列表
            let selectedThemes = [];
            if (config.themes?.whitelist && config.themes.whitelist.length > 0) {
                // 使用配置中指定的主题白名单，清理可能的引号
                const cleanWhitelist = config.themes.whitelist.map(t => t.replace(/^["']|["']$/g, ''));
                selectedThemes = cleanWhitelist.filter(t => availableThemes.includes(t));
                if (selectedThemes.length === 0) {
                    console.log(`[PlayableScriptsAdapter] ⚠️ 配置的主题未找到，使用当前默认主题`);
                    selectedThemes = [];
                }
            }
            // 如果有指定主题，传入 -t 参数
            if (selectedThemes.length > 0) {
                args.push('-t', selectedThemes.join(','));
                console.log(`[PlayableScriptsAdapter] 构建主题: ${selectedThemes.join(', ')}`);
            }
            else {
                // 未指定主题：使用 devkit 默认行为（当前 index.ts 指向的主题）
                console.log(`[PlayableScriptsAdapter] 使用当前默认主题 (src/theme/index.ts)`);
            }
            modeLabel = '批量构建模式';
        }
        else {
            // 单渠道模式: <devkit> build <channel> [options]
            const channel = config.channels?.[0]
                ? mapChannel(config.channels[0])
                : 'google';
            const filteredArgs = this.removeArg(cliArgs, '--channels');
            args = ['build', channel, ...filteredArgs];
            modeLabel = `单渠道构建模式: ${channel}`;
        }
        console.log(`[PlayableScriptsAdapter] ${modeLabel}`);
        // devkit 必须通过 npm script 调用，不能直接执行 .js 文件
        // 1. 确保 package.json 中有 build/builds 脚本
        await this.ensureBuildScripts();
        // 2. 通过 pnpm run 执行
        const scriptName = isBatchMode ? 'builds' : 'build';
        // 单渠道: args = ['build', channel, ...options]，需要传递 [channel, ...options]
        // 批量:   args = ['builds', ...options]，需要传递 [...options]
        const runArgs = isBatchMode
            ? args.slice(1) // builds 命令，去掉第一个 'builds'
            : args.slice(1); // build 命令，去掉第一个 'build'，保留 channel 和所有参数
        console.log(`[PlayableScriptsAdapter] 运行: pnpm run ${scriptName} ${runArgs.length > 0 ? runArgs.join(' ') : ''}`);
        try {
            // 注意: package.json 的 scripts 已经是 "playable-scripts build/builds"
            // 所以不需要 '--' 分隔符，直接传参数即可
            const pnpmArgs = runArgs.length > 0
                ? ['run', scriptName, ...runArgs]
                : ['run', scriptName];
            await this.runCommandWithTimeout('pnpm', pnpmArgs, {
                cwd: this.projectDir,
                timeout: 30 * 60 * 1000, // 30 分钟
                env: {
                    ...process.env,
                    NODE_ENV: 'production',
                    CI: 'true',
                },
            });
            console.log('[PlayableScriptsAdapter] playable-scripts 构建命令执行完成');
        }
        catch (error) {
            console.error('[PlayableScriptsAdapter] ❌ playable-scripts 构建失败:');
            console.error(error.message || error);
            throw error;
        }
        // 检查输出目录是否存在
        const outputDir = config.outputDir ?? 'dist';
        const fullOutputDir = path.isAbsolute(outputDir)
            ? outputDir
            : path.join(this.projectDir, outputDir);
        try {
            await fs.access(fullOutputDir);
            const entries = await fs.readdir(fullOutputDir);
            console.log(`[PlayableScriptsAdapter] ✓ 输出目录存在: ${fullOutputDir} (${entries.length} 项)`);
            if (entries.length === 0) {
                console.warn(`[PlayableScriptsAdapter] ⚠️ 输出目录为空，构建可能失败`);
            }
        }
        catch {
            console.error(`[PlayableScriptsAdapter] ❌ 输出目录不存在: ${fullOutputDir}`);
            console.error(`[PlayableScriptsAdapter] 请检查 devkit 构建日志，确认构建是否成功`);
        }
    }
    /**
     * 从参数列表中移除指定参数及其值
     */
    removeArg(args, argName) {
        const result = [];
        for (let i = 0; i < args.length; i++) {
            if (args[i] === argName) {
                i++; // 跳过参数值
            }
            else {
                result.push(args[i]);
            }
        }
        return result;
    }
    /**
     * 查找项目本地的 playable-scripts 可执行文件
     * 优先查找项目 node_modules 中的版本，确保 webpack 能正确解析 loader
     */
    async findLocalPlayableScripts() {
        // 可能的可执行文件路径（Windows 和非 Windows）
        const possiblePaths = [
            path.join(this.projectDir, 'node_modules', '.bin', 'playable-scripts.cmd'), // Windows cmd
            path.join(this.projectDir, 'node_modules', '.bin', 'playable-scripts.ps1'), // Windows PowerShell
            path.join(this.projectDir, 'node_modules', '.bin', 'playable-scripts'), // Unix/Mac
            path.join(this.projectDir, 'node_modules', '@playcraft', 'devkit', 'cli', 'bin', 'playable-scripts.js'),
        ];
        for (const executablePath of possiblePaths) {
            try {
                await fs.access(executablePath);
                console.log(`[PlayableScriptsAdapter] 找到本地 playable-scripts: ${executablePath}`);
                return executablePath;
            }
            catch {
                // 文件不存在，继续尝试下一个
            }
        }
        // 尝试通过 require.resolve 查找（处理 pnpm 等特殊情况）
        try {
            const devkitPath = require.resolve('@playcraft/devkit/package.json', {
                paths: [this.projectDir],
            });
            const devkitDir = path.dirname(devkitPath);
            // 尝试常见的 CLI 路径
            const possiblePaths = [
                path.join(devkitDir, 'cli', 'bin', 'playable-scripts.js'),
                path.join(devkitDir, 'dist', 'cli.js'),
                path.join(devkitDir, 'bin', 'playable-scripts.js'),
            ];
            for (const binPath of possiblePaths) {
                try {
                    await fs.access(binPath);
                    console.log(`[PlayableScriptsAdapter] 通过 require.resolve 找到本地 playable-scripts: ${binPath}`);
                    return binPath;
                }
                catch {
                    // 继续尝试下一个
                }
            }
        }
        catch {
            // 未找到
        }
        console.log('[PlayableScriptsAdapter] 未在项目 node_modules 中找到 playable-scripts');
        return null;
    }
    /**
     * 确保项目本地存在 playable-scripts（@playcraft/devkit）
     * 若缺失则执行 npm install 安装 package.json 中定义的所有依赖
     */
    async ensureLocalDevkit() {
        const found = await this.findLocalPlayableScripts();
        if (found) {
            return found;
        }
        console.log('[PlayableScriptsAdapter] 未找到本地 playable-scripts，执行 pnpm install 安装项目依赖');
        try {
            // 执行 pnpm install 安装 package.json 中定义的所有依赖
            // 包括 @playcraft/devkit 和 babel-loader 等构建工具
            // 注意：不要设置 NODE_ENV=production，否则会跳过 devDependencies
            // 创建 .npmrc 文件，允许所有构建脚本执行
            // 这样可以让 esbuild 等 native 模块正确编译
            const npmrcPath = path.join(this.projectDir, '.npmrc');
            const npmrcContent = [
                '# Temporary config for PlayCraft build',
                'enable-pre-post-scripts=true',
                'shamefully-hoist=true',
            ].join('\n');
            try {
                await fs.writeFile(npmrcPath, npmrcContent, 'utf-8');
                console.log('[PlayableScriptsAdapter] 已创建 .npmrc 配置（允许构建脚本）');
            }
            catch (error) {
                console.log('[PlayableScriptsAdapter] ⚠️ 无法创建 .npmrc:', error.message);
            }
            await this.runCommandWithTimeout('pnpm', ['install'], {
                cwd: this.projectDir,
                timeout: 5 * 60 * 1000,
                env: process.env,
            });
            // 清理临时 .npmrc
            try {
                await fs.unlink(npmrcPath);
                console.log('[PlayableScriptsAdapter] 已清理临时 .npmrc');
            }
            catch {
                // 忽略删除错误
            }
            // 安装完成后，尝试通过 require.resolve 查找 devkit 路径
            try {
                const devkitPath = require.resolve('@playcraft/devkit/package.json', {
                    paths: [this.projectDir],
                });
                const devkitDir = path.dirname(devkitPath);
                // 尝试常见的 CLI 路径
                const possiblePaths = [
                    path.join(devkitDir, 'cli', 'bin', 'playable-scripts.js'),
                    path.join(devkitDir, 'dist', 'cli.js'),
                    path.join(devkitDir, 'bin', 'playable-scripts.js'),
                ];
                for (const binPath of possiblePaths) {
                    try {
                        await fs.access(binPath);
                        console.log(`[PlayableScriptsAdapter] 安装后找到 playable-scripts: ${binPath}`);
                        return binPath;
                    }
                    catch {
                        // 继续尝试下一个
                    }
                }
                console.error(`[PlayableScriptsAdapter] ❌ 找到 devkit 但无法定位 CLI: ${devkitDir}`);
                return null;
            }
            catch {
                // 如果 require.resolve 失败，回退到 findLocalPlayableScripts
            }
            // 再次尝试查找
            return await this.findLocalPlayableScripts();
        }
        catch (error) {
            console.log(`[PlayableScriptsAdapter] ⚠️ npm install 失败: ${error.message}`);
            return null;
        }
    }
    /**
     * 检查构建所需 loader 是否存在（babel-loader）
     * 注意：babel-loader 是必须的，esbuild-loader/esbuild 是 devkit 内部可选依赖
     */
    async ensureBuildLoaders() {
        const required = ['babel-loader'];
        const missing = [];
        for (const name of required) {
            const modulePath = path.join(this.projectDir, 'node_modules', name);
            try {
                await fs.access(modulePath);
            }
            catch {
                missing.push(name);
            }
        }
        if (missing.length > 0) {
            console.log(`[PlayableScriptsAdapter] ⚠️ 以下构建依赖缺失: ${missing.join(', ')}`);
            console.log(`[PlayableScriptsAdapter] 请确保这些依赖已在 package.json 的 devDependencies 中定义`);
        }
    }
    /**
     * 确保 package.json 中有 build/builds 脚本
     * devkit 必须通过 npm script 调用，不能直接执行 .js 文件
     */
    async ensureBuildScripts() {
        const pkgPath = path.join(this.projectDir, 'package.json');
        try {
            const pkgContent = await fs.readFile(pkgPath, 'utf-8');
            const pkg = JSON.parse(pkgContent);
            if (!pkg.scripts) {
                pkg.scripts = {};
            }
            let modified = false;
            // 确保有 build 脚本
            if (!pkg.scripts.build) {
                pkg.scripts.build = 'playable-scripts build';
                modified = true;
                console.log('[PlayableScriptsAdapter] 注入 package.json script: build');
            }
            // 确保有 builds 脚本
            if (!pkg.scripts.builds) {
                pkg.scripts.builds = 'playable-scripts builds';
                modified = true;
                console.log('[PlayableScriptsAdapter] 注入 package.json script: builds');
            }
            if (modified) {
                await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf-8');
                console.log('[PlayableScriptsAdapter] ✓ package.json 已更新');
            }
        }
        catch (error) {
            console.warn('[PlayableScriptsAdapter] ⚠️ 无法更新 package.json:', error.message);
        }
    }
    // ==================== 7. 收集产物 ====================
    /**
     * 扫描 playable-scripts 的输出目录，收集构建产物
     *
     * 单渠道输出: dist/<file>.html
     * 批量输出: dist/<theme>/<channel>/<file>.html
     */
    async collectOutputs() {
        const config = this.config ?? {};
        // 统一使用 dist 作为默认输出目录
        // 注意：config.outputDir 可能是绝对路径，需要判断
        const outputDir = config.outputDir ?? 'dist';
        const fullOutputDir = path.isAbsolute(outputDir)
            ? outputDir
            : path.join(this.projectDir, outputDir);
        const outputs = [];
        console.log(`[PlayableScriptsAdapter] 开始收集产物，目标目录: ${fullOutputDir}`);
        // 检查目录是否存在
        try {
            await fs.access(fullOutputDir);
            console.log(`[PlayableScriptsAdapter] 目录存在: ${fullOutputDir}`);
        }
        catch {
            console.log(`[PlayableScriptsAdapter] 目录不存在: ${fullOutputDir}`);
            throw new Error(`[PlayableScriptsAdapter] 未找到构建输出目录\n` +
                `目录: ${fullOutputDir}\n` +
                `请确保 playable-scripts 构建正确完成。`);
        }
        // 递归扫描输出目录
        await this.scanOutputDir(fullOutputDir, fullOutputDir, outputs);
        if (outputs.length === 0) {
            // 目录存在但没有产物，列出目录内容帮助调试
            try {
                const entries = await fs.readdir(fullOutputDir, { withFileTypes: true });
                console.log(`[PlayableScriptsAdapter] 目录内容 (${entries.length} 项):`);
                for (const entry of entries) {
                    console.log(`  - ${entry.name} (${entry.isDirectory() ? 'dir' : 'file'})`);
                    // 如果是目录，列出其子目录
                    if (entry.isDirectory()) {
                        const subDir = path.join(fullOutputDir, entry.name);
                        const subEntries = await fs.readdir(subDir, { withFileTypes: true });
                        for (const subEntry of subEntries) {
                            console.log(`    - ${subEntry.name} (${subEntry.isDirectory() ? 'dir' : 'file'})`);
                        }
                    }
                }
            }
            catch (e) {
                console.log(`[PlayableScriptsAdapter] 无法读取目录: ${e}`);
            }
            throw new Error(`[PlayableScriptsAdapter] 构建输出目录为空\n` +
                `目录: ${fullOutputDir}\n` +
                `请确保 playable-scripts 构建正确完成。`);
        }
        console.log(`[PlayableScriptsAdapter] 找到 ${outputs.length} 个构建产物:`);
        for (const output of outputs) {
            console.log(`  - ${output.fileName} (channel: ${output.channel}, theme: ${output.theme || 'none'})`);
        }
        return outputs;
    }
    /**
     * 递归扫描输出目录，收集 HTML/ZIP 文件
     *
     * devkit 的输出结构:
     * - 批量构建: dist/<theme>/<channel>/<file>.html|zip
     * - 单渠道:   dist/<file>.html|zip
     *
     * 注意：只收集目标渠道的产物，过滤掉其他渠道
     */
    async scanOutputDir(dir, baseDir, outputs) {
        const entries = await fs.readdir(dir, { withFileTypes: true });
        // 获取目标渠道列表（已映射）
        const targetChannels = (this.config?.channels ?? [])
            .filter(ch => !UNSUPPORTED_CHANNELS.has(ch))
            .map(ch => mapChannel(ch));
        for (const entry of entries) {
            const fullPath = path.join(dir, entry.name);
            if (entry.isDirectory()) {
                await this.scanOutputDir(fullPath, baseDir, outputs);
            }
            else if (entry.name.endsWith('.html') || entry.name.endsWith('.zip')) {
                // 从相对路径推断 theme 和 channel
                const relativePath = path.relative(baseDir, fullPath);
                // 统一使用正斜杠分割，兼容 Windows 和 Unix
                const parts = relativePath.split(/[\\/]/);
                let theme;
                let channel = 'unknown';
                if (parts.length >= 3) {
                    // dist/<theme>/<channel>/<file>.html
                    theme = parts[0];
                    channel = parts[1];
                }
                else if (parts.length === 2) {
                    // dist/<channel>/<file>.html 或 dist/<theme>/<file>.html
                    // 判断第一个部分是渠道还是主题
                    const firstPart = parts[0];
                    if (targetChannels.includes(firstPart)) {
                        channel = firstPart;
                    }
                    else {
                        // 可能是主题目录（没有渠道子目录）
                        // 这种情况不太可能，但保留兼容性
                        theme = firstPart;
                    }
                }
                // parts.length === 1: 直接在 dist 下的文件（单渠道模式）
                // 过滤：只收集目标渠道的产物
                if (targetChannels.length > 0 && channel !== 'unknown' && !targetChannels.includes(channel)) {
                    console.log(`[PlayableScriptsAdapter] 跳过非目标渠道: ${channel} (文件: ${relativePath})`);
                    continue;
                }
                outputs.push({
                    filePath: fullPath,
                    fileName: entry.name,
                    channel,
                    theme,
                });
            }
        }
    }
    // ==================== 6. 复制到输出目录 ====================
    /**
     * 清理输出目录，删除所有旧产物
     */
    async cleanOutputDir(dir) {
        try {
            await fs.access(dir);
            // 目录存在，递归删除所有内容
            const entries = await fs.readdir(dir, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = path.join(dir, entry.name);
                if (entry.isDirectory()) {
                    await this.removeDirRecursive(fullPath);
                }
                else {
                    await fs.unlink(fullPath);
                }
            }
            console.log(`[PlayableScriptsAdapter] 已清理输出目录: ${dir}`);
        }
        catch {
            // 目录不存在，无需清理
        }
    }
    /**
     * 递归删除目录
     */
    async removeDirRecursive(dir) {
        const entries = await fs.readdir(dir, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = path.join(dir, entry.name);
            if (entry.isDirectory()) {
                await this.removeDirRecursive(fullPath);
            }
            else {
                await fs.unlink(fullPath);
            }
        }
        await fs.rmdir(dir);
    }
    async copyToOutputDir(outputs) {
        const files = {
            html: '',
            engine: null,
            config: '',
            settings: null,
            modules: null,
            start: '',
            scenes: [],
            assets: [],
        };
        // 判断构建模式
        const isBatchMode = (this.config?.channels && this.config.channels.length > 1) ||
            (this.config?.themes?.enabled);
        // 按渠道分组产物（每个渠道只保留一个产物，优先 HTML，其次 ZIP）
        const channelOutputs = new Map();
        for (const output of outputs) {
            const channelList = channelOutputs.get(output.channel) || [];
            channelList.push(output);
            channelOutputs.set(output.channel, channelList);
        }
        console.log(`[PlayableScriptsAdapter] ${isBatchMode ? '批量构建' : '单渠道构建'}模式：处理 ${channelOutputs.size} 个渠道的产物`);
        // 记录需要清理的所有主题目录（从所有产物中收集，不只是最佳产物）
        const themeDirsToClean = new Set();
        for (const output of outputs) {
            if (output.theme) {
                const themeDir = path.join(this.options.outputDir, output.theme);
                themeDirsToClean.add(themeDir);
            }
        }
        for (const [channel, channelFiles] of channelOutputs) {
            // 选择最佳产物：优先带渠道名的文件，其次 HTML，最后 ZIP
            let bestOutput = channelFiles[0];
            for (const output of channelFiles) {
                // 优先选择带有完整命名的文件（如 xxx-facebook.html）
                if (output.fileName.includes(channel) || output.fileName.includes('-')) {
                    bestOutput = output;
                    break;
                }
                // 其次优先 HTML
                if (output.fileName.endsWith('.html') && bestOutput.fileName.endsWith('.zip')) {
                    bestOutput = output;
                }
            }
            // 确定目标文件名
            let destFileName;
            // HTML 文件统一重命名为 index.html（与 PlayCanvas 项目保持一致）
            if (bestOutput.fileName.endsWith('.html')) {
                destFileName = 'index.html';
            }
            else if (bestOutput.fileName.endsWith('.zip')) {
                // ZIP 文件统一重命名为 playable.zip（与 PlayCanvas 项目保持一致）
                destFileName = 'playable.zip';
            }
            else {
                destFileName = bestOutput.fileName;
            }
            // 确定目标路径
            let destPath;
            if (isBatchMode) {
                // 批量构建：按渠道分目录 outputDir/<channel>/<file>
                const subDir = path.join(this.options.outputDir, channel);
                await fs.mkdir(subDir, { recursive: true });
                destPath = path.join(subDir, destFileName);
            }
            else {
                // 单渠道构建：直接放到 outputDir/<file>
                // 确保输出目录存在
                await fs.mkdir(this.options.outputDir, { recursive: true });
                destPath = path.join(this.options.outputDir, destFileName);
            }
            await fs.copyFile(bestOutput.filePath, destPath);
            console.log(`[PlayableScriptsAdapter] 复制: ${path.relative(this.projectDir, bestOutput.filePath)} -> ${path.relative(this.projectDir, destPath)}`);
            // 第一个 HTML 文件作为主产物
            if (!files.html && destFileName.endsWith('.html')) {
                files.html = destPath;
            }
            // 所有产物记录到 assets
            files.assets.push(destPath);
        }
        // 批量构建模式：清理 devkit 的原始主题目录
        if (isBatchMode && themeDirsToClean.size > 0) {
            console.log('[PlayableScriptsAdapter] 批量构建模式：清理 devkit 原始目录...');
            for (const dir of themeDirsToClean) {
                try {
                    await this.removeDirRecursive(dir);
                    console.log(`[PlayableScriptsAdapter] 已清理主题目录: ${path.relative(this.projectDir, dir)}`);
                }
                catch (err) {
                    console.log(`[PlayableScriptsAdapter] 清理目录失败: ${dir} - ${err}`);
                }
            }
        }
        console.log(`[PlayableScriptsAdapter] 产物已复制到: ${this.options.outputDir}`);
        return files;
    }
    /**
     * 是否对 playable-scripts 使用 --zip。
     * Google/Facebook 等双格式渠道默认 HTML，与 CLI、Portal、GoogleAdapter 一致；仅 ZIP 的渠道仍强制 ZIP。
     */
    isZipOutput() {
        const config = this.config ?? {};
        const channels = config.channels ?? ['google'];
        if (config.zip === true) {
            return true;
        }
        const zipOnlyPlatforms = new Set([
            'tiktok',
            'snapchat',
            'liftoff',
            'bigo',
            'inmobi',
            'mintegral',
        ]);
        if (channels.some((ch) => zipOnlyPlatforms.has(ch))) {
            return true;
        }
        const fmt = this.options.format;
        return fmt === 'zip';
    }
    /**
     * 获取平台默认的文件名格式
     */
    getPlatformFilename(isZipFormat) {
        const config = this.config ?? {};
        const channels = config.channels ?? ['google'];
        const channel = channels[0] || 'google';
        if (isZipFormat) {
            // ZIP 格式统一使用 playable.zip
            return 'playable';
        }
        // HTML 格式根据平台使用不同的文件名
        const platformFileNames = {
            'facebook': 'index',
            'snapchat': 'ad',
            'applovin': 'index',
            'ironsource': 'index',
            'unity': 'index',
            'moloco': 'index',
            'adikteev': 'index',
            'remerge': 'index',
        };
        return platformFileNames[channel] || 'index';
    }
    // ==================== 9. 保存元数据 ====================
    async saveBuildMetadata(metadata) {
        // 不再保存 .build-metadata.json 文件，元数据通过返回值传递
        console.log(`[PlayableScriptsAdapter] 构建完成（buildTool: playable-scripts, skipChannelBuild: true）`);
    }
    // ==================== 工具方法 ====================
    /**
     * 执行命令并设置超时
     */
    runCommandWithTimeout(cmd, args, options) {
        return new Promise((resolve, reject) => {
            const controller = new AbortController();
            const { signal } = controller;
            const timeoutId = setTimeout(() => {
                controller.abort();
                reject(new Error(`[PlayableScriptsAdapter] 命令超时（${options.timeout / 1000}秒）\n` +
                    `命令: ${cmd} ${args.join(' ')}\n` +
                    `playable-scripts 批量构建可能需要更长时间，请考虑减少渠道/主题数量。`));
            }, options.timeout);
            // shell: true 时 Node.js 把 cmd + args 拼接成 shell 字符串，
            // 但不会转义 args 中的特殊字符（如 URL 里的 &）。
            // 需要按平台分别处理：
            //  - Windows (cmd.exe) 不认识单引号，用双引号；cmd 的特殊字符用 ^ 转义
            //  - POSIX (bash/sh) 使用单引号强引用
            const isWindows = process.platform === 'win32';
            const shellSafeArgs = args.map((a) => {
                if (isWindows) {
                    // cmd.exe：用双引号包裹，内部的 " 转义为 ""，其它 shell 元字符用 ^ 转义
                    const escaped = a.replace(/"/g, '""').replace(/([&|<>^%])/g, '^$1');
                    return `"${escaped}"`;
                }
                // POSIX：单引号包裹，单引号本身通过 '\'' 断开转义
                return `'${a.replace(/'/g, "'\\''")}'`;
            });
            const child = spawn(cmd, shellSafeArgs, {
                cwd: options.cwd,
                env: options.env,
                stdio: 'pipe',
                signal,
                shell: true, // Windows 下必须，否则无法找到 pnpm/yarn 等命令
            });
            let stdout = '';
            let stderr = '';
            child.stdout?.on('data', (data) => {
                const text = data.toString();
                stdout += text;
                // 实时输出 playable-scripts 的日志
                process.stdout.write(text);
                if (stdout.length > 50000) {
                    stdout = stdout.slice(-25000);
                }
            });
            child.stderr?.on('data', (data) => {
                const text = data.toString();
                stderr += text;
                process.stderr.write(text);
                if (stderr.length > 50000) {
                    stderr = stderr.slice(-25000);
                }
            });
            child.on('error', (error) => {
                clearTimeout(timeoutId);
                if (error.name === 'AbortError') {
                    reject(new Error(`命令被中止: ${cmd} ${args.join(' ')}`));
                }
                else {
                    reject(new Error(`[PlayableScriptsAdapter] 命令执行失败\n` +
                        `命令: ${cmd} ${args.join(' ')}\n` +
                        `错误: ${error.message}`));
                }
            });
            child.on('close', (code) => {
                clearTimeout(timeoutId);
                if (code === 0) {
                    resolve();
                }
                else {
                    reject(new Error(`[PlayableScriptsAdapter] 命令退出码非零: ${code}\n` +
                        `命令: ${cmd} ${args.join(' ')}\n` +
                        `stdout: ${stdout.slice(-2000)}\n` +
                        `stderr: ${stderr.slice(-2000)}`));
                }
            });
        });
    }
}
// PlayCraft 强制默认值（不依赖 playable-scripts 的默认值）
PlayableScriptsAdapter.DEFAULTS = {
    obfuscateLevel: 4,
    fflateCompression: true,
};
// ==================== 4. 执行 playable-scripts ====================
/**
 * 包名迁移映射表
 * 旧包名 -> 新包名
 */
PlayableScriptsAdapter.PACKAGE_MIGRATIONS = {
    '@tencent/playable-sdk': '@playcraft/adsdk',
    '@tencent/playable-scripts': '@playcraft/devkit',
};
