// wall-e/tools/coding-middleware.js
// Inspired by OpenCode's plugin hook system
// (https://github.com/anomalyco/opencode, packages/opencode/src/plugin/index.ts)
// Adapted as internal middleware (not external plugins) for Wall-E
//
// OpenCode capabilities ported here:
// 1. Hook-based middleware pipeline (original)
// 2. Event subscription — plugins can subscribe to bus events
//    (OpenCode: packages/opencode/src/plugin/index.ts lines 247-256)
// 3. Config notification — plugins get notified of config changes
//    (OpenCode: packages/opencode/src/plugin/index.ts lines 237-245)
// 4. Plugin-provided tools — plugins can register custom tool definitions
//    (OpenCode: packages/plugin/src/index.ts — Hooks.tool field)
'use strict';

const HOOK_TYPES = new Set([
  'llm.before',       // Before LLM call — modify system prompt, params, tools
  'llm.after',        // After LLM response — separate reasoning, track cost
  'tool.before',      // Before tool execution — modify args, check permissions
  'tool.after',       // After tool execution — transform output, add diagnostics
  'shell.env',        // Before shell execution — inject environment variables
  'tool.definitions', // Before sending tools to LLM — adapt descriptions per model
  'tool.register',    // When a tool is registered — middleware can modify/reject tool definitions
  'onEvent',          // When an event bus event fires — middleware reacts to file edits, reads, context overflow
]);

class CodingMiddleware {
  constructor({ hookRuntime = null } = {}) {
    this._hooks = {};
    for (const type of HOOK_TYPES) this._hooks[type] = [];
    this.hookRuntime = hookRuntime;

    // Config notification
    // Ported from OpenCode plugin/index.ts — hook.config?.() pattern (lines 237-245)
    this._configListeners = []; // Array of (config) => void

    // Plugin-provided tools
    // Ported from OpenCode plugin/src/index.ts — Hooks.tool field (line 225-227)
    this._registeredTools = {};  // toolName -> { name, description, parameters, execute }
  }

  use(hookName, fn) {
    if (!HOOK_TYPES.has(hookName)) throw new Error(`Unknown hook: ${hookName}`);
    this._hooks[hookName].push(fn);
  }

  attachHookRuntime(hookRuntime) {
    this.hookRuntime = hookRuntime;
  }

  /**
   * Run all hooks for a given type.
   * - For context-mutating hooks (llm.before, llm.after): hooks mutate ctx in place
   * - For value-returning hooks (tool.before, tool.after, shell.env, tool.definitions):
   *   last non-undefined return value wins
   */
  async run(hookName, ...args) {
    const hooks = this._hooks[hookName] || [];

    // B5: onEvent hooks are fire-and-forget — don't chain return values
    if (hookName === 'onEvent') {
      for (const fn of hooks) {
        try { await fn(...args); } catch (err) {
          console.error(`[middleware] Error in ${hookName} hook:`, err.message);
        }
      }
      return;
    }

    let result = args[args.length - 1]; // default: last arg
    for (const fn of hooks) {
      try {
        // For value-returning hooks, pass the previous result as the last arg
        // so hooks chain properly (e.g., tool.definitions hook 2 gets hook 1's output)
        const chainedArgs = [...args.slice(0, -1), result];
        const ret = await fn(...chainedArgs);
        if (ret !== undefined) result = ret;
      } catch (err) {
        // Log error but don't break the pipeline
        console.error(`[middleware] Error in ${hookName} hook:`, err.message);
      }
    }
    return this._runExternalHook(hookName, args, result);
  }

  async _runExternalHook(hookName, args, result) {
    if (!this.hookRuntime?.trigger) return result;
    if (hookName === 'llm.before') {
      const payload = await this.hookRuntime.trigger('llm_input', { context: args[0] || {} }, { hookName });
      return payload?.context || result;
    }
    if (hookName === 'llm.after') {
      const payload = await this.hookRuntime.trigger('llm_output', {
        context: args[0] || {},
        response: result,
      }, { hookName });
      return payload?.response || result;
    }
    if (hookName === 'tool.after') {
      const payload = await this.hookRuntime.trigger('tool_result', {
        context: args[0] || {},
        toolName: args[1],
        args: args[2],
        result,
      }, { hookName });
      return payload?.result ?? result;
    }
    return result;
  }

  // ── Config notification ──
  // Ported from OpenCode plugin/index.ts (lines 237-245).
  // OpenCode calls hook.config?(cfg) for each plugin on startup and config changes.

  /**
   * Subscribe to configuration changes.
   * @param {Function} fn - Callback receiving the config object
   */
  onConfig(fn) {
    this._configListeners.push(fn);
  }

  /**
   * Notify all listeners of a config change.
   * @param {object} config - The updated configuration
   */
  notifyConfig(config) {
    for (const fn of this._configListeners) {
      try {
        fn(config);
      } catch (err) {
        console.error('[middleware] Error in config listener:', err.message);
      }
    }
  }

  // ── Plugin-provided tools ──
  // Ported from OpenCode plugin/src/index.ts — Hooks.tool field (line 225-227).
  // OpenCode plugins can register custom tools via hooks.tool = { [key]: ToolDefinition }.

  /**
   * Register a custom tool provided by middleware/plugin.
   * Fires the 'tool.register' hook, allowing middleware to modify or reject the definition.
   * If a hook returns false, the tool is not registered.
   *
   * @param {string} name - Tool name (must be unique)
   * @param {object} definition - { description, parameters, execute: async (args) => result }
   * @returns {Promise<boolean>} true if registered, false if rejected by a hook
   */
  async registerTool(name, definition) {
    const toolDef = { name, ...definition };
    // Run tool.register hooks — they can modify the definition or return false to reject
    const result = await this.run('tool.register', toolDef);
    if (result === false) return false;
    if (this._registeredTools[name]) {
      console.warn(`[middleware] Overwriting existing tool: ${name}`);
    }
    // Use the (possibly modified) definition from the hook pipeline
    this._registeredTools[name] = typeof result === 'object' && result !== null ? result : toolDef;
    return true;
  }

  /**
   * Unregister a custom tool.
   * @param {string} name
   */
  unregisterTool(name) {
    delete this._registeredTools[name];
  }

  /**
   * Get all registered tools (for inclusion in LLM tool definitions).
   * @returns {object} Map of toolName -> tool definition
   */
  getRegisteredTools() {
    return { ...this._registeredTools };
  }

  /**
   * Execute a registered tool by name.
   * @param {string} name - Tool name
   * @param {object} args - Tool arguments
   * @returns {Promise<any>} Tool result
   */
  async executeTool(name, args) {
    const tool = this._registeredTools[name];
    if (!tool) throw new Error(`Unknown registered tool: ${name}`);
    if (typeof tool.execute !== 'function') throw new Error(`Tool ${name} has no execute function`);
    return tool.execute(args);
  }

  clear() {
    for (const type of HOOK_TYPES) this._hooks[type] = [];
    this._configListeners = [];
    this._registeredTools = {};
  }
}

module.exports = { CodingMiddleware, HOOK_TYPES };
