'use strict';

const fs = require('fs');
const path = require('path');
const { normalizePluginManifest } = require('./manifest-schema');
const { MAX_PLUGIN_MANIFEST_BYTES, isInside } = require('./discovery');

const MANIFEST_FILENAME = 'walle.plugin.json';

/**
 * Load a manifest from a directory containing `walle.plugin.json`.
 * Returns { manifest, diagnostics } — diagnostics may include errors that
 * caused validation to fail.
 *
 * @param {string} pluginDir - Directory to look in
 * @param {string} [source]  - Plugin source for diagnostics (bundled/user/...)
 */
function loadManifestFromDir(pluginDir, source, options = {}) {
  const manifestPath = path.join(pluginDir, MANIFEST_FILENAME);
  if (!fs.existsSync(manifestPath)) {
    return { manifest: null, diagnostics: [] };
  }

  let raw;
  try {
    const stat = fs.statSync(manifestPath);
    if (stat.size > (options.maxManifestBytes || MAX_PLUGIN_MANIFEST_BYTES)) {
      return {
        manifest: null,
        diagnostics: [{
          level: 'error',
          message: `Manifest too large: ${manifestPath}`,
          source: source || 'unknown',
        }],
      };
    }
    if (options.rootDir) {
      const rootReal = fs.realpathSync(options.rootDir);
      const manifestReal = fs.realpathSync(manifestPath);
      const dirReal = fs.realpathSync(pluginDir);
      if (!isInside(rootReal, manifestReal) || !isInside(rootReal, dirReal)) {
        return {
          manifest: null,
          diagnostics: [{
            level: 'error',
            message: `Manifest escapes plugin root: ${manifestPath}`,
            source: source || 'unknown',
          }],
        };
      }
    }
    raw = fs.readFileSync(manifestPath, 'utf8');
  } catch (e) {
    return {
      manifest: null,
      diagnostics: [
        {
          level: 'error',
          message: `Failed to read ${manifestPath}: ${e.message}`,
          source: source || 'unknown',
        },
      ],
    };
  }

  let parsed;
  try {
    parsed = JSON.parse(raw);
  } catch (e) {
    return {
      manifest: null,
      diagnostics: [
        {
          level: 'error',
          message: `Invalid JSON in ${manifestPath}: ${e.message}`,
          source: source || 'unknown',
        },
      ],
    };
  }

  const result = normalizePluginManifest(parsed, { source });
  if (result.valid) {
    return {
      manifest: { ...parsed, dir: pluginDir, manifestPath, source: source || 'unknown' },
      diagnostics: result.diagnostics,
    };
  }
  return { manifest: null, diagnostics: result.diagnostics };
}

/**
 * Scan a directory for subdirs each containing a walle.plugin.json.
 * Returns { manifests: [...], diagnostics: [...] }.
 *
 * Inspired by OpenClaw's `src/plugins/discovery.ts:discoverOpenClawPlugins`,
 * but scoped to Wall-E's local filesystem: manifest size and realpath
 * containment are enforced here; ownership/world-writable checks live in
 * plugins/discovery.js for callers that need full candidate hardening.
 */
function loadManifestsFromDir(rootDir, source, options = {}) {
  const manifests = [];
  const diagnostics = [];

  if (!fs.existsSync(rootDir)) {
    return { manifests, diagnostics };
  }

  let entries;
  try {
    entries = fs.readdirSync(rootDir, { withFileTypes: true });
  } catch (e) {
    diagnostics.push({
      level: 'warn',
      message: `Cannot read directory ${rootDir}: ${e.message}`,
      source: source || 'unknown',
    });
    return { manifests, diagnostics };
  }

  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
    const result = loadManifestFromDir(path.join(rootDir, entry.name), source, { ...options, rootDir });
    diagnostics.push(...result.diagnostics);
    if (result.manifest) manifests.push(result.manifest);
  }

  return { manifests, diagnostics };
}

/**
 * Load a manifest from a sibling file in a flat directory.
 * Used by llm/, channels/, adapters/ where the plugins are individual files.
 *
 * Example: `wall-e/llm/anthropic.js` + `wall-e/llm/anthropic.plugin.json`.
 *
 * @param {string} entryFile - Path to the JS module (sibling .plugin.json is read)
 * @param {string} [source]
 */
function loadSiblingManifest(entryFile, source, options = {}) {
  const dir = path.dirname(entryFile);
  const base = path.basename(entryFile, path.extname(entryFile));
  const manifestPath = path.join(dir, `${base}.plugin.json`);

  if (!fs.existsSync(manifestPath)) {
    return { manifest: null, diagnostics: [] };
  }

  let raw;
  try {
    const stat = fs.statSync(manifestPath);
    if (stat.size > (options.maxManifestBytes || MAX_PLUGIN_MANIFEST_BYTES)) {
      return {
        manifest: null,
        diagnostics: [{
          level: 'error',
          message: `Manifest too large: ${manifestPath}`,
          source: source || 'unknown',
        }],
      };
    }
    raw = fs.readFileSync(manifestPath, 'utf8');
  } catch (e) {
    return {
      manifest: null,
      diagnostics: [
        {
          level: 'error',
          message: `Failed to read ${manifestPath}: ${e.message}`,
          source: source || 'unknown',
        },
      ],
    };
  }

  let parsed;
  try {
    parsed = JSON.parse(raw);
  } catch (e) {
    return {
      manifest: null,
      diagnostics: [
        {
          level: 'error',
          message: `Invalid JSON in ${manifestPath}: ${e.message}`,
          source: source || 'unknown',
        },
      ],
    };
  }

  const result = normalizePluginManifest(parsed, { source });
  if (result.valid) {
    return {
      manifest: {
        ...parsed,
        entryFile,
        manifestPath,
        source: source || 'unknown',
      },
      diagnostics: result.diagnostics,
    };
  }
  return { manifest: null, diagnostics: result.diagnostics };
}

/**
 * Scan a flat directory for *.plugin.json sibling files.
 * Used for providers/channels/adapters where each plugin is a single .js file
 * with an optional sibling manifest.
 */
function loadSiblingManifestsFromDir(rootDir, source, options = {}) {
  const manifests = [];
  const diagnostics = [];
  const exclude = new Set(options.exclude || []);

  if (!fs.existsSync(rootDir)) {
    return { manifests, diagnostics };
  }

  let entries;
  try {
    entries = fs.readdirSync(rootDir, { withFileTypes: true });
  } catch (e) {
    diagnostics.push({
      level: 'warn',
      message: `Cannot read directory ${rootDir}: ${e.message}`,
      source: source || 'unknown',
    });
    return { manifests, diagnostics };
  }

  for (const entry of entries) {
    if (!entry.isFile()) continue;
    if (!entry.name.endsWith('.plugin.json')) continue;
    if (exclude.has(entry.name)) continue;
    const stem = entry.name.replace(/\.plugin\.json$/, '');
    const entryFile = path.join(rootDir, `${stem}.js`);
    const result = loadSiblingManifest(entryFile, source, options);
    diagnostics.push(...result.diagnostics);
    if (result.manifest) manifests.push(result.manifest);
  }

  return { manifests, diagnostics };
}

module.exports = {
  MANIFEST_FILENAME,
  loadManifestFromDir,
  loadManifestsFromDir,
  loadSiblingManifest,
  loadSiblingManifestsFromDir,
  normalizePluginManifest,
};
