import { ensureDir, getDataDir, readJsonFile, writeJsonFile } from '../storage/json-store.js';
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { builtinModules } from 'node:module';
import { spawnSync } from 'node:child_process';
import vm from 'node:vm';
import AdmZip from 'adm-zip';
import { createBuiltinPluginApi } from './plugin-runtime-api.js';
import { getNpmSettings } from '../storage/npm-settings-store.js';
const require = createRequire(import.meta.url);
const STATE_FILE = () => path.join(pluginsDir(), 'state.json');
const pluginRuntimeState = new Map();
function templatesPluginsDir() {
    const candidates = [
        path.resolve(process.cwd(), 'packages/templates/plugins'),
        path.resolve(process.cwd(), '../templates/plugins'),
        path.resolve(process.cwd(), 'templates/plugins'),
    ];
    return candidates.find(candidate => existsSync(candidate)) || candidates[0];
}
function pluginsDir() {
    return path.join(getDataDir(), 'plugins');
}
function pluginDir(pluginId) {
    return path.join(pluginsDir(), pluginId);
}
function resolvePluginDir(pluginId) {
    const direct = pluginDir(pluginId);
    if (existsSync(direct))
        return direct;
    const root = pluginsDir();
    if (!existsSync(root))
        return null;
    for (const entry of readdirSync(root, { withFileTypes: true })) {
        if (!entry.isDirectory())
            continue;
        const dir = path.join(root, entry.name);
        const manifest = readManifestFromDir(dir);
        if ((manifest?.id || entry.name) === pluginId)
            return dir;
    }
    return null;
}
function readState() {
    return readJsonFile(STATE_FILE()) ?? { enabled: {}, config: {} };
}
function writeState(state) {
    writeJsonFile(STATE_FILE(), state);
}
function readManifestFromDir(dir) {
    const candidates = ['plugin.json', 'manifest.json', 'info.json', 'web-plugin.json', 'package.json'];
    for (const filename of candidates) {
        const manifest = readJsonFile(path.join(dir, filename));
        if (manifest?.id || manifest?.name)
            return manifest;
    }
    return null;
}
function normalizeLocale(locale) {
    const value = String(locale || '').toLowerCase();
    if (value.startsWith('en'))
        return 'en';
    return 'zh';
}
function readPluginLang(dir) {
    const lang = readJsonFile(path.join(dir, 'lang.json'));
    return lang && typeof lang === 'object' ? lang : {};
}
function createPluginTranslator(dir, locale) {
    const lang = readPluginLang(dir);
    const currentLocale = normalizeLocale(locale);
    const current = lang[currentLocale] || {};
    const fallbackLocale = currentLocale === 'en' ? 'zh' : 'en';
    const fallbackLang = lang[fallbackLocale] || {};
    return (key, fallback) => {
        if (typeof key !== 'string' || !key)
            return fallback ?? '';
        return current[key] || fallbackLang[key] || fallback || key;
    };
}
function hasPackageDependencies(dir) {
    const pkg = readJsonFile(path.join(dir, 'package.json'));
    return Boolean(pkg?.dependencies && Object.keys(pkg.dependencies).length > 0);
}
function installPluginDependencies(dir) {
    if (!hasPackageDependencies(dir))
        return;
    if (existsSync(path.join(dir, 'node_modules')))
        return;
    const npmSettings = getNpmSettings();
    const npmCommand = 'npm';
    const npmArgs = ['install', '--omit=dev', `--registry=${npmSettings.registry}`];
    if (npmSettings.proxy) {
        npmArgs.push(`--proxy=${npmSettings.proxy}`, `--https-proxy=${npmSettings.proxy}`);
    }
    const result = spawnSync(npmCommand, npmArgs, {
        cwd: dir,
        encoding: 'utf-8',
        env: {
            ...process.env,
            NPM_CONFIG_REGISTRY: npmSettings.registry,
            ...(npmSettings.proxy ? {
                NPM_CONFIG_PROXY: npmSettings.proxy,
                NPM_CONFIG_HTTPS_PROXY: npmSettings.proxy,
            } : {}),
        },
        shell: process.platform === 'win32',
        stdio: 'pipe',
    });
    if (result.error || result.status !== 0) {
        const detail = (result.error?.message || result.stderr || result.stdout || '').trim();
        console.error('[plugin] failed to install dependencies', {
            dir,
            command: `${npmCommand} ${npmArgs.join(' ')}`,
            registry: npmSettings.registry,
            proxy: npmSettings.proxy ? 'configured' : 'none',
            status: result.status,
            signal: result.signal,
            error: result.error?.message,
            stderr: result.stderr?.trim(),
            stdout: result.stdout?.trim(),
        });
        throw new Error(`Failed to install plugin dependencies in ${dir}${detail ? `: ${detail}` : ''}`);
    }
}
const ICON_FILENAMES = ['icon.svg', 'icon.png', 'icon.jpg', 'icon.jpeg', 'icon.webp'];
function detectIconFile(dirName) {
    const dir = resolvePluginDir(dirName);
    if (!dir)
        return null;
    for (const name of ICON_FILENAMES) {
        if (existsSync(path.join(dir, name)))
            return name;
    }
    return null;
}
function localizedManifestString(manifest, field, locale) {
    const normalizedLocale = normalizeLocale(locale);
    const localized = manifest[`${field}_${normalizedLocale}`];
    if (typeof localized === 'string' && localized.trim())
        return localized;
    const fallback = manifest[field];
    return typeof fallback === 'string' ? fallback : undefined;
}
function localizedManifestTags(manifest, locale) {
    const normalizedLocale = normalizeLocale(locale);
    const localized = manifest[`tags_${normalizedLocale}`];
    if (Array.isArray(localized))
        return localized.map(String);
    return Array.isArray(manifest.tags) ? manifest.tags : [];
}
function normalizePlugin(dirName, manifest, state, locale) {
    const id = String(manifest.id || dirName);
    return {
        id,
        name: String(localizedManifestString(manifest, 'name', locale) || id),
        version: String(manifest.version || '0.0.0'),
        description: String(localizedManifestString(manifest, 'description', locale) || ''),
        author: manifest.author || { name: 'Unknown' },
        tags: localizedManifestTags(manifest, locale),
        hasView: Boolean(manifest.hasView),
        hasWorkflow: Boolean(manifest.hasWorkflow || manifest.workflowNodes?.length || manifest.entries?.workflow),
        type: manifest.type,
        enabled: state.enabled[id] ?? Boolean(manifest.enabled),
        config: Array.isArray(manifest.config) ? manifest.config : [],
        iconPath: manifest.iconPath || manifest.icon || detectIconFile(dirName) || '',
        md5: state.installedMd5?.[id],
        installedAt: state.installedAt?.[id],
    };
}
function getManifest(pluginId) {
    const dir = resolvePluginDir(pluginId);
    return dir ? readManifestFromDir(dir) : null;
}
export function getPluginIconPath(pluginId) {
    const dir = resolvePluginDir(pluginId);
    if (!dir)
        return null;
    const manifest = readManifestFromDir(dir);
    const iconRel = manifest?.iconPath || manifest?.icon || detectIconFile(pluginId);
    if (!iconRel || typeof iconRel !== 'string')
        return null;
    const iconAbs = path.resolve(dir, iconRel);
    if (!existsSync(iconAbs))
        return null;
    return iconAbs;
}
function createRequireStub() {
    const target = () => createRequireStub();
    return new Proxy(target, {
        get: (_target, prop) => {
            if (prop === 'then')
                return undefined;
            if (prop === Symbol.toPrimitive)
                return () => '';
            return createRequireStub();
        },
        apply: () => createRequireStub(),
        construct: () => createRequireStub(),
    });
}
export function coerceByDataType(value, dataType) {
    if (!dataType)
        return value;
    if (value === undefined || value === null || value === '')
        return value;
    switch (dataType) {
        case 'number': {
            if (typeof value === 'number')
                return value;
            const num = Number(value);
            return Number.isFinite(num) ? num : value;
        }
        case 'boolean': {
            if (typeof value === 'boolean')
                return value;
            if (value === 'true' || value === '1')
                return true;
            if (value === 'false' || value === '0')
                return false;
            return value;
        }
        case 'string[]': {
            if (Array.isArray(value))
                return value;
            if (typeof value === 'string') {
                try {
                    const parsed = JSON.parse(value);
                    return Array.isArray(parsed) ? parsed : [value];
                }
                catch {
                    return [value];
                }
            }
            return value;
        }
        case 'number[]': {
            if (Array.isArray(value))
                return value;
            if (typeof value === 'string') {
                try {
                    const parsed = JSON.parse(value);
                    return Array.isArray(parsed) ? parsed : value;
                }
                catch {
                    return value;
                }
            }
            return value;
        }
        case 'object[]': {
            if (Array.isArray(value))
                return value;
            if (typeof value === 'string') {
                try {
                    const parsed = JSON.parse(value);
                    return Array.isArray(parsed) ? parsed : value;
                }
                catch {
                    return value;
                }
            }
            return value;
        }
        case 'object': {
            if (typeof value === 'object' && value !== null)
                return value;
            if (typeof value === 'string') {
                try {
                    return JSON.parse(value);
                }
                catch {
                    return value;
                }
            }
            return value;
        }
        default:
            return value;
    }
}
function toJsonSchemaType(type, dataType) {
    if (dataType) {
        switch (dataType) {
            case 'string': return 'string';
            case 'number': return 'number';
            case 'boolean': return 'boolean';
            case 'string[]':
            case 'number[]':
            case 'object[]': return 'array';
            case 'object': return 'object';
            case 'any': return 'string';
            default: break;
        }
    }
    switch (type) {
        case 'textarea':
        case 'select':
        case 'text':
            return 'string';
        case 'number':
            return 'number';
        case 'boolean':
        case 'checkbox':
            return 'boolean';
        case 'array':
        case 'output_fields':
        case 'conditions':
            return 'array';
        default:
            return 'string';
    }
}
function propertyToSchema(property) {
    const schema = {
        type: property.schemaType || toJsonSchemaType(property.type, property.dataType),
    };
    const description = property.description || property.tooltip || property.label;
    if (property.label)
        schema.title = property.label;
    if (description)
        schema.description = description;
    if (property.items)
        schema.items = property.items;
    if (property.enum)
        schema.enum = property.enum;
    if (property.options) {
        schema.options = property.options;
        if (!schema.enum) {
            schema.enum = property.options.map(option => {
                if (option && typeof option === 'object' && 'value' in option) {
                    return option.value;
                }
                return option;
            });
        }
    }
    if (property.default !== undefined) {
        schema.default = property.default;
    }
    // Auto-derive items for known array dataTypes
    if (!property.items && property.dataType) {
        if (property.dataType === 'string[]') {
            schema.items = { type: 'string' };
        }
        else if (property.dataType === 'number[]') {
            schema.items = { type: 'number' };
        }
    }
    return schema;
}
function applyUiPropertyDefaults(toolProperties, uiProperties) {
    if (!uiProperties?.length)
        return [...toolProperties];
    const defaultByKey = new Map(uiProperties
        .filter(property => property.default !== undefined)
        .map(property => [property.key, property.default]));
    return toolProperties.map((property) => {
        if (property.default !== undefined || !defaultByKey.has(property.key))
            return property;
        return { ...property, default: defaultByKey.get(property.key) };
    });
}
function actionPropertiesToToolInputSchema(action) {
    const baseProperties = action.toolProperties || action.properties || [];
    if (Array.isArray(baseProperties)) {
        const toolProperties = action.toolProperties
            ? applyUiPropertyDefaults(baseProperties, action.properties)
            : [...baseProperties];
        return { properties: [...toolProperties, ...(action.configProperties || [])] };
    }
    if (typeof baseProperties === 'object') {
        const schema = baseProperties;
        const configProperties = action.configProperties || [];
        if (configProperties.length === 0)
            return { properties: [], inputSchema: schema };
        return {
            properties: [],
            inputSchema: {
                ...schema,
                type: schema.type || 'object',
                properties: {
                    ...((schema.properties && typeof schema.properties === 'object') ? schema.properties : {}),
                    ...Object.fromEntries(configProperties.map(property => [property.key, propertyToSchema(property)])),
                },
                required: [
                    ...(Array.isArray(schema.required) ? schema.required : []),
                    ...configProperties
                        .filter(property => property.required && property.toolRequired !== false)
                        .map(property => property.key),
                ],
            },
        };
    }
    return { properties: [] };
}
function createPluginActions(actions) {
    const workflowNodes = actions.map(({ run, tool: _tool, ...action }) => {
        const properties = [...(action.properties || []), ...(action.configProperties || [])];
        return {
            type: action.name,
            label: action.label,
            category: action.category,
            icon: action.icon,
            description: action.description,
            properties,
            outputs: action.outputs || [],
            customView: action.customView,
            customViewMinSize: action.customViewMinSize,
            handler: run,
        };
    });
    const tools = actions
        .filter(action => action.tool !== false)
        .map((action) => {
        const tool = action.tool || {};
        const { properties, inputSchema } = actionPropertiesToToolInputSchema(action);
        const outputs = action.outputs || [];
        const required = properties
            .filter(property => property.required && property.toolRequired !== false)
            .map(property => property.key);
        return {
            name: tool.name || action.name,
            description: tool.description || action.description,
            input_schema: inputSchema || {
                type: 'object',
                properties: Object.fromEntries(properties.map(property => [property.key, propertyToSchema(property)])),
                required,
            },
            outputs,
        };
    });
    const handlers = new Map();
    for (const action of actions) {
        handlers.set(action.name, action.run);
        if (action.tool && action.tool.name)
            handlers.set(action.tool.name, action.run);
    }
    return {
        workflow: () => ({ nodes: workflowNodes }),
        tools: () => ({
            tools,
            handler: async (name, args, api) => {
                const run = handlers.get(name);
                if (!run)
                    return { success: false, message: `Unknown tool: ${name}` };
                return run({ api, logger: api?.logger || console }, args);
            },
        }),
    };
}
function createPluginRequire(entryPath) {
    const relativeRequire = createRequire(entryPath);
    return (request) => {
        const normalized = request.replace(/^node:/, '');
        if (builtinModules.includes(normalized))
            return require(request);
        if (request.startsWith('./') || request.startsWith('../') || path.isAbsolute(request)) {
            return relativeRequire(request);
        }
        return createRequireStub();
    };
}
function createPluginContext(plugin, dir) {
    const t = createPluginTranslator(dir);
    return {
        plugin,
        config: getPluginConfig(plugin.id),
        t,
        logger: {
            info: (message) => console.info(`[plugin:${plugin.id}] ${message}`),
            warning: (message) => console.warn(`[plugin:${plugin.id}] ${message}`),
            error: (message) => console.error(`[plugin:${plugin.id}] ${message}`),
        },
        registerActions: (actions) => {
            const state = pluginRuntimeState.get(plugin.id) ?? {
                activated: false,
                registeredActions: null,
                localizedActions: new Map(),
            };
            state.registeredActions = Array.isArray(actions) || typeof actions === 'function' ? actions : [];
            state.localizedActions.clear();
            pluginRuntimeState.set(plugin.id, state);
        },
    };
}
function activatePlugin(plugin) {
    const current = pluginRuntimeState.get(plugin.id);
    if (current?.activated)
        return;
    const nextState = current ?? {
        activated: false,
        registeredActions: null,
        localizedActions: new Map(),
    };
    pluginRuntimeState.set(plugin.id, nextState);
    const manifest = getManifest(plugin.id);
    const dir = resolvePluginDir(plugin.id);
    if (!manifest || !dir) {
        nextState.activated = true;
        return;
    }
    const serverEntry = manifest.entries?.server || 'main.js';
    const serverPath = path.join(dir, serverEntry);
    if (!existsSync(serverPath) || (!serverPath.endsWith('.js') && !serverPath.endsWith('.cjs'))) {
        nextState.activated = true;
        return;
    }
    const source = readFileSync(serverPath, 'utf-8');
    const module = { exports: {} };
    const script = new vm.Script(`(function(require, module, exports, __filename, __dirname) {\n${source}\n})`, { filename: serverPath });
    const runner = script.runInNewContext({ console, Buffer, URL, URLSearchParams, fetch, setTimeout, clearTimeout });
    runner(createPluginRequire(serverPath), module, module.exports, serverPath, path.dirname(serverPath));
    if (typeof module.exports.activate === 'function') {
        module.exports.activate(createPluginContext(plugin, dir));
    }
    nextState.activated = true;
}
function getRegisteredPluginActions(plugin, locale) {
    activatePlugin(plugin);
    const state = pluginRuntimeState.get(plugin.id);
    if (!state?.registeredActions)
        return [];
    const normalizedLocale = normalizeLocale(locale);
    const cached = state.localizedActions.get(normalizedLocale);
    if (cached)
        return cached;
    const dir = resolvePluginDir(plugin.id);
    const t = dir ? createPluginTranslator(dir, normalizedLocale) : ((key, fallback) => fallback || key);
    const actions = typeof state.registeredActions === 'function'
        ? state.registeredActions(t)
        : state.registeredActions;
    const normalizedActions = Array.isArray(actions) ? actions : [];
    state.localizedActions.set(normalizedLocale, normalizedActions);
    return normalizedActions;
}
function loadCommonJsWorkflowNodes(workflowPath) {
    const source = readFileSync(workflowPath, 'utf-8');
    const module = { exports: {} };
    const localRequire = createPluginRequire(workflowPath);
    const script = new vm.Script(`(function(require, module, exports, __filename, __dirname) {\n${source}\n})`, { filename: workflowPath });
    const runner = script.runInNewContext({ console, Buffer, URL, URLSearchParams, fetch, setTimeout, clearTimeout });
    runner(localRequire, module, module.exports, workflowPath, path.dirname(workflowPath));
    const payload = module.exports;
    if (Array.isArray(payload))
        return payload;
    return Array.isArray(payload?.nodes) ? payload.nodes : [];
}
function loadCommonJsWorkflowModule(workflowPath) {
    const source = readFileSync(workflowPath, 'utf-8');
    const module = { exports: {} };
    const localRequire = createPluginRequire(workflowPath);
    const script = new vm.Script(`(function(require, module, exports, __filename, __dirname) {\n${source}\n})`, { filename: workflowPath });
    const runner = script.runInNewContext({ console, Buffer, URL, URLSearchParams, fetch, setTimeout, clearTimeout });
    runner(localRequire, module, module.exports, workflowPath, path.dirname(workflowPath));
    const payload = module.exports;
    const rawNodes = Array.isArray(payload) ? payload : Array.isArray(payload?.nodes) ? payload.nodes : [];
    const handlers = new Map();
    const nodes = rawNodes.map((node) => {
        const { handler, ...serializable } = node;
        if (typeof handler === 'function' && typeof serializable.type === 'string') {
            handlers.set(serializable.type, handler);
        }
        return serializable;
    });
    return { nodes, handlers };
}
export function listPlugins(locale) {
    const root = pluginsDir();
    ensureDir(root);
    const state = readState();
    return readdirSync(root, { withFileTypes: true })
        .filter(entry => entry.isDirectory())
        .map((entry) => {
        const manifest = readManifestFromDir(path.join(root, entry.name));
        return manifest ? normalizePlugin(entry.name, manifest, state, locale) : null;
    })
        .filter((plugin) => Boolean(plugin));
}
export function listWorkflowPlugins(locale) {
    return listPlugins(locale).filter(plugin => plugin.hasWorkflow);
}
export function uninstallPlugin(pluginId) {
    const dir = resolvePluginDir(pluginId);
    if (!dir)
        throw new Error('Plugin not found');
    // remove plugin directory from disk
    rmSync(dir, { recursive: true, force: true });
    // clean state.json
    const state = readState();
    delete state.enabled[pluginId];
    delete state.config[pluginId];
    if (state.installedMd5)
        delete state.installedMd5[pluginId];
    if (state.installedAt)
        delete state.installedAt[pluginId];
    writeState(state);
}
export function setPluginEnabled(pluginId, enabled) {
    const state = readState();
    state.enabled[pluginId] = enabled;
    writeState(state);
    const plugin = listPlugins().find(item => item.id === pluginId);
    if (!plugin)
        throw new Error('Plugin not found');
    return plugin;
}
function findTemplatePluginDir(pluginId) {
    const root = templatesPluginsDir();
    if (!existsSync(root))
        return null;
    for (const entry of readdirSync(root, { withFileTypes: true })) {
        if (!entry.isDirectory())
            continue;
        const dir = path.join(root, entry.name);
        const manifest = readManifestFromDir(dir);
        if ((manifest?.id || entry.name) === pluginId)
            return dir;
    }
    return null;
}
function sanitizeStorePluginPath(sourceUrl) {
    const pathname = new URL(sourceUrl).pathname.replace(/\/+$/, '');
    return path.basename(decodeURIComponent(pathname)).replace(/[<>:"/\\|?*\x00-\x1F]/g, '_') || 'plugin';
}
function inferStoreZipUrl(sourceUrl) {
    return sourceUrl.endsWith('.zip') ? sourceUrl : `${sourceUrl.replace(/\/+$/, '')}.zip`;
}
function unwrapGithubProxyUrl(sourceUrl) {
    const match = sourceUrl.match(/https:\/\/(?:raw\.githubusercontent\.com|github\.com)\/.+/i);
    return match?.[0] || sourceUrl;
}
function getGithubStoreParts(sourceUrl) {
    const unwrapped = unwrapGithubProxyUrl(sourceUrl);
    const rawMatch = unwrapped.match(/^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/i);
    if (rawMatch) {
        return {
            owner: rawMatch[1],
            repo: rawMatch[2],
            ref: rawMatch[3],
            dirPath: rawMatch[4].replace(/\/+$/, ''),
        };
    }
    const githubMatch = unwrapped.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/raw\/refs\/heads\/([^/]+)\/(.+)$/i);
    if (githubMatch) {
        return {
            owner: githubMatch[1],
            repo: githubMatch[2],
            ref: githubMatch[3],
            dirPath: githubMatch[4].replace(/\/+$/, ''),
        };
    }
    return null;
}
async function fetchBuffer(url) {
    const res = await fetch(url);
    if (!res.ok)
        throw new Error(`fetch ${url} failed: ${res.status}`);
    return Buffer.from(await res.arrayBuffer());
}
async function fetchJson(url) {
    const res = await fetch(url);
    if (!res.ok)
        throw new Error(`fetch ${url} failed: ${res.status}`);
    return res.json();
}
async function tryInstallStoreZip(pluginId, sourceUrl) {
    try {
        const zip = new AdmZip(await fetchBuffer(inferStoreZipUrl(sourceUrl)));
        const targetDir = path.join(pluginsDir(), sanitizeStorePluginPath(sourceUrl));
        ensureDir(pluginsDir());
        if (existsSync(targetDir))
            rmSync(targetDir, { recursive: true, force: true });
        ensureDir(targetDir);
        zip.extractAllTo(targetDir, true);
        const entries = readdirSync(targetDir, { withFileTypes: true });
        if (!readManifestFromDir(targetDir) && entries.length === 1 && entries[0].isDirectory()) {
            const nestedDir = path.join(targetDir, entries[0].name);
            const manifest = readManifestFromDir(nestedDir);
            if (manifest) {
                rmSync(targetDir, { recursive: true, force: true });
                cpSync(nestedDir, targetDir, { recursive: true });
            }
        }
        const manifest = readManifestFromDir(targetDir);
        if (!manifest)
            throw new Error('Store plugin manifest not found');
        if ((manifest.id || pluginId) !== pluginId)
            throw new Error(`Store plugin id mismatch: ${manifest.id || ''}`);
        installPluginDependencies(targetDir);
        return setPluginEnabled(pluginId, true);
    }
    catch (error) {
        console.warn('[plugin] store zip install skipped', {
            pluginId,
            sourceUrl,
            error: error instanceof Error ? error.message : String(error),
        });
        return null;
    }
}
function githubRawUrl(file) {
    return file.downloadUrl;
}
async function listGithubStoreFiles(sourceUrl) {
    const parts = getGithubStoreParts(sourceUrl);
    if (!parts)
        return null;
    const githubParts = parts;
    const apiUrl = `https://api.github.com/repos/${githubParts.owner}/${githubParts.repo}/contents/${encodeURIComponent(githubParts.dirPath).replace(/%2F/g, '/')}?ref=${encodeURIComponent(githubParts.ref)}`;
    const items = await fetchJson(apiUrl);
    const files = [];
    async function visit(entries) {
        for (const item of entries) {
            if (item.type === 'file' && item.download_url) {
                files.push({ path: item.path.slice(githubParts.dirPath.length).replace(/^\/+/, ''), downloadUrl: item.download_url });
            }
            else if (item.type === 'dir' && item.url) {
                await visit(await fetchJson(`${item.url}&ref=${encodeURIComponent(githubParts.ref)}`));
            }
        }
    }
    await visit(items);
    return files;
}
async function tryInstallGithubStoreDir(pluginId, sourceUrl) {
    const files = await listGithubStoreFiles(sourceUrl);
    if (!files?.length)
        return null;
    const targetDir = path.join(pluginsDir(), sanitizeStorePluginPath(sourceUrl));
    ensureDir(pluginsDir());
    if (existsSync(targetDir))
        rmSync(targetDir, { recursive: true, force: true });
    for (const file of files) {
        if (file.path.split('/').includes('node_modules'))
            continue;
        const targetPath = path.join(targetDir, file.path);
        mkdirSync(path.dirname(targetPath), { recursive: true });
        writeFileSync(targetPath, await fetchBuffer(githubRawUrl(file)));
    }
    const manifest = readManifestFromDir(targetDir);
    if (!manifest)
        throw new Error('Store plugin manifest not found');
    if ((manifest.id || pluginId) !== pluginId)
        throw new Error(`Store plugin id mismatch: ${manifest.id || ''}`);
    installPluginDependencies(targetDir);
    return setPluginEnabled(pluginId, true);
}
async function tryInstallStoreCommonFiles(pluginId, sourceUrl) {
    const base = sourceUrl.replace(/\/+$/, '');
    const targetDir = path.join(pluginsDir(), sanitizeStorePluginPath(sourceUrl));
    const manifestNames = ['plugin.json', 'manifest.json', 'info.json', 'web-plugin.json', 'package.json'];
    let manifestName = '';
    let manifest = null;
    for (const name of manifestNames) {
        try {
            manifest = await fetchJson(`${base}/${name}`);
            if (manifest?.id || manifest?.name) {
                manifestName = name;
                break;
            }
        }
        catch {
            continue;
        }
    }
    if (!manifest || !manifestName)
        return null;
    if ((manifest.id || pluginId) !== pluginId)
        throw new Error(`Store plugin id mismatch: ${manifest.id || ''}`);
    const files = new Set([
        manifestName,
        'package.json',
        'package-lock.json',
        'main.js',
        'workflow.js',
        'tools.js',
        'actions.js',
        'lang.json',
        'shared.js',
    ]);
    const iconRel = manifest.iconPath || manifest?.icon;
    if (typeof iconRel === 'string' && iconRel)
        files.add(iconRel);
    const entries = manifest.entries || {};
    for (const entry of [entries.server, entries.workflow, entries.tools]) {
        for (const file of Array.isArray(entry) ? entry : [entry]) {
            if (file)
                files.add(file);
        }
    }
    ensureDir(pluginsDir());
    if (existsSync(targetDir))
        rmSync(targetDir, { recursive: true, force: true });
    ensureDir(targetDir);
    for (const file of files) {
        try {
            const buffer = await fetchBuffer(`${base}/${file}`);
            const targetPath = path.join(targetDir, file);
            mkdirSync(path.dirname(targetPath), { recursive: true });
            writeFileSync(targetPath, buffer);
        }
        catch {
            continue;
        }
    }
    installPluginDependencies(targetDir);
    return setPluginEnabled(pluginId, true);
}
async function installStorePlugin(pluginId, sourceUrl) {
    const fromGithub = await tryInstallGithubStoreDir(pluginId, sourceUrl).catch((error) => {
        console.warn('[plugin] github store install skipped', {
            pluginId,
            sourceUrl,
            error: error instanceof Error ? error.message : String(error),
        });
        return null;
    });
    if (fromGithub)
        return fromGithub;
    const fromZip = await tryInstallStoreZip(pluginId, sourceUrl);
    if (fromZip)
        return fromZip;
    return tryInstallStoreCommonFiles(pluginId, sourceUrl);
}
export async function installTemplatePlugin(pluginId, sourceUrl, md5) {
    // Save md5 + installedAt before install so normalizePlugin picks it up
    {
        const state = readState();
        if (md5) {
            if (!state.installedMd5)
                state.installedMd5 = {};
            state.installedMd5[pluginId] = md5;
        }
        if (!state.installedAt)
            state.installedAt = {};
        state.installedAt[pluginId] = Date.now();
        writeState(state);
    }
    if (sourceUrl) {
        const plugin = await installStorePlugin(pluginId, sourceUrl);
        if (plugin)
            return plugin;
    }
    const sourceDir = findTemplatePluginDir(pluginId);
    if (!sourceDir)
        throw new Error('Template plugin not found');
    const manifest = readManifestFromDir(sourceDir);
    const id = String(manifest?.id || path.basename(sourceDir));
    const targetDir = path.join(pluginsDir(), path.basename(sourceDir));
    ensureDir(pluginsDir());
    if (!existsSync(targetDir)) {
        cpSync(sourceDir, targetDir, {
            recursive: true,
            filter: (src) => !src.split(path.sep).includes('node_modules'),
        });
    }
    installPluginDependencies(targetDir);
    if (id !== pluginId) {
        const state = readState();
        if (md5) {
            if (!state.installedMd5)
                state.installedMd5 = {};
            state.installedMd5[id] = md5;
        }
        if (!state.installedAt)
            state.installedAt = {};
        state.installedAt[id] = state.installedAt?.[pluginId] ?? Date.now();
        delete state.installedAt?.[pluginId];
        writeState(state);
    }
    return setPluginEnabled(id, true);
}
export function getPluginConfig(pluginId) {
    const state = readState();
    const manifest = getManifest(pluginId);
    const defaults = {};
    for (const field of manifest?.config || []) {
        defaults[field.key] = field.value ?? '';
    }
    return { ...defaults, ...(state.config[pluginId] || {}) };
}
export function savePluginConfig(pluginId, data) {
    const state = readState();
    state.config[pluginId] = data;
    writeState(state);
    return { success: true };
}
function getWorkflowEntryPath(pluginId) {
    const manifest = getManifest(pluginId);
    if (!manifest)
        return null;
    const dir = resolvePluginDir(pluginId);
    if (!dir)
        return null;
    const workflowEntry = manifest.entries?.workflow || 'workflow.json';
    const workflowPath = path.join(dir, workflowEntry);
    return existsSync(workflowPath) ? workflowPath : null;
}
export function getWorkflowNodes(pluginId, locale) {
    const manifest = getManifest(pluginId);
    if (!manifest)
        throw new Error('Plugin not found');
    if (Array.isArray(manifest.workflowNodes))
        return manifest.workflowNodes;
    const plugin = listPlugins().find(item => item.id === pluginId);
    if (plugin) {
        const actions = getRegisteredPluginActions(plugin, locale);
        if (actions.length)
            return createPluginActions(actions).workflow().nodes;
    }
    const workflowPath = getWorkflowEntryPath(pluginId);
    if (!workflowPath)
        return [];
    if (workflowPath.endsWith('.js') || workflowPath.endsWith('.cjs')) {
        return loadCommonJsWorkflowNodes(workflowPath);
    }
    const payload = readJsonFile(workflowPath);
    if (Array.isArray(payload))
        return payload;
    return Array.isArray(payload?.nodes) ? payload.nodes : [];
}
export function getPluginTools(pluginId, locale) {
    const plugin = listPlugins().find(item => item.id === pluginId);
    if (!plugin)
        throw new Error('Plugin not found');
    const actions = getRegisteredPluginActions(plugin, locale);
    if (!actions.length)
        return [];
    return createPluginActions(actions).tools().tools;
}
export async function executePluginTool(pluginId, name, args, api = {}, locale) {
    const plugin = listPlugins().find(item => item.id === pluginId);
    if (!plugin)
        throw new Error('Plugin not found');
    const actions = getRegisteredPluginActions(plugin, locale);
    if (!actions.length)
        throw new Error(`Plugin has no registered tools: ${pluginId}`);
    const mergedArgs = Object.assign({}, getPluginConfig(pluginId), args);
    return createPluginActions(actions).tools().handler(name, mergedArgs, api);
}
function getExecutablePluginByNodeType(nodeType) {
    for (const plugin of listWorkflowPlugins()) {
        if (!plugin.enabled)
            continue;
        const actions = getRegisteredPluginActions(plugin);
        if (actions.length) {
            const { nodes } = createPluginActions(actions).workflow();
            const rawNode = nodes.find(node => node.type === nodeType);
            const handler = rawNode?.handler;
            if (handler)
                return { plugin, handler, api: createBuiltinPluginApi() };
        }
        const workflowPath = getWorkflowEntryPath(plugin.id);
        if (!workflowPath || (!workflowPath.endsWith('.js') && !workflowPath.endsWith('.cjs')))
            continue;
        try {
            const { handlers } = loadCommonJsWorkflowModule(workflowPath);
            const handler = handlers.get(nodeType);
            if (handler)
                return { plugin, handler, api: createBuiltinPluginApi() };
        }
        catch (error) {
            const nodes = loadCommonJsWorkflowNodes(workflowPath);
            if (nodes.some(node => node.type === nodeType)) {
                const message = error instanceof Error ? error.message : String(error);
                throw new Error(`Plugin node "${nodeType}" from "${plugin.id}" failed to load: ${message}`);
            }
            continue;
        }
    }
    return null;
}
export function canExecuteWorkflowNode(nodeType) {
    return Boolean(getExecutablePluginByNodeType(nodeType));
}
export function requiresClientExecution(nodeType) {
    const executable = getExecutablePluginByNodeType(nodeType);
    return executable?.plugin.type === 'client' || executable?.plugin.type === 'both';
}
export function getPluginIdByNodeType(nodeType) {
    return getExecutablePluginByNodeType(nodeType)?.plugin.id ?? null;
}
export async function executeWorkflowNode(nodeType, args, hooks) {
    const executable = getExecutablePluginByNodeType(nodeType);
    if (!executable)
        throw new Error(`Plugin node is not enabled or has no executable handler: ${nodeType}`);
    return executable.handler({
        api: executable.api,
        nodeId: '',
        nodeLabel: nodeType,
        upstream: {},
        logger: hooks.logger,
    }, args);
}
//# sourceMappingURL=plugin.js.map