'use strict';

const { PLUGIN_KINDS } = require('./manifest-types');

/**
 * Wall-E plugin SDK ABI version. Bump on breaking changes to the manifest
 * contract or the registries' public surface.
 *
 * Inspired by OpenClaw's `packages/plugin-package-contract/` `pluginApi`
 * field — third-party (and bundled) plugins declare the range they support;
 * loader refuses out-of-range plugins with a diagnostic.
 *
 * Bump policy:
 *   - 0.x.y: pre-stable; ANY break bumps minor
 *   - 1.x.y onward: SemVer (major = break, minor = additive, patch = fix)
 */
const PLUGIN_API_VERSION = '0.1.0';

/** Required fields for every manifest. */
const REQUIRED_FIELDS = ['id', 'kind', 'version', 'pluginApi'];

/** Per-kind required fields beyond the common set. */
const KIND_REQUIRED = Object.freeze({
  skill:    [],
  provider: ['modelPrefixes'],
  channel:  [],
  adapter:  [],
  tool:     [],
  mcp:      [],
});

/**
 * Parse a semver string. Returns { major, minor, patch } or null.
 * Strict-ish: rejects pre-release/build metadata (we don't use them).
 */
function parseSemver(s) {
  if (typeof s !== 'string') return null;
  const m = s.trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
  if (!m) return null;
  return { major: parseInt(m[1], 10), minor: parseInt(m[2], 10), patch: parseInt(m[3], 10) };
}

/** Compare two parsed semvers. -1 / 0 / 1. */
function semverCompare(a, b) {
  if (a.major !== b.major) return a.major < b.major ? -1 : 1;
  if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
  if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
  return 0;
}

/**
 * Test whether `version` satisfies a `range`.
 * Supported syntax (subset of node-semver):
 *   - "1.2.3"        — exact match
 *   - ">=1.2.3"      — greater than or equal
 *   - "^1.2.3"       — compatible-with (same major, >= minor.patch)
 *   - "~1.2.3"       — patch-flexible (same major.minor, >= patch)
 *   - "1.2.3 - 2.0.0" — range (inclusive)
 *   - "*"            — anything
 *
 * Returns boolean. Returns false if either argument is unparseable.
 */
function semverSatisfies(version, range) {
  if (typeof range !== 'string') return false;
  const r = range.trim();
  if (r === '*') return parseSemver(version) !== null;

  const v = parseSemver(version);
  if (!v) return false;

  // Range: "X.Y.Z - A.B.C"
  const rangeMatch = r.match(/^(\S+)\s*-\s*(\S+)$/);
  if (rangeMatch) {
    const lo = parseSemver(rangeMatch[1]);
    const hi = parseSemver(rangeMatch[2]);
    if (!lo || !hi) return false;
    return semverCompare(v, lo) >= 0 && semverCompare(v, hi) <= 0;
  }

  // Caret: "^X.Y.Z" — same major, version >= X.Y.Z, < (X+1).0.0
  if (r.startsWith('^')) {
    const lo = parseSemver(r.slice(1));
    if (!lo) return false;
    if (v.major !== lo.major) return false;
    return semverCompare(v, lo) >= 0;
  }

  // Tilde: "~X.Y.Z" — same major.minor, version >= X.Y.Z, < X.(Y+1).0
  if (r.startsWith('~')) {
    const lo = parseSemver(r.slice(1));
    if (!lo) return false;
    if (v.major !== lo.major || v.minor !== lo.minor) return false;
    return v.patch >= lo.patch;
  }

  // ">=X.Y.Z"
  if (r.startsWith('>=')) {
    const lo = parseSemver(r.slice(2).trim());
    if (!lo) return false;
    return semverCompare(v, lo) >= 0;
  }

  // ">X.Y.Z"
  if (r.startsWith('>')) {
    const lo = parseSemver(r.slice(1).trim());
    if (!lo) return false;
    return semverCompare(v, lo) > 0;
  }

  // Exact match
  const exact = parseSemver(r);
  if (!exact) return false;
  return semverCompare(v, exact) === 0;
}

/**
 * Validate a plugin manifest.
 * @param {object} manifest - The parsed manifest object.
 * @param {object} [opts]
 * @param {string} [opts.source] - Where the manifest came from (for diagnostics).
 * @returns {{ valid: boolean, manifest?: object, diagnostics: Array<object> }}
 */
function validateManifest(manifest, opts = {}) {
  const diagnostics = [];
  const source = opts.source || 'unknown';

  if (!manifest || typeof manifest !== 'object') {
    diagnostics.push({
      level: 'error',
      message: 'Manifest must be a non-null object',
      source,
    });
    return { valid: false, diagnostics };
  }

  const pluginId = typeof manifest.id === 'string' ? manifest.id : undefined;

  // Required fields
  for (const field of REQUIRED_FIELDS) {
    if (manifest[field] === undefined || manifest[field] === null || manifest[field] === '') {
      diagnostics.push({
        level: 'error',
        message: `Required field missing: ${field}`,
        fieldPath: field,
        pluginId,
        source,
      });
    }
  }

  // Kind must be in PLUGIN_KINDS
  if (manifest.kind && !PLUGIN_KINDS.includes(manifest.kind)) {
    diagnostics.push({
      level: 'error',
      message: `Unknown kind: ${manifest.kind} (must be one of: ${PLUGIN_KINDS.join(', ')})`,
      fieldPath: 'kind',
      pluginId,
      source,
    });
  }

  // Per-kind required fields
  if (manifest.kind && KIND_REQUIRED[manifest.kind]) {
    for (const field of KIND_REQUIRED[manifest.kind]) {
      if (manifest[field] === undefined || manifest[field] === null) {
        diagnostics.push({
          level: 'error',
          message: `Required field for kind=${manifest.kind} missing: ${field}`,
          fieldPath: field,
          pluginId,
          source,
        });
      }
    }
  }

  // version must be a semver
  if (manifest.version && parseSemver(manifest.version) === null) {
    diagnostics.push({
      level: 'error',
      message: `version is not a valid semver: ${manifest.version}`,
      fieldPath: 'version',
      pluginId,
      source,
    });
  }

  // pluginApi must satisfy current PLUGIN_API_VERSION
  if (manifest.pluginApi) {
    if (!semverSatisfies(PLUGIN_API_VERSION, manifest.pluginApi)) {
      diagnostics.push({
        level: 'error',
        message:
          `pluginApi=${manifest.pluginApi} does not satisfy current ` +
          `PLUGIN_API_VERSION=${PLUGIN_API_VERSION}`,
        fieldPath: 'pluginApi',
        pluginId,
        source,
      });
    }
  }

  // modelPrefixes for providers must be an array of non-empty strings.
  // Empty array is allowed: it means "this provider claims no model
  // names" (e.g. ollama/mlx use tag-style identifiers like `gemma4:e4b`
  // that no prefix-matching provider could claim).
  if (manifest.kind === 'provider' && manifest.modelPrefixes !== undefined) {
    if (!Array.isArray(manifest.modelPrefixes)) {
      diagnostics.push({
        level: 'error',
        message: 'modelPrefixes must be an array of strings',
        fieldPath: 'modelPrefixes',
        pluginId,
        source,
      });
    } else if (!manifest.modelPrefixes.every((p) => typeof p === 'string' && p.length > 0)) {
      diagnostics.push({
        level: 'error',
        message: 'modelPrefixes entries must be non-empty strings',
        fieldPath: 'modelPrefixes',
        pluginId,
        source,
      });
    }
  }

  const errorCount = diagnostics.filter((d) => d.level === 'error').length;
  return {
    valid: errorCount === 0,
    manifest: errorCount === 0 ? manifest : undefined,
    diagnostics,
  };
}

module.exports = {
  PLUGIN_API_VERSION,
  parseSemver,
  semverCompare,
  semverSatisfies,
  validateManifest,
};
