/**
 * Runtime validation of endpoint arguments against the compact schemas the compiler derives from the
 * TypeScript parameter types (src/compile/serverTypes.ts). Dependency-free; the schema is JSON so it
 * travels inside the server bundle's manifest.
 */

export type Schema =
  | { t: "any" }
  | { t: "string" } | { t: "number" } | { t: "boolean" } | { t: "null" } | { t: "undefined" }
  | { t: "lit", v: string | number | boolean }
  | { t: "array", of: Schema }
  | { t: "tuple", items: Schema[], rest?: Schema }
  | { t: "object", props: Record<string, Schema>, optional?: string[], index?: Schema }
  | { t: "union", of: Schema[] }

/** One endpoint parameter: its schema and whether it may be omitted (`?`, default value, rest). */
export type ParamSchema = { name: string, schema: Schema, optional?: boolean, rest?: boolean }

export class ValidationError extends Error {
  readonly status = 400
  constructor(message: string) { super(message); this.name = "ValidationError" }
}

const typeName = (v: unknown) => v === null ? "null" : Array.isArray(v) ? "array" : typeof v

/** Returns the first problem as a message, or null when `value` matches. */
export const check = (schema: Schema, value: unknown, path: string): string | null => {
  switch (schema.t) {
    case "any": return null
    case "string": case "number": case "boolean":
      return typeof value === schema.t ? null : `${path}: expected ${schema.t}, got ${typeName(value)}`
    case "null": return value === null ? null : `${path}: expected null, got ${typeName(value)}`
    case "undefined": return value === undefined ? null : `${path}: expected undefined, got ${typeName(value)}`
    case "lit": return value === schema.v ? null : `${path}: expected ${JSON.stringify(schema.v)}, got ${JSON.stringify(value)}`
    case "array":
      if (!Array.isArray(value)) return `${path}: expected array, got ${typeName(value)}`
      for (let i = 0; i < value.length; i++) { const e = check(schema.of, value[i], `${path}[${i}]`); if (e) return e }
      return null
    case "tuple": {
      if (!Array.isArray(value)) return `${path}: expected tuple, got ${typeName(value)}`
      if (value.length < schema.items.length || (!schema.rest && value.length > schema.items.length))
        return `${path}: expected ${schema.items.length} items, got ${value.length}`
      for (let i = 0; i < value.length; i++) {
        const s = i < schema.items.length ? schema.items[i] : schema.rest!
        const e = check(s, value[i], `${path}[${i}]`); if (e) return e
      }
      return null
    }
    case "object": {
      if (typeof value !== "object" || value === null || Array.isArray(value)) return `${path}: expected object, got ${typeName(value)}`
      const obj = value as Record<string, unknown>
      const optional = new Set(schema.optional ?? [])
      for (const [k, s] of Object.entries(schema.props)) {
        if (!(k in obj) || obj[k] === undefined) { if (optional.has(k)) continue; return `${path}.${k}: required` }
        const e = check(s, obj[k], `${path}.${k}`); if (e) return e
      }
      for (const k of Object.keys(obj)) {
        if (k in schema.props) continue
        if (schema.index) { const e = check(schema.index, obj[k], `${path}.${k}`); if (e) return e }
        // unknown keys without an index signature are ignored (structural typing)
      }
      return null
    }
    case "union": {
      const errors: string[] = []
      for (const s of schema.of) { const e = check(s, value, path); if (!e) return null; errors.push(e) }
      return `${path}: no union member matched (${errors.join("; ")})`
    }
  }
}

/** Validate a positional argument list. Extra arguments are ignored (JS call semantics). */
export const validateArgs = (params: ParamSchema[], args: unknown[]): void => {
  for (let i = 0; i < params.length; i++) {
    const p = params[i]
    if (p.rest) {
      const rest = args.slice(i)
      const e = check(p.schema, rest, p.name)
      if (e) throw new ValidationError(e)
      return
    }
    const v = args[i]
    if (v === undefined) { if (p.optional) continue; throw new ValidationError(`${p.name}: required`) }
    const e = check(p.schema, v, p.name)
    if (e) throw new ValidationError(e)
  }
}
