// lib/skills/skill-validator.js — strict, pure validator for skill specs.
//
// This is the single source of truth for what a skill spec must look like.
// Both internal upsertSkill() and external authoring (agent tool) flow through
// validateSkillSpec. The validator never touches the DB or filesystem; it only
// inspects the spec object and returns a structured result.
//
// Contract:
//   validateSkillSpec(spec, opts?) → { ok, errors, warnings, normalized }
//
// On ok:false, errors[] is populated and normalized is null.
// On ok:true, normalized is a defensively-cloned canonical form (defaults
// applied, fields placed in a stable order). Warnings can appear in both
// branches but never block.
//
// Each violation pushes a "path: reason" string into errors[].

export const SKILL_SLUG_REGEX = /^[a-z][a-z0-9-]{2,63}$/;
export const MAX_SKILL_SPEC_BYTES = 64 * 1024;
export const MAX_SKILL_MARKDOWN_CHARS = 100000;

const INVOCATION_REGEX = /^\/[a-z][a-z0-9-]{1,63}$/;

function pushErr(errors, path, reason) {
  errors.push(`${path}: ${reason}`);
}

function isPlainObject(v) {
  return v !== null && typeof v === 'object' && !Array.isArray(v);
}

export function validateSkillSpec(spec, opts = {}) {
  const errors = [];
  const warnings = [];

  // Size cap first — protects everything else from runaway JSON.
  let serialized;
  try {
    serialized = JSON.stringify(spec);
  } catch {
    return { ok: false, errors: ['spec: not JSON-serialisable'], warnings: [], normalized: null };
  }
  if (!serialized || serialized === 'undefined') {
    return { ok: false, errors: ['spec: required'], warnings: [], normalized: null };
  }
  if (Buffer.byteLength(serialized, 'utf8') > MAX_SKILL_SPEC_BYTES) {
    return {
      ok: false,
      errors: [`spec: exceeds 64KB`],
      warnings: [],
      normalized: null,
    };
  }

  if (!isPlainObject(spec)) {
    return { ok: false, errors: ['spec: must be an object'], warnings: [], normalized: null };
  }

  // slug — required, must match SKILL_SLUG_REGEX.
  if (!spec.slug || typeof spec.slug !== 'string') {
    pushErr(errors, 'slug', 'required (string)');
  } else if (!SKILL_SLUG_REGEX.test(spec.slug)) {
    pushErr(errors, 'slug', `does not match ${SKILL_SLUG_REGEX} (lowercase, starts with letter, 3-64 chars, [a-z0-9-])`);
  }

  // title — required non-empty string.
  if (!spec.title || typeof spec.title !== 'string') {
    pushErr(errors, 'title', 'required (non-empty string)');
  }

  // description — optional string.
  if (spec.description != null && typeof spec.description !== 'string') {
    pushErr(errors, 'description', 'must be a string');
  }

  // markdown — required non-empty string; length capped.
  if (!spec.markdown || typeof spec.markdown !== 'string') {
    pushErr(errors, 'markdown', 'required (non-empty string)');
  } else if (spec.markdown.length > MAX_SKILL_MARKDOWN_CHARS) {
    pushErr(errors, 'markdown', `exceeds ${MAX_SKILL_MARKDOWN_CHARS} chars (got ${spec.markdown.length})`);
  }

  // invocation — optional; if present must match /^\/[a-z][a-z0-9-]{1,63}$/.
  if (spec.invocation != null) {
    if (typeof spec.invocation !== 'string') {
      pushErr(errors, 'invocation', 'must be a string');
    } else if (!INVOCATION_REGEX.test(spec.invocation)) {
      pushErr(errors, 'invocation', `must match ${INVOCATION_REGEX} (e.g. "/my-skill")`);
    }
  }

  // trigger_examples — optional array of non-empty strings.
  if (spec.trigger_examples != null) {
    if (!Array.isArray(spec.trigger_examples)) {
      pushErr(errors, 'trigger_examples', 'must be an array of strings');
    } else {
      spec.trigger_examples.forEach((ex, i) => {
        if (typeof ex !== 'string' || !ex.trim()) {
          pushErr(errors, `trigger_examples[${i}]`, 'must be a non-empty string');
        }
      });
    }
  }

  // category — optional string.
  if (spec.category != null && typeof spec.category !== 'string') {
    pushErr(errors, 'category', 'must be a string');
  }

  if (errors.length > 0) {
    return { ok: false, errors, warnings, normalized: null };
  }

  return { ok: true, errors: [], warnings, normalized: normalize(spec) };
}

// Defensive clone with defaults applied and stable field ordering.
function normalize(spec) {
  return {
    slug: spec.slug,
    title: spec.title,
    description: spec.description || null,
    category: typeof spec.category === 'string' && spec.category ? spec.category : 'authored',
    invocation: typeof spec.invocation === 'string' && spec.invocation ? spec.invocation : `/${spec.slug}`,
    markdown: spec.markdown,
    trigger_examples: Array.isArray(spec.trigger_examples)
      ? spec.trigger_examples.filter((ex) => typeof ex === 'string' && ex.trim())
      : [],
  };
}
