'use strict';
/**
 * Internal-skill (script) plugin registry.
 *
 * Borrowed from OpenClaw's sandbox backend registry pattern
 * (src/agents/sandbox/backend.ts):
 *   - Map<id, RegisteredEntry> keyed by skill name
 *   - registerInternalSkill returns an unregister closure
 *   - requireInternalSkill throws with a remediation hint when missing
 *
 * Wall-E's adaptation: the previous skill-planner.js had a hardcoded
 * `if moduleName === 'slack-ingest'` switch followed by a directory
 * scan fallback. This registry replaces both with a uniform lookup.
 *
 * Resolution order:
 *   1. Explicit registration (registerInternalSkill('foo', factory))
 *   2. _bundled/<name>/run.js or index.js
 *   3. _bundled/<name>/SKILL.md `entry:` field
 *
 * Bundled (filesystem-discovered) skills are invoked as forked child
 * processes — a stray `process.exit(1)` inside a bundled script can no
 * longer take the daemon down with it. Manually-registered factories
 * remain in-process (they're trusted code paths inside wall-e itself).
 *
 * Item D of skill-dispatch deep-dive.
 */
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');

const BUNDLED_DIR = path.join(__dirname, '_bundled');
const BUNDLED_SKILL_TIMEOUT_MS = parseInt(
  process.env.WALLE_BUNDLED_SKILL_TIMEOUT_MS || '300000',
  10,
);

// Build a factory that runs a bundled skill in a forked child process.
// This isolates the daemon from any unexpected `process.exit(1)` inside
// the bundled script (which historically crashed wall-e — see the
// `gws-workspace/run.js` "no accounts" path for the canonical case).
function _makeBundledFactory(modPath, name) {
  return async function bundledFactory({ brain: _brain, log } = {}) {
    void _brain; // children load their own brain singleton
    return new Promise((resolve) => {
      const child = spawn(process.execPath, [modPath], {
        cwd: path.dirname(modPath),
        env: {
          ...process.env,
          WALLE_INSTANCE_TAG: `skill-${name}`,
          WALLE_BUNDLED_SKILL_NAME: name,
        },
        stdio: ['ignore', 'pipe', 'pipe'],
      });
      let stdout = '';
      let stderr = '';
      let finished = false;
      const finish = (val) => { if (finished) return; finished = true; clearTimeout(timer); resolve(val); };
      const timer = setTimeout(() => {
        try { child.kill('SIGKILL'); } catch {}
        finish({ error: `bundled skill "${name}" timed out after ${BUNDLED_SKILL_TIMEOUT_MS}ms` });
      }, BUNDLED_SKILL_TIMEOUT_MS);
      child.stdout.setEncoding('utf8');
      child.stderr.setEncoding('utf8');
      child.stdout.on('data', (chunk) => { stdout += chunk; });
      child.stderr.on('data', (chunk) => {
        stderr += chunk;
        // Forward child stderr to the daemon's stderr so it lands in
        // crash.log (where bundled-skill output was visible historically).
        try { process.stderr.write(chunk); } catch {}
        if (typeof log === 'function') {
          for (const line of String(chunk).split('\n')) {
            const trimmed = line.replace(/\s+$/, '');
            if (trimmed) { try { log(trimmed); } catch {} }
          }
        }
      });
      child.on('error', (err) => finish({ error: `spawn failed: ${err.message}` }));
      child.on('exit', (code, signal) => {
        if (signal) {
          finish({ error: `bundled skill "${name}" was killed by signal ${signal}`, exit_code: null });
          return;
        }
        // Try to extract the last JSON object printed to stdout as the
        // skill's structured result. Bundled skills already follow this
        // convention (e.g. gws-workspace, file-ingest, email-sync).
        let parsed = null;
        const lines = stdout.split('\n').map((l) => l.trim()).filter(Boolean);
        for (let i = lines.length - 1; i >= 0; i--) {
          const l = lines[i];
          if (l.startsWith('{') && l.endsWith('}')) {
            try { parsed = JSON.parse(l); break; } catch {}
          }
        }
        if (code === 0) {
          finish(parsed || {});
          return;
        }
        finish({
          error: `bundled skill "${name}" exited code ${code}`,
          exit_code: code,
          stderr_tail: stderr.slice(-500),
        });
      });
    });
  };
}

const _registry = new Map();

/**
 * Register an internal-skill factory by name. Returns a deregister
 * closure (matches OpenClaw's `registerSandboxBackend → () => void`
 * pattern).
 *
 * @param {string} name - skill name (e.g. 'slack-ingest')
 * @param {Function} factory - async ({ brain, log }) => result
 * @returns {Function} deregister
 */
function registerInternalSkill(name, factory) {
  if (!name || typeof name !== 'string') {
    throw new Error('registerInternalSkill requires a string name');
  }
  if (typeof factory !== 'function') {
    throw new Error(`registerInternalSkill("${name}") requires a factory function`);
  }
  const previous = _registry.get(name);
  _registry.set(name, factory);
  return () => {
    if (previous) {
      _registry.set(name, previous);
    } else {
      _registry.delete(name);
    }
  };
}

/**
 * Resolve a registered or filesystem-discoverable factory.
 * Returns null if not found.
 */
function resolveInternalSkill(name) {
  if (typeof name !== 'string' || !name) return null;
  if (_registry.has(name)) {
    return { factory: _registry.get(name), source: 'registered' };
  }

  // Defensive: reject path-traversal-shaped names. Skill names should be
  // simple identifiers — no '..', no absolute paths, no slashes that
  // could escape BUNDLED_DIR via path.join. Wall-E is local-only today,
  // so the prior code (`if moduleName === 'slack-ingest' else require...`)
  // didn't guard this. Cheap defense for any future hosted variant.
  if (name.includes('..') || path.isAbsolute(name) || name.includes('/') || name.includes('\\')) {
    return null;
  }

  // Filesystem fallback: _bundled/<name>/run.js or index.js
  const bundledDir = path.join(BUNDLED_DIR, name);
  let modPath = null;
  for (const candidate of ['run.js', 'index.js']) {
    const p = path.join(bundledDir, candidate);
    if (fs.existsSync(p)) { modPath = p; break; }
  }

  // SKILL.md `entry:` field
  if (!modPath) {
    const skillMd = path.join(bundledDir, 'SKILL.md');
    if (fs.existsSync(skillMd)) {
      try {
        const content = fs.readFileSync(skillMd, 'utf8');
        const entryMatch = content.match(/^entry:\s*(.+)$/m);
        if (entryMatch) {
          const entryPath = path.resolve(bundledDir, entryMatch[1].trim());
          if (fs.existsSync(entryPath)) modPath = entryPath;
        }
      } catch {}
    }
  }

  if (!modPath) return null;

  // Bundled skills run in a forked child process — see _makeBundledFactory
  // above for rationale. We deliberately do NOT require(modPath) here so a
  // syntax error or top-level throw in a bundled script can never crash the
  // daemon at resolve time.
  return {
    factory: _makeBundledFactory(modPath, name),
    source: 'bundled',
    modPath,
  };
}

/**
 * Throws with a remediation hint when the skill cannot be resolved.
 * Mirrors OpenClaw's requireSandboxBackendFactory.
 */
function requireInternalSkill(name) {
  const resolved = resolveInternalSkill(name);
  if (resolved) return resolved;
  throw new Error(
    `Internal skill "${name}" is not registered.\n`
    + `  - Register it via registerInternalSkill("${name}", factory)\n`
    + `  - Or provide _bundled/${name}/run.js, index.js, or SKILL.md entry: <path>`
  );
}

function listRegisteredInternalSkills() {
  return Array.from(_registry.keys()).sort();
}

function clearInternalSkillRegistry() {
  _registry.clear();
}

// ── Bundled auto-registrations ──
// The pre-existing slack-ingest hardcoded path becomes an explicit
// registration here. Other bundled skills continue to work through
// the filesystem fallback in resolveInternalSkill.
registerInternalSkill('slack-ingest', async ({ brain, log } = {}) => {
  const slackIngest = require('./slack-ingest');
  return await slackIngest.runIngestBatch({ brain, log });
});

module.exports = {
  registerInternalSkill,
  resolveInternalSkill,
  requireInternalSkill,
  listRegisteredInternalSkills,
  clearInternalSkillRegistry,
};
