import fs from 'fs/promises';
import path from 'path';
import { spawn } from 'child_process';
/**
 * 通用引擎适配器
 * 负责外部引擎（Phaser、Three.js、PixiJS 等）的 Base Build
 * 核心逻辑：npm install + npm run build
 */
export class GenericAdapter {
    constructor(projectDir, options, engine) {
        this.projectDir = projectDir;
        this.options = options;
        this.engine = engine;
        console.log(`[GenericAdapter] 初始化: projectDir=${projectDir}, engine=${engine}`);
    }
    /**
     * 执行通用基础构建
     * 流程：
     * 1. 检测包管理器
     * 2. 检查 node_modules（存在则跳过安装）
     * 3. 执行 npm install（超时 3 分钟）
     * 4. 检测 build script
     * 5. 执行 npm run build（超时 5 分钟）
     * 6. 查找构建产物目录
     * 7. 复制构建产物到 outputDir
     * 8. 写入 .build-metadata.json
     */
    async baseBuild() {
        console.log(`[GenericAdapter] 开始构建引擎: ${this.engine}`);
        // 1. 检测包管理器
        const packageManager = await this.detectPackageManager();
        console.log(`[GenericAdapter] 检测到包管理器: ${packageManager}`);
        // 2. 检查 node_modules 是否存在
        const hasNodeModules = await this.checkNodeModules();
        if (hasNodeModules) {
            console.log('[GenericAdapter] node_modules 已存在，跳过依赖安装');
        }
        else {
            // 3. 执行安装
            console.log('[GenericAdapter] 执行依赖安装...');
            await this.runNpmInstall(packageManager);
        }
        // 4. 检测构建命令
        const buildScript = await this.detectBuildScript();
        if (!buildScript) {
            throw new Error(`[GenericAdapter] 未找到 build script\n` +
                `请确保 package.json 中包含 scripts.build 字段。`);
        }
        console.log(`[GenericAdapter] 检测到构建命令: ${buildScript}`);
        // 5. 执行构建
        console.log('[GenericAdapter] 执行项目构建...');
        const originalBuildDir = await this.runNpmBuild(packageManager, buildScript);
        // 6. 复制构建产物
        console.log('[GenericAdapter] 复制构建产物...');
        const files = await this.copyBuildOutput(originalBuildDir);
        // 7. 构建元数据
        const metadata = {
            mode: 'classic', // 外部引擎统一使用 classic 模式
            engine: this.engine,
            buildTool: 'generic',
            buildOutputDir: originalBuildDir,
        };
        // 8. 保存元数据
        await this.saveBuildMetadata(metadata);
        return {
            outputDir: this.options.outputDir,
            metadata,
            files,
        };
    }
    /**
     * 检测包管理器
     */
    async detectPackageManager() {
        const lockFiles = [
            { file: 'pnpm-lock.yaml', manager: 'pnpm' },
            { file: 'yarn.lock', manager: 'yarn' },
            { file: 'package-lock.json', manager: 'npm' },
        ];
        for (const { file, manager } of lockFiles) {
            try {
                await fs.access(path.join(this.projectDir, file));
                return manager;
            }
            catch {
                // 继续检查下一个
            }
        }
        // 默认使用 npm
        return 'npm';
    }
    /**
     * 检查 node_modules 是否存在且不为空
     * 用于判断是否需要执行安装
     */
    async checkNodeModules() {
        try {
            const nodeModulesPath = path.join(this.projectDir, 'node_modules');
            const stat = await fs.stat(nodeModulesPath);
            if (!stat.isDirectory()) {
                return false;
            }
            // 检查目录是否不为空
            const entries = await fs.readdir(nodeModulesPath);
            return entries.length > 0;
        }
        catch {
            return false;
        }
    }
    /**
     * 执行 npm install
     * 超时：3 分钟
     */
    async runNpmInstall(packageManager) {
        const cmd = packageManager === 'npm' ? 'npm' : packageManager;
        const args = ['install'];
        // npm 使用 --legacy-peer-deps 避免冲突
        if (packageManager === 'npm') {
            args.push('--legacy-peer-deps');
        }
        console.log(`[GenericAdapter] 运行: ${cmd} ${args.join(' ')}`);
        await this.runCommandWithTimeout(cmd, args, {
            cwd: this.projectDir,
            timeout: 3 * 60 * 1000, // 3 分钟
            env: {
                ...process.env,
                // 注意：install 阶段不设置 NODE_ENV=production，
                // 否则 pnpm/npm 会跳过 devDependencies 安装
                // （playable-scripts 等构建工具通常在 devDependencies 中）
                CI: 'true', // 避免交互式提示
            },
        });
        console.log('[GenericAdapter] 依赖安装完成');
    }
    /**
     * 检测 build script
     */
    async detectBuildScript() {
        try {
            const pkgPath = path.join(this.projectDir, 'package.json');
            const content = await fs.readFile(pkgPath, 'utf-8');
            const pkg = JSON.parse(content);
            if (!pkg.scripts) {
                return null;
            }
            // 优先查找常见的 build 命令
            const buildScripts = ['build', 'build:prod', 'build:production', 'dist', 'compile'];
            for (const script of buildScripts) {
                if (pkg.scripts[script]) {
                    return script;
                }
            }
            return null;
        }
        catch {
            return null;
        }
    }
    /**
     * 执行 npm run build
     * 超时：5 分钟
     */
    async runNpmBuild(packageManager, script) {
        const cmd = packageManager === 'npm' ? 'npm' : packageManager;
        const args = packageManager === 'npm' ? ['run', script] : [script];
        console.log(`[GenericAdapter] 运行: ${cmd} ${args.join(' ')}`);
        await this.runCommandWithTimeout(cmd, args, {
            cwd: this.projectDir,
            timeout: 5 * 60 * 1000, // 5 分钟
            env: {
                ...process.env,
                NODE_ENV: 'production',
                CI: 'true',
            },
        });
        console.log('[GenericAdapter] 项目构建完成');
        // 查找构建输出目录
        return this.findBuildOutputDir();
    }
    /**
     * 查找构建输出目录
     * 按优先级搜索：dist/、build/、out/、public/
     */
    async findBuildOutputDir() {
        const possibleDirs = ['dist', 'build', 'out', 'public'];
        for (const dir of possibleDirs) {
            const fullPath = path.join(this.projectDir, dir);
            try {
                const stat = await fs.stat(fullPath);
                if (stat.isDirectory()) {
                    // 检查目录中是否有 index.html
                    const files = await fs.readdir(fullPath);
                    if (files.includes('index.html')) {
                        console.log(`[GenericAdapter] 找到构建输出目录: ${dir}`);
                        return fullPath;
                    }
                }
            }
            catch {
                // 目录不存在，继续检查下一个
            }
        }
        // 如果没找到，尝试从 package.json 读取
        try {
            const pkgPath = path.join(this.projectDir, 'package.json');
            const content = await fs.readFile(pkgPath, 'utf-8');
            const pkg = JSON.parse(content);
            // 常见配置字段
            const possibleFields = ['build.outDir', 'build.outdir', 'outDir', 'output', 'dist', 'buildDir'];
            for (const field of possibleFields) {
                const value = this.getNestedValue(pkg, field);
                if (value && typeof value === 'string') {
                    const fullPath = path.join(this.projectDir, value);
                    try {
                        const stat = await fs.stat(fullPath);
                        if (stat.isDirectory()) {
                            console.log(`[GenericAdapter] 从 package.json 找到构建目录: ${value}`);
                            return fullPath;
                        }
                    }
                    catch {
                        // 目录不存在
                    }
                }
            }
        }
        catch {
            // 无法读取 package.json
        }
        throw new Error(`[GenericAdapter] 无法找到构建输出目录\n` +
            `请确保构建输出包含 index.html，且位于以下目录之一：\n` +
            possibleDirs.join(', '));
    }
    /**
     * 获取嵌套对象值
     */
    getNestedValue(obj, path) {
        return path.split('.').reduce((current, key) => {
            return current && current[key] !== undefined ? current[key] : undefined;
        }, obj);
    }
    /**
     * 复制构建产物到输出目录
     */
    async copyBuildOutput(buildDir) {
        const files = {
            html: '',
            engine: null,
            config: '',
            settings: null,
            modules: null,
            start: '',
            scenes: [],
            assets: [],
        };
        // 验证构建目录
        const indexPath = path.join(buildDir, 'index.html');
        try {
            await fs.access(indexPath);
        }
        catch {
            throw new Error(`[GenericAdapter] 构建输出缺少 index.html\n` +
                `构建目录: ${buildDir}\n` +
                `请确保构建脚本生成有效的 HTML 入口文件。`);
        }
        // 复制整个构建目录到输出目录
        await this.copyDirectory(buildDir, this.options.outputDir);
        // 设置文件路径
        files.html = path.join(this.options.outputDir, 'index.html');
        // 扫描其他文件
        await this.scanOutputFiles(files);
        console.log(`[GenericAdapter] 构建产物已复制到: ${this.options.outputDir}`);
        return files;
    }
    /**
     * 递归复制目录
     */
    async 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);
            // 安全检查：确保目标路径在输出目录内
            const resolvedDest = path.resolve(destPath);
            const resolvedOutput = path.resolve(this.options.outputDir);
            if (!resolvedDest.startsWith(resolvedOutput)) {
                console.warn(`[GenericAdapter] 跳过不安全的文件路径: ${srcPath}`);
                continue;
            }
            if (entry.isDirectory()) {
                await this.copyDirectory(srcPath, destPath);
            }
            else {
                await fs.copyFile(srcPath, destPath);
            }
        }
    }
    /**
     * 扫描输出文件
     */
    async scanOutputFiles(files) {
        const scanDir = async (dir, basePath) => {
            const entries = await fs.readdir(dir, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = path.join(dir, entry.name);
                const relativePath = path.relative(basePath, fullPath);
                if (entry.isDirectory()) {
                    await scanDir(fullPath, basePath);
                }
                else {
                    // 分类文件
                    const ext = path.extname(entry.name).toLowerCase();
                    if (['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.bmp'].includes(ext)) {
                        files.assets.push(fullPath);
                    }
                    else if (['.mp3', '.wav', '.ogg', '.mp4', '.webm'].includes(ext)) {
                        files.assets.push(fullPath);
                    }
                    else if (['.json', '.yaml', '.yml', '.xml'].includes(ext)) {
                        if (entry.name === 'config.json') {
                            files.config = fullPath;
                        }
                        else {
                            files.assets.push(fullPath);
                        }
                    }
                }
            }
        };
        try {
            await scanDir(this.options.outputDir, this.options.outputDir);
        }
        catch (error) {
            console.warn('[GenericAdapter] 扫描输出文件失败:', error);
        }
    }
    /**
     * 保存构建元数据
     */
    async saveBuildMetadata(metadata) {
        const metadataPath = path.join(this.options.outputDir, '.build-metadata.json');
        await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
        console.log(`[GenericAdapter] 构建元数据已保存（引擎: ${this.engine}）`);
    }
    /**
     * 执行命令并设置超时
     */
    runCommandWithTimeout(cmd, args, options) {
        return new Promise((resolve, reject) => {
            const controller = new AbortController();
            const { signal } = controller;
            const timeoutId = setTimeout(() => {
                controller.abort();
                reject(new Error(`[GenericAdapter] 命令超时（${options.timeout / 1000}秒）\n` +
                    `命令: ${cmd} ${args.join(' ')}\n` +
                    `请检查项目依赖是否过多，或构建脚本是否有死循环。`));
            }, options.timeout);
            const child = spawn(cmd, args, {
                cwd: options.cwd,
                env: options.env,
                stdio: 'pipe',
                signal,
                shell: true, // Windows 下必须，否则无法找到 pnpm/yarn 等命令
            });
            let stdout = '';
            let stderr = '';
            child.stdout?.on('data', (data) => {
                stdout += data.toString();
                // 限制输出长度
                if (stdout.length > 10000) {
                    stdout = stdout.slice(-5000);
                }
            });
            child.stderr?.on('data', (data) => {
                stderr += data.toString();
                // 限制输出长度
                if (stderr.length > 10000) {
                    stderr = stderr.slice(-5000);
                }
            });
            child.on('error', (error) => {
                clearTimeout(timeoutId);
                if (error.name === 'AbortError') {
                    reject(new Error(`命令被中止: ${cmd} ${args.join(' ')}`));
                }
                else {
                    reject(new Error(`[GenericAdapter] 命令执行失败\n` +
                        `命令: ${cmd} ${args.join(' ')}\n` +
                        `错误: ${error.message}`));
                }
            });
            child.on('close', (code) => {
                clearTimeout(timeoutId);
                if (code === 0) {
                    resolve();
                }
                else {
                    reject(new Error(`[GenericAdapter] 命令退出码非零: ${code}\n` +
                        `命令: ${cmd} ${args.join(' ')}\n` +
                        `stdout: ${stdout.slice(-1000)}\n` +
                        `stderr: ${stderr.slice(-1000)}`));
                }
            });
        });
    }
}
