///|
/// Convert a moon_zod @core.Schema into a standard JSON @core.Schema @core.object.
///
/// Uses JsonSchemaRenderer trait with FullJsonRenderer / SkeletonJsonRenderer / NamedJsonRenderer.

// ── Helper functions (shared across renderers) ──

///|
fn merge_annotations(base : Json, rules : Array[@core.Rule]) -> Json {
  guard base is Object(fields) else { return base }
  for rule in rules {
    if rule.annotation is Object(annotation) {
      for key, value in annotation {
        match (fields.get(key), value) {
          (Some(Number(previous, ..)), Number(next, ..)) =>
            if key == "minLength" ||
              key == "minItems" ||
              key == "minimum" ||
              key == "exclusiveMinimum" {
              fields.set(
                key,
                Json::number(if previous > next { previous } else { next }),
              )
            } else if key == "maxLength" ||
              key == "maxItems" ||
              key == "maximum" ||
              key == "exclusiveMaximum" {
              fields.set(
                key,
                Json::number(if previous < next { previous } else { next }),
              )
            } else if previous != next {
              append_annotation_intersection(fields, key, value)
            }
          (Some(previous), _) =>
            if previous != value {
              if key == "type" &&
                value == Json::string("integer") &&
                previous == Json::string("number") {
                fields.set(key, value)
              } else {
                append_annotation_intersection(fields, key, value)
              }
            }
          (None, _) => fields.set(key, value)
        }
      }
    }
  }
  base
}

///|
fn append_annotation_intersection(
  fields : Map[String, Json],
  key : String,
  value : Json,
) -> Unit {
  let additions = match fields.get("allOf") {
    Some(Array(items)) => items
    _ => []
  }
  let annotation : Map[String, Json] = Map([])
  annotation.set(key, value)
  additions.push(Json::object(annotation))
  fields.set("allOf", Json::array(additions))
}

///|
fn json_string_schema() -> Json {
  Json::object({ "type": Json::string("string") })
}

///|
fn json_number_schema() -> Json {
  Json::object({ "type": Json::string("number") })
}

///|
fn object_allows_additional_properties(mode : @core.ObjectMode) -> Bool {
  match mode {
    Passthrough => true
    Strict | Strip => false
  }
}

///|
fn maybe_merge_annotations(
  base : Json,
  rules : Array[@core.Rule],
  include_annotations : Bool,
) -> Json {
  if include_annotations {
    merge_annotations(base, rules)
  } else {
    base
  }
}

///|
fn add_default(schema_json : Json, default_val : Json) -> Json {
  match schema_json {
    Object(map) => {
      map.set("default", default_val)
      schema_json
    }
    _ => schema_json
  }
}

///|
fn[R : JsonSchemaRenderer] render_json_child(
  renderer : R,
  schema : @core.Schema,
  named_names : Array[String],
) -> Json {
  if named_names.is_empty() {
    render_json_type(renderer, schema)
  } else {
    render_json_type_ref(renderer, schema, named_names)
  }
}

///|
fn[R : JsonSchemaRenderer] render_object_schema(
  renderer : R,
  spec : Map[String, @core.Schema],
  mode : @core.ObjectMode,
  schema : @core.Schema,
  named_names : Array[String],
  include_annotations : Bool,
) -> Json {
  let props : Map[String, Json] = Map([])
  let required_arr : Array[String] = []
  for key, val in spec {
    props.set(key, render_json_child(renderer, val, named_names))
    if !@core.is_optional_schema(val) {
      required_arr.push(key)
    }
  }
  maybe_merge_annotations(
    Json::object({
      "type": Json::string("object"),
      "properties": Json::object(props),
      "additionalProperties": Json::boolean(
        object_allows_additional_properties(mode),
      ),
      "required": Json::array(required_arr.map(fn(k) { Json::string(k) })),
    }),
    schema.rules,
    include_annotations,
  )
}

///|
fn[R : JsonSchemaRenderer] render_array_schema(
  renderer : R,
  elem : @core.Schema,
  schema : @core.Schema,
  named_names : Array[String],
  include_annotations : Bool,
) -> Json {
  maybe_merge_annotations(
    Json::object({
      "type": Json::string("array"),
      "items": render_json_child(renderer, elem, named_names),
    }),
    schema.rules,
    include_annotations,
  )
}

///|
fn[R : JsonSchemaRenderer] render_union_schema(
  renderer : R,
  schemas : Array[@core.Schema],
  schema : @core.Schema,
  named_names : Array[String],
  include_annotations : Bool,
) -> Json {
  maybe_merge_annotations(
    Json::object({
      "anyOf": Json::array(
        schemas.map(fn(s) { render_json_child(renderer, s, named_names) }),
      ),
    }),
    schema.rules,
    include_annotations,
  )
}

///|
fn[R : JsonSchemaRenderer] render_intersection_schema(
  renderer : R,
  schemas : Array[@core.Schema],
  schema : @core.Schema,
  named_names : Array[String],
  include_annotations : Bool,
) -> Json {
  if can_merge_object_intersection(schemas) {
    return maybe_merge_annotations(
      render_merged_object_intersection_schema(renderer, schemas, named_names),
      schema.rules,
      include_annotations,
    )
  }
  maybe_merge_annotations(
    Json::object({
      "allOf": Json::array(
        schemas.map(fn(s) { render_json_child(renderer, s, named_names) }),
      ),
    }),
    schema.rules,
    include_annotations,
  )
}

///|
fn[R : JsonSchemaRenderer] render_json_tuple_type(
  renderer : R,
  items : Array[@core.Schema],
) -> Json {
  let mut minimum = items.length()
  while minimum > 0 && @core.is_optional_schema(items[minimum - 1]) {
    minimum = minimum - 1
  }
  Json::object({
    "type": Json::string("array"),
    "prefixItems": Json::array(
      items.map(fn(item) { render_json_type(renderer, item) }),
    ),
    "minItems": Json::number(minimum.to_double()),
    "maxItems": Json::number(items.length().to_double()),
  })
}

///|
fn can_merge_object_intersection(schemas : Array[@core.Schema]) -> Bool {
  for schema in schemas {
    match schema.schema_type {
      ObjectType(_, _) => ()
      IntersectionType(nested) =>
        if !can_merge_object_intersection(nested) {
          return false
        }
      _ => return false
    }
  }
  true
}

///|
fn[R : JsonSchemaRenderer] render_merged_object_intersection_schema(
  renderer : R,
  schemas : Array[@core.Schema],
  named_names : Array[String],
) -> Json {
  let props : Map[String, Json] = Map([])
  let required_arr : Array[String] = []
  collect_object_intersection_parts(
    renderer, schemas, named_names, props, required_arr,
  )
  Json::object({
    "type": Json::string("object"),
    "properties": Json::object(props),
    "additionalProperties": Json::boolean(
      object_intersection_allows_additional_properties(schemas),
    ),
    "required": Json::array(required_arr.map(fn(k) { Json::string(k) })),
  })
}

///|
fn[R : JsonSchemaRenderer] collect_object_intersection_parts(
  renderer : R,
  schemas : Array[@core.Schema],
  named_names : Array[String],
  props : Map[String, Json],
  required_arr : Array[String],
) -> Unit {
  for schema in schemas {
    match schema.schema_type {
      ObjectType(spec, _) =>
        for key, val in spec {
          let rendered = render_json_child(renderer, val, named_names)
          match props.get(key) {
            Some(existing) =>
              props.set(key, combine_property_schema(existing, rendered))
            None => props.set(key, rendered)
          }
          if !@core.is_optional_schema(val) &&
            !@core.value_in_array(key, required_arr) {
            required_arr.push(key)
          }
        }
      IntersectionType(nested) =>
        collect_object_intersection_parts(
          renderer, nested, named_names, props, required_arr,
        )
      _ => ()
    }
  }
}

///|
fn combine_property_schema(left : Json, right : Json) -> Json {
  if left == right {
    left
  } else {
    Json::object({ "allOf": Json::array([left, right]) })
  }
}

///|
fn object_intersection_allows_additional_properties(
  schemas : Array[@core.Schema],
) -> Bool {
  if object_intersection_has_strict(schemas) {
    false
  } else {
    object_intersection_has_passthrough(schemas)
  }
}

///|
fn object_intersection_has_strict(schemas : Array[@core.Schema]) -> Bool {
  for schema in schemas {
    match schema.schema_type {
      ObjectType(_, Strict) => return true
      IntersectionType(nested) =>
        if object_intersection_has_strict(nested) {
          return true
        }
      _ => ()
    }
  }
  false
}

///|
fn object_intersection_has_passthrough(schemas : Array[@core.Schema]) -> Bool {
  for schema in schemas {
    match schema.schema_type {
      ObjectType(_, Passthrough) => return true
      IntersectionType(nested) =>
        if object_intersection_has_passthrough(nested) {
          return true
        }
      _ => ()
    }
  }
  false
}

// ── NamedJsonRenderer: shared JSON Schema renderer ──

///|
pub struct NamedJsonRenderer {
  named_names : Array[String]
  include_annotations : Bool
}

///|
pub fn NamedJsonRenderer::NamedJsonRenderer(
  named_names~ : Array[String],
  include_annotations~ : Bool = true,
) -> NamedJsonRenderer {
  { named_names, include_annotations, }
}

///|
#deprecated
pub extend NamedJsonRenderer with JsonSchemaRenderer::{
  render_string,
  render_number,
  render_boolean,
  render_null,
  render_object,
  render_array,
  render_optional,
  render_default,
  render_enum,
  render_union,
  render_intersection,
  render_transform,
  render_literal,
  render_any,
  render_unknown,
  render_tuple,
  render_preprocess,
  render_lazy,
}

///|
/// The named renderer's methods call render_json_type_ref for sub-elements,
/// which emits $ref for named schemas in the named_names list.
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_string(
  self : NamedJsonRenderer,
  schema : @core.Schema,
) -> Json {
  maybe_merge_annotations(
    json_string_schema(),
    schema.rules,
    self.include_annotations,
  )
}

///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_number(
  self : NamedJsonRenderer,
  schema : @core.Schema,
) -> Json {
  maybe_merge_annotations(
    json_number_schema(),
    schema.rules,
    self.include_annotations,
  )
}

///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_boolean(
  self : NamedJsonRenderer,
  schema : @core.Schema,
) -> Json {
  maybe_merge_annotations(
    Json::object({ "type": Json::string("boolean") }),
    schema.rules,
    self.include_annotations,
  )
}

///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_null(
  self : NamedJsonRenderer,
  schema : @core.Schema,
) -> Json {
  maybe_merge_annotations(
    Json::object({ "type": Json::string("null") }),
    schema.rules,
    self.include_annotations,
  )
}

///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_object(
  self : NamedJsonRenderer,
  spec : Map[String, @core.Schema],
  mode : @core.ObjectMode,
  schema : @core.Schema,
) -> Json {
  render_object_schema(
    self,
    spec,
    mode,
    schema,
    self.named_names,
    self.include_annotations,
  )
}

///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_array(
  self : NamedJsonRenderer,
  elem : @core.Schema,
  schema : @core.Schema,
) -> Json {
  render_array_schema(
    self,
    elem,
    schema,
    self.named_names,
    self.include_annotations,
  )
}

///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_optional(
  self : NamedJsonRenderer,
  inner : @core.Schema,
  schema : @core.Schema,
) -> Json {
  render_json_type_ref(
    self,
    without_inherited_name(schema, inner),
    self.named_names,
  )
}

///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_default(
  self : NamedJsonRenderer,
  inner : @core.Schema,
  default_val : Json,
  schema : @core.Schema,
) -> Json {
  add_default(
    render_json_type_ref(
      self,
      without_inherited_name(schema, inner),
      self.named_names,
    ),
    default_val,
  )
}

///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_transform(
  self : NamedJsonRenderer,
  inner : @core.Schema,
  closure : @core.TransformClosure,
  schema : @core.Schema,
) -> Json {
  let base = render_json_type_ref(
    self,
    without_inherited_name(schema, inner),
    self.named_names,
  )
  if closure.kind is CustomTransform {
    base
  } else {
    maybe_merge_annotations(base, schema.rules, self.include_annotations)
  }
}

///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_enum(
  self : NamedJsonRenderer,
  values : Array[String],
  schema : @core.Schema,
) -> Json {
  maybe_merge_annotations(
    Json::object({
      "type": Json::string("string"),
      "enum": Json::array(values.map(fn(s) { Json::string(s) })),
    }),
    schema.rules,
    self.include_annotations,
  )
}

///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_union(
  self : NamedJsonRenderer,
  schemas : Array[@core.Schema],
  schema : @core.Schema,
) -> Json {
  render_union_schema(
    self,
    schemas,
    schema,
    self.named_names,
    self.include_annotations,
  )
}

///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_intersection(
  self : NamedJsonRenderer,
  schemas : Array[@core.Schema],
  schema : @core.Schema,
) -> Json {
  render_intersection_schema(
    self,
    schemas,
    schema,
    self.named_names,
    self.include_annotations,
  )
}

///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_literal(
  self : NamedJsonRenderer,
  value : Json,
  schema : @core.Schema,
) -> Json {
  maybe_merge_annotations(
    Json::object({ "const": value }),
    schema.rules,
    self.include_annotations,
  )
}

///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_any(
  self : NamedJsonRenderer,
  _schema : @core.Schema,
) -> Json {
  Json::empty_object()
}

///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_unknown(
  self : NamedJsonRenderer,
  _schema : @core.Schema,
) -> Json {
  Json::empty_object()
}

///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_tuple(
  self : NamedJsonRenderer,
  items : Array[@core.Schema],
  _schema : @core.Schema,
) -> Json {
  render_json_tuple_type(self, items)
}

///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_preprocess(
  self : NamedJsonRenderer,
  inner : @core.Schema,
  schema : @core.Schema,
) -> Json {
  render_json_type_ref(
    self,
    without_inherited_name(schema, inner),
    self.named_names,
  )
}

// ── Public API ──

///|
/// Convert a moon_zod @core.Schema into a standard JSON @core.Schema @core.object with full
/// constraint annotations (minLength, maximum, pattern, format, etc.).
pub fn to_json_schema(schema : @core.Schema) -> Json {
  let _ = @core.collect_named_schemas(schema)
  if contains_lazy(schema, Map([])) {
    return to_json_schema_named(schema)
  }
  render_json_type(NamedJsonRenderer(named_names=[]), schema)
}

///|
/// Convert a moon_zod @core.Schema into a lightweight JSON @core.Schema skeleton.
/// Only structural type information is exported.
pub fn to_json_schema_skeleton(schema : @core.Schema) -> Json {
  let _ = @core.collect_named_schemas(schema)
  if contains_lazy(schema, Map([])) {
    return to_json_schema_named_impl(schema, None, false)
  }
  render_json_type(
    NamedJsonRenderer(named_names=[], include_annotations=false),
    schema,
  )
}

///|
/// Export a JSON @core.Schema with named sub-schemas in a `$defs` section.
/// If root schema has no name, it is assigned the name "Root".
pub fn to_json_schema_named(
  schema : @core.Schema,
  include_names~ : Array[String]? = None,
) -> Json {
  to_json_schema_named_impl(schema, include_names, true)
}

///|
fn to_json_schema_named_impl(
  schema : @core.Schema,
  include_names : Array[String]?,
  include_annotations : Bool,
) -> Json {
  let mut schema = schema
  if schema.name.is_empty() {
    schema = schema.name("Root")
  }
  let all_named = @core.collect_named_schemas(schema)
  let selected_schemas = @core.filter_named_schemas(all_named, include_names)
  let named_names : Array[String] = selected_schemas.map(fn(s) { s.name })
  let sorted_schemas = @core.topological_sort_schemas(selected_schemas)

  let defs_map : Map[String, Json] = Map([])
  for ns in sorted_schemas {
    let ref_names = named_names
    let renderer = NamedJsonRenderer(
      named_names=ref_names,
      include_annotations~,
    )
    defs_map.set(ns.name, render_json_type(renderer, ns))
  }

  let renderer = NamedJsonRenderer(named_names~, include_annotations~)
  let root = render_json_type_ref(renderer, schema, named_names)

  let result_map : Map[String, Json] = Map([])
  match root {
    Object(m) =>
      for k, v in m {
        result_map.set(k, v)
      }
    _ => ()
  }
  result_map.set("$defs", Json::object(defs_map))
  Json::object(result_map)
}

///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_lazy(
  self,
  resolved,
  schema,
) {
  render_json_type_ref(
    self,
    without_inherited_name(schema, resolved),
    self.named_names,
  )
}

///|
fn without_inherited_name(
  wrapper : @core.Schema,
  inner : @core.Schema,
) -> @core.Schema {
  if !wrapper.name.is_empty() && wrapper.name == inner.name {
    { ..inner, name: "", }
  } else {
    inner
  }
}

///|
fn contains_lazy(schema : @core.Schema, visited : Map[Int32, Bool]) -> Bool {
  if schema.schema_type is LazyType(_) {
    return true
  }
  if visited.contains(schema.id) {
    return false
  }
  visited.set(schema.id, true)
  @core.schema_children(schema).any(fn(child) { contains_lazy(child, visited) })
}