///|
/// Type-driven Protobuf textproto-fragment renderer. Distinct from the
/// generic textproto serializer in `eval_render_textproto.mbt`: this
/// path is class-aware and resolves Pkl union annotations to the
/// `it_` discriminant fields Apple Pkl's `protobuf.Renderer`
/// emits. Functions here previously lived in `eval_expr.mbt` and were
/// extracted to keep that file focused on the core eval pipeline.

///|
fn protobuf_renderer_value_error(
  value : Value,
  cache : Array[ValueBinding],
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
) -> String? {
  match value {
    MappingValue(_) | DefaultedMappingValue(_, _, _) | MapValue(_) =>
      Some(
        "The top-level value of a protobuf file must have type `Typed`, but got type `\{renderer_error_type_name(value, cache)}`.",
      )
    _ => protobuf_nested_value_error(value, cache, class_env, declarations)
  }
}

///|
fn protobuf_nested_value_error(
  value : Value,
  cache : Array[ValueBinding],
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
) -> String? {
  match value {
    DataSizeValue(_, _) =>
      Some(renderer_cannot_render_message("Protobuf", value, cache, false))
    ObjectValue(members) => {
      match find_object_class_tag(members) {
        Some("Dynamic") =>
          return Some(
            renderer_cannot_render_message("Protobuf", value, cache, false),
          )
        Some(class_name) =>
          match
            protobuf_typed_object_members_error(
              members, class_name, cache, class_env, declarations,
            ) {
            Some(message) => return Some(message)
            None => ()
          }
        None => ()
      }
      for field in visible_members(members) {
        match
          protobuf_nested_value_error(
            field.value,
            cache,
            class_env,
            declarations,
          ) {
          Some(message) => return Some(message)
          None => ()
        }
      }
      None
    }
    ListingValue(elements)
    | DefaultedListingValue(_, elements, _)
    | ListValue(elements)
    | SetValue(elements) => {
      for element in elements {
        match
          protobuf_nested_value_error(element, cache, class_env, declarations) {
          Some(message) => return Some(message)
          None => ()
        }
      }
      None
    }
    MappingValue(entries)
    | DefaultedMappingValue(_, entries, _)
    | MapValue(entries) =>
      protobuf_mapping_entries_error(entries, cache, class_env, declarations)
    PairValue(first, second) =>
      match protobuf_nested_value_error(first, cache, class_env, declarations) {
        Some(message) => Some(message)
        None =>
          protobuf_nested_value_error(second, cache, class_env, declarations)
      }
    _ => None
  }
}

///|
fn protobuf_mapping_entries_error(
  entries : Array[ValueEntry],
  cache : Array[ValueBinding],
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
) -> String? {
  for entry in entries {
    if !protobuf_map_key_is_supported(entry.key) {
      return Some(
        "Cannot render map with a non-scalar, floating point or byte array key type.",
      )
    }
    match
      protobuf_nested_value_error(entry.value, cache, class_env, declarations) {
      Some(message) => return Some(message)
      None => ()
    }
  }
  None
}

///|
fn protobuf_map_key_is_supported(value : Value) -> Bool {
  // PKL-153f: a RenderDirective key flattens to its `text` at
  // render time, so the protobuf "scalar key only" rule needs to
  // see through the directive wrapper rather than reject it.
  if render_directive_text(value) is Some(_) {
    return true
  }
  match value {
    StringValue(_) | IntValue(_) | BoolValue(_) => true
    _ => false
  }
}

///|
fn render_value_as_protobuf_fragment_with_context(
  value : Value,
  indent_text : String,
  class_env : Array[ClassBinding],
  cache : Array[ValueBinding],
  declarations : Array[Declaration],
) -> String {
  // PKL-153f: directive short-circuit — `renderValue(new
  // RenderDirective {...})` returns the text raw before the
  // protobuf-object pipeline kicks in.
  match render_directive_text(value) {
    Some(text) => return text
    None => ()
  }
  match value {
    DurationValue(_, _) => render_value_as_protobuf_fragment(value, indent_text)
    ObjectValue(members) => {
      let buf = StringBuilder::new()
      render_protobuf_object_members(
        members, 0, indent_text, class_env, cache, declarations, buf,
      )
      textproto_trim_trailing_newline(buf.to_string())
    }
    _ => render_value_as_textproto_fragment(value, indent_text)
  }
}

///|
fn render_protobuf_object_members(
  members : Array[ValueMember],
  level : Int,
  indent_text : String,
  class_env : Array[ClassBinding],
  cache : Array[ValueBinding],
  declarations : Array[Declaration],
  buf : StringBuilder,
) -> Unit {
  let class_name = find_object_class_tag(members)
  for field in visible_members(members) {
    let annotation = match class_name {
      Some(name) =>
        protobuf_class_property_type(name, field.name, class_env, declarations)
      None => None
    }
    render_protobuf_field(
      protobuf_member_field_name(field),
      field.value,
      annotation,
      level,
      indent_text,
      class_env,
      cache,
      declarations,
      buf,
    )
  }
}

///|
fn protobuf_class_property_type(
  class_name : String,
  property_name : String,
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
) -> String? {
  let properties : Array[ClassProperty] = []
  collect_class_properties_in_order(
    properties, class_name, class_env, declarations,
  )
  for property in properties {
    if property.name == property_name {
      return property.type_name
    }
  }
  None
}

///|
fn render_protobuf_field(
  name : String,
  value : Value,
  type_name : String?,
  level : Int,
  indent_text : String,
  class_env : Array[ClassBinding],
  cache : Array[ValueBinding],
  declarations : Array[Declaration],
  buf : StringBuilder,
) -> Unit {
  if value is NullValue {
    return
  }
  match type_name {
    Some(annotation) =>
      match protobuf_union_choices_for_annotation(annotation, declarations) {
        Some(choices) => {
          render_protobuf_union_field(
            name, value, choices, level, indent_text, class_env, cache, declarations,
            buf,
          )
          return
        }
        None => ()
      }
    None => ()
  }
  match value {
    ListingValue(elements)
    | DefaultedListingValue(_, elements, _)
    | ListValue(elements)
    | SetValue(elements) => {
      let element_type = match type_name {
        Some(annotation) =>
          collection_element_type_from_annotation(annotation, declarations)
        None => None
      }
      for element in elements {
        render_protobuf_collection_element_field(
          name, element, element_type, level, indent_text, class_env, cache, declarations,
          buf,
        )
      }
    }
    MappingValue(entries)
    | DefaultedMappingValue(_, entries, _)
    | MapValue(entries) =>
      render_textproto_entries_as_fields(name, entries, level, indent_text, buf)
    _ =>
      render_protobuf_single_field(
        name, value, level, indent_text, class_env, cache, declarations, buf,
      )
  }
}

///|
fn render_protobuf_collection_element_field(
  name : String,
  value : Value,
  element_type : String?,
  level : Int,
  indent_text : String,
  class_env : Array[ClassBinding],
  cache : Array[ValueBinding],
  declarations : Array[Declaration],
  buf : StringBuilder,
) -> Unit {
  match element_type {
    Some(annotation) =>
      match protobuf_union_choices_for_annotation(annotation, declarations) {
        Some(choices) => {
          render_protobuf_union_field(
            name, value, choices, level, indent_text, class_env, cache, declarations,
            buf,
          )
          return
        }
        None => ()
      }
    None => ()
  }
  render_protobuf_single_field(
    name, value, level, indent_text, class_env, cache, declarations, buf,
  )
}

///|
fn render_protobuf_single_field(
  name : String,
  value : Value,
  level : Int,
  indent_text : String,
  class_env : Array[ClassBinding],
  cache : Array[ValueBinding],
  declarations : Array[Declaration],
  buf : StringBuilder,
) -> Unit {
  match render_directive_text(value) {
    Some(text) => {
      write_textproto_indent(buf, level, indent_text)
      buf.write_string(name)
      buf.write_string(": ")
      buf.write_string(text)
      buf.write_char('\n')
      return
    }
    None => ()
  }
  match value {
    ObjectValue(members) => {
      let visible = visible_members(members)
      write_textproto_indent(buf, level, indent_text)
      buf.write_string(name)
      if visible.length() == 0 {
        buf.write_string(": {}\n")
      } else {
        buf.write_string(": {\n")
        render_protobuf_object_members(
          members,
          level + 1,
          indent_text,
          class_env,
          cache,
          declarations,
          buf,
        )
        write_textproto_indent(buf, level, indent_text)
        buf.write_string("}\n")
      }
    }
    DurationValue(_, _) => {
      write_textproto_indent(buf, level, indent_text)
      buf.write_string(name)
      buf.write_string(": {\n")
      render_textproto_duration_body(value, level + 1, indent_text, buf)
      write_textproto_indent(buf, level, indent_text)
      buf.write_string("}\n")
    }
    _ => {
      write_textproto_indent(buf, level, indent_text)
      buf.write_string(name)
      buf.write_string(": ")
      render_textproto_scalar(value, buf)
      buf.write_char('\n')
    }
  }
}

///|
fn render_protobuf_union_field(
  name : String,
  value : Value,
  choices : Array[String],
  level : Int,
  indent_text : String,
  class_env : Array[ClassBinding],
  cache : Array[ValueBinding],
  declarations : Array[Declaration],
  buf : StringBuilder,
) -> Unit {
  write_textproto_indent(buf, level, indent_text)
  buf.write_string(name)
  buf.write_string(": {\n")
  let label = protobuf_union_label_for_value(
    choices, value, class_env, declarations,
  )
  render_protobuf_single_field(
    "it_" + label,
    value,
    level + 1,
    indent_text,
    class_env,
    cache,
    declarations,
    buf,
  )
  write_textproto_indent(buf, level, indent_text)
  buf.write_string("}\n")
}

///|
fn protobuf_union_label_for_value(
  choices : Array[String],
  value : Value,
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
) -> String {
  match value {
    StringValue(_) => "String"
    IntValue(_) => "Int"
    FloatValue(_) => "Float"
    BoolValue(_) => "Boolean"
    ObjectValue(members) =>
      match find_object_class_tag(members) {
        Some(runtime_name) =>
          protobuf_union_label_for_object(
            runtime_name, choices, value, class_env, declarations,
          )
        None => "Dynamic"
      }
    _ => eval_value_type_name(value)
  }
}

///|
fn protobuf_union_label_for_object(
  runtime_name : String,
  choices : Array[String],
  value : Value,
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
) -> String {
  for choice in choices {
    let head = protobuf_type_annotation_head(choice)
    if head == runtime_name {
      return head
    }
  }
  for choice in choices {
    let head = protobuf_type_annotation_head(choice)
    if lookup_class_binding(class_env, head) is Some(_) &&
      value_satisfies_user_class_annotation(head, value, declarations) {
      return head
    }
  }
  runtime_name
}

///|
fn protobuf_union_choices_for_annotation(
  type_name : String,
  declarations : Array[Declaration],
) -> Array[String]? {
  let normalized = protobuf_normalize_type_annotation(type_name, declarations)
  let choices = split_top_level_union_choices(normalized)
  if choices.length() <= 1 {
    return None
  }
  for choice in choices {
    if !protobuf_union_choice_is_stringy(choice, declarations) {
      return Some(choices)
    }
  }
  None
}

///|
fn protobuf_union_choice_is_stringy(
  choice : String,
  declarations : Array[Declaration],
) -> Bool {
  let normalized = protobuf_normalize_type_annotation(choice, declarations)
  let choices = split_top_level_union_choices(normalized)
  if choices.length() > 1 {
    for nested in choices {
      if !protobuf_union_choice_is_stringy(nested, declarations) {
        return false
      }
    }
    return true
  }
  if normalized.length() >= 2 &&
    normalized[0] == '"' &&
    normalized[normalized.length() - 1] == '"' {
    return true
  }
  protobuf_type_annotation_head(normalized) == "String"
}

///|
fn protobuf_normalize_type_annotation(
  type_name : String,
  declarations : Array[Declaration],
) -> String {
  let aliases = eval_type_alias_bindings(declarations)
  let mut normalized = strip_balanced_outer_type_parens(
    eval_resolved_type_alias(
      pkl_strip_default_type_marker(pkl_constraint_trim(type_name)),
      aliases,
    ),
  )
  if normalized.has_prefix("*") {
    normalized = trim_spaces(
      String::unsafe_substring(normalized, start=1, end=normalized.length()),
    )
  }
  if normalized.has_suffix("?") {
    normalized = trim_spaces(
      String::unsafe_substring(normalized, start=0, end=normalized.length() - 1),
    )
    normalized = strip_balanced_outer_type_parens(normalized)
  }
  normalized
}

///|
fn protobuf_typed_object_members_error(
  members : Array[ValueMember],
  class_name : String,
  cache : Array[ValueBinding],
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
) -> String? {
  let properties : Array[ClassProperty] = []
  collect_class_properties_in_order(
    properties, class_name, class_env, declarations,
  )
  for property in properties {
    match lookup_visible_member(members, property.name) {
      Some(value) =>
        match
          protobuf_property_annotation_value_error(
            property.type_name,
            value,
            cache,
            class_env,
            declarations,
          ) {
          Some(message) => return Some(message)
          None => ()
        }
      None =>
        match
          protobuf_type_annotation_default_error(property.type_name, cache) {
          Some(message) => return Some(message)
          None => ()
        }
    }
  }
  None
}

///|
fn protobuf_property_annotation_value_error(
  type_name : String?,
  value : Value,
  cache : Array[ValueBinding],
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
) -> String? {
  let annotation = match type_name {
    Some(t) => t
    None => return None
  }
  match
    protobuf_property_subtype_error(
      annotation, value, cache, class_env, declarations,
    ) {
    Some(message) => return Some(message)
    None => ()
  }
  match mapping_entry_types_from_annotation(annotation, declarations) {
    Some((key_type, _)) =>
      if protobuf_mapping_value_has_entries(value) &&
        !protobuf_mapping_key_type_is_supported(key_type, declarations) {
        Some(
          "Cannot render map with a non-scalar, floating point or byte array key type.",
        )
      } else {
        None
      }
    None => None
  }
}

///|
fn protobuf_property_subtype_error(
  type_name : String,
  value : Value,
  cache : Array[ValueBinding],
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
) -> String? {
  if value is NullValue ||
    protobuf_union_choices_for_annotation(type_name, declarations) is Some(_) {
    return None
  }
  let specified = protobuf_type_annotation_head(type_name)
  if lookup_class_binding(class_env, specified) is None {
    return None
  }
  match value {
    ObjectValue(members) =>
      match find_object_class_tag(members) {
        Some(actual) =>
          if actual != specified &&
            lookup_class_binding(class_env, actual) is Some(_) &&
            value_satisfies_user_class_annotation(
              specified, value, declarations,
            ) {
            Some(
              "Cannot render subtype \{protobuf_qualified_user_type_name(specified, cache, class_env)} of specified type \{protobuf_qualified_user_type_name(actual, cache, class_env)} in protobuf.",
            )
          } else {
            None
          }
        None => None
      }
    _ => None
  }
}

///|
fn protobuf_qualified_user_type_name(
  name : String,
  cache : Array[ValueBinding],
  class_env : Array[ClassBinding],
) -> String {
  if is_stdlib_class_name(name) || lookup_class_binding(class_env, name) is None {
    name
  } else {
    match module_name_from_cache(cache) {
      Some(module_name) => "\{module_name}#\{name}"
      None => name
    }
  }
}

///|
fn protobuf_mapping_value_has_entries(value : Value) -> Bool {
  match value {
    MappingValue(entries)
    | DefaultedMappingValue(_, entries, _)
    | MapValue(entries) => entries.length() > 0
    _ => false
  }
}

///|
fn protobuf_mapping_key_type_is_supported(
  type_name : String,
  declarations : Array[Declaration],
) -> Bool {
  let aliases = eval_type_alias_bindings(declarations)
  let resolved = eval_resolved_type_alias(type_name, aliases)
  let choices = split_top_level_union_choices(resolved)
  if choices.length() > 1 {
    for choice in choices {
      if !protobuf_mapping_key_type_is_supported(choice, declarations) {
        return false
      }
    }
    return true
  }
  let normalized = pkl_strip_default_type_marker(pkl_constraint_trim(resolved))
  if normalized.length() >= 2 &&
    normalized[0] == '"' &&
    normalized[normalized.length() - 1] == '"' {
    return true
  }
  let base = match pkl_constrained_type_base_name(normalized) {
    Some(b) => b
    None => normalized
  }
  let head = match base.find("<") {
    Some(idx) => String::unsafe_substring(base, start=0, end=idx)
    None => base
  }
  match trim_spaces(head) {
    "String"
    | "Int"
    | "Int8"
    | "Int16"
    | "Int32"
    | "UInt"
    | "UInt8"
    | "UInt16"
    | "UInt32"
    | "Boolean" => true
    _ => false
  }
}

///|
fn protobuf_type_annotation_default_error(
  type_name : String?,
  cache : Array[ValueBinding],
) -> String? {
  match type_name {
    Some(raw) => {
      if trim_spaces(raw).has_suffix("?") {
        return None
      }
      let base = protobuf_type_annotation_head(raw)
      if base == "Dynamic" {
        Some(
          renderer_cannot_render_message(
            "Protobuf",
            ObjectValue(tag_object_with_class([], "Dynamic")),
            cache,
            false,
          ),
        )
      } else {
        None
      }
    }
    None => None
  }
}

///|
fn protobuf_type_annotation_head(type_name : String) -> String {
  let trimmed = pkl_constraint_trim(type_name)
  let without_constraint = match pkl_constrained_type_base_name(trimmed) {
    Some(base) => base
    None => trimmed
  }
  let without_nullable = if without_constraint.has_suffix("?") {
    String::unsafe_substring(
      without_constraint,
      start=0,
      end=without_constraint.length() - 1,
    )
  } else {
    without_constraint
  }
  let head = match without_nullable.find("<") {
    Some(idx) => String::unsafe_substring(without_nullable, start=0, end=idx)
    None => without_nullable
  }
  trim_spaces(head)
}