'use strict';

const fs = require('fs');
const path = require('path');
const { validateManifest } = require('./manifest-validator');

/**
 * 3rd-party plugin scanner — discovers Wall-E plugins installed via
 * `npm install <pkg>` (or `pnpm add`, `yarn add`).
 *
 * Discovery rules (kept simple for single-user Wall-E):
 *
 *   1. Walk all `node_modules/<pkg>/` directories under the workspace.
 *   2. A directory qualifies as a Wall-E plugin if EITHER:
 *      a) It contains a top-level `walle.plugin.json` (preferred), OR
 *      b) Its `package.json` has a top-level `walle: { kind, ... }`
 *         block with at least `kind` set.
 *   3. The manifest is fed through validateManifest() — failures emit
 *      diagnostics and the plugin is skipped.
 *   4. Scoped namespaces (`@walle/*`, `@walle-plugin/*`) are scanned
 *      preferentially; everything else also matches if the manifest
 *      criteria are met (no namespace lockout for single-user Wall-E).
 *
 * NOT in scope (deferred per gap report):
 *   - npm registry queries (we don't reach out to ClawHub-equivalent)
 *   - signature/checksum verification
 *   - sandboxing — 3rd-party code runs in-process, same as bundled
 *
 * Returns { manifests: [...], diagnostics: [...] }. Manifests carry
 * `entryFile`, `manifestPath`, `packageDir`, `packageName`, `packageVersion`,
 * and `source: 'npm'`.
 */

/** Read package.json, return parsed object or null on failure. */
function _readPackageJson(packageDir) {
  const pkgJsonPath = path.join(packageDir, 'package.json');
  if (!fs.existsSync(pkgJsonPath)) return null;
  try {
    return JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
  } catch {
    return null;
  }
}

/** Read sibling walle.plugin.json — null if absent or invalid JSON. */
function _readSidecarManifest(packageDir) {
  const sidecarPath = path.join(packageDir, 'walle.plugin.json');
  if (!fs.existsSync(sidecarPath)) return null;
  try {
    return { manifest: JSON.parse(fs.readFileSync(sidecarPath, 'utf8')), path: sidecarPath };
  } catch {
    return { manifest: null, path: sidecarPath, parseError: true };
  }
}

/**
 * Resolve a plugin from a single package directory. Returns
 * { manifest, diagnostics } where `manifest` is null if the package
 * doesn't qualify as a Wall-E plugin.
 */
function scanPackageDir(packageDir) {
  const diagnostics = [];
  const pkg = _readPackageJson(packageDir);
  if (!pkg) {
    return { manifest: null, diagnostics };
  }

  // Sidecar manifest wins
  const sidecar = _readSidecarManifest(packageDir);
  let raw = null;
  let manifestPath = null;
  if (sidecar) {
    if (sidecar.parseError) {
      diagnostics.push({
        level: 'error',
        message: `Failed to parse ${sidecar.path}: invalid JSON`,
        source: 'npm',
        component: 'npm-scanner',
        pluginId: pkg.name,
      });
      return { manifest: null, diagnostics };
    }
    raw = sidecar.manifest;
    manifestPath = sidecar.path;
  } else if (pkg.walle && typeof pkg.walle === 'object' && pkg.walle.kind) {
    raw = pkg.walle;
    manifestPath = path.join(packageDir, 'package.json');
  } else {
    return { manifest: null, diagnostics };
  }

  // Inherit package metadata into manifest defaults
  if (!raw.id && pkg.name) raw.id = pkg.name.replace(/^@[^/]+\//, '');
  if (!raw.version && pkg.version) raw.version = pkg.version;
  if (!raw.description && pkg.description) raw.description = pkg.description;

  const result = validateManifest(raw, { source: 'npm' });
  diagnostics.push(...(result.diagnostics || []));
  if (!result.valid) {
    return { manifest: null, diagnostics };
  }

  // Resolve entry file relative to packageDir
  let entryFile = null;
  if (raw.entry) {
    entryFile = path.join(packageDir, raw.entry);
  } else if (pkg.main) {
    entryFile = path.join(packageDir, pkg.main);
  }

  return {
    manifest: {
      ...raw,
      manifestPath,
      packageDir,
      packageName: pkg.name,
      packageVersion: pkg.version,
      source: 'npm',
      entryFile,
    },
    diagnostics,
  };
}

/**
 * Walk `<workspaceDir>/node_modules/` and `<workspaceDir>/node_modules/@*`
 * scopes for Wall-E plugins.
 *
 * @param {string} workspaceDir - Root that contains a node_modules dir.
 * @param {object} [opts]
 * @param {string[]} [opts.scopes] - extra scope dirs to scan inside
 *                                   node_modules (e.g. ['@walle']).
 *                                   Defaults to scanning ALL @* scopes.
 */
function scanWorkspace(workspaceDir, opts = {}) {
  const manifests = [];
  const diagnostics = [];
  const nm = path.join(workspaceDir, 'node_modules');
  if (!fs.existsSync(nm)) {
    return { manifests, diagnostics };
  }

  let entries;
  try {
    entries = fs.readdirSync(nm, { withFileTypes: true });
  } catch (err) {
    diagnostics.push({
      level: 'warn',
      message: `Cannot read ${nm}: ${err.message}`,
      source: 'npm',
      component: 'npm-scanner',
    });
    return { manifests, diagnostics };
  }

  const scopeAllowList = opts.scopes ? new Set(opts.scopes) : null;

  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    if (entry.name === '.bin' || entry.name === '.cache') continue;

    if (entry.name.startsWith('@')) {
      // Scope dir
      if (scopeAllowList && !scopeAllowList.has(entry.name)) continue;
      const scopeDir = path.join(nm, entry.name);
      let scopedEntries;
      try {
        scopedEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
      } catch {
        continue;
      }
      for (const scoped of scopedEntries) {
        if (!scoped.isDirectory()) continue;
        const r = scanPackageDir(path.join(scopeDir, scoped.name));
        diagnostics.push(...r.diagnostics);
        if (r.manifest) manifests.push(r.manifest);
      }
    } else {
      // Top-level pkg
      const r = scanPackageDir(path.join(nm, entry.name));
      diagnostics.push(...r.diagnostics);
      if (r.manifest) manifests.push(r.manifest);
    }
  }
  return { manifests, diagnostics };
}

module.exports = {
  scanPackageDir,
  scanWorkspace,
};
