// lib/schema-validate.js
//
// Minimal, dependency-free JSON-shape validator used by lib/engine-author.js to
// check structured output from either engine before it is trusted. No new npm
// dependency — this is the load-bearing reliability control for the CLI tier,
// where output is free-text JSON rather than an API forced-tool call.
//
// Supported schema subset (a pragmatic slice of JSON Schema we actually author):
//   - object with `properties` + `required`
//   - primitive leaves: { type: 'string' | 'number' | 'boolean' }
//   - arrays: { type: 'array', items: { type: 'string' } } → array-of-string is
//     fully validated; other array item types get a shallow container check
//   - nested object / array-of-object: container type is checked, not deeply
//     recursed (callers that need per-element rules — e.g. fact citation checks
//     in mines_extract_facts — apply their own validation downstream).
//
// validateShape(value, schema) → { ok: boolean, errors: string[] }

function typeOf(v) {
  if (Array.isArray(v)) return 'array';
  if (v === null) return 'null';
  return typeof v; // 'string' | 'number' | 'boolean' | 'object' | 'undefined'
}

function validateNode(value, schema, path, errors) {
  if (!schema || typeof schema !== 'object') return;
  const declaredType = schema.type;

  // Object (explicit type:'object' or an inferred object via `properties`).
  if (declaredType === 'object' || (!declaredType && schema.properties)) {
    if (typeOf(value) !== 'object') {
      errors.push(`${path}: expected object, got ${typeOf(value)}`);
      return;
    }
    const props = schema.properties || {};
    const required = Array.isArray(schema.required) ? schema.required : [];
    for (const key of required) {
      if (!(key in value) || value[key] === undefined || value[key] === null) {
        errors.push(`${path}.${key}: required key missing`);
      }
    }
    for (const [key, sub] of Object.entries(props)) {
      if (key in value && value[key] !== undefined && value[key] !== null) {
        validateNode(value[key], sub, `${path}.${key}`, errors);
      }
    }
    return;
  }

  if (declaredType === 'array') {
    if (!Array.isArray(value)) {
      errors.push(`${path}: expected array, got ${typeOf(value)}`);
      return;
    }
    const itemType = schema.items?.type;
    if (itemType === 'string') {
      value.forEach((el, i) => {
        if (typeof el !== 'string') errors.push(`${path}[${i}]: expected string, got ${typeOf(el)}`);
      });
    } else if (itemType === 'object') {
      // Shallow: confirm each element is an object. Per-field rules are the
      // caller's responsibility (keeps this validator minimal + dependency-free).
      value.forEach((el, i) => {
        if (typeOf(el) !== 'object') errors.push(`${path}[${i}]: expected object, got ${typeOf(el)}`);
      });
    }
    return;
  }

  if (declaredType === 'string' || declaredType === 'number' || declaredType === 'boolean') {
    if (typeOf(value) !== declaredType) {
      errors.push(`${path}: expected ${declaredType}, got ${typeOf(value)}`);
    }
    return;
  }

  // Unknown / absent type → no constraint applied.
}

/**
 * Validate a value against the supported JSON-shape subset.
 * @param {*} value
 * @param {object} schema  Either a full object schema ({type:'object',properties,required})
 *                         or a bare properties map (every key treated as required).
 * @returns {{ ok: boolean, errors: string[] }}
 */
export function validateShape(value, schema) {
  const errors = [];
  validateNode(value, normalizeSchema(schema), '$', errors);
  return { ok: errors.length === 0, errors };
}

/**
 * Accept either a full object schema or a bare properties map. A bare map (the
 * shape used by the mines NARRATIVE_SCHEMAS) is promoted to an object schema
 * with every property required.
 */
export function normalizeSchema(schema) {
  if (schema && typeof schema === 'object' && schema.properties) {
    return {
      type: 'object',
      properties: schema.properties,
      required: Array.isArray(schema.required) ? schema.required : Object.keys(schema.properties),
    };
  }
  const properties = schema && typeof schema === 'object' ? schema : {};
  return { type: 'object', properties, required: Object.keys(properties) };
}
