// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.
// NERF is a trademark of defconxt.

/**
 * plugins.js — Plugin discovery and loading for NERF gateway.
 *
 * Ports Python gateway/plugins.py:
 * - PluginManager: discovers plugins from ~/.config/nerf/plugins/
 * - Each plugin dir needs plugin.yaml (parsed with yaml npm) and SKILL.md
 * - Returns PluginInfo objects with name, version, modes, priority, skill_content
 * - Graceful fallback when plugin directory doesn't exist
 *
 * @module gateway/plugins
 */

import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { parse as parseYaml } from 'yaml';

const USER_PLUGIN_DIR = join(homedir(), '.config', 'nerf', 'plugins');

/**
 * @typedef {Object} PluginInfo
 * @property {string} name
 * @property {string} version
 * @property {string} author
 * @property {string} description
 * @property {string[]} modes - Uppercase mode names this plugin activates for
 * @property {string[]} triggers - Lowercase trigger keywords
 * @property {number} priority - 0-100, lower = loaded first
 * @property {string} path - Directory containing SKILL.md
 * @property {string} source - "user" | "entrypoint"
 * @property {string} skill_content - Loaded SKILL.md content
 */

/**
 * Load and validate plugin.yaml from a directory. Returns null on failure.
 *
 * @param {string} pluginDir
 * @returns {Record<string, any> | null}
 */
function loadPluginYaml(pluginDir) {
  const yamlFile = join(pluginDir, 'plugin.yaml');
  if (!existsSync(yamlFile)) return null;

  try {
    const raw = readFileSync(yamlFile, 'utf-8');
    const data = parseYaml(raw);
    if (!data || typeof data !== 'object' || Array.isArray(data)) return null;
    if (!data.name) return null;
    return data;
  } catch {
    return null;
  }
}

/**
 * Load SKILL.md from a plugin directory.
 *
 * @param {string} pluginDir
 * @returns {string}
 */
function loadSkillContent(pluginDir) {
  const skillFile = join(pluginDir, 'SKILL.md');
  if (!existsSync(skillFile)) return '';
  return readFileSync(skillFile, 'utf-8');
}

/**
 * Create a PluginInfo from a plugin directory.
 *
 * @param {string} pluginDir
 * @param {string} source
 * @returns {PluginInfo | null}
 */
function pluginFromDir(pluginDir, source) {
  let meta = loadPluginYaml(pluginDir);
  if (!meta) {
    // If no plugin.yaml but has SKILL.md, create minimal metadata
    if (existsSync(join(pluginDir, 'SKILL.md'))) {
      const dirName = pluginDir.split(/[/\\]/).pop() || 'unknown';
      meta = { name: dirName };
    } else {
      return null;
    }
  }

  const content = loadSkillContent(pluginDir);
  if (!content) return null;

  const modesRaw = meta.modes || [];
  const modes = Array.isArray(modesRaw) ? modesRaw.map(m => String(m).toUpperCase()) : [];

  return {
    name: meta.name || pluginDir.split(/[/\\]/).pop(),
    version: meta.version || '0.0.0',
    author: meta.author || 'unknown',
    description: meta.description || '',
    modes,
    triggers: Array.isArray(meta.triggers)
      ? meta.triggers.map(t => String(t).toLowerCase())
      : [],
    priority: parseInt(meta.priority ?? 50, 10),
    path: pluginDir,
    source,
    skill_content: content,
  };
}

export class PluginManager {
  /**
   * @param {Object} [opts]
   * @param {string} [opts.dataRoot] - Not used directly but kept for API compat
   * @param {string} [opts.pluginDir] - Override plugin directory for testing
   */
  constructor(opts = {}) {
    this._pluginDir = opts.pluginDir || USER_PLUGIN_DIR;
    /** @type {PluginInfo[]} */
    this._plugins = [];
    this._discovered = false;
  }

  /** @returns {PluginInfo[]} */
  get plugins() {
    if (!this._discovered) this.discover();
    return this._plugins;
  }

  /**
   * Scan all plugin sources and return discovered plugins.
   *
   * @returns {PluginInfo[]}
   */
  discover() {
    this._plugins = [];

    // Scan user plugin directory
    if (existsSync(this._pluginDir)) {
      try {
        const entries = readdirSync(this._pluginDir).sort();
        for (const entry of entries) {
          const entryPath = join(this._pluginDir, entry);
          try {
            if (statSync(entryPath).isDirectory()) {
              const plugin = pluginFromDir(entryPath, 'user');
              if (plugin) this._plugins.push(plugin);
            }
          } catch {
            // Skip entries we can't stat
          }
        }
      } catch {
        // Plugin directory unreadable — graceful fallback
      }
    }

    // Sort by priority (lower first)
    this._plugins.sort((a, b) => a.priority - b.priority);
    this._discovered = true;

    return this._plugins;
  }

  /**
   * Get all plugins that activate for a given mode.
   *
   * @param {string} mode - NERF mode name
   * @returns {PluginInfo[]}
   */
  getSkillsForMode(mode) {
    const upper = mode.toUpperCase();
    return this.plugins.filter(p => p.modes.includes(upper));
  }

  /**
   * Get additional trigger keywords from plugins.
   *
   * @returns {Record<string, string[]>}
   */
  getExtraTriggers() {
    /** @type {Record<string, string[]>} */
    const triggers = {};
    for (const plugin of this.plugins) {
      if (!plugin.triggers || !plugin.triggers.length) continue;
      for (const mode of plugin.modes) {
        if (!triggers[mode]) triggers[mode] = [];
        triggers[mode].push(...plugin.triggers);
      }
    }
    return triggers;
  }

  /**
   * Find a plugin by name.
   *
   * @param {string} name
   * @returns {PluginInfo | null}
   */
  getPluginByName(name) {
    return this.plugins.find(p => p.name === name) || null;
  }

  /**
   * Return a serializable list of plugin metadata for CLI display.
   *
   * @returns {Array<Record<string, any>>}
   */
  listPlugins() {
    return this.plugins.map(p => ({
      name: p.name,
      version: p.version,
      author: p.author,
      description: p.description,
      modes: p.modes,
      source: p.source,
      path: p.path,
      priority: p.priority,
    }));
  }
}
