'use strict';

const fs = require('node:fs');
const path = require('node:path');

const MAX_PLUGIN_MANIFEST_BYTES = 256 * 1024;

function inspectPluginCandidate(pluginDir, { rootDir = null, maxManifestBytes = MAX_PLUGIN_MANIFEST_BYTES } = {}) {
  const diagnostics = [];
  let dirReal;
  try {
    dirReal = fs.realpathSync(pluginDir);
  } catch (err) {
    return { ok: false, diagnostics: [{ level: 'warn', message: err.message, path: pluginDir }] };
  }
  const rootReal = rootDir ? safeRealpath(rootDir) : dirReal;
  if (rootDir && (!rootReal || !isInside(rootReal, dirReal))) {
    diagnostics.push({ level: 'error', message: 'plugin directory escapes root', path: pluginDir });
  }
  const manifestPath = path.join(dirReal, 'walle.plugin.json');
  if (!fs.existsSync(manifestPath)) return { ok: false, diagnostics };
  const stat = fs.statSync(manifestPath);
  if (stat.size > maxManifestBytes) {
    diagnostics.push({ level: 'error', message: 'plugin manifest too large', path: manifestPath });
  }
  const dirMode = fs.statSync(dirReal).mode;
  if ((dirMode & 0o002) !== 0) {
    diagnostics.push({ level: 'error', message: 'plugin directory is world-writable', path: dirReal });
  }
  return {
    ok: diagnostics.filter(d => d.level === 'error').length === 0,
    realpath: dirReal,
    manifestPath,
    diagnostics,
  };
}

function discoverPluginCandidates({ roots = [], source = 'user' } = {}) {
  const candidates = [];
  const diagnostics = [];
  for (const root of roots || []) {
    const rootReal = safeRealpath(root);
    if (!rootReal) {
      diagnostics.push({ level: 'warn', message: `cannot read plugin root: ${root}`, source });
      continue;
    }
    let entries = [];
    try {
      entries = fs.readdirSync(rootReal, { withFileTypes: true });
    } catch (err) {
      diagnostics.push({ level: 'warn', message: `cannot scan plugin root ${root}: ${err.message}`, source });
      continue;
    }
    for (const entry of entries) {
      if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
      const dir = path.join(rootReal, entry.name);
      const inspected = inspectPluginCandidate(dir, { rootDir: rootReal });
      diagnostics.push(...inspected.diagnostics.map(d => ({ ...d, source })));
      if (inspected.ok) candidates.push({ dir: inspected.realpath, manifestPath: inspected.manifestPath, source });
    }
  }
  return { candidates, diagnostics };
}

function safeRealpath(target) {
  try {
    return fs.realpathSync(target);
  } catch {
    return null;
  }
}

function isInside(root, target) {
  const rel = path.relative(root, target);
  return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
}

module.exports = {
  MAX_PLUGIN_MANIFEST_BYTES,
  discoverPluginCandidates,
  inspectPluginCandidate,
  isInside,
};
