// Server-side validation for a page's `blocks` array, run on every save before
// persisting. The admin UI validates client-side too, but that can be bypassed
// by a direct API call, so this is the authoritative gate: it enforces the block
// envelope (reserved keys only), that every `type` is a registered block, that
// each block's props satisfy the registry schema, and the one-level nesting cap
// (only container blocks may hold sub-blocks, and sub-blocks may not themselves
// be containers).
//
// Returns { valid, errors } — a flat list of human-readable error strings, each
// prefixed with the path to the offending block (e.g. `blocks[2].props.text`).
// It never throws on bad input; callers turn a non-empty `errors` into a 400.

const { getBlock, RESERVED_KEYS } = require('./registry')

// Bound the payload so a single page can't carry an unreasonable block tree.
const MAX_BLOCKS = 100 // top-level blocks per page
const MAX_SUBBLOCKS = 50 // sub-blocks per container slot
const ID_RE = /^[A-Za-z0-9_-]{1,40}$/

/**
 * Validate a stored blocks array against the registry.
 * @param {unknown} blocks
 * @returns {{ valid: boolean, errors: string[] }}
 */
function validateBlocks(blocks) {
  const errors = []
  if (!Array.isArray(blocks)) {
    return { valid: false, errors: ['blocks must be an array'] }
  }
  if (blocks.length > MAX_BLOCKS) {
    errors.push(`blocks may not exceed ${MAX_BLOCKS} top-level entries`)
  }
  const seenIds = new Set()
  blocks.forEach((block, i) => {
    validateBlock(block, `blocks[${i}]`, seenIds, errors, { nested: false })
  })
  return { valid: errors.length === 0, errors }
}

// Envelope: only the reserved keys, nothing smuggled at the top level.
function checkEnvelope(block, path, errors) {
  for (const key of Object.keys(block)) {
    if (!RESERVED_KEYS.includes(key)) {
      errors.push(`${path}.${key} is not an allowed top-level key`)
    }
  }
}

// id — stable, unique across the whole page (top-level and nested share one
// namespace since ids are the future join point for revision history).
function checkId(block, path, seenIds, errors) {
  if (typeof block.id !== 'string' || !ID_RE.test(block.id)) {
    errors.push(`${path}.id must be a short id string`)
  } else if (seenIds.has(block.id)) {
    errors.push(`${path}.id duplicates another block id (${block.id})`)
  } else {
    seenIds.add(block.id)
  }
}

// Per-block prop schema from the registry (skipped when props isn't an object —
// that's already reported separately).
function checkPropSchema(def, props, path, errors) {
  if (!def.schema || !props || typeof props !== 'object') return
  let schemaErrors = []
  try {
    schemaErrors = def.schema(props) || []
  } catch (err) {
    schemaErrors = [`schema threw: ${err.message}`]
  }
  for (const e of schemaErrors) errors.push(`${path}.props.${e}`)
}

// Nesting: only container blocks may hold sub-blocks, capped at one level.
function checkNesting(def, props, path, seenIds, errors, nested) {
  if (nested) {
    errors.push(`${path} is a container and may not be nested inside another container`)
    return
  }
  for (const slot of def.containerSlots) {
    const sub = props ? props[slot] : undefined
    if (sub === undefined) continue // an empty slot is allowed
    if (!Array.isArray(sub)) {
      errors.push(`${path}.props.${slot} must be an array of blocks`)
      continue
    }
    if (sub.length > MAX_SUBBLOCKS) {
      errors.push(`${path}.props.${slot} may not exceed ${MAX_SUBBLOCKS} blocks`)
    }
    sub.forEach((child, j) => {
      validateBlock(child, `${path}.props.${slot}[${j}]`, seenIds, errors, { nested: true })
    })
  }
}

/**
 * Validate one block envelope in place. `nested` = true when validating a
 * sub-block inside a container slot, which forbids further nesting.
 */
function validateBlock(block, path, seenIds, errors, { nested }) {
  if (block === null || typeof block !== 'object' || Array.isArray(block)) {
    errors.push(`${path} must be an object`)
    return
  }

  checkEnvelope(block, path, errors)
  checkId(block, path, seenIds, errors)

  // visible — optional in input, but if present must be a boolean.
  if (block.visible !== undefined && typeof block.visible !== 'boolean') {
    errors.push(`${path}.visible must be a boolean`)
  }

  // props — always an object bag.
  const props = block.props
  if (props === null || typeof props !== 'object' || Array.isArray(props)) {
    errors.push(`${path}.props must be an object`)
  }

  // type — must resolve to a registered block.
  const def = typeof block.type === 'string' ? getBlock(block.type) : null
  if (!def) {
    errors.push(`${path}.type is not a registered block type (${String(block.type)})`)
    return // can't validate props or nesting without a definition
  }

  checkPropSchema(def, props, path, errors)
  if (def.container) checkNesting(def, props, path, seenIds, errors, nested)
}

module.exports = { validateBlocks, MAX_BLOCKS, MAX_SUBBLOCKS }
