const fs = require('fs');
const path = require('path');
const { copyHookScripts, removeHookScripts, removeMarkedSection, assertSafeConfigDir } = require('./hookAdapter');

const HOOK_SCRIPTS_DIR_NAME = 'hooks';
const PLUGINS_DIR_NAME = 'plugins';
const PLUGIN_FILE_NAME = 'evolver.js';
const EVOLVER_MARKER = '<!-- evolver-evolution-memory -->';
// First line of the plugin file. Used to (a) prove the plugin file is one we
// wrote, so uninstall can safely delete it, and (b) skip overwriting if the
// user replaced it with a custom plugin.
const PLUGIN_HEADER = '// _evolver_managed: true (do not remove this line)';

function buildPluginSource(hooksDirAbsolute) {
  // The plugin shells out to the same three Node scripts that every other
  // evolver adapter uses, so the runtime behavior stays consistent across
  // hosts. opencode loads .js files synchronously at startup and invokes
  // the default-exported async factory with a context object.
  return `${PLUGIN_HEADER}
// Auto-generated by \`evolver setup-hooks --platform=opencode\`.
// See https://opencode.ai/docs/plugins/ for the plugin API.
//
// This plugin wires opencode events to the three evolver hook scripts:
//   session.created      -> evolver-session-start.js  (inject memory)
//   tool.execute.after   -> evolver-signal-detect.js  (detect signals on writes/edits)
//   session.idle         -> evolver-session-end.js    (record outcome)
//
// Each script is a stdin/stdout JSON filter -- nothing else is shared with
// opencode, so removing this file fully detaches evolver from opencode.

const { spawnSync } = require('node:child_process');
const path = require('node:path');

const HOOKS_DIR = ${JSON.stringify(hooksDirAbsolute)};

function runHook(scriptName, payload, timeoutMs) {
  try {
    const result = spawnSync('node', [path.join(HOOKS_DIR, scriptName)], {
      input: JSON.stringify(payload || {}),
      encoding: 'utf8',
      timeout: timeoutMs,
    });
    if (!result || !result.stdout) return {};
    try { return JSON.parse(result.stdout); } catch { return {}; }
  } catch {
    return {};
  }
}

const Evolver = async () => ({
  event: async ({ event }) => {
    if (!event || typeof event.type !== 'string') return;
    if (event.type === 'session.created') {
      runHook('evolver-session-start.js', {
        session_id: event.properties && event.properties.info && event.properties.info.id,
      }, 3000);
      return;
    }
    if (event.type === 'session.idle') {
      runHook('evolver-session-end.js', {
        session_id: event.properties && event.properties.sessionID,
      }, 8000);
    }
  },
  'tool.execute.after': async (input, output) => {
    if (!input || typeof input.tool !== 'string') return;
    if (input.tool !== 'write' && input.tool !== 'edit') return;
    runHook('evolver-signal-detect.js', {
      tool_input: (output && output.args) || {},
      tool_response: (output && output.output) || (output && output.result) || {},
    }, 2000);
  },
});

module.exports = { Evolver };
module.exports.default = Evolver;
`;
}

function buildAgentsMdSection() {
  return `${EVOLVER_MARKER}
## Evolution Memory (Evolver)

This project uses evolver for self-evolution. Hooks automatically:
1. Inject recent evolution memory at session start
2. Detect evolution signals during file edits
3. Record outcomes at session end

For substantive tasks, call \`gep_recall\` before work and \`gep_record_outcome\` after.
Signals: log_error, perf_bottleneck, user_feature_request, capability_gap, deployment_issue, test_failure.`;
}

function appendSectionToFile(filePath, marker, content) {
  let existing = '';
  try { existing = fs.readFileSync(filePath, 'utf8'); } catch { /* new file */ }
  if (existing.includes(marker)) return false;
  const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n\n' : '\n';
  fs.writeFileSync(filePath, existing + separator + content + '\n', 'utf8');
  return true;
}

function isEvolverManagedPluginFile(filePath) {
  try {
    if (!fs.existsSync(filePath)) return false;
    const raw = fs.readFileSync(filePath, 'utf8');
    return raw.includes('_evolver_managed: true');
  } catch {
    return false;
  }
}

function writePluginFile(pluginsDir, source) {
  fs.mkdirSync(pluginsDir, { recursive: true });
  const dest = path.join(pluginsDir, PLUGIN_FILE_NAME);
  const tmp = dest + '.tmp';
  fs.writeFileSync(tmp, source, 'utf8');
  fs.renameSync(tmp, dest);
  return dest;
}

function install({ configRoot, evolverRoot, force }) {
  const opencodeDir = path.join(configRoot, '.opencode');
  const hooksDir = path.join(opencodeDir, HOOK_SCRIPTS_DIR_NAME);
  const pluginsDir = path.join(opencodeDir, PLUGINS_DIR_NAME);
  const pluginPath = path.join(pluginsDir, PLUGIN_FILE_NAME);
  const agentsMdPath = path.join(configRoot, 'AGENTS.md');
  assertSafeConfigDir(opencodeDir, '.opencode', { subdirs: [HOOK_SCRIPTS_DIR_NAME, PLUGINS_DIR_NAME] });

  if (!force && isEvolverManagedPluginFile(pluginPath)) {
    console.log('[opencode] Evolver plugin already installed. Use --force to overwrite.');
    printPostInstallNotice(pluginPath);
    return { ok: true, skipped: true, plugin_path: pluginPath };
  }

  fs.mkdirSync(opencodeDir, { recursive: true });

  const written = writePluginFile(pluginsDir, buildPluginSource(hooksDir));
  console.log('[opencode] Wrote ' + written);

  const copied = copyHookScripts(hooksDir, path.join(evolverRoot, 'src', 'adapters'));
  console.log('[opencode] Copied ' + copied.length + ' hook scripts to ' + hooksDir);

  const injected = appendSectionToFile(agentsMdPath, EVOLVER_MARKER, buildAgentsMdSection());
  if (injected) {
    console.log('[opencode] Injected evolution section into ' + agentsMdPath);
  }

  console.log('[opencode] Installation complete.');
  printPostInstallNotice(written);

  return {
    ok: true,
    platform: 'opencode',
    plugin_path: written,
    files: [written, agentsMdPath, ...copied],
  };
}

function printPostInstallNotice(pluginPath) {
  // The opencode TUI "Plugins" tab only lists TUI plugins (registered via
  // tui.json with `default export { id, tui }`). Server plugins like ours,
  // auto-loaded from .opencode/plugins/, are functional but invisible in
  // that tab. This is by opencode design, not a bug. See:
  //   https://github.com/sst/opencode -> packages/opencode/specs/tui-plugins.md
  // Surfacing this proactively here prevents the "is it actually loaded?"
  // confusion that motivated EvoMap/evolver issue #531.
  console.log('');
  console.log('[opencode] Restart opencode for the plugin to take effect.');
  console.log('[opencode] Note: this is a SERVER plugin (event hooks). It will NOT appear in');
  console.log('[opencode]       the TUI "Plugins" tab -- that tab only lists TUI plugins');
  console.log('[opencode]       (registered via tui.json). The plugin is still loaded and');
  console.log('[opencode]       running silently in the background.');
  console.log('[opencode] To verify the plugin is loaded, run:');
  console.log('[opencode]   opencode --print-logs --log-level INFO debug config 2>&1 | grep ' + JSON.stringify(pluginPath));
  console.log('[opencode] Or run:');
  console.log('[opencode]   evolver setup-hooks --platform=opencode --verify');
}

// Walk the install state and report what is healthy / what is missing.
// Used by the `--verify` flag to give users a definitive answer to
// "is the plugin actually installed and loadable?". Returns a structured
// result so callers (CLI, future TUI plugin) can format it as they like.
function verify({ configRoot }) {
  const opencodeDir = path.join(configRoot, '.opencode');
  const hooksDir = path.join(opencodeDir, HOOK_SCRIPTS_DIR_NAME);
  const pluginsDir = path.join(opencodeDir, PLUGINS_DIR_NAME);
  const pluginPath = path.join(pluginsDir, PLUGIN_FILE_NAME);
  const agentsMdPath = path.join(configRoot, 'AGENTS.md');
  const expectedScripts = [
    'evolver-session-start.js',
    'evolver-signal-detect.js',
    'evolver-session-end.js',
  ];

  const checks = [];

  const pluginExists = fs.existsSync(pluginPath);
  checks.push({
    id: 'plugin_file_present',
    ok: pluginExists,
    detail: pluginExists ? pluginPath : 'missing: ' + pluginPath,
  });

  const managed = pluginExists && isEvolverManagedPluginFile(pluginPath);
  checks.push({
    id: 'plugin_managed_marker',
    ok: managed,
    detail: managed
      ? 'first line contains _evolver_managed: true'
      : 'plugin file is not evolver-managed (was it edited or replaced by another tool?)',
  });

  let pluginLoadable = false;
  let pluginLoadError = null;
  if (pluginExists) {
    try {
      // Require the plugin in an isolated module cache slot to confirm it
      // parses and exports the expected shape. opencode does an ESM dynamic
      // import; CommonJS require here is a strict subset of that, so a
      // failure here is a guaranteed failure under opencode too.
      delete require.cache[require.resolve(pluginPath)];
      const mod = require(pluginPath);
      const fn = mod && (mod.Evolver || mod.default);
      pluginLoadable = typeof fn === 'function';
      if (!pluginLoadable) pluginLoadError = 'no Evolver/default function export';
    } catch (err) {
      pluginLoadError = (err && err.message) || String(err);
    }
  }
  checks.push({
    id: 'plugin_loadable',
    ok: pluginLoadable,
    detail: pluginLoadable
      ? 'require() succeeded and exports Evolver()'
      : 'require() failed: ' + (pluginLoadError || 'unknown'),
  });

  const missingScripts = expectedScripts.filter(
    (name) => !fs.existsSync(path.join(hooksDir, name))
  );
  checks.push({
    id: 'hook_scripts_present',
    ok: missingScripts.length === 0,
    detail: missingScripts.length === 0
      ? 'all 3 hook scripts present in ' + hooksDir
      : 'missing: ' + missingScripts.join(', '),
  });

  let agentsMdHasSection = false;
  try {
    if (fs.existsSync(agentsMdPath)) {
      agentsMdHasSection = fs.readFileSync(agentsMdPath, 'utf8').includes(EVOLVER_MARKER);
    }
  } catch { /* ignore */ }
  checks.push({
    id: 'agents_md_section',
    ok: agentsMdHasSection,
    detail: agentsMdHasSection
      ? 'AGENTS.md contains evolution memory section'
      : 'AGENTS.md is missing or has no evolution section (re-run install)',
  });

  const allOk = checks.every((c) => c.ok);

  return {
    ok: allOk,
    plugin_path: pluginPath,
    hooks_dir: hooksDir,
    config_root: configRoot,
    checks,
    note: allOk
      ? 'Plugin is installed and loadable. opencode\'s TUI "Plugins" tab will not list it -- that tab only shows TUI plugins. Run `opencode --print-logs --log-level INFO debug config` and grep for the plugin path to confirm opencode loads it at startup.'
      : 'One or more checks failed. Re-run `evolver setup-hooks --platform=opencode --force` to repair.',
  };
}

function printVerifyReport(report) {
  console.log('[opencode] Verify report');
  console.log('[opencode]   plugin path : ' + report.plugin_path);
  console.log('[opencode]   hooks dir   : ' + report.hooks_dir);
  console.log('[opencode]   config root : ' + report.config_root);
  console.log('[opencode] Checks:');
  for (const c of report.checks) {
    console.log('[opencode]   ' + (c.ok ? '[OK]  ' : '[FAIL]') + ' ' + c.id + ' -- ' + c.detail);
  }
  console.log('[opencode] ' + (report.ok ? 'All checks passed.' : 'Some checks failed.'));
  console.log('[opencode] ' + report.note);
}

function uninstall({ configRoot }) {
  const opencodeDir = path.join(configRoot, '.opencode');
  const hooksDir = path.join(opencodeDir, HOOK_SCRIPTS_DIR_NAME);
  const pluginsDir = path.join(opencodeDir, PLUGINS_DIR_NAME);
  const pluginPath = path.join(pluginsDir, PLUGIN_FILE_NAME);
  const agentsMdPath = path.join(configRoot, 'AGENTS.md');
  assertSafeConfigDir(opencodeDir, '.opencode', { subdirs: [HOOK_SCRIPTS_DIR_NAME, PLUGINS_DIR_NAME] });

  let changed = false;

  if (isEvolverManagedPluginFile(pluginPath)) {
    try { fs.unlinkSync(pluginPath); changed = true; } catch { /* ignore */ }
  }

  const scripts = removeHookScripts(hooksDir);
  if (scripts > 0) changed = true;

  if (removeMarkedSection(agentsMdPath, EVOLVER_MARKER)) {
    changed = true;
  }

  console.log(changed
    ? '[opencode] Uninstalled evolver plugin and hooks.'
    : '[opencode] No evolver plugin found to uninstall.');

  return { ok: true, removed: changed };
}

module.exports = {
  install,
  uninstall,
  verify,
  printVerifyReport,
  buildPluginSource,
  isEvolverManagedPluginFile,
  PLUGIN_FILE_NAME,
};
