///|
/// Generated MoonBit declarations, their audit report, and function counts.
pub struct GenerateResult {
  bindings : String
  report : String
  shim : String?
  generated : Int
  skipped : Int
}

///|
/// Clang identity recorded in `moonbindgen-report-v2` output.
pub struct ToolMetadata {
  clang_version : String
  clang_target : String
}

///|
/// Creates metadata from the first Clang version line and target triple.
pub fn ToolMetadata::new(
  clang_version : String,
  clang_target : String,
) -> ToolMetadata {
  { clang_version, clang_target, }
}

///|
/// Creates placeholder metadata for callers that already have AST JSON but do
/// not know which Clang executable produced it.
pub fn ToolMetadata::unknown() -> ToolMetadata {
  { clang_version: "unknown", clang_target: "unknown", }
}

///|
fn reason_code(status : String, reason : String) -> String {
  if status == "generated" && reason.contains("manual resource management") {
    "manual_resource_management"
  } else if status == "generated" {
    "generated"
  } else if reason == "filtered by configuration" {
    "filtered"
  } else if reason == "duplicate declaration" {
    "duplicate_declaration"
  } else if reason.contains("name collision") {
    "name_collision"
  } else if reason == "variadic function" {
    "variadic_function"
  } else if reason.contains("unsupported return type") {
    "unsupported_return_type"
  } else if reason.contains("unsupported parameter type") {
    "unsupported_parameter_type"
  } else if reason.contains("enum value outside") {
    "enum_value_out_of_range"
  } else {
    "unsupported_declaration"
  }
}

///|
fn source_for_report(
  source : SourceLocation,
  header : String,
) -> SourceLocation {
  if same_header(source.file, header) {
    let parts = normalized_path(source.file).split("/").to_array()
    { file: parts[parts.length() - 1].to_owned(), line: source.line, }
  } else {
    { file: normalized_path(source.file), line: source.line, }
  }
}

///|
fn report_item(
  kind : String,
  name : String,
  moon_name : String,
  source : SourceLocation,
  c_signature : String,
  moonbit_signature : String,
  status : String,
  reason : String,
  bridge? : Bool = false,
  policy? : String = "",
) -> Json {
  let fields : Map[String, Json] = {
    "kind": Json::string(kind),
    "c_name": Json::string(name),
    "moonbit_name": Json::string(moon_name),
    "source": Json::object({
      "file": Json::string(source.file),
      "line": Json::number(source.line.to_double()),
    }),
    "c_signature": Json::string(c_signature),
    "moonbit_signature": Json::string(moonbit_signature),
    "status": Json::string(status),
    "reason_code": Json::string(reason_code(status, reason)),
    "reason": Json::string(reason),
  }
  if bridge {
    fields["bridge"] = Json(true)
  }
  if policy != "" {
    fields["policy"] = Json::string(policy)
  }
  Json::object(fields)
}

///|
fn enum_number(node : Json) -> Int? {
  for child in array_field(node, "inner") {
    match text_field(child, "value") {
      Some(value) => {
        let number = @string.parse_int(value) catch { _ => return None }
        return Some(number)
      }
      None => ()
    }
  }
  None
}

///|
/// Generates bindings with the default configuration and unknown Clang
/// metadata.
///
/// Raises when `ast_text` is not a recognized Clang translation-unit AST or
/// when a contradictory declaration structure cannot be interpreted safely.
pub fn generate(ast_text : String, header : String) -> GenerateResult raise {
  generate_with_metadata(
    ast_text,
    header,
    GenerateConfig::default(),
    ToolMetadata::unknown(),
  )
}

///|
/// Generates bindings with explicit configuration and unknown Clang metadata.
///
/// `header` identifies the main header whose functions are eligible for
/// emission; included headers may still contribute typedef and enum facts.
pub fn generate_with_config(
  ast_text : String,
  header : String,
  config : GenerateConfig,
) -> GenerateResult raise {
  generate_with_metadata(ast_text, header, config, ToolMetadata::unknown())
}

///|
/// Generates deterministic bindings and a `moonbindgen-report-v2` audit report
/// with explicit configuration and Clang metadata.
///
/// Raises for malformed AST JSON, unsupported declarations under strict policy,
/// name collisions that cannot be represented safely, or contradictory Clang
/// declaration structures.
pub fn generate_with_metadata(
  ast_text : String,
  header : String,
  config : GenerateConfig,
  metadata : ToolMetadata,
) -> GenerateResult raise {
  let ast = @json.parse(ast_text)
  if text_field(ast, "kind") != Some("TranslationUnitDecl") {
    fail("Clang output is not a translation unit")
  }
  let aliases : Map[String, String] = Map([])
  let opaque_types : Map[String, Bool] = Map([])
  let enums : Map[String, Bool] = Map([])
  let value_names : Map[String, String] = Map([])
  let enum_lines : Array[String] = []
  let enum_reports : Array[Json] = []
  let mut enum_generated = 0
  let mut enum_skipped = 0
  let mut unsupported = 0
  let nodes = located_nodes(ast)
  validate_bridge_config(nodes, header, config)
  validate_v2_buffers(nodes, header, config)
  validate_v2_multi_outputs(nodes, header, config)
  validate_v2_resources(nodes, header, config)
  for entry in nodes {
    let node = entry.node
    let on_header = same_header(entry.source.file, header)
    match text_field(node, "kind") {
      Some("TypedefDecl") =>
        match (text_field(node, "name"), type_spelling(node)) {
          (Some(name), Some(ctype)) =>
            if ctype.has_prefix("struct ") {
              let tag = ctype[7:].to_owned()
              if name == tag {
                opaque_types[name] = true
              } else {
                aliases[name] = ctype
              }
            } else {
              aliases[name] = ctype
            }
          _ => ()
        }
      Some("EnumDecl") => {
        let mut current = -1
        let mut valid = true
        let lines : Array[String] = []
        for constant in array_field(node, "inner") {
          if text_field(constant, "kind") == Some("EnumConstantDecl") {
            let value = if array_field(constant, "inner").length() > 0 {
              enum_number(constant)
            } else if current < 2147483647 {
              Some(current + 1)
            } else {
              None
            }
            match (text_field(constant, "name"), value) {
              (Some(name), Some(number)) => {
                current = number
                let output_name = "c_" + moonbit_value_name(name)
                if on_header {
                  match value_names.get(output_name) {
                    Some(previous) if previous != name => valid = false
                    _ => value_names[output_name] = name
                  }
                }
                lines.push(
                  "///|\npub let " +
                  output_name +
                  " : Int = " +
                  number.to_string() +
                  "\n",
                )
              }
              _ => valid = false
            }
          }
        }
        let enum_name = match text_field(node, "name") {
          Some(name) => name
          None => ""
        }
        if valid {
          if enum_name != "" {
            enums[enum_name] = true
          }
          if on_header {
            for line in lines {
              enum_lines.push(line)
            }
            enum_generated += 1
            enum_reports.push(
              report_item(
                "enum",
                enum_name,
                moonbit_value_name(enum_name),
                source_for_report(entry.source, header),
                "enum " + enum_name,
                lines.join("\n"),
                "generated",
                "",
              ),
            )
          }
        } else if on_header {
          enum_skipped += 1
          unsupported += 1
          enum_reports.push(
            report_item(
              "enum",
              enum_name,
              moonbit_value_name(enum_name),
              source_for_report(entry.source, header),
              "enum " + enum_name,
              "",
              "skipped",
              "enum value outside supported Int range",
            ),
          )
        }
      }
      _ => ()
    }
  }
  let function_lines : Array[String] = []
  let shim_lines : Array[String] = []
  let items : Array[Json] = []
  let seen : Map[String, Bool] = Map([])
  let mut generated = 0
  let mut skipped = 0
  for entry in nodes {
    let node = entry.node
    if !same_header(entry.source.file, header) ||
      text_field(node, "kind") != Some("FunctionDecl") {
      continue
    }
    if text_field(node, "name") is None || type_spelling(node) is None {
      fail("Unrecognized Clang FunctionDecl structure")
    }
    for child in array_field(node, "inner") {
      if text_field(child, "kind") == Some("ParmVarDecl") &&
        type_spelling(child) is None {
        fail("Unrecognized Clang ParmVarDecl structure")
      }
    }
    let name = match text_field(node, "name") {
      Some(value) => value
      None => ""
    }
    if !config.allows(name) {
      skipped += 1
      items.push(
        report_item(
          "function",
          name,
          moonbit_value_name(config.renamed(name)),
          source_for_report(entry.source, header),
          type_spelling(node).unwrap_or(""),
          "",
          "skipped",
          "filtered by configuration",
        ),
      )
      continue
    }
    if seen.contains(name) {
      skipped += 1
      items.push(
        report_item(
          "function",
          name,
          moonbit_value_name(config.renamed(name)),
          source_for_report(entry.source, header),
          type_spelling(node).unwrap_or(""),
          "",
          "skipped",
          "duplicate declaration",
        ),
      )
      continue
    }
    seen[name] = true
    let function = match c_function(node, entry.source) {
      Some(value) => value
      None => fail("Unrecognized Clang FunctionDecl structure")
    }
    let function = function.with_moon_name(config.renamed(name))
    match value_names.get(function.moon_name) {
      Some(previous) if previous != function.name => {
        skipped += 1
        unsupported += 1
        items.push(
          report_item(
            "function",
            function.name,
            function.moon_name,
            source_for_report(function.source, header),
            function.c_signature,
            "",
            "skipped",
            "MoonBit name collision with: " + previous,
          ),
        )
        continue
      }
      _ => value_names[function.moon_name] = function.name
    }
    let manual_resource = v2_resource_needs_manual_release(
      nodes,
      config,
      function.name,
    )
    let bridged = config.has_bridge(function.name) && !manual_resource
    let generation_policy = if manual_resource {
      "manual_resource"
    } else if config.resource_create_for(function.name) is Some(_) {
      "managed_resource"
    } else if config.resource_view_for(function.name) is Some(_) {
      "borrowed_resource_view"
    } else if config.multi_output_for(function.name) is Some(_) {
      "multi_output"
    } else if config.buffer_for(function.name) is Some(buffer) {
      if buffer.direction == "in" {
        "input_buffer"
      } else {
        "output_buffer"
      }
    } else if config.return_buffer_for(function.name) is Some(_) {
      "borrowed_return_buffer"
    } else if needs_v2_abi_bridge(function, config) {
      "scalar_abi_bridge"
    } else if bridged {
      "configured_bridge"
    } else {
      "direct"
    }
    let (rendered, shim_code, reason) = if manual_resource {
      let (code, reason) = render_function(
        function, aliases, opaque_types, enums, config,
      )
      (code, None, reason)
    } else if config.resource_create_for(function.name) is Some(_) {
      render_v2_resource_create(function, aliases, opaque_types, enums, config)
    } else if config.resource_view_for(function.name) is Some(_) {
      render_v2_resource_view(function, config)
    } else if config.multi_output_for(function.name) is Some(_) {
      render_v2_multi_output(function, aliases, opaque_types, enums, config)
    } else if config.buffer_for(function.name) is Some(_) ||
      config.return_buffer_for(function.name) is Some(_) {
      render_v2_buffer(function, aliases, opaque_types, enums, config)
    } else if bridged {
      render_bridge(function, aliases, opaque_types, enums, config)
    } else if needs_v2_abi_bridge(function, config) {
      render_v2_abi_bridge(function, aliases, opaque_types, enums, config)
    } else {
      let (code, reason) = render_function(
        function, aliases, opaque_types, enums, config,
      )
      (code, None, reason)
    }
    if bridged && rendered is None {
      fail("invalid bridge for " + function.name + ": " + reason)
    }
    match rendered {
      Some(code) => {
        function_lines.push(code)
        match shim_code {
          Some(value) => shim_lines.push(value)
          None => ()
        }
        generated += 1
        items.push(
          report_item(
            "function",
            function.name,
            function.moon_name,
            source_for_report(function.source, header),
            function.c_signature,
            code.trim().to_owned(),
            "generated",
            if manual_resource {
              "release may fail; manual resource management required"
            } else {
              ""
            },
            bridge=shim_code is Some(_),
            policy=if config.schema_version == 2 {
              generation_policy
            } else {
              ""
            },
          ),
        )
      }
      None => {
        skipped += 1
        unsupported += 1
        items.push(
          report_item(
            "function",
            function.name,
            function.moon_name,
            source_for_report(function.source, header),
            function.c_signature,
            "",
            "skipped",
            reason,
          ),
        )
      }
    }
  }
  let declarations : Array[String] = [
    "// Generated by MoonBindgen. C resources require manual lifetime management.\n",
  ]
  let function_text = function_lines.join("\n")
  let type_names : Map[String, String] = Map([])
  for pair in opaque_types {
    let (name, _) = pair
    let output_name = opaque_type_name(name)
    if function_text.contains(output_name) {
      match type_names.get(output_name) {
        Some(previous) if previous != name =>
          fail("Opaque type name collision: " + previous + " and " + name)
        _ => type_names[output_name] = name
      }
      declarations.push("///|\n#external\npub type " + output_name + "\n")
    }
  }
  for line in enum_lines {
    declarations.push(line)
  }
  declarations.push(function_text)
  if config.unsupported_policy == Error && unsupported > 0 {
    fail(
      "generation rejected " +
      unsupported.to_string() +
      " unsupported declarations",
    )
  }
  let path_parts = normalized_path(header).split("/").to_array()
  let shim = if shim_lines.length() == 0 {
    None
  } else {
    let original_parts = header
      .replace_all(old="\\", new="/")
      .split("/")
      .to_array()
    let header_name = original_parts[original_parts.length() - 1]
      .to_owned()
      .replace_all(old="\"", new="\\\"")
    Some(
      "// Generated by MoonBindgen.\n#include \n" +
      "#include \n#include \n" +
      (if config.schema_version == 2 {
        "#include \n#include \n"
      } else {
        ""
      }) +
      "#include \"" +
      header_name +
      "\"\n\n" +
      shim_lines.join("\n"),
    )
  }
  let included_seen : Map[String, Bool] = Map([])
  let included_files : Array[Json] = []
  for entry in nodes {
    let source = normalized_path(entry.source.file)
    if source != "" &&
      !same_header(source, header) &&
      !included_seen.contains(source) {
      included_seen[source] = true
      included_files.push(Json::string(source))
    }
  }
  let declaration_reports : Array[Json] = []
  for item in enum_reports {
    declaration_reports.push(item)
  }
  for item in items {
    declaration_reports.push(item)
  }
  let report_fields : Map[String, Json] = {
    "schema": Json::string("moonbindgen-report-v2"),
    "header": Json::string(path_parts[path_parts.length() - 1].to_owned()),
    "clang": Json::object({
      "version": Json::string(metadata.clang_version),
      "target": Json::string(metadata.clang_target),
    }),
    "included_files": Json::array(included_files),
    "generated": Json::number(generated.to_double()),
    "skipped": Json::number(skipped.to_double()),
    "summary": Json::object({
      "functions_generated": Json::number(generated.to_double()),
      "functions_skipped": Json::number(skipped.to_double()),
      "enums_generated": Json::number(enum_generated.to_double()),
      "enums_skipped": Json::number(enum_skipped.to_double()),
      "unsupported": Json::number(unsupported.to_double()),
    }),
    "declarations": Json::array(declaration_reports),
    "functions": Json::array(items),
    "enums": Json::array(enum_reports),
  }
  if config.schema_version == 2 {
    report_fields["config_schema"] = Json::string("moonbindgen-config-v2")
  }
  if shim is Some(_) {
    report_fields["has_shim"] = Json(true)
  }
  let report = Json::object(report_fields).stringify(indent=2)
  { bindings: declarations.join("\n"), report, shim, generated, skipped, }
}