// wall-e/tools/command-registry.js
// Slash command registry for chat messages.
// Inspired by OpenCode's Command service
// (https://github.com/anomalyco/opencode, packages/opencode/src/command/index.ts)
//
// Parses `/command args` from chat input, dispatches to registered handlers,
// and bridges to Wall-E skills by name.
'use strict';

/**
 * Registry of slash commands available in Wall-E chat.
 *
 * OpenCode registers commands from config, MCP prompts, and skills.
 * Wall-E's registry mirrors this with built-in commands plus skill bridging.
 */
class CommandRegistry {
  constructor() {
    /** @type {Map<string, {name:string, description?:string, source:string, handler:Function}>} */
    this._commands = new Map();
    this._skillBridge = null;
  }

  /**
   * Register a slash command.
   *
   * @param {string} name - command name (without leading /)
   * @param {object} def
   * @param {string} [def.description] - human-readable description
   * @param {string} [def.source] - 'builtin' | 'skill' | 'custom'
   * @param {Function} def.handler - async (args: string, context: object) => { reply: string } | void
   */
  register(name, def) {
    if (!name || typeof name !== 'string') throw new Error('Command name is required');
    if (!def || typeof def.handler !== 'function') throw new Error('Command handler is required');
    this._commands.set(name.toLowerCase(), {
      name: name.toLowerCase(),
      description: def.description || '',
      source: def.source || 'custom',
      handler: def.handler,
    });
  }

  /**
   * Unregister a command.
   * @param {string} name
   * @returns {boolean}
   */
  unregister(name) {
    return this._commands.delete(name.toLowerCase());
  }

  /**
   * List all registered commands.
   * Mirrors OpenCode Command.list().
   * @returns {Array<{name:string, description:string, source:string}>}
   */
  list() {
    return Array.from(this._commands.values()).map(c => ({
      name: c.name,
      description: c.description,
      source: c.source,
    }));
  }

  /**
   * Get a command by name.
   * @param {string} name
   * @returns {{name:string, description:string, source:string, handler:Function}|undefined}
   */
  get(name) {
    return this._commands.get(name.toLowerCase());
  }

  /**
   * Parse and execute a command from raw chat input.
   *
   * @param {string} input - full chat message (e.g. "/review src/")
   * @param {object} [context] - passed to handler (brain, session, etc.)
   * @returns {Promise<{handled:boolean, reply?:string, command?:string}>}
   */
  async execute(input, context = {}) {
    const parsed = CommandRegistry.parse(input);
    if (!parsed) return { handled: false };

    const cmd = this._commands.get(parsed.command);
    if (!cmd) {
      const directSkill = await this._executeDirectSkill(parsed, context);
      if (directSkill) return directSkill;
      return { handled: false, command: parsed.command };
    }

    try {
      const result = await cmd.handler(parsed.args, context);
      return {
        handled: true,
        command: parsed.command,
        reply: result?.reply || result?.message || undefined,
      };
    } catch (err) {
      return {
        handled: true,
        command: parsed.command,
        reply: `Error running /${parsed.command}: ${err.message}`,
      };
    }
  }

  /**
   * Parse a slash or dollar-prefixed command from input text.
   * Returns null if the input is not a command.
   *
   * @param {string} input
   * @returns {{command:string, args:string}|null}
   */
  static parse(input) {
    if (!input || typeof input !== 'string') return null;
    const trimmed = input.trim();
    const prefix = trimmed[0];
    if (prefix !== '/' && prefix !== '$') return null;

    const spaceIdx = trimmed.indexOf(' ');
    if (spaceIdx === -1) {
      const command = trimmed.slice(1).toLowerCase();
      return command ? { command, args: '', prefix } : null;
    }

    const command = trimmed.slice(1, spaceIdx).toLowerCase();
    const args = trimmed.slice(spaceIdx + 1).trim();
    return command ? { command, args, prefix } : null;
  }

  /**
   * Register built-in commands.
   * Mirrors OpenCode's Default commands (init, review) plus Wall-E additions.
   *
   * @param {object} deps - { brain?, codingOrchestrator?, skillLoader? }
   * @returns {CommandRegistry} this (for chaining)
   */
  registerBuiltins(deps = {}) {
    // /review — trigger code review
    this.register('review', {
      description: 'Review current changes (uncommitted diff)',
      source: 'builtin',
      handler: async (args, ctx) => {
        const orchestrator = deps.codingOrchestrator || (() => {
          try { return require('../coding-orchestrator'); } catch { return null; }
        })();
        if (!orchestrator) return { reply: 'Coding orchestrator not available.' };

        const cwd = args || ctx.cwd || process.cwd();
        const diff = await orchestrator.getGitDiff(cwd);
        if (!diff || diff.trim().length === 0) {
          return { reply: 'No uncommitted changes to review.' };
        }
        return { reply: `Reviewing changes in ${cwd}...\n\n\`\`\`diff\n${diff.slice(0, 3000)}\n\`\`\`` };
      },
    });

    // /fix — start a fix task (I7: clearly marked as coming soon)
    this.register('fix', {
      description: 'Start a coding fix task (coming soon)',
      source: 'builtin',
      handler: async (args, ctx) => {
        if (!args) return { reply: 'Usage: /fix <description of what to fix>' };
        return { reply: '[Coming soon] The /fix command is not yet implemented. For now, describe the fix in a regular chat message and the coding agent will handle it.' };
      },
    });

    // /test — run tests (B1: safe command allowlist to prevent command injection)
    this.register('test', {
      description: 'Run project tests',
      source: 'builtin',
      handler: async (args, ctx) => {
        const { execFile } = require('node:child_process');
        const { promisify } = require('node:util');
        const execFileAsync = promisify(execFile);
        const cwd = ctx.cwd || process.cwd();
        const SAFE_TEST_COMMANDS = ['npm test', 'yarn test', 'pnpm test', 'bun test', 'pytest', 'go test ./...', 'cargo test', 'make test'];
        const cmd = args ? args.trim() : 'npm test';
        if (!SAFE_TEST_COMMANDS.some(safe => cmd === safe || cmd.startsWith(safe + ' '))) {
          return { reply: `Unsafe test command: "${cmd}". Allowed: ${SAFE_TEST_COMMANDS.join(', ')}` };
        }
        try {
          const { stdout, stderr } = await execFileAsync('sh', ['-c', cmd], {
            cwd,
            timeout: 60000,
            env: { ...process.env, CI: '1' },
          });
          const output = (stdout + stderr).slice(-2000);
          return { reply: `Test output:\n\`\`\`\n${output}\n\`\`\`` };
        } catch (err) {
          const output = ((err.stdout || '') + (err.stderr || '')).slice(-2000);
          return { reply: `Tests failed (exit ${err.code}):\n\`\`\`\n${output}\n\`\`\`` };
        }
      },
    });

    return this;
  }

  /**
   * Bridge: register Wall-E skills as slash commands.
   * Mirrors OpenCode's skill-to-command mapping.
   *
   * @param {Function} findSkill - (name) => skill definition or null
   * @param {Function} executeSkill - (taskId, task) => result string
   */
  registerSkills(findSkill, executeSkill) {
    if (!findSkill) return;
    this._skillBridge = { findSkill, executeSkill };
    // We don't enumerate all skills upfront — instead, provide a catch-all
    // via a /skill command that bridges to any skill by name.
    this.register('skill', {
      description: 'Run a Wall-E skill by name (e.g., /skill morning-briefing)',
      source: 'builtin',
      handler: async (args, ctx) => {
        const parts = String(args || '').trim().split(/\s+/).filter(Boolean);
        const skillName = parts[0] || '';
        const skillArgs = parts.slice(1).join(' ');
        if (!skillName) return { reply: 'Usage: /skill <name>' };

        const skill = findSkill(skillName);
        if (!skill) return { reply: `Skill not found: ${skillName}` };

        if (skill.execution === 'script' && executeSkill) {
          try {
            const fakeId = `cmd-${Date.now()}`;
            const result = await executeSkill(fakeId, { id: fakeId, skill: skillName, title: skill.name, args: skillArgs });
            return { reply: result ? result.slice(0, 5000) : 'Skill completed (no output).' };
          } catch (err) {
            return { reply: `Skill error: ${err.message}` };
          }
        }

        return { reply: `Skill "${skillName}" is ${skill.execution}-based. Use the task system to run it.` };
      },
    });
  }

  async _executeDirectSkill(parsed, context = {}) {
    const bridge = this._skillBridge;
    if (!bridge?.findSkill) return null;

    const skillName = parsed.command;
    const skill = bridge.findSkill(skillName, {
      ...context,
      prefix: parsed.prefix || '',
      agentType: parsed.prefix === '/'
        ? 'claude'
        : parsed.prefix === '$'
          ? 'codex'
          : '',
    });
    if (!skill) return null;

    if (skill.execution !== 'script' || !bridge.executeSkill) return null;

    try {
      const fakeId = `cmd-${Date.now()}`;
      const result = await bridge.executeSkill(fakeId, {
        id: fakeId,
        skill: skillName,
        title: skill.name,
        args: parsed.args,
      });
      return {
        handled: true,
        command: skillName,
        reply: result ? result.slice(0, 5000) : 'Skill completed (no output).',
      };
    } catch (err) {
      return {
        handled: true,
        command: skillName,
        reply: `Skill error: ${err.message}`,
      };
    }
  }

  /** Remove all registered commands. */
  clear() {
    this._commands.clear();
    this._skillBridge = null;
  }
}

module.exports = { CommandRegistry };
