/**
 * Inline JSON-Schema validator for the MCP schema-conformance matchers.
 *
 * The schema matchers (`tool-input-schema` / `tool-output-schema`) validate a
 * recorded tool call's input/output against a supplied JSON Schema. Unlike the
 * value-equality matchers (`tool-input-matches` / `tool-output-matches`), which
 * compare exact key/value pairs, these answer a shape/type question ("is
 * `query` a string?", "is the result a non-empty array?") with a real JSON
 * Schema validator rather than a homegrown key-comparison loop (D0079).
 *
 * ## Why the validator is compiled to an inlined string
 *
 * Promptfoo runs a multiline `javascript` assertion via
 * `new Function("output", "context", "process", body)` — a sandbox with no
 * module scope and no `require` (see the bridge in `./assertions.ts`). So the
 * validator can't `import` Ajv at assertion-run time; it has to BE part of the
 * generated body. Ajv's standalone code generation is built for exactly this:
 * it compiles a (compile-time-known) schema into a self-contained validate
 * function as JS source. We wrap that source with a `module`/`require` shim so
 * it runs verbatim, supply the two tiny Ajv runtime helpers it can reference
 * (`ucs2length` for string length, deep-equal for `uniqueItems`/object-valued
 * `enum`/`const`), and base64-encode the whole factory.
 *
 * ## Why base64
 *
 * Promptfoo nunjucks-renders the assertion value before executing it, so the
 * body must contain none of `{{`, `}}`, `{%`, `%}`, `{#` (the bridge test
 * enforces this). Ajv's generated code is dense and routinely contains `}}`
 * (nested closing braces), which nunjucks would mangle. Base64 keeps the
 * varying, codegen-produced part to the base64 alphabet, which has no nunjucks
 * delimiters; the surrounding hand-written body stays delimiter-free by
 * construction.
 */
import Ajv from "ajv";
import standaloneCode from "ajv/dist/standalone/index.js";
// ---------------------------------------------------------------------------
// Vendored Ajv runtime helpers
// ---------------------------------------------------------------------------
// Ajv's standalone output references a small set of runtime helpers via
// `require("ajv/dist/runtime/<name>").default`. We resolve those through a
// `require` shim (below) that returns these vendored copies, so the inlined
// validator stays self-contained. Copied verbatim from the upstream sources
// (both MIT-licensed) so behavior matches the validator we compile against:
//   - ucs2length: ajv/dist/runtime/ucs2length (counts UTF-16 code points for
//     minLength/maxLength).
//   - equal: fast-deep-equal (ajv re-exports it as ajv/dist/runtime/equal;
//     used by uniqueItems and object/array-valued enum/const).
const UCS2LENGTH_SRC = `function ucs2length(str) {
  const len = str.length
  let length = 0
  let pos = 0
  let value
  while (pos < len) {
    length++
    value = str.charCodeAt(pos++)
    if (value >= 0xd800 && value <= 0xdbff && pos < len) {
      value = str.charCodeAt(pos)
      if ((value & 0xfc00) === 0xdc00) pos++
    }
  }
  return length
}`;
const EQUAL_SRC = `function equal(a, b) {
  if (a === b) return true
  if (a && b && typeof a == "object" && typeof b == "object") {
    if (a.constructor !== b.constructor) return false
    var length, i, keys
    if (Array.isArray(a)) {
      length = a.length
      if (length != b.length) return false
      for (i = length; i-- !== 0; ) if (!equal(a[i], b[i])) return false
      return true
    }
    if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags
    if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf()
    if (a.toString !== Object.prototype.toString) return a.toString() === b.toString()
    keys = Object.keys(a)
    length = keys.length
    if (length !== Object.keys(b).length) return false
    for (i = length; i-- !== 0; ) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false
    for (i = length; i-- !== 0; ) {
      var key = keys[i]
      if (!equal(a[key], b[key])) return false
    }
    return true
  }
  return a !== a && b !== b
}`;
/** Ajv runtime modules the inlined `require` shim knows how to resolve. */
const ALLOWED_RUNTIME_DEPS = new Set([
    "ajv/dist/runtime/ucs2length",
    "ajv/dist/runtime/equal",
]);
/** Keyword set we can validate self-contained — used in the failure message. */
const SUPPORTED_KEYWORDS = "type, properties, required, items, additionalProperties, enum, const, " +
    "pattern, minItems/maxItems, minLength/maxLength, uniqueItems, " +
    "minimum/maximum, and nested combinations of these";
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Compile a JSON Schema into a base64-encoded validator factory.
 *
 * The decoded factory is a function body that returns Ajv's standalone
 * `validate` function (with `.errors` populated on failure). The caller embeds
 * the base64 string in an assertion body and instantiates it at runtime via
 * `new Function(atob(<base64>))()` — see {@link inlineSchemaValidator}.
 *
 * Throws (at compile time, surfaced as a task warning) when the schema is
 * invalid or uses a feature that would need an unsupported Ajv runtime
 * dependency — failing loud beats emitting an assertion that can't run.
 */
export function compileSchemaValidatorFactory(schema, errorLabel) {
    const ajv = new Ajv({
        code: { source: true, esm: false },
        allErrors: true,
        // Hand-authored task schemas are plain shape/type checks, not strict-mode
        // meta-schemas; tolerate unknown formats/keywords rather than reject.
        strict: false,
    });
    let standalone;
    try {
        const validate = ajv.compile(schema);
        standalone = standaloneCode(ajv, validate);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        throw new Error(`${errorLabel}: invalid JSON Schema — ${message}`, {
            cause: err,
        });
    }
    for (const match of standalone.matchAll(/require\((['"])(.*?)\1\)/g)) {
        const dep = match[2];
        if (!ALLOWED_RUNTIME_DEPS.has(dep)) {
            throw new Error(`${errorLabel}: schema uses a feature requiring an unsupported validator ` +
                `runtime dependency ("${dep}"). Supported keywords: ${SUPPORTED_KEYWORDS}.`);
        }
    }
    const factory = [
        '"use strict";',
        "var module = { exports: {} };",
        UCS2LENGTH_SRC,
        EQUAL_SRC,
        "function require(p) {",
        '  if (p === "ajv/dist/runtime/ucs2length") return { default: ucs2length }',
        '  if (p === "ajv/dist/runtime/equal") return { default: equal }',
        '  throw new Error("unsupported ajv runtime dependency: " + p)',
        "}",
        standalone,
        "return module.exports;",
    ].join("\n");
    return Buffer.from(factory, "utf8").toString("base64");
}
/**
 * JS snippet that decodes a base64 validator factory and instantiates it into
 * a local `validate` function. Pairs with {@link compileSchemaValidatorFactory}.
 *
 * Emitted into an assertion body; assumes the `new Function(output, context,
 * process)` sandbox promptfoo uses. `atob` is a Node global (16+); the `Buffer`
 * branch is a defensive fallback. The decoded factory contains no nunjucks
 * delimiters (it's base64), so this is rendering-safe.
 */
export function inlineSchemaValidator(base64Factory) {
    return `
      var __vSrc = (typeof atob === 'function')
        ? atob(${JSON.stringify(base64Factory)})
        : Buffer.from(${JSON.stringify(base64Factory)}, 'base64').toString('utf8');
      var validate = (new Function(__vSrc))();`;
}
/**
 * JS snippet defining `formatSchemaErrors(errors)` — turns Ajv's error array
 * into a compact human-readable string for an assertion `reason`. Defined once
 * per assertion body. Delimiter-free by construction.
 */
export function schemaErrorFormatterSource() {
    return `function formatSchemaErrors(errs) {
        if (!errs || errs.length === 0) { return 'validation failed'; }
        return errs.map(function(e) {
          var where = e.instancePath ? e.instancePath : '(root)';
          return where + ' ' + e.message;
        }).join('; ');
      }`;
}
