/**
 * Deterministic validator for the Draft 2020-12 keywords used by the local
 * V3 contracts. It intentionally rejects unsupported keywords so schema
 * coverage cannot silently weaken.
 */
export function validateSchema(root: any, value: unknown): string[] {
  const errors: string[] = [];
  const supported = new Set([
    "$schema", "$id", "$defs", "title", "description", "type", "const", "enum",
    "required", "properties", "additionalProperties", "items", "minItems",
    "maxItems", "uniqueItems", "minLength", "maxLength", "minimum", "maximum",
    "pattern", "format", "oneOf", "anyOf", "allOf", "not", "if", "then", "$ref",
  ]);
  const resolveRef = (ref: string) => {
    if (!ref.startsWith("#/")) throw new Error(`external schema reference is unsupported: ${ref}`);
    return ref.slice(2).split("/").reduce((node, key) => node[key.replace(/~1/g, "/").replace(/~0/g, "~")], root);
  };
  const semanticJson = (input: unknown): string => {
    if (input === null || typeof input !== "object") return JSON.stringify(input);
    if (Array.isArray(input)) return `[${input.map(semanticJson).join(",")}]`;
    const record = input as Record<string, unknown>;
    return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${semanticJson(record[key])}`).join(",")}}`;
  };
  const isRfc3339DateTime = (input: string): boolean => {
    const match = input.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})$/);
    if (!match || !Number.isFinite(Date.parse(input))) return false;
    const [, year, month, day, hour, minute, second, zone] = match;
    if (+month < 1 || +month > 12 || +day < 1 || +day > 31 || +hour > 23 || +minute > 59 || +second > 59) return false;
    if (zone !== "Z" && (+zone.slice(1, 3) > 23 || +zone.slice(4, 6) > 59)) return false;
    const probe = new Date(`${year}-${month}-${day}T00:00:00Z`);
    return probe.getUTCFullYear() === +year && probe.getUTCMonth() + 1 === +month && probe.getUTCDate() === +day;
  };
  const visit = (schema: any, candidate: any, path: string): void => {
    for (const key of Object.keys(schema)) {
      if (!supported.has(key)) throw new Error(`unsupported schema keyword ${key} at ${path}`);
    }
    if (schema.$ref) return visit(resolveRef(schema.$ref), candidate, path);
    if (schema.allOf) {
      for (const choice of schema.allOf) visit(choice, candidate, path);
    }
    if (schema.anyOf) {
      const matches = schema.anyOf.some((choice: any) => {
        const before = errors.length;
        visit(choice, candidate, path);
        const valid = errors.length === before;
        errors.splice(before);
        return valid;
      });
      if (!matches) errors.push(`${path} must match at least one anyOf branch`);
    }
    if (schema.not) {
      const before = errors.length;
      visit(schema.not, candidate, path);
      const forbiddenMatch = errors.length === before;
      errors.splice(before);
      if (forbiddenMatch) errors.push(`${path} must not match the forbidden schema`);
    }
    if (schema.if) {
      const before = errors.length;
      visit(schema.if, candidate, path);
      const conditionMatches = errors.length === before;
      errors.splice(before);
      if (conditionMatches && schema.then) visit(schema.then, candidate, path);
    }
    if (schema.oneOf) {
      const matches = schema.oneOf.filter((choice: any) => {
        const before = errors.length;
        visit(choice, candidate, path);
        const valid = errors.length === before;
        errors.splice(before);
        return valid;
      }).length;
      if (matches !== 1) errors.push(`${path} must match exactly one oneOf branch`);
      return;
    }
    const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
    const actual = candidate === null ? "null" : Array.isArray(candidate) ? "array" :
      Number.isInteger(candidate) ? "integer" : typeof candidate;
    if (types.length && !types.some((type: string) => type === actual || (type === "number" && typeof candidate === "number"))) {
      errors.push(`${path} must be ${types.join("|")}`);
      return;
    }
    if ("const" in schema && candidate !== schema.const) errors.push(`${path} must equal ${JSON.stringify(schema.const)}`);
    if (schema.enum && !schema.enum.includes(candidate)) errors.push(`${path} is outside enum`);
    if (typeof candidate === "string") {
      if (schema.minLength !== undefined && candidate.length < schema.minLength) errors.push(`${path} is too short`);
      if (schema.maxLength !== undefined && candidate.length > schema.maxLength) errors.push(`${path} is too long`);
      if (schema.pattern && !new RegExp(schema.pattern).test(candidate)) errors.push(`${path} does not match pattern`);
      if (schema.format === "date-time" && !isRfc3339DateTime(candidate)) errors.push(`${path} is not RFC 3339 date-time`);
    }
    if (typeof candidate === "number") {
      if (schema.minimum !== undefined && candidate < schema.minimum) errors.push(`${path} is below minimum`);
      if (schema.maximum !== undefined && candidate > schema.maximum) errors.push(`${path} is above maximum`);
    }
    if (Array.isArray(candidate)) {
      if (schema.minItems !== undefined && candidate.length < schema.minItems) errors.push(`${path} has too few items`);
      if (schema.maxItems !== undefined && candidate.length > schema.maxItems) errors.push(`${path} has too many items`);
      if (schema.uniqueItems && new Set(candidate.map(semanticJson)).size !== candidate.length) {
        errors.push(`${path} has duplicate items`);
      }
      if (schema.items) candidate.forEach((item, index) => visit(schema.items, item, `${path}[${index}]`));
    }
    if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
      for (const key of schema.required ?? []) if (!(key in candidate)) errors.push(`${path}.${key} is required`);
      for (const [key, child] of Object.entries(schema.properties ?? {})) {
        if (key in candidate) visit(child, candidate[key], `${path}.${key}`);
      }
      if (schema.additionalProperties === false) {
        for (const key of Object.keys(candidate)) if (!(key in (schema.properties ?? {}))) errors.push(`${path}.${key} is not allowed`);
      }
    }
  };
  visit(root, value, "$");
  return errors;
}
