import fs from "fs";
import path from "path";
import { createPluginContext } from "./plugin-context.js";
import { freshImport } from "./fresh-import.js";
import { normalizePluginConfigSchema } from "./plugin-config.js";

const KNOWN_CONTRIBUTION_DIRS = [
  "tools", "routes", "skills", "agents", "commands", "providers",
];
const KNOWN_UI_HOST_CAPABILITIES = new Set([
  "external.open",
  "clipboard.writeText",
  "sessionFile.open",
]);
const DEFAULT_PLUGIN_LOAD_TIMEOUT_MS = 15_000;

class PluginLoadTimeoutError extends Error {
  constructor(pluginId, stage, ms) {
    super(`Plugin "${pluginId}" ${stage} timed out after ${ms}ms`);
    this.name = "PluginLoadTimeoutError";
    this.pluginId = pluginId;
    this.stage = stage;
    this.timeoutMs = ms;
  }
}

/** Semver compare: returns true if a >= b */
function semverGte(a, b) {
  const pa = (a || "0.0.0").split(".").map(Number);
  const pb = (b || "0.0.0").split(".").map(Number);
  for (let i = 0; i < 3; i++) {
    if ((pa[i] || 0) > (pb[i] || 0)) return true;
    if ((pa[i] || 0) < (pb[i] || 0)) return false;
  }
  return true;
}

function normalizeUiHostCapabilities(raw, pluginId) {
  if (!Array.isArray(raw)) return [];
  const result = [];
  const seen = new Set();
  for (const item of raw) {
    if (typeof item !== "string" || item.trim() === "") continue;
    const capability = item.trim();
    if (!KNOWN_UI_HOST_CAPABILITIES.has(capability)) {
      console.warn(`[plugin-manager] plugin "${pluginId}" declares unknown UI host capability "${capability}", ignoring`);
      continue;
    }
    if (seen.has(capability)) continue;
    seen.add(capability);
    result.push(capability);
  }
  return result;
}

function normalizeActivationEvents(raw, hasLifecycle) {
  if (Array.isArray(raw)) {
    return [...new Set(raw.map((item) => String(item).trim()).filter(Boolean))];
  }
  return hasLifecycle ? ["onStartup"] : [];
}

function activationMatches(events = [], reason = {}) {
  const event = reason.event || "";
  if (!event) return false;
  if (events.includes("*") || events.includes(event)) return true;
  if (event.startsWith("onToolCall:") && events.includes("onToolCall")) return true;
  if (event.startsWith("onBusRequest:") && events.includes("onBusRequest")) return true;
  return false;
}

export class PluginManager {
  /**
   * @param {{ pluginsDirs: string[], dataDir: string, bus: object }} opts
   * pluginsDirs: 多个扫描目录，先内嵌后用户（靠前的优先）
   * 兼容旧签名 { pluginsDir: string } → 自动转为单元素数组
   */
  constructor({
    pluginsDirs,
    pluginsDir,
    dataDir,
    bus,
    preferencesManager,
    appVersion,
    getSessionPath,
    registerSessionFile,
    slashRegistry,
    loadTimeoutMs,
    lifecycleTimeoutMs,
  }) {
    this._pluginsDirs = pluginsDirs || (pluginsDir ? [pluginsDir] : []);
    this._dataDir = dataDir;
    this._bus = bus;
    this._preferencesManager = preferencesManager || null;
    this._appVersion = appVersion || "0.0.0";
    this._getSessionPath = getSessionPath || (() => null);
    this._registerSessionFile = registerSessionFile || null;
    this._plugins = new Map();
    this._scanned = [];
    this._opQueue = Promise.resolve();
    this.routeRegistry = new Map();

    // Contribution registries
    this._tools = [];
    this._commands = [];
    this._skillPaths = [];
    this._agentTemplates = [];
    this._providerPlugins = [];
    this._configSchemas = [];
    // extensionFactories: Array<{ pluginId: string, factory: Function }>
    this._extensionFactories = [];
    this._pages = [];
    this._widgets = [];
    this._settingsTabs = [];

    // Slash command registry（可选；无则仅保留 palette 路径向后兼容）
    this._slashRegistry = slashRegistry || null;
    const timeoutMs = Number(loadTimeoutMs ?? lifecycleTimeoutMs);
    this._loadTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0
      ? timeoutMs
      : DEFAULT_PLUGIN_LOAD_TIMEOUT_MS;
  }

  scan() {
    const results = [];
    const seen = new Set();
    for (let i = 0; i < this._pluginsDirs.length; i++) {
      const dir = this._pluginsDirs[i];
      const source = i === 0 ? "builtin" : "community";
      if (!fs.existsSync(dir)) continue;
      for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
        if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
        if (seen.has(entry.name)) continue;
        seen.add(entry.name);
        const pluginDir = path.join(dir, entry.name);
        try {
          const desc = this._readPluginDescriptor(pluginDir, entry.name);
          desc.source = source;
          if (seen.has(`id:${desc.id}`)) {
            console.warn(`[plugin-manager] plugin id "${desc.id}" 冲突（目录 "${entry.name}"），跳过`);
            continue;
          }
          seen.add(`id:${desc.id}`);
          results.push(desc);
        } catch (err) {
          console.error(`[plugin-manager] failed to read plugin "${entry.name}":`, err.message);
        }
      }
    }
    this._scanned = results;
    return results;
  }

  _readPluginDescriptor(pluginDir, dirName) {
    const manifestPath = path.join(pluginDir, "manifest.json");
    let manifest = null;
    if (fs.existsSync(manifestPath)) {
      manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
    }
    const id = manifest?.id || dirName;
    const name = manifest?.name || dirName;
    const version = manifest?.version || "0.0.0";
    const description = manifest?.description || "";
    const uiHostCapabilities = normalizeUiHostCapabilities(manifest?.ui?.hostCapabilities, id);
    const configSchema = manifest?.contributes?.configuration
      ? normalizePluginConfigSchema(id, manifest.contributes.configuration)
      : normalizePluginConfigSchema(id, {});
    const contributions = [];
    for (const dir of KNOWN_CONTRIBUTION_DIRS) {
      if (fs.existsSync(path.join(pluginDir, dir))) contributions.push(dir);
    }
    if (fs.existsSync(path.join(pluginDir, "extensions"))) contributions.push("extensions");
    const hasLifecycle = fs.existsSync(path.join(pluginDir, "index.js"));
    if (hasLifecycle) contributions.push("lifecycle");
    const trust = manifest?.trust === "full-access" ? "full-access" : "restricted";
    const hidden = !!manifest?.hidden;
    const activationEvents = normalizeActivationEvents(manifest?.activationEvents, hasLifecycle);
    return { id, name, version, description, pluginDir, manifest, contributions, trust, hidden, uiHostCapabilities, configSchema, activationEvents, hasLifecycle };
  }

  async loadAll() {
    const descriptors = this._scanned.length > 0 ? this._scanned : this.scan();
    const disabledList = this._preferencesManager?.getDisabledPlugins() || [];
    for (const desc of descriptors) {
      const entry = { ...desc, status: "loading", activationState: "inactive", activationReason: null, instance: null, _disposables: [] };

      // builtin 插件不受 disabled 列表和全权开关约束，始终加载
      if (desc.source !== "builtin" && disabledList.includes(desc.id)) {
        entry.status = "disabled";
        this._plugins.set(desc.id, entry);
        continue;
      }

      if (desc.source === "community" && desc.trust === "full-access") {
        const allowed = this._preferencesManager?.getAllowFullAccessPlugins() || false;
        if (!allowed) {
          entry.status = "restricted";
          this._plugins.set(desc.id, entry);
          continue;
        }
      }

      // minAppVersion check
      const minVer = desc.manifest?.minAppVersion;
      if (minVer && !semverGte(this._appVersion, minVer)) {
        entry.status = "incompatible";
        entry.error = `requires app v${minVer}+, current v${this._appVersion}`;
        this._plugins.set(desc.id, entry);
        console.warn(`[plugin-manager] "${desc.id}" skipped: ${entry.error}`);
        continue;
      }

      this._plugins.set(desc.id, entry);
      try {
        await this._loadPluginWithBoundary(entry);
        entry.status = "loaded";
        entry.error = null;
      } catch (err) {
        entry.status = "failed";
        entry.error = err.message;
        console.error(`[plugin-manager] plugin "${desc.id}" failed to load:`, err.message);
      }
    }
  }

  async _loadPluginWithBoundary(entry) {
    const loadToken = Symbol(entry.id);
    entry._loadToken = loadToken;
    entry._loadCancelled = false;
    entry.error = null;
    const start = Date.now();
    console.log(`[plugin-manager] loading plugin "${entry.id}"...`);

    try {
      await this._withLoadTimeout(
        entry,
        this._loadPlugin(entry, loadToken),
        "load",
      );
      if (entry._loadToken !== loadToken || entry._loadCancelled) {
        throw new Error(`Plugin "${entry.id}" load was cancelled`);
      }
      console.log(`[plugin-manager] plugin "${entry.id}" loaded (${Date.now() - start}ms)`);
    } catch (err) {
      entry._loadCancelled = true;
      await this._cleanupPluginEntry(entry);
      throw err;
    }
  }

  async _withLoadTimeout(entry, promise, stage) {
    let timer = null;
    try {
      return await Promise.race([
        promise,
        new Promise((_, reject) => {
          timer = setTimeout(() => {
            entry._loadCancelled = true;
            reject(new PluginLoadTimeoutError(entry.id, entry._loadStage || stage, this._loadTimeoutMs));
          }, this._loadTimeoutMs);
          timer.unref?.();
        }),
      ]);
    } finally {
      if (timer) clearTimeout(timer);
    }
  }

  async _runLoadStage(entry, stage, fn) {
    const start = Date.now();
    entry._loadStage = stage;
    console.log(`[plugin-manager] loading "${entry.id}" ${stage}...`);
    try {
      const result = await fn();
      console.log(`[plugin-manager] loaded "${entry.id}" ${stage} (${Date.now() - start}ms)`);
      return result;
    } catch (err) {
      console.error(`[plugin-manager] "${entry.id}" ${stage} failed:`, err?.message || err);
      throw err;
    }
  }

  async _runLoadStageIf(entry, condition, stage, fn) {
    if (!condition) return undefined;
    return this._runLoadStage(entry, stage, fn);
  }

  _hasContributionDir(entry, dirName) {
    return fs.existsSync(path.join(entry.pluginDir, dirName));
  }

  _assertActiveLoad(entry, loadToken) {
    if (entry._loadToken !== loadToken || entry._loadCancelled) {
      throw new Error(`Plugin "${entry.id}" load was cancelled`);
    }
  }

  async _loadPlugin(entry, loadToken) {
    const accessLevel = (entry.source === "builtin" || entry.trust === "full-access")
      ? "full-access"
      : "restricted";
    entry.accessLevel = accessLevel;

    entry.ctx = createPluginContext({
      pluginId: entry.id,
      pluginDir: entry.pluginDir,
      dataDir: path.join(this._dataDir, entry.id),
      bus: this._bus,
      accessLevel,
      registerSessionFile: this._registerSessionFile,
      configSchema: entry.configSchema,
    });

    // All plugins: declarative contributions
    this._assertActiveLoad(entry, loadToken);
    await this._runLoadStageIf(entry, this._hasContributionDir(entry, "tools"), "tools", () => this._loadTools(entry));
    this._assertActiveLoad(entry, loadToken);
    await this._runLoadStageIf(entry, this._hasContributionDir(entry, "skills"), "skills", () => this._loadSkillPaths(entry));
    this._assertActiveLoad(entry, loadToken);
    await this._runLoadStageIf(entry, this._hasContributionDir(entry, "commands"), "commands", () => this._loadCommands(entry));
    this._assertActiveLoad(entry, loadToken);
    await this._runLoadStageIf(entry, this._hasContributionDir(entry, "agents"), "agent templates", () => this._loadAgentTemplates(entry));  // JSON declaration, no code execution
    this._assertActiveLoad(entry, loadToken);
    await this._runLoadStageIf(entry, !!entry.manifest?.contributes?.configuration, "configuration", () => this._loadConfiguration(entry));

    // Full-access only: system-level extension points
    if (accessLevel === "full-access") {
      this._assertActiveLoad(entry, loadToken);
      await this._runLoadStageIf(entry, this._hasContributionDir(entry, "routes"), "routes", () => this._loadRoutes(entry));
      this._assertActiveLoad(entry, loadToken);
      await this._runLoadStageIf(entry, this._hasContributionDir(entry, "extensions"), "extensions", () => this._loadExtensions(entry));
      this._assertActiveLoad(entry, loadToken);
      await this._runLoadStageIf(entry, this._hasContributionDir(entry, "providers"), "providers", () => this._loadProviders(entry));
      this._assertActiveLoad(entry, loadToken);
      await this._runLoadStageIf(entry, !!entry.manifest?.contributes?.page, "page", () => this._loadPage(entry));
      this._assertActiveLoad(entry, loadToken);
      await this._runLoadStageIf(entry, !!entry.manifest?.contributes?.widget, "widget", () => this._loadWidget(entry));
      this._assertActiveLoad(entry, loadToken);
      await this._runLoadStageIf(entry, !!entry.manifest?.contributes?.settingsTab, "settings tab", () => this._loadSettingsTab(entry));

      if (activationMatches(entry.activationEvents, { event: "onStartup" })) {
        this._assertActiveLoad(entry, loadToken);
        await this._activatePluginEntry(entry, { event: "onStartup" }, loadToken);
      }
    }
  }

  async _activatePluginEntry(entry, reason = {}, loadToken = entry._loadToken) {
    if (!entry.hasLifecycle || entry.activationState === "activated") return entry;
    if (entry._activationPromise) return entry._activationPromise;

    entry.activationState = "activating";
    entry.activationReason = reason;
    const run = async () => {
      try {
        const indexPath = path.join(entry.pluginDir, "index.js");
        const mod = await this._runLoadStage(entry, "lifecycle import", () => freshImport(indexPath));
        const PluginClass = mod.default;
        if (PluginClass && typeof PluginClass === "function") {
          const instance = new PluginClass();
          entry.instance = instance;
          instance.ctx = entry.ctx;
          instance.register = (disposable) => {
            if (typeof disposable !== "function") return;
            if (entry._loadToken !== loadToken || entry._loadCancelled) {
              try { disposable(); } catch (err) {
                console.error(`[plugin-manager] "${entry.id}" late disposable error:`, err.message);
              }
              return;
            }
            entry._disposables.push(disposable);
          };
          instance.ctx.registerTool = (toolDef) => {
            const dispose = this.addTool(entry.id, toolDef);
            if (entry._loadToken !== loadToken || entry._loadCancelled) {
              try { dispose(); } catch (err) {
                console.error(`[plugin-manager] "${entry.id}" late dynamic tool cleanup error:`, err.message);
              }
              return () => {};
            }
            return dispose;
          };
          if (typeof instance.onload === "function") {
            this._assertActiveLoad(entry, loadToken);
            await this._runLoadStage(entry, "lifecycle onload", () => instance.onload());
          }
        }
        entry.activationState = "activated";
        entry.activationError = null;
        return entry;
      } catch (err) {
        entry.activationState = "failed";
        entry.activationError = err.message;
        throw err;
      } finally {
        entry._activationPromise = null;
      }
    };

    entry._activationPromise = this._withLoadTimeout(entry, run(), `activation ${reason.event || "manual"}`);
    return entry._activationPromise;
  }

  async activatePlugin(pluginId, reason = {}) {
    const entry = this._plugins.get(pluginId);
    if (!entry) throw new Error(`Plugin "${pluginId}" not found`);
    if (!activationMatches(entry.activationEvents, reason)) return entry;
    return this._activatePluginEntry(entry, reason);
  }

  async activatePluginRoute(pluginId, routePath) {
    const entry = this._plugins.get(pluginId);
    if (!entry) return null;
    const page = this._pages.find((item) => item.pluginId === pluginId && item.route === routePath);
    const widget = this._widgets.find((item) => item.pluginId === pluginId && item.route === routePath);
    if (page) return this.activatePlugin(pluginId, { event: "onPageOpen", route: routePath });
    if (widget) return this.activatePlugin(pluginId, { event: "onWidgetOpen", route: routePath });
    return entry;
  }

  // ── Task 5: Tool loader ──────────────────────────────────────────────────

  async _loadTools(entry) {
    const toolsDir = path.join(entry.pluginDir, "tools");
    if (!fs.existsSync(toolsDir)) return;
    const files = fs.readdirSync(toolsDir).filter((f) => f.endsWith(".js"));
    const ctx = entry.ctx;
    for (const file of files) {
      const filePath = path.join(toolsDir, file);
      try {
        const mod = await freshImport(filePath);
        if (!mod.name || !mod.description || typeof mod.execute !== "function") continue;
        const origExecute = mod.execute;
        this._tools.push({
          name: `${entry.id}_${mod.name}`,
          description: mod.description,
          parameters: mod.parameters ?? {},
          ...(mod.promptSnippet ? { promptSnippet: mod.promptSnippet } : {}),
          ...(mod.promptGuidelines ? { promptGuidelines: mod.promptGuidelines } : {}),
          execute: async (_toolCallId, params, runtimeCtx) => {
            await this.activatePlugin(entry.id, { event: `onToolCall:${mod.name}`, toolName: mod.name });
            // 优先从 Pi SDK runtime ctx 获取 sessionPath，fallback 到焦点回调（过渡期）
            const sessionPath = runtimeCtx?.sessionManager?.getSessionFile?.()
              || this._getSessionPath?.();
            const sessionCtx = { sessionPath };
            const mergedCtx = runtimeCtx
              ? { ...ctx, ...sessionCtx, ...runtimeCtx }
              : { ...ctx, ...sessionCtx };
            const raw = await origExecute(params, mergedCtx);
            // Pi SDK 期望 { content: ContentBlock[], details? }
            // Plugin tool 可能返回纯字符串，需要包装
            let result;
            if (typeof raw === "string") {
              result = { content: [{ type: "text", text: raw }] };
            } else if (raw && raw.content) {
              result = raw;
            } else {
              result = { content: [{ type: "text", text: String(raw ?? "") }] };
            }
            // Plugin Card: auto-inject pluginId
            if (result.details?.card && !result.details.card.pluginId) {
              result.details.card.pluginId = ctx.pluginId;
            }
            return result;
          },
          _pluginId: entry.id,
        });
      } catch (err) {
        console.error(`[plugin-manager] tool "${file}" in "${entry.id}" failed to load:`, err.message);
      }
    }
  }

  /**
   * 动态注册工具（供 plugin 在 onload 中调用，如 MCP bridge）
   * @param {string} pluginId
   * @param {{ name: string, description: string, parameters?: object, execute: Function }} toolDef
   * @returns {Function} 清理函数（调用即移除该工具）
   */
  addTool(pluginId, toolDef) {
    const tool = {
      name: `${pluginId}_${toolDef.name}`,
      description: toolDef.description || "",
      parameters: toolDef.parameters || { type: "object", properties: {} },
      execute: toolDef.execute,
      _pluginId: pluginId,
      _dynamic: true,
    };
    if (typeof toolDef.isEnabledForAgentConfig === "function") {
      tool.isEnabledForAgentConfig = toolDef.isEnabledForAgentConfig;
    }
    if (toolDef.metadata && typeof toolDef.metadata === "object") {
      tool.metadata = { ...toolDef.metadata };
    }
    this._tools.push(tool);
    return () => {
      const idx = this._tools.indexOf(tool);
      if (idx !== -1) this._tools.splice(idx, 1);
    };
  }

  getAllTools() {
    return [...this._tools];
  }

  // ── Task 6: Skill paths + Command loader ────────────────────────────────

  async _loadSkillPaths(entry) {
    const skillsDir = path.join(entry.pluginDir, "skills");
    if (!fs.existsSync(skillsDir)) return;
    this._skillPaths.push({
      dirPath: skillsDir,
      label: `plugin:${entry.id}`,
      builtin: entry.source === "builtin",
    });
  }

  getSkillPaths() {
    return [...this._skillPaths];
  }

  async _loadCommands(entry) {
    const cmdsDir = path.join(entry.pluginDir, "commands");
    if (!fs.existsSync(cmdsDir)) return;
    const files = fs.readdirSync(cmdsDir).filter((f) => f.endsWith(".js"));
    for (const file of files) {
      const filePath = path.join(cmdsDir, file);
      try {
        const mod = await freshImport(filePath);
        if (!mod.name) continue;
        const hasHandler = typeof mod.handler === "function";
        const hasExecute = typeof mod.execute === "function";
        if (!hasHandler && !hasExecute) continue;

        // 纪律 #2：handler 优先。双写时只注册 slash，不进 palette
        if (hasHandler) {
          // 纪律 #1：Full-access 闸门。restricted 插件不得注册 slash handler
          if (entry.accessLevel !== "full-access") {
            console.warn(
              `[plugin-manager] "${entry.id}/${file}" declares slash handler but plugin is restricted; skipped. ` +
              `Requires builtin source or manifest.trust="full-access".`
            );
            continue;
          }
          if (this._slashRegistry) {
            this._slashRegistry.registerCommand(
              {
                name: mod.name,
                aliases: Array.isArray(mod.aliases) ? mod.aliases : [],
                description: mod.description ?? "",
                scope: mod.scope || "session",
                // 纪律 #6：permission 缺省默认 owner（最严）
                permission: mod.permission || "owner",
                handler: mod.handler,
                usage: mod.usage,
              },
              { source: "plugin", sourceId: entry.id },
            );
            // registry 返回 null 表示被保留名闸门（#3）拒绝；已在 registry 内部 warn，此处不再打印
          }
        } else {
          // 仅 execute → palette 路径（向后兼容，保持原有 _commands 行为不变）
          this._commands.push({
            name: `${entry.id}.${mod.name}`,
            description: mod.description ?? "",
            execute: mod.execute,
            _pluginId: entry.id,
          });
        }
      } catch (err) {
        console.error(`[plugin-manager] command "${file}" in "${entry.id}" failed to load:`, err.message);
      }
    }
  }

  getAllCommands() {
    return [...this._commands];
  }

  // ── Task 7: Route loader ─────────────────────────────────────────────────

  async _loadRoutes(entry) {
    const routesDir = path.join(entry.pluginDir, "routes");
    if (!fs.existsSync(routesDir)) return;
    const { Hono } = await import("hono");
    const app = new Hono();
    const ctx = entry.ctx;

    // Error isolation: Hono's onError is the correct hook for handler throws
    app.onError((err, c) => {
      ctx.log.error("route error:", err.message);
      return c.json({ error: "Plugin internal error", plugin: entry.id }, 500);
    });

    // Middleware: inject ctx + agentId (from proxy header)
    app.use("*", async (c, next) => {
      c.set("pluginCtx", ctx);
      const agentId = c.req.header("X-Hana-Agent-Id") || null;
      c.set("agentId", agentId);
      await next();
    });

    const files = fs.readdirSync(routesDir).filter((f) => f.endsWith(".js"));
    for (const file of files) {
      const filePath = path.join(routesDir, file);
      try {
        const mod = await freshImport(filePath);
        if (typeof mod.default === "function") {
          const sub = mod.default;
          if (sub && typeof sub.fetch === "function") {
            // Static Hono app — inject ctx + agentId middleware onto sub-app too
            sub.use("*", async (c, next) => {
              c.set("pluginCtx", ctx);
              const agentId = c.req.header("X-Hana-Agent-Id") || null;
              c.set("agentId", agentId);
              await next();
            });
            const prefix = "/" + path.basename(file, ".js");
            app.route(prefix, sub);
          } else if (typeof sub === "function") {
            // Factory function — pass ctx as second arg
            sub(app, ctx);
          }
        }
        if (mod.register && typeof mod.register === "function") {
          mod.register(app, ctx);
        }
      } catch (err) {
        console.error(`[plugin-manager] route "${file}" in "${entry.id}" failed to load:`, err.message);
      }
    }
    this.routeRegistry.set(entry.id, app);
  }

  // ── Task 8: Extension loader ─────────────────────────────────────────────

  /**
   * 加载 extensions/ 目录下的 Pi SDK extension 工厂函数。
   * 每个 .js 文件导出 (pi: ExtensionAPI) => void，在 session 创建时被 Pi SDK 调用。
   */
  async _loadExtensions(entry) {
    const extDir = path.join(entry.pluginDir, "extensions");
    if (!fs.existsSync(extDir)) return;
    const files = fs.readdirSync(extDir).filter((f) => f.endsWith(".js"));
    for (const file of files) {
      const filePath = path.join(extDir, file);
      try {
        const mod = await freshImport(filePath);
        const factory = mod.default ?? mod;
        if (typeof factory !== "function") {
          console.warn(`[plugin-manager] extension "${file}" in "${entry.id}" does not export a function, skipped`);
          continue;
        }
        this._extensionFactories.push({ pluginId: entry.id, factory });
      } catch (err) {
        console.error(`[plugin-manager] extension "${file}" in "${entry.id}" failed to load:`, err.message);
      }
    }
  }

  // ── Task 9: Configuration loader ─────────────────────────────────────────

  _loadConfiguration(entry) {
    const schema = entry.configSchema;
    if (!schema || Object.keys(schema.properties || {}).length === 0) return;
    this._configSchemas.push({ pluginId: entry.id, schema });
  }

  getConfigSchema(pluginId) {
    return this._configSchemas.find((s) => s.pluginId === pluginId)?.schema ?? null;
  }

  getAllConfigSchemas() {
    return [...this._configSchemas];
  }

  getConfig(pluginId, options = {}) {
    const entry = this._plugins.get(pluginId);
    if (!entry?.ctx?.config) return null;
    return {
      pluginId,
      schema: entry.ctx.config.getSchema(),
      values: entry.ctx.config.getAll({ ...options, redacted: true }),
    };
  }

  setConfig(pluginId, values, options = {}) {
    const entry = this._plugins.get(pluginId);
    if (!entry?.ctx?.config) throw new Error(`Plugin "${pluginId}" not found`);
    const nextValues = entry.ctx.config.setMany(values, options);
    this._bus?.emit({ type: "plugin_config_changed", pluginId, scope: options.scope || "global" });
    return {
      pluginId,
      schema: entry.ctx.config.getSchema(),
      values: entry.ctx.config.getAll({ ...options, redacted: true }),
      rawValues: nextValues,
    };
  }

  // ── Page / Widget loader ──────────────────────────────────────────────────

  _loadPage(entry) {
    const page = entry.manifest?.contributes?.page;
    if (!page) return;
    if (entry.accessLevel !== 'full-access') {
      entry.ctx?.log?.warn('page contribution requires full-access, skipping');
      return;
    }
    const routesDir = path.join(entry.pluginDir, 'routes');
    if (!fs.existsSync(routesDir)) {
      entry.ctx?.log?.warn(`page declares route "${page.route}" but routes/ directory not found`);
      return;
    }
    this._pages.push({
      pluginId: entry.id,
      title: page.title || entry.id,
      icon: page.icon || null,
      route: page.route,
      hostCapabilities: [...(entry.uiHostCapabilities || [])],
    });
  }

  _loadWidget(entry) {
    const widget = entry.manifest?.contributes?.widget;
    if (!widget) return;
    if (entry.accessLevel !== 'full-access') {
      entry.ctx?.log?.warn('widget contribution requires full-access, skipping');
      return;
    }
    const routesDir = path.join(entry.pluginDir, 'routes');
    if (!fs.existsSync(routesDir)) {
      entry.ctx?.log?.warn(`widget declares route "${widget.route}" but routes/ directory not found`);
      return;
    }
    this._widgets.push({
      pluginId: entry.id,
      title: widget.title || entry.id,
      icon: widget.icon || null,
      route: widget.route,
      hostCapabilities: [...(entry.uiHostCapabilities || [])],
    });
  }

  _loadSettingsTab(entry) {
    const settingsTab = entry.manifest?.contributes?.settingsTab;
    if (!settingsTab) return;
    if (entry.source !== "builtin") {
      entry.ctx?.log?.warn('settingsTab contribution is only available to bundled built-in plugins, skipping');
      return;
    }
    if (entry.accessLevel !== 'full-access') {
      entry.ctx?.log?.warn('settingsTab contribution requires full-access, skipping');
      return;
    }
    if (typeof settingsTab.nativeComponent !== "string" || !settingsTab.nativeComponent) {
      entry.ctx?.log?.warn('settingsTab contribution requires nativeComponent, skipping');
      return;
    }
    this._settingsTabs.push({
      pluginId: entry.id,
      id: settingsTab.id || entry.id,
      title: settingsTab.title || entry.name || entry.id,
      icon: settingsTab.icon || null,
      nativeComponent: settingsTab.nativeComponent,
    });
  }

  // ── Task 10: Agent templates + Provider loader ───────────────────────────

  async _loadAgentTemplates(entry) {
    const agentsDir = path.join(entry.pluginDir, "agents");
    if (!fs.existsSync(agentsDir)) return;
    const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".json"));
    for (const file of files) {
      const filePath = path.join(agentsDir, file);
      try {
        const template = JSON.parse(fs.readFileSync(filePath, "utf-8"));
        template._pluginId = entry.id;
        this._agentTemplates.push(template);
      } catch (err) {
        console.error(`[plugin-manager] agent template "${file}" in "${entry.id}" failed to load:`, err.message);
      }
    }
  }

  getAgentTemplates() {
    return [...this._agentTemplates];
  }

  async _loadProviders(entry) {
    const providersDir = path.join(entry.pluginDir, "providers");
    if (!fs.existsSync(providersDir)) return;
    const files = fs.readdirSync(providersDir).filter((f) => f.endsWith(".js"));
    for (const file of files) {
      const filePath = path.join(providersDir, file);
      try {
        const mod = await freshImport(filePath);
        if (!mod.id) continue;
        this._providerPlugins.push({ ...mod, _pluginId: entry.id });
      } catch (err) {
        console.error(`[plugin-manager] provider "${file}" in "${entry.id}" failed to load:`, err.message);
      }
    }
  }

  getProviderPlugins() {
    return [...this._providerPlugins];
  }

  // ── Operation queue ───────────────────────────────────────────────────────

  _enqueue(fn) {
    const op = this._opQueue.then(fn);
    this._opQueue = op.catch(err => {
      console.error("[plugin-manager] op failed:", err);
    });
    return op; // caller gets success/failure
  }

  // ── Hot operations ───────────────────────────────────────────────────────

  async installPlugin(pluginDir) {
    return this._enqueue(async () => {
      const dirName = path.basename(pluginDir);
      // Check for existing (upgrade scenario)
      const existing = [...this._plugins.values()].find(
        p => path.basename(p.pluginDir) === dirName
      );
      if (existing) {
        await this.unloadPlugin(existing.id);
        this._plugins.delete(existing.id);
      }

      const desc = this._readPluginDescriptor(pluginDir, dirName);
      desc.source = "community";
      const disabledList = this._preferencesManager?.getDisabledPlugins() || [];

      const entry = { ...desc, status: "loading", activationState: "inactive", activationReason: null, instance: null, _disposables: [] };
      this._plugins.set(desc.id, entry);

      if (disabledList.includes(desc.id)) {
        entry.status = "disabled";
        return entry;
      }

      if (desc.trust === "full-access") {
        const allowed = this._preferencesManager?.getAllowFullAccessPlugins() || false;
        if (!allowed) {
          entry.status = "restricted";
          return entry;
        }
      }

      try {
        await this._loadPluginWithBoundary(entry);
        entry.status = "loaded";
        entry.error = null;
      } catch (err) {
        entry.status = "failed";
        entry.error = err.message;
      }
      this._bus?.emit({ type: "plugin_ui_changed" });
      return entry;
    });
  }

  async removePlugin(pluginId) {
    return this._enqueue(async () => {
      const entry = this._plugins.get(pluginId);
      if (!entry) throw new Error(`Plugin "${pluginId}" not found`);
      if (entry.source === "builtin") throw new Error(`Builtin plugin "${pluginId}" cannot be removed`);
      if (entry.status === "loaded" || entry.status === "failed") {
        await this.unloadPlugin(pluginId);
      }
      this._plugins.delete(pluginId);
      if (this._preferencesManager) {
        const disabled = this._preferencesManager.getDisabledPlugins();
        this._preferencesManager.setDisabledPlugins(
          disabled.filter(id => id !== pluginId)
        );
      } else {
        console.warn("[plugin-manager] removePlugin: preferencesManager unavailable, disabled list not updated");
      }
      this._bus?.emit({ type: "plugin_ui_changed" });
      return entry.pluginDir;
    });
  }

  async disablePlugin(pluginId) {
    return this._enqueue(async () => {
      const entry = this._plugins.get(pluginId);
      if (!entry) throw new Error(`Plugin "${pluginId}" not found`);
      if (entry.source === "builtin") throw new Error(`Builtin plugin "${pluginId}" cannot be disabled`);
      if (entry.status === "loaded") {
        await this.unloadPlugin(pluginId);
      }
      entry.status = "disabled";
      if (this._preferencesManager) {
        const disabled = this._preferencesManager.getDisabledPlugins();
        if (!disabled.includes(pluginId)) {
          this._preferencesManager.setDisabledPlugins([...disabled, pluginId]);
        }
      } else {
        console.warn("[plugin-manager] disablePlugin: preferencesManager unavailable, preference not persisted");
      }
      this._bus?.emit({ type: "plugin_ui_changed" });
    });
  }

  async enablePlugin(pluginId) {
    return this._enqueue(async () => {
      const entry = this._plugins.get(pluginId);
      if (!entry) throw new Error(`Plugin "${pluginId}" not found`);
      // builtin 插件始终 loaded，跳过偏好写入
      if (entry.source === "builtin") return;
      if (this._preferencesManager) {
        const disabled = this._preferencesManager.getDisabledPlugins();
        this._preferencesManager.setDisabledPlugins(
          disabled.filter(id => id !== pluginId)
        );
      } else {
        console.warn("[plugin-manager] enablePlugin: preferencesManager unavailable, preference not persisted");
      }
      if (entry.trust === "full-access" && entry.source === "community") {
        const allowed = this._preferencesManager?.getAllowFullAccessPlugins() || false;
        if (!allowed) {
          entry.status = "restricted";
          this._bus?.emit({ type: "plugin_ui_changed" });
          return;
        }
      }
      // Guard: unload before re-loading to prevent duplicate tool/command/route registration
      if (entry.status === "loaded") {
        await this.unloadPlugin(pluginId);
      }
      try {
        await this._loadPluginWithBoundary(entry);
        entry.status = "loaded";
        entry.error = null;
      } catch (err) {
        entry.status = "failed";
        entry.error = err.message;
      }
      this._bus?.emit({ type: "plugin_ui_changed" });
    });
  }

  async setFullAccess(allow) {
    return this._enqueue(async () => {
      if (this._preferencesManager) {
        this._preferencesManager.setAllowFullAccessPlugins(allow);
      } else {
        console.warn("[plugin-manager] setFullAccess: preferencesManager unavailable, preference not persisted");
      }
      for (const entry of this._plugins.values()) {
        if (entry.source !== "community" || entry.trust !== "full-access") continue;
        const disabledList = this._preferencesManager?.getDisabledPlugins() || [];
        if (disabledList.includes(entry.id)) continue;

        if (allow && entry.status === "restricted") {
          try {
            await this._loadPluginWithBoundary(entry);
            entry.status = "loaded";
            entry.error = null;
          } catch (err) {
            entry.status = "failed";
            entry.error = err.message;
          }
        } else if (!allow && entry.status === "loaded") {
          await this.unloadPlugin(entry.id);
          entry.status = "restricted";
        }
      }
      this._bus?.emit({ type: "plugin_ui_changed" });
    });
  }

  // ── Lifecycle ─────────────────────────────────────────────────────────────

  async _cleanupPluginEntry(entry) {
    const pluginId = entry.id;

    // 1. 生命周期清理（onunload + disposables）
    if (entry.instance) {
      if (typeof entry.instance.onunload === "function") {
        try { await entry.instance.onunload(); } catch (err) {
          console.error(`[plugin-manager] "${pluginId}" onunload error:`, err.message);
        }
      }
      for (const d of entry._disposables.reverse()) {
        try { d(); } catch (err) {
          console.error(`[plugin-manager] "${pluginId}" disposable error:`, err.message);
        }
      }
      entry._disposables = [];
    }
    entry.instance = null;
    entry.activationState = entry.hasLifecycle ? "inactive" : "none";

    // 2. 清理静态贡献（文件约定加载的 tools、commands 等）
    this._tools = this._tools.filter(t => t._pluginId !== pluginId);
    this._commands = this._commands.filter(c => c._pluginId !== pluginId);
    this._slashRegistry?.unregisterBySource("plugin", pluginId);
    this._skillPaths = this._skillPaths.filter(s => s.label !== `plugin:${pluginId}`);
    this._agentTemplates = this._agentTemplates.filter(t => t._pluginId !== pluginId);
    this._providerPlugins = this._providerPlugins.filter(p => p._pluginId !== pluginId);
    this._configSchemas = this._configSchemas.filter(s => s.pluginId !== pluginId);
    this._extensionFactories = this._extensionFactories.filter(e => e.pluginId !== pluginId);
    this._pages = this._pages.filter(p => p.pluginId !== pluginId);
    this._widgets = this._widgets.filter(w => w.pluginId !== pluginId);
    this._settingsTabs = this._settingsTabs.filter(t => t.pluginId !== pluginId);
    this.routeRegistry.delete(pluginId);
  }

  async unloadPlugin(pluginId) {
    const entry = this._plugins.get(pluginId);
    if (!entry) return;

    entry._loadCancelled = true;
    await this._cleanupPluginEntry(entry);

    entry.status = "unloaded";
  }

  // ── Public getters (route 层通过这些方法访问，不穿透私有字段) ──

  /** 用户（社区）插件目录 */
  getUserPluginsDir() {
    return this._pluginsDirs[this._pluginsDirs.length - 1] || null;
  }

  /** 是否允许 full-access 社区插件 */
  getAllowFullAccess() {
    return this._preferencesManager?.getAllowFullAccessPlugins() || false;
  }

  /** 检测目录是否为合法插件 */
  isValidPluginDir(dirPath) {
    const validMarkers = [
      ...KNOWN_CONTRIBUTION_DIRS,
      "manifest.json", "index.js", "extensions",
    ];
    return validMarkers.some(marker => {
      const p = path.join(dirPath, marker);
      return fs.existsSync(p);
    });
  }

  /** 获取指定插件的路由 app */
  getRouteApp(pluginId) {
    return this.routeRegistry.get(pluginId) || null;
  }

  /** 获取所有活跃插件的 extension 工厂函数（供 Engine 注入 Pi SDK） */
  getExtensionFactories() {
    return this._extensionFactories.map(e => e.factory);
  }

  getPages() { return [...this._pages]; }
  getWidgets() { return [...this._widgets]; }
  getSettingsTabs() { return [...this._settingsTabs]; }
  getDiagnostics() {
    return [...this._plugins.values()].map((entry) => {
      const pluginId = entry.id;
      return {
        id: pluginId,
        name: entry.name,
        version: entry.version,
        source: entry.source || "community",
        trust: entry.trust || "restricted",
        hidden: !!entry.hidden,
        status: entry.status,
        error: entry.error || null,
        activationState: entry.activationState || null,
        activationEvents: Array.isArray(entry.activationEvents) ? [...entry.activationEvents] : [],
        activationReason: entry.activationReason || null,
        activationError: entry.activationError || null,
        contributions: Array.isArray(entry.contributions) ? [...entry.contributions] : [],
        uiHostCapabilities: Array.isArray(entry.uiHostCapabilities) ? [...entry.uiHostCapabilities] : [],
        routes: {
          hasRouteApp: this.routeRegistry.has(pluginId),
          pages: this._pages.filter((item) => item.pluginId === pluginId).map(clonePlain),
          widgets: this._widgets.filter((item) => item.pluginId === pluginId).map(clonePlain),
          settingsTabs: this._settingsTabs.filter((item) => item.pluginId === pluginId).map(clonePlain),
        },
        tools: this._tools
          .filter((item) => item._pluginId === pluginId)
          .map((item) => ({ name: item.name, dynamic: !!item._dynamic })),
        commands: this._commands
          .filter((item) => item._pluginId === pluginId)
          .map((item) => ({ name: item.name })),
        providers: this._providerPlugins
          .filter((item) => item._pluginId === pluginId)
          .map((item) => ({ id: item.id, name: item.name || item.id })),
        config: {
          hasSchema: !!entry.configSchema,
          keys: Object.keys(entry.configSchema?.properties || {}),
        },
      };
    });
  }
  getUiHostCapabilityGrants() {
    return [...this._plugins.values()]
      .filter(entry => entry.status === "loaded" && Array.isArray(entry.uiHostCapabilities) && entry.uiHostCapabilities.length > 0)
      .map(entry => ({
        pluginId: entry.id,
        hostCapabilities: [...entry.uiHostCapabilities],
      }));
  }

  getPlugin(id) { return this._plugins.get(id) || null; }
  listPlugins() { return [...this._plugins.values()]; }
}

function clonePlain(value) {
  return structuredClone(value);
}
