///|
/// Select the structural input contract or the parsed output contract.
pub(all) enum SchemaView {
  InputView
  OutputView
} derive(Debug)

///|
#deprecated
pub extend SchemaView with @debug.Debug::{to_repr}

///|
/// Export an explicit structural view, returning unsupported projection errors.
/// A projection describes a stage's JSON shape, not arbitrary callback behavior.
pub fn try_to_json_schema(
  schema : @core.Schema,
  view~ : SchemaView = InputView,
) -> Result[Json, String] {
  let nodes = match @core.try_schema_nodes(schema) {
    Ok(nodes) => nodes
    Err(message) => return Err(message)
  }
  if contains_lazy(schema, Map([])) {
    if view is OutputView {
      return Err(
        "Recursive output projection requires an explicit output schema",
      )
    }
    for node in nodes {
      if node.schema_type is IntersectionType(_) {
        return Err(
          "Recursive intersection input projection requires an explicit input schema",
        )
      }
      if node.schema_type is ObjectType(_, Strip) {
        return Err(
          "Recursive Strip input projection requires an explicit input schema",
        )
      }
      if (
          node.schema_type is TransformType(_, _) ||
          node.schema_type is PipeType(_, _, _)
        ) &&
        !node.rules.is_empty() {
        return Err(
          "Post-effect checks cannot be projected onto recursive raw input",
        )
      }
      if node.schema_type is PreprocessType(_, _) {
        return Err("Preprocess input shape is unknown")
      }
      if node.rules.any(fn(rule) { rule.constraint is Opaque }) {
        return Err("Opaque refinements cannot be represented in JSON Schema")
      }
    }
    return Ok(to_json_schema_named(schema))
  }
  match project_view(schema, view, 0) {
    Ok(projected) => Ok(to_json_schema(projected))
    Err(message) => Err(message)
  }
}

///|
fn project_view(
  schema : @core.Schema,
  view : SchemaView,
  depth : Int32,
) -> Result[@core.Schema, String] {
  if depth > 256 {
    return Err("Schema projection exceeded 256 levels")
  }
  if schema.rules.any(fn(rule) { rule.constraint is Opaque }) {
    return Err("Opaque refinements cannot be represented in JSON Schema")
  }
  let output = view is OutputView
  let schema_type : @core.SchemaType = match schema.schema_type {
    TransformType(inner, closure) => {
      if output {
        if closure.kind is CustomTransform {
          return Err(
            "Custom transform output is unknown; add pipe(output_schema)",
          )
        }
        let present = { ..schema, schema_type: @core.SchemaType::StringType, }
        if @core.output_is_optional(schema) {
          return Ok(present.optional())
        }
        return Ok(present)
      }
      if !schema.rules.is_empty() {
        return Err("Post-transform checks cannot be projected onto raw input")
      }
      return project_view(
        without_inherited_name(schema, inner),
        view,
        depth + 1,
      )
    }
    PreprocessType(_, inner) => {
      if !output {
        return Err("Preprocess input shape is unknown")
      }
      return match
        project_view(without_inherited_name(schema, inner), view, depth + 1) {
        Ok(inner) =>
          Ok({ ..inner, name: schema.name, rules: inner.rules + schema.rules, })
        Err(message) => Err(message)
      }
    }
    PipeType(input, _, target) => {
      if !output && !schema.rules.is_empty() {
        return Err("Pipeline output checks cannot be projected onto raw input")
      }
      let selected = if output { target } else { input }
      match project_view(selected, view, depth + 1) {
        Ok(projected) =>
          return Ok({
            ..projected,
            name: schema.name,
            rules: projected.rules + (if output { schema.rules } else { [] }),
          })
        Err(message) => return Err(message)
      }
    }
    ObjectType(fields, mode) => {
      let projected : Map[String, @core.Schema] = Map([])
      for key, child in fields {
        match project_view(child, view, depth + 1) {
          Ok(child) => projected.set(key, child)
          Err(message) => return Err(message)
        }
      }
      ObjectType(
        projected,
        if !output && mode is Strip {
          Passthrough
        } else {
          mode
        },
      )
    }
    ArrayType(inner) =>
      match project_view(inner, view, depth + 1) {
        Ok(inner) => ArrayType(inner)
        Err(message) => return Err(message)
      }
    OptionalType(inner) =>
      match
        project_view(without_inherited_name(schema, inner), view, depth + 1) {
        Ok(inner) =>
          if output && !@core.output_is_optional(schema) {
            return Ok({
              ..inner,
              name: schema.name,
              rules: inner.rules + schema.rules,
            })
          } else {
            OptionalType(inner)
          }
        Err(message) => return Err(message)
      }
    NullableType(inner) =>
      match
        project_view(without_inherited_name(schema, inner), view, depth + 1) {
        Ok(inner) => NullableType(inner)
        Err(message) => return Err(message)
      }
    DefaultType(inner, value) =>
      match
        project_view(without_inherited_name(schema, inner), view, depth + 1) {
        Ok(inner) =>
          if output {
            UnionType([inner, @core.literal(value)])
          } else {
            DefaultType(inner, value)
          }
        Err(message) => return Err(message)
      }
    PrefaultType(inner, value) =>
      match
        project_view(without_inherited_name(schema, inner), view, depth + 1) {
        Ok(inner) =>
          if output {
            return Ok({
              ..inner,
              name: schema.name,
              rules: inner.rules + schema.rules,
            })
          } else {
            PrefaultType(inner, value)
          }
        Err(message) => return Err(message)
      }
    TupleType(items) =>
      match project_many(items, view, depth) {
        Ok(items) => TupleType(items)
        Err(message) => return Err(message)
      }
    UnionType(items) =>
      match project_many(items, view, depth) {
        Ok(items) => UnionType(items)
        Err(message) => return Err(message)
      }
    IntersectionType(items) => {
      if !output &&
        items.any(fn(child) {
          @core.unwrap_schema(child).schema_type is ObjectType(_, Strict)
        }) {
        return Err(
          "Strict object intersections require a separate input contract",
        )
      }
      match project_many(items, view, depth) {
        Ok(items) => {
          if output {
            return match merge_output_shapes(items) {
              Ok(merged) =>
                Ok({
                  ..merged,
                  name: schema.name,
                  rules: merged.rules + schema.rules,
                })
              Err(message) => Err(message)
            }
          }
          IntersectionType(items)
        }
        Err(message) => return Err(message)
      }
    }
    DiscriminatedUnionType(tag, options) => {
      let projected : Map[String, @core.Schema] = Map([])
      for key, child in options {
        match project_view(child, view, depth + 1) {
          Ok(child) => projected.set(key, child)
          Err(message) => return Err(message)
        }
      }
      DiscriminatedUnionType(tag, projected)
    }
    LazyType(_) =>
      return Err("Use named recursive export for recursive schemas")
    other => other
  }
  Ok({ ..schema, schema_type, })
}

///|
fn project_many(
  items : Array[@core.Schema],
  view : SchemaView,
  depth : Int32,
) -> Result[Array[@core.Schema], String] {
  let projected : Array[@core.Schema] = []
  for child in items {
    match project_view(child, view, depth + 1) {
      Ok(child) => projected.push(child)
      Err(message) => return Err(message)
    }
  }
  Ok(projected)
}