///|
/// Convert a moon_zod @core.Schema into a TypeScript-interface-style prompt @core.string
/// suitable for LLM system prompts or correction feedback.
///
/// Uses StringRenderer trait with NamedPromptRenderer.

///|
/// Named prompt renderer: uses name references for named schemas.
pub struct NamedPromptRenderer {
  named_schemas : Array[@core.Schema]
}

///|
pub fn NamedPromptRenderer::NamedPromptRenderer(
  named_schemas~ : Array[@core.Schema],
) -> NamedPromptRenderer {
  { named_schemas, }
}

///|
#warnings("-unused_value")
pub impl StringRenderer for NamedPromptRenderer with fn render_string(
  self : NamedPromptRenderer,
  schema : @core.Schema,
  indent : Int,
) -> String {
  if has_name_in(schema, self.named_schemas) {
    schema.name
  } else {
    "string"
  }
}

///|
#warnings("-unused_value")
pub impl StringRenderer for NamedPromptRenderer with fn render_number(
  self : NamedPromptRenderer,
  schema : @core.Schema,
  indent : Int,
) -> String {
  if has_name_in(schema, self.named_schemas) {
    schema.name
  } else {
    "number"
  }
}

///|
#warnings("-unused_value")
pub impl StringRenderer for NamedPromptRenderer with fn render_boolean(
  self : NamedPromptRenderer,
  schema : @core.Schema,
  indent : Int,
) -> String {
  if has_name_in(schema, self.named_schemas) {
    schema.name
  } else {
    "boolean"
  }
}

///|
#warnings("-unused_value")
pub impl StringRenderer for NamedPromptRenderer with fn render_null(
  self : NamedPromptRenderer,
  schema : @core.Schema,
  indent : Int,
) -> String {
  if has_name_in(schema, self.named_schemas) {
    schema.name
  } else {
    "null"
  }
}

///|
pub impl StringRenderer for NamedPromptRenderer with fn render_optional(
  self : NamedPromptRenderer,
  inner : @core.Schema,
  _schema : @core.Schema,
  indent : Int,
) -> String {
  render_type_named(self, inner, indent, self.named_schemas) + " | null"
}

///|
pub impl StringRenderer for NamedPromptRenderer with fn render_default(
  self : NamedPromptRenderer,
  inner : @core.Schema,
  _default_val : Json,
  _schema : @core.Schema,
  indent : Int,
) -> String {
  render_type_named(self, inner, indent, self.named_schemas) + " | null"
}

///|
pub impl StringRenderer for NamedPromptRenderer with fn render_transform(
  self : NamedPromptRenderer,
  inner : @core.Schema,
  _closure : @core.TransformClosure,
  _schema : @core.Schema,
  indent : Int,
) -> String {
  render_type_named(self, inner, indent, self.named_schemas)
}

///|
pub impl StringRenderer for NamedPromptRenderer with fn render_enum(
  self : NamedPromptRenderer,
  values : Array[String],
  schema : @core.Schema,
  _indent : Int,
) -> String {
  if has_name_in(schema, self.named_schemas) {
    return schema.name
  }
  let mut result = ""
  let mut first = true
  for v in values {
    if first {
      result = "\"" + v + "\""
    } else {
      result = result + " | \"" + v + "\""
    }
    first = false
  }
  result
}

///|
pub impl StringRenderer for NamedPromptRenderer with fn render_array(
  self : NamedPromptRenderer,
  elem : @core.Schema,
  _schema : @core.Schema,
  indent : Int,
) -> String {
  render_type_named(self, elem, indent, self.named_schemas) + "[]"
}

///|
pub impl StringRenderer for NamedPromptRenderer with fn render_union(
  self : NamedPromptRenderer,
  schemas : Array[@core.Schema],
  schema : @core.Schema,
  indent : Int,
) -> String {
  if has_name_in(schema, self.named_schemas) {
    return schema.name
  }
  let mut result = ""
  let mut first = true
  for s in schemas {
    if first {
      result = render_type_named(self, s, indent, self.named_schemas)
    } else {
      result = result +
        " | " +
        render_type_named(self, s, indent, self.named_schemas)
    }
    first = false
  }
  result
}

///|
pub impl StringRenderer for NamedPromptRenderer with fn render_intersection(
  self : NamedPromptRenderer,
  schemas : Array[@core.Schema],
  schema : @core.Schema,
  indent : Int,
) -> String {
  if has_name_in(schema, self.named_schemas) {
    return schema.name
  }
  let mut result = ""
  let mut first = true
  for s in schemas {
    if first {
      result = render_type_named(self, s, indent, self.named_schemas)
    } else {
      result = result +
        " & " +
        render_type_named(self, s, indent, self.named_schemas)
    }
    first = false
  }
  result
}

///|
pub impl StringRenderer for NamedPromptRenderer with fn render_object(
  self : NamedPromptRenderer,
  spec : Map[String, @core.Schema],
  _mode : @core.ObjectMode,
  schema : @core.Schema,
  indent : Int,
) -> String {
  if has_name_in(schema, self.named_schemas) {
    return schema.name
  }
  if spec.length() == 0 {
    return "{}"
  }
  let mut result = "{\n"
  let inner_indent = indent + 1
  for key, val_schema in spec {
    let key_is_optional = @core.is_optional_schema(val_schema)
    let inner = if key_is_optional {
      @core.peel_optional(val_schema)
    } else {
      val_schema
    }

    let field_type = render_type_named(
      self,
      inner,
      inner_indent,
      self.named_schemas,
    )
    let comment = schema_comment(val_schema)
    let opt_mark = if key_is_optional { "?" } else { "" }
    let line = @core.indent_str(inner_indent) +
      key +
      opt_mark +
      ": " +
      field_type

    if comment.is_empty() {
      result = result + line + ",\n"
    } else {
      result = result + line + ",  // " + comment + "\n"
    }
  }
  result = result + @core.indent_str(indent) + "}"
  result
}

///|
pub impl StringRenderer for NamedPromptRenderer with fn render_literal(
  self : NamedPromptRenderer,
  value : Json,
  schema : @core.Schema,
  _indent : Int,
) -> String {
  if has_name_in(schema, self.named_schemas) {
    schema.name
  } else {
    json_to_ts_literal(value)
  }
}

///|
#warnings("-unused_value")
pub impl StringRenderer for NamedPromptRenderer with fn render_any(
  self : NamedPromptRenderer,
  _schema : @core.Schema,
  _indent : Int,
) -> String {
  "any"
}

///|
#warnings("-unused_value")
pub impl StringRenderer for NamedPromptRenderer with fn render_unknown(
  self : NamedPromptRenderer,
  _schema : @core.Schema,
  _indent : Int,
) -> String {
  "unknown"
}

///|
#warnings("-unused_value")
pub impl StringRenderer for NamedPromptRenderer with fn render_tuple(
  self : NamedPromptRenderer,
  items : Array[@core.Schema],
  _schema : @core.Schema,
  indent : Int,
) -> String {
  render_tuple_type(self, items, indent)
}

///|
#warnings("-unused_value")
pub impl StringRenderer for NamedPromptRenderer with fn render_preprocess(
  self : NamedPromptRenderer,
  inner : @core.Schema,
  _schema : @core.Schema,
  indent : Int,
) -> String {
  render_type(self, inner, indent)
}

///|
/// Public API: convert a schema to a TypeScript-interface-style prompt @core.string.
pub fn schema_to_prompt(schema : @core.Schema) -> String {
  schema_to_prompt_inline(schema, [])
}

///|
fn schema_to_prompt_inline(
  schema : @core.Schema,
  named_schemas : Array[@core.Schema],
) -> String {
  let renderer = NamedPromptRenderer(named_schemas~)
  let tp = render_type(renderer, schema, 0)
  let comment = schema_comment(schema)
  if comment.is_empty() {
    tp
  } else {
    tp + "  // " + comment
  }
}

///|
/// Public API: convert a schema with named sub-schemas into a prompt with
/// separate interface definitions and name-based references.
///
/// If `include_names` is None, exports all named schemas.
/// If `include_names` is Some(names), exports only the specified schema names.
/// If root schema has no name, it is assigned the name "Root".
pub fn schema_to_prompt_named(
  schema : @core.Schema,
  include_names? : Array[String]? = None,
) -> String {
  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)
  generate_prompt_with_interfaces(selected_schemas)
}

///|
/// Build the constraint comment for a schema.
/// For arrays, also merges element-level constraints.
fn schema_comment(schema : @core.Schema) -> String {
  let unwrapped = @core.unwrap_schema(schema)

  let inner = @core.constraint_comment(schema)

  let merged = match unwrapped.schema_type {
    ArrayType(elem) => {
      let ec = @core.constraint_comment(elem)
      if inner.is_empty() {
        ec
      } else if ec.is_empty() {
        inner
      } else {
        inner + ", " + ec
      }
    }
    _ => inner
  }

  let constraint_part = if merged.is_empty() { "" } else { "[" + merged + "]" }
  let desc = schema.description
  if constraint_part.is_empty() && desc.is_empty() {
    ""
  } else if constraint_part.is_empty() {
    desc
  } else if desc.is_empty() {
    constraint_part
  } else {
    constraint_part + " — " + desc
  }
}

///|
/// Convert a Json value to its TypeScript @core.literal representation.
fn json_to_ts_literal(json : Json) -> String {
  match json {
    String(s) => "\"" + s + "\""
    Number(v, ..) =>
      if v == v.to_int().to_double() {
        v.to_int().to_string()
      } else {
        v.to_string()
      }
    True => "true"
    False => "false"
    Null => "null"
    Array(arr) => {
      let parts = arr.map(fn(v) { json_to_ts_literal(v) })
      "[" + parts.join(", ") + "]"
    }
    Object(map) => {
      let pairs : Array[String] = []
      for k, v in map {
        pairs.push("\"" + k + "\": " + json_to_ts_literal(v))
      }
      "{" + pairs.join(", ") + "}"
    }
  }
}

///|
/// Generate the full prompt with interface definitions and the root schema.
fn generate_prompt_with_interfaces(
  named_schemas : Array[@core.Schema],
) -> String {
  let sorted_schemas = @core.topological_sort_schemas(named_schemas)

  let mut prompt = ""

  for schema in sorted_schemas {
    let interface_def = schema_to_interface_definition_with_names(
      schema, sorted_schemas,
    )
    if !interface_def.is_empty() {
      if !prompt.is_empty() {
        prompt = prompt + "\n\n"
      }
      prompt = prompt + interface_def
    }
  }

  prompt
}

///|
/// Generate a TypeScript interface/type definition with proper name references.
fn schema_to_interface_definition_with_names(
  schema : @core.Schema,
  named_schemas : Array[@core.Schema],
) -> String {
  if schema.name.is_empty() {
    return ""
  }

  let ref_schemas = named_schemas.filter(fn(s) { s.name != schema.name })
  let renderer = NamedPromptRenderer(named_schemas=ref_schemas)

  match schema.schema_type {
    ObjectType(spec, _) =>
      "export interface " +
      schema.name +
      " " +
      render_object_prompt(renderer, spec, ref_schemas, 0)
    EnumType(values) =>
      "type " + schema.name + " = " + enum_values_to_string(values)
    UnionType(schemas) => {
      let union_type = union_to_prompt_inline_str(
        renderer, schemas, ref_schemas, 0,
      )
      "export type " + schema.name + " = " + union_type
    }
    IntersectionType(schemas) => {
      let intersection_type = intersection_to_prompt_inline_str(
        renderer, schemas, ref_schemas, 0,
      )
      "export type " + schema.name + " = " + intersection_type
    }
    LiteralType(value) =>
      "export type " + schema.name + " = " + json_to_ts_literal(value)
    _ => schema_to_type_alias_with_names(schema, renderer)
  }
}

///|
fn schema_to_type_alias_with_names(
  schema : @core.Schema,
  renderer : NamedPromptRenderer,
) -> String {
  let rendered = render_type(renderer, schema, 0)
  let comment = schema_comment(schema)
  let base = "export type " + schema.name + " = " + rendered
  if comment.is_empty() {
    base
  } else {
    base + "  // " + comment
  }
}

///|
/// Helper: render an @core.object spec with named references.
fn render_object_prompt(
  renderer : NamedPromptRenderer,
  spec : Map[String, @core.Schema],
  named_schemas : Array[@core.Schema],
  indent : Int,
) -> String {
  if spec.length() == 0 {
    return "{}"
  }
  let mut result = "{\n"
  let inner_indent = indent + 1
  for key, val_schema in spec {
    let key_is_optional = @core.is_optional_schema(val_schema)
    let field_type = field_type_with_names(
      renderer, val_schema, inner_indent, named_schemas,
    )
    let comment = schema_comment(val_schema)
    let opt_mark = if key_is_optional { "?" } else { "" }
    let line = @core.indent_str(inner_indent) +
      key +
      opt_mark +
      ": " +
      field_type

    if comment.is_empty() {
      result = result + line + ",\n"
    } else {
      result = result + line + ",  // " + comment + "\n"
    }
  }
  result = result + @core.indent_str(indent) + "}"
  result
}

///|
fn field_type_with_names(
  renderer : NamedPromptRenderer,
  schema : @core.Schema,
  indent : Int,
  named_schemas : Array[@core.Schema],
) -> String {
  if has_name_in(schema, named_schemas) {
    schema.name
  } else if @core.is_optional_schema(schema) {
    render_type_named(
      renderer,
      @core.peel_optional(schema),
      indent,
      named_schemas,
    )
  } else {
    render_type_named(renderer, schema, indent, named_schemas)
  }
}

///|
/// Helper: render @core.union with named references.
fn union_to_prompt_inline_str(
  renderer : NamedPromptRenderer,
  schemas : Array[@core.Schema],
  named_schemas : Array[@core.Schema],
  indent : Int,
) -> String {
  let mut result = ""
  let mut first = true
  for s in schemas {
    if first {
      result = render_type_named(renderer, s, indent, named_schemas)
    } else {
      result = result +
        " | " +
        render_type_named(renderer, s, indent, named_schemas)
    }
    first = false
  }
  result
}

///|
/// Helper: render @core.intersection with named references.
fn intersection_to_prompt_inline_str(
  renderer : NamedPromptRenderer,
  schemas : Array[@core.Schema],
  named_schemas : Array[@core.Schema],
  indent : Int,
) -> String {
  let mut result = ""
  let mut first = true
  for s in schemas {
    if first {
      result = render_type_named(renderer, s, indent, named_schemas)
    } else {
      result = result +
        " & " +
        render_type_named(renderer, s, indent, named_schemas)
    }
    first = false
  }
  result
}

///|
/// Helper: render enum values as a @core.string.
fn enum_values_to_string(values : Array[String]) -> String {
  let mut result = ""
  let mut first = true
  for v in values {
    if first {
      result = "\"" + v + "\""
    } else {
      result = result + " | \"" + v + "\""
    }
    first = false
  }
  result
}

///|
fn[R : StringRenderer] render_tuple_type(
  renderer : R,
  items : Array[@core.Schema],
  indent : Int,
) -> String {
  let parts : Array[String] = []
  for item in items {
    parts.push(render_type(renderer, item, indent))
  }
  "[" + parts.join(", ") + "]"
}

///|
/// Helper: check if a schema has a name present in the named schemas list.
fn has_name_in(
  schema : @core.Schema,
  named_schemas : Array[@core.Schema],
) -> Bool {
  if schema.name.is_empty() {
    return false
  }
  for ns in named_schemas {
    if ns.name == schema.name {
      return true
    }
  }
  false
}