import { spawn } from 'node:child_process';
import { createHash, randomUUID } from 'node:crypto';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import axios from 'axios';
import { schedulerBaseUrl } from '../config.js';
import { getRuntimeAccessToken } from '../services/runtime-binding.js';
const fileLocks = new Map();
export async function ensureDir(targetDir) {
    await fs.mkdir(targetDir, { recursive: true });
    return targetDir;
}
export async function withFileLock(targetPath, operation) {
    const lockKey = path.resolve(targetPath);
    const previous = fileLocks.get(lockKey) ?? Promise.resolve();
    let release;
    const current = new Promise((resolve) => {
        release = resolve;
    });
    const chained = previous.then(() => current);
    fileLocks.set(lockKey, chained);
    await previous;
    try {
        return await operation();
    }
    finally {
        release();
        if (fileLocks.get(lockKey) === chained) {
            fileLocks.delete(lockKey);
        }
    }
}
function isPlainObject(value) {
    if (!value || typeof value !== 'object') {
        return false;
    }
    const prototype = Object.getPrototypeOf(value);
    return prototype === Object.prototype || prototype === null;
}
function sortJsonValue(value) {
    if (Array.isArray(value)) {
        return value.map((item) => sortJsonValue(item));
    }
    if (isPlainObject(value)) {
        return Object.keys(value)
            .sort()
            .reduce((acc, key) => {
            acc[key] = sortJsonValue(value[key]);
            return acc;
        }, {});
    }
    return value;
}
export function stringifyStableJson(value) {
    return `${JSON.stringify(sortJsonValue(value), null, 2)}\n`;
}
export async function atomicWriteFile(targetPath, content) {
    await ensureDir(path.dirname(targetPath));
    const targetDir = path.dirname(targetPath);
    const targetBase = path.basename(targetPath);
    const tmpPath = path.join(targetDir, `.${targetBase}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
    try {
        await fs.writeFile(tmpPath, content, 'utf-8');
        await fs.rename(tmpPath, targetPath);
    }
    catch (error) {
        await fs.rm(tmpPath, { force: true }).catch(() => undefined);
        throw error;
    }
}
export async function atomicWriteJsonFile(targetPath, value) {
    await atomicWriteFile(targetPath, stringifyStableJson(value));
}
export async function downloadSkillZip(skillPackage, targetDir) {
    if (!skillPackage || !skillPackage.downloadUrl) {
        return null;
    }
    const url = String(skillPackage.downloadUrl).trim();
    if (!url) {
        return null;
    }
    let absoluteUrl = url;
    if (absoluteUrl.startsWith('/')) {
        absoluteUrl = `${schedulerBaseUrl}${absoluteUrl}`;
    }
    const fileName = skillPackage.fileName && String(skillPackage.fileName).trim()
        ? String(skillPackage.fileName).trim()
        : 'skill-package.zip';
    const outputPath = path.join(targetDir, fileName);
    const runtimeAccessToken = getRuntimeAccessToken(undefined, schedulerBaseUrl) || getRuntimeAccessToken();
    if (!runtimeAccessToken) {
        throw new Error('runtime access token is required to download skill package');
    }
    const response = await axios.get(absoluteUrl, {
        responseType: 'arraybuffer',
        headers: {
            'X-Runtime-Token': runtimeAccessToken,
        },
    });
    const bytes = Buffer.from(response.data);
    await fs.writeFile(outputPath, bytes);
    if (skillPackage.sha256) {
        const hash = createHash('sha256').update(bytes).digest('hex');
        if (hash !== String(skillPackage.sha256).trim().toLowerCase()) {
            throw new Error('skill zip checksum mismatch');
        }
    }
    return {
        fileName,
        outputPath,
        bytes: bytes.length,
    };
}
export async function runCommand(command, args) {
    await new Promise((resolve, reject) => {
        const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
        let stderr = '';
        child.stderr?.on('data', (chunk) => {
            stderr += String(chunk || '');
        });
        child.on('error', (error) => {
            reject(error);
        });
        child.on('close', (code) => {
            if (code !== 0) {
                reject(new Error(stderr || `${command} exited with code ${code}`));
                return;
            }
            resolve();
        });
    });
}
export function escapePowerShellLiteral(text) {
    return String(text || '').replace(/'/g, "''");
}
export async function extractSkillZip(zipFilePath, extractRootDir) {
    await fs.rm(extractRootDir, { recursive: true, force: true });
    await fs.mkdir(extractRootDir, { recursive: true });
    if (process.platform === 'win32') {
        const zipLiteral = escapePowerShellLiteral(zipFilePath);
        const targetLiteral = escapePowerShellLiteral(extractRootDir);
        await runCommand('powershell.exe', [
            '-NoLogo',
            '-NoProfile',
            '-Command',
            `Expand-Archive -LiteralPath '${zipLiteral}' -DestinationPath '${targetLiteral}' -Force`,
        ]);
        return;
    }
    await runCommand('unzip', ['-o', zipFilePath, '-d', extractRootDir]);
}
export async function resolveSkillSourceDirFromExtractRoot(extractRootDir) {
    const entries = await fs.readdir(extractRootDir, { withFileTypes: true });
    const visible = entries.filter((entry) => !entry.name.startsWith('.'));
    if (visible.length === 1 && visible[0].isDirectory()) {
        return path.join(extractRootDir, visible[0].name);
    }
    return extractRootDir;
}
export function toSdkAppsFromEnabledTools(enabledTools) {
    const tools = Array.isArray(enabledTools) ? enabledTools : [];
    const includes = (name) => tools.includes(name);
    return {
        acode: includes('acode'),
        claude: includes('claude'),
        claudecode: includes('claudecode'),
        codex: includes('codex'),
        gemini: includes('gemini'),
        opencode: includes('opencode'),
        openclaw: includes('openclaw'),
    };
}
export function resolveSkillSourceDir(skill) {
    const sourceDirCandidate = String(skill?.sourceDir || skill?.directory || skill?.filePath || '').trim();
    if (!sourceDirCandidate) {
        throw new Error('skill.sourceDir is required');
    }
    return path.resolve(sourceDirCandidate);
}
export async function ensureDirectoryExists(directoryPath) {
    const stat = await fs.stat(directoryPath);
    if (!stat.isDirectory()) {
        throw new Error(`not a directory: ${directoryPath}`);
    }
}
export async function installSkillPackageToSdk(sdk, skillPackage, skillDir, sdkApps) {
    const downloaded = await downloadSkillZip(skillPackage, skillDir);
    if (!downloaded) {
        return null;
    }
    const extractRootDir = path.join(skillDir, `${path.parse(downloaded.fileName).name}-${randomUUID()}`);
    await extractSkillZip(downloaded.outputPath, extractRootDir);
    const sourceDir = await resolveSkillSourceDirFromExtractRoot(extractRootDir);
    const desiredName = String(skillPackage?.skillName || downloaded.fileName || '').trim() || downloaded.fileName;
    const allSkills = sdk.getAllSkills();
    const existing = allSkills.find((item) => String(item?.name || '').trim() === desiredName);
    if (existing) {
        return sdk.updateSkill(existing.id, {
            name: desiredName,
            apps: sdkApps,
        });
    }
    return sdk.installSkill({
        sourceDir,
        name: desiredName,
        apps: sdkApps,
    });
}
