///|
fn first_value(matches : @argparse.Matches, name : String) -> String? {
  match matches.values.get(name) {
    Some(values) => if values.length() > 0 { Some(values[0]) } else { None }
    None => None
  }
}

///|
fn flag_enabled(matches : @argparse.Matches, name : String) -> Bool {
  matches.flags.get_or_default(name, false)
}

///|
fn push_all(target : Array[String], incoming : Array[String]) -> Unit {
  for item in incoming {
    target.push(item)
  }
}

///|
fn parse_assignment(entry : String) -> (String, String)? {
  match entry.split_once("=") {
    Some((name, value)) => Some((name.to_owned(), value.to_owned()))
    None => None
  }
}

///|
fn unique_diagnostics(errors : Array[String]) -> Array[String] {
  let unique : Array[String] = []
  let seen : Map[String, Bool] = {}
  for error in errors {
    if !seen.get_or_default(error, false) {
      seen[error] = true
      unique.push(error)
    }
  }
  unique
}

///|
fn diagnostic_tip(message : String) -> String? {
  if message.contains("invalid --var assignment") {
    Some(
      "Use --var key=value. Dotted keys such as user.name=Alice are supported.",
    )
  } else if message.contains("invalid --partial assignment") {
    Some(
      "Use --partial name=template, or load multiple partials with --partials-json-file.",
    )
  } else if message.contains("missing partial:") {
    Some(
      "Add the partial inline with --partial name=..., or load a JSON partial map with --partials-json-file.",
    )
  } else if message.contains("missing variable:") {
    Some(
      "Provide the value through --var, --json, --json-file, or the selected bundle profile context.",
    )
  } else if message.contains("invalid JSON context") {
    Some(
      "Check shell quoting first. On PowerShell, --json-base64 is often the easiest way to pass JSON safely.",
    )
  } else if message.contains("invalid partials JSON") {
    Some(
      "The partials file must be a JSON object that maps partial names to template strings.",
    )
  } else if message.contains("failed to read template file") ||
    message.contains("failed to read JSON context file") ||
    message.contains("failed to read partials file") ||
    message.contains("failed to read bundle manifest file") {
    Some(
      "Double-check the path, and use --target js for file-backed CLI flows.",
    )
  } else if message.contains(
      "file IO is only supported when running the CLI with --target js",
    ) {
    Some(
      "Re-run the command as moon run --target js cli ... when reading or writing files.",
    )
  } else if message.contains("invalid bundle manifest:") {
    Some(
      "Validate the manifest shape first, or print the sample manifest with --print-sample-manifest.",
    )
  } else if message.contains("unsupported bundle report format") {
    Some("Use --bundle-report-format markdown or --bundle-report-format json.")
  } else if message.contains("parent traversal") ||
    message.contains("invalid-output-path") {
    Some(
      "Bundle output paths must stay inside the generated project tree and cannot use parent traversal segments.",
    )
  } else if message.contains("unterminated section:") {
    Some(
      "Every {{#name}} or {{^name}} block needs a matching closing tag such as {{/name}}.",
    )
  } else if message.contains("partial depth limit exceeded:") {
    Some(
      "This usually means recursive partial expansion. Check whether a partial includes itself directly or indirectly.",
    )
  } else {
    None
  }
}

///|
fn print_diagnostics(errors : Array[String]) -> Unit {
  let unique = unique_diagnostics(errors)
  if unique.length() == 0 {
    return
  }
  println(
    "moon-mustache found " + unique.length().to_string() + " diagnostic(s):",
  )
  for error in unique {
    println("- " + error)
    match diagnostic_tip(error) {
      Some(tip) => println("  tip: " + tip)
      None => ()
    }
  }
  println("")
}

///|
fn print_cli_examples() -> Unit {
  println("Moon Mustache examples")
  println("")
  println("Inline render")
  println("  moon run cli --template \"Hello {{name}}\" --var name=MoonBit")
  println("")
  println("JSON context")
  println(
    "  moon run cli --json \"{\\\"project\\\":{\\\"name\\\":\\\"moon-mustache\\\"}}\" --template \"{{project.name}}\"",
  )
  println("")
  println("File-backed render (js target)")
  println(
    "  moon run --target js cli --template-file examples/files/template.mustache --json-file examples/files/context.json --partials-json-file examples/files/partials.json",
  )
  println("")
  println("Bundle manifest validation without writing files")
  println(
    "  moon run --target js cli --bundle-manifest-file examples/bundle/manifest.json --bundle-profile dev --json-file examples/bundle/context.json --bundle-check-only --bundle-validation-file out_bundle_check/validation.md --bundle-plan-file out_bundle_check/plan.md",
  )
  println("")
  println("Scan template variables")
  println(
    "  moon run cli --template \"{{#user}}{{name}}{{/user}}{{> footer}}\" --scan",
  )
  println("")
}

///|
fn print_quickstart_banner() -> Unit {
  println("Moon Mustache quick start")
  println("")
  println("Try one of these next:")
  println("  moon run cli --examples")
  println("  moon run showcase")
  println("  moon run scenario_report")
  println("  moon run official_spec_report")
  println("")
  println("Sample output from the built-in demo template:")
  println("")
}

///|
fn parse_json_input(input : String) -> (@src.Value, Array[String]) {
  (@src.parse_json_context(input), ([] : Array[String])) catch {
    err =>
      (@src.Value::empty_object(), ["invalid JSON context: " + err.to_string()])
  }
}

///|
fn build_template(matches : @argparse.Matches) -> (String, Array[String]) {
  match first_value(matches, "template-file") {
    Some(path) =>
      match read_text_file(path) {
        Ok(content) => (content, [])
        Err(message) => ("", ["failed to read template file: " + message])
      }
    None =>
      match first_value(matches, "template") {
        Some(value) => (value, [])
        None => (default_template(), [])
      }
  }
}

///|
fn has_explicit_context(matches : @argparse.Matches) -> Bool {
  first_value(matches, "json-file") is Some(_) ||
  first_value(matches, "json-base64") is Some(_) ||
  first_value(matches, "json") is Some(_) ||
  matches.values.get("var") is Some(_)
}

///|
fn build_base_context(
  matches : @argparse.Matches,
) -> (@src.Value, Array[String]) {
  match first_value(matches, "json-file") {
    Some(path) =>
      match read_text_file(path) {
        Ok(content) => return parse_json_input(content)
        Err(message) =>
          return (
            @src.Value::empty_object(),
            ["failed to read JSON context file: " + message],
          )
      }
    None => ()
  }
  match first_value(matches, "json-base64") {
    Some(encoded) => {
      let decoded = @base64.decode(encoded) catch {
        _ =>
          return (@src.Value::empty_object(), ["invalid Base64 JSON context"])
      }
      let json_input = @utf8.decode(decoded) catch {
        _ => return (@src.Value::empty_object(), ["invalid UTF-8 JSON context"])
      }
      return parse_json_input(json_input)
    }
    None => ()
  }
  match first_value(matches, "json") {
    Some(input) => parse_json_input(input)
    None => (@src.Value::empty_object(), [])
  }
}

///|
fn build_context_from_assignments(
  matches : @argparse.Matches,
) -> (@src.Value, Array[String]) {
  let entries : Array[(String, String)] = []
  let errors : Array[String] = []
  match matches.values.get("var") {
    Some(values) =>
      for entry in values {
        match parse_assignment(entry) {
          Some((name, value)) => entries.push((name, value))
          None =>
            errors.push(
              "invalid --var assignment (expected key=value): " + entry,
            )
        }
      }
    None => ()
  }
  let (context, parse_errors) = @src.object_from_string_entries(entries)
  push_all(errors, parse_errors)
  (context, errors)
}

///|
fn build_context(matches : @argparse.Matches) -> (@src.Value, Array[String]) {
  let (base_context, base_errors) = build_base_context(matches)
  let (override_context, override_errors) = build_context_from_assignments(
    matches,
  )
  let merged_context = @src.merge_values(base_context, override_context)
  let merged_errors = base_errors.copy()
  for error in override_errors {
    merged_errors.push(error)
  }
  if !has_explicit_context(matches) {
    let fallback : Map[String, @src.Value] = {
      "name": @src.Value::string("MoonBit"),
      "role": @src.Value::string("template engine"),
    }
    return (@src.Value::object(fallback), merged_errors)
  }
  (merged_context, merged_errors)
}

///|
fn build_partials(
  matches : @argparse.Matches,
) -> (Map[String, String], Array[String]) {
  let partials : Map[String, String] = {}
  let errors : Array[String] = []
  match first_value(matches, "partials-json-file") {
    Some(path) =>
      match read_text_file(path) {
        Ok(content) =>
          match @src.parse_json_partials_checked(content) {
            Ok(file_partials) =>
              for name, template in file_partials {
                partials[name] = template
              }
            Err(message) => errors.push(message)
          }
        Err(message) => errors.push("failed to read partials file: " + message)
      }
    None => ()
  }
  match matches.values.get("partial") {
    Some(values) =>
      for entry in values {
        match parse_assignment(entry) {
          Some((name, value)) => partials[name] = value
          None =>
            errors.push(
              "invalid --partial assignment (expected name=template): " + entry,
            )
        }
      }
    None => ()
  }
  (partials, errors)
}

///|
fn build_render_options(matches : @argparse.Matches) -> @src.RenderOptions {
  if flag_enabled(matches, "strict-missing") {
    @src.RenderOptions::strict()
  } else {
    @src.RenderOptions::default()
  }
}

///|
fn default_template() -> String {
  "Project {{name}} builds a {{role}}.\n{{> footer}}"
}

///|
fn effective_argv() -> ArrayView[String] {
  let args = @env.args()
  if args.length() > 2 && args[1].strip_suffix(".js") is Some(_) {
    args[2:]
  } else if args.length() > 1 {
    args[1:]
  } else {
    []
  }
}

///|
fn print_sample_manifest() -> Unit {
  println(@src.sample_bundle_manifest_json())
}

///|
fn print_scan_result(result : @src.TemplateScanResult) -> Unit {
  let current_context_usage = if result.uses_current_context {
    "yes"
  } else {
    "no"
  }
  let dotted_name_usage = if result.uses_dotted_names { "yes" } else { "no" }
  println("Template scan summary")
  println("")
  println("Data names (" + result.data_names.length().to_string() + ")")
  for name in result.data_names {
    println("- " + name)
  }
  println("")
  println("Partial names (" + result.partial_names.length().to_string() + ")")
  for name in result.partial_names {
    println("- " + name)
  }
  println("")
  println("Reference count: " + result.references.length().to_string())
  println("Uses current context: " + current_context_usage)
  println("Uses dotted names: " + dotted_name_usage)
  println("Delimiter changes: " + result.delimiter_change_count.to_string())
  if result.parse_errors.length() > 0 {
    println("")
    println("Parse diagnostics")
    for error in result.parse_errors {
      println("- " + error)
    }
  }
}

///|
fn lint_source_name(matches : @argparse.Matches) -> String {
  match first_value(matches, "template-file") {
    Some(path) => path
    None => ""
  }
}

///|
fn finish_lint(report : @src.LintReport, input_errors : Array[String]) -> Unit {
  for error in input_errors {
    println(":1:1: error [MST-CLI-001] " + error)
  }
  println(@src.format_lint_report(report))
  if input_errors.length() > 0 || @src.lint_has_errors(report) {
    set_process_exit_code(1)
  }
}

///|
fn handle_lint_mode(
  matches : @argparse.Matches,
  context : @src.Value,
  context_errors : Array[String],
  partials : Map[String, String],
  partial_errors : Array[String],
) -> Unit {
  let input_errors = context_errors.copy()
  push_all(input_errors, partial_errors)
  if bundle_manifest_enabled(matches) {
    let (manifest_opt, manifest_errors) = build_bundle_manifest(matches)
    push_all(input_errors, manifest_errors)
    match manifest_opt {
      Some(manifest) => {
        let profile_name = match first_value(matches, "bundle-profile") {
          Some(value) => value
          None => ""
        }
        match
          @src.resolve_bundle_manifest_profile(manifest, context, profile_name~) {
          Ok(resolved) => {
            let final_bundle = @src.TemplateBundle::new(
              resolved.bundle.files,
              merge_partial_maps(resolved.bundle.partials, partials),
            )
            finish_lint(
              @src.lint_template_bundle(final_bundle, resolved.context),
              input_errors,
            )
          }
          Err(message) => {
            input_errors.push(message)
            finish_lint(@src.lint_template("", context, {}), input_errors)
          }
        }
      }
      None => finish_lint(@src.lint_template("", context, {}), input_errors)
    }
  } else {
    let (template, template_errors) = build_template(matches)
    push_all(input_errors, template_errors)
    let resolved_partials = if partials.length() == 0 {
      { "footer": "Rendered by moon-mustache." }
    } else {
      partials
    }
    let report = @src.lint_template_with_options(
      template,
      context,
      resolved_partials,
      @src.LintOptions::default(),
      source_name=lint_source_name(matches),
    )
    finish_lint(report, input_errors)
  }
}

///|
fn bundle_manifest_enabled(matches : @argparse.Matches) -> Bool {
  first_value(matches, "bundle-manifest-file") is Some(_)
}

///|
fn merge_partial_maps(
  base : Map[String, String],
  incoming : Map[String, String],
) -> Map[String, String] {
  let merged : Map[String, String] = {}
  for name, template in base {
    merged[name] = template
  }
  for name, template in incoming {
    merged[name] = template
  }
  merged
}

///|
fn join_output_path(root : String, relative : String) -> String {
  if root == "" {
    relative
  } else {
    root + "/" + relative
  }
}

///|
fn print_generated_file(file : @src.GeneratedFile) -> Unit {
  println("== " + file.path + " ==")
  println(file.output)
  println("")
}

///|
fn build_bundle_manifest(
  matches : @argparse.Matches,
) -> (@src.BundleManifest?, Array[String]) {
  match first_value(matches, "bundle-manifest-file") {
    Some(path) =>
      match read_text_file(path) {
        Ok(content) =>
          match @src.parse_bundle_manifest_json(content) {
            Ok(manifest) => (Some(manifest), [])
            Err(message) => (None, ["invalid bundle manifest: " + message])
          }
        Err(message) =>
          (None, ["failed to read bundle manifest file: " + message])
      }
    None => (None, [])
  }
}

///|
fn build_bundle_report_output(
  format : String,
  bundle_name : String,
  result : @src.BundleRenderResult,
) -> (String, String)? {
  if format == "json" {
    Some(("json", @src.format_bundle_result_json(bundle_name, result)))
  } else if format == "markdown" {
    Some(("markdown", @src.format_bundle_result_markdown(bundle_name, result)))
  } else {
    None
  }
}

///|
fn build_bundle_plan_output(
  bundle_name : String,
  render_result : @src.BundleRenderResult,
  validation_result : @src.BundleValidationResult,
) -> String {
  @src.format_bundle_plan_markdown(
    @src.build_bundle_plan(bundle_name, render_result, validation_result),
  )
}

///|
fn validation_issue_diagnostic(issue : @src.BundleValidationIssue) -> String {
  let severity = match issue.severity {
    Error => "error"
    Warning => "warning"
  }
  "bundle " +
  severity +
  " [" +
  issue.code +
  "] " +
  issue.target +
  ": " +
  issue.message
}

///|
fn handle_bundle_mode(
  matches : @argparse.Matches,
  context : @src.Value,
  context_errors : Array[String],
  partials : Map[String, String],
  partial_errors : Array[String],
  render_options : @src.RenderOptions,
) -> Unit {
  let (manifest_opt, manifest_errors) = build_bundle_manifest(matches)
  let all_errors = context_errors.copy()
  push_all(all_errors, partial_errors)
  push_all(all_errors, manifest_errors)
  let manifest = match manifest_opt {
    Some(value) => value
    None => {
      print_diagnostics(all_errors)
      return
    }
  }
  let validation_result = @src.validate_bundle_manifest(manifest)
  for issue in validation_result.issues {
    all_errors.push(validation_issue_diagnostic(issue))
  }
  let profile_name = match first_value(matches, "bundle-profile") {
    Some(value) => value
    None => ""
  }
  let resolved = match
    @src.resolve_bundle_manifest_profile(manifest, context, profile_name~) {
    Ok(value) => value
    Err(message) => {
      print_diagnostics([message])
      return
    }
  }
  let merged_partials = merge_partial_maps(resolved.bundle.partials, partials)
  let final_bundle = @src.TemplateBundle::new(
    resolved.bundle.files,
    merged_partials,
  )
  let prepared_bundle = @src.prepare_template_bundle(final_bundle)
  let result = @src.render_prepared_template_bundle_checked(
    prepared_bundle,
    resolved.context,
    render_options,
  )
  for error in result.errors {
    all_errors.push(error)
  }
  for name in result.missing_variables {
    all_errors.push("missing variable: " + name)
  }
  if all_errors.length() > 0 {
    print_diagnostics(all_errors)
    if flag_enabled(matches, "strict") ||
      result.errors.length() > 0 ||
      @src.validation_has_errors(validation_result) {
      println(
        "No bundle files were written because strict mode, render errors, or validation errors blocked output.",
      )
      return
    }
  }
  if !flag_enabled(matches, "bundle-check-only") {
    match first_value(matches, "bundle-output-dir") {
      Some(root) =>
        for file in result.files {
          let path = join_output_path(root, file.path)
          match write_text_file(path, file.output) {
            Ok(_) => ()
            Err(message) =>
              print_diagnostics([
                "failed to write bundle file `" + path + "`: " + message,
              ])
          }
        }
      None =>
        for file in result.files {
          print_generated_file(file)
        }
    }
  }
  match first_value(matches, "bundle-report-file") {
    Some(path) => {
      let format = match first_value(matches, "bundle-report-format") {
        Some(value) => value
        None => "markdown"
      }
      match build_bundle_report_output(format, manifest.name, result) {
        Some((_, content)) =>
          match write_text_file(path, content) {
            Ok(_) => println("Wrote bundle report to " + path)
            Err(message) =>
              print_diagnostics(["failed to write bundle report: " + message])
          }
        None => print_diagnostics(["unsupported bundle report format"])
      }
    }
    None => ()
  }
  match first_value(matches, "bundle-validation-file") {
    Some(path) =>
      match
        write_text_file(
          path,
          @src.format_validation_report_markdown(
            manifest.name,
            validation_result,
          ),
        ) {
        Ok(_) => println("Wrote bundle validation report to " + path)
        Err(message) =>
          print_diagnostics([
            "failed to write bundle validation report: " + message,
          ])
      }
    None => ()
  }
  match first_value(matches, "bundle-plan-file") {
    Some(path) =>
      match
        write_text_file(
          path,
          build_bundle_plan_output(manifest.name, result, validation_result),
        ) {
        Ok(_) => println("Wrote bundle generation plan to " + path)
        Err(message) =>
          print_diagnostics([
            "failed to write bundle generation plan: " + message,
          ])
      }
    None => ()
  }
  if flag_enabled(matches, "bundle-check-only") {
    println("Bundle check completed without writing generated files.")
  }
}

///|
fn handle_single_template_mode(
  matches : @argparse.Matches,
  context : @src.Value,
  context_errors : Array[String],
  partials : Map[String, String],
  partial_errors : Array[String],
  render_options : @src.RenderOptions,
) -> Unit {
  let (template, template_errors) = build_template(matches)
  let resolved_partials = if partials.length() == 0 {
    { "footer": "Rendered by moon-mustache." }
  } else {
    partials
  }

  let prepared = @src.prepare(template, resolved_partials)
  let result = @src.render_prepared_checked_with_options(
    prepared, context, render_options,
  )
  let all_errors = template_errors.copy()
  for error in context_errors {
    all_errors.push(error)
  }
  for error in partial_errors {
    all_errors.push(error)
  }
  for error in result.errors {
    all_errors.push(error)
  }
  for name in result.missing_variables {
    all_errors.push("missing variable: " + name)
  }

  if all_errors.length() > 0 {
    print_diagnostics(all_errors)
    if flag_enabled(matches, "strict") {
      println("No rendered output was produced because strict mode is enabled.")
      return
    }
  }

  match first_value(matches, "output") {
    Some(path) =>
      match write_text_file(path, result.output) {
        Ok(_) => println("Wrote rendered output to " + path)
        Err(message) =>
          print_diagnostics(["failed to write output file: " + message])
      }
    None => println(result.output)
  }
}

///|
fn main {
  let command = @argparse.Command(
    "moon-mustache",
    about="Render Mustache templates with MoonBit.",
    arg_required_else_help=false,
    options=[
      OptionArg(
        "template",
        short='t',
        long="template",
        about="Inline Mustache template to render.",
      ),
      OptionArg(
        "template-file",
        long="template-file",
        about="Read Mustache template content from a file.",
      ),
      OptionArg(
        "bundle-manifest-file",
        long="bundle-manifest-file",
        about="Read a JSON bundle manifest and render multiple files in one run.",
      ),
      OptionArg(
        "bundle-output-dir",
        long="bundle-output-dir",
        about="Write rendered bundle files under this directory.",
      ),
      OptionArg(
        "bundle-validation-file",
        long="bundle-validation-file",
        about="Write a Markdown validation report for the bundle manifest.",
      ),
      OptionArg(
        "bundle-plan-file",
        long="bundle-plan-file",
        about="Write a Markdown generation plan for the rendered bundle.",
      ),
      OptionArg(
        "bundle-profile",
        long="bundle-profile",
        about="Optional bundle profile defined inside the manifest.",
      ),
      OptionArg(
        "bundle-report-file",
        long="bundle-report-file",
        about="Write a bundle render report to a file.",
      ),
      OptionArg(
        "bundle-report-format",
        long="bundle-report-format",
        about="Bundle report format: markdown or json.",
      ),
      OptionArg(
        "json",
        long="json",
        about="Inline JSON object or array used as the template context.",
      ),
      OptionArg(
        "json-file",
        long="json-file",
        about="Read JSON context from a file.",
      ),
      OptionArg(
        "json-base64",
        long="json-base64",
        about="Base64-encoded JSON context, useful when shell quoting is inconvenient.",
      ),
      OptionArg(
        "partials-json-file",
        long="partials-json-file",
        about="Read partial templates from a JSON file that maps names to template strings.",
      ),
      OptionArg(
        "var",
        short='v',
        long="var",
        about="Template variable in key=value form. Repeatable and dotted paths are supported.",
        action=Append,
      ),
      OptionArg(
        "partial",
        short='p',
        long="partial",
        about="Partial template in name=template form. Repeatable.",
        action=Append,
      ),
      OptionArg(
        "output",
        short='o',
        long="output",
        about="Write rendered output to a file instead of stdout.",
      ),
    ],
    flags=[
      FlagArg(
        "strict",
        long="strict",
        about="Print diagnostics and skip output when rendering errors are found.",
      ),
      FlagArg(
        "strict-missing",
        long="strict-missing",
        about="Track missing variables as diagnostics for stricter template validation.",
      ),
      FlagArg(
        "bundle-check-only",
        long="bundle-check-only",
        about="Validate and render the bundle without writing generated files.",
      ),
      FlagArg(
        "examples",
        long="examples",
        about="Print a short cookbook of common CLI commands.",
      ),
      FlagArg(
        "scan",
        long="scan",
        about="Scan a template and print referenced data names, partials, and delimiter usage.",
      ),
      FlagArg(
        "lint",
        long="lint",
        about="Validate template syntax, context references, partial graphs, and bundle paths.",
      ),
      FlagArg(
        "print-default-template",
        long="print-default-template",
        about="Print the built-in demo template and exit.",
      ),
      FlagArg(
        "print-sample-manifest",
        long="print-sample-manifest",
        about="Print the sample bundle manifest JSON and exit.",
      ),
    ],
  )

  let matches = command.parse(argv=effective_argv()) catch {
    err => {
      println(err.to_string())
      return
    }
  }
  if flag_enabled(matches, "examples") {
    print_cli_examples()
    return
  }
  if flag_enabled(matches, "print-default-template") {
    println(default_template())
    return
  }
  if flag_enabled(matches, "print-sample-manifest") {
    print_sample_manifest()
    return
  }
  if effective_argv().length() == 0 {
    print_quickstart_banner()
  }
  if flag_enabled(matches, "scan") {
    let (template, template_errors) = build_template(matches)
    if template_errors.length() > 0 {
      print_diagnostics(template_errors)
      return
    }
    print_scan_result(@src.scan_template_references(template))
    return
  }
  let (context, context_errors) = build_context(matches)
  let (partials, partial_errors) = build_partials(matches)
  let render_options = build_render_options(matches)
  if flag_enabled(matches, "lint") {
    handle_lint_mode(matches, context, context_errors, partials, partial_errors)
    return
  }
  if bundle_manifest_enabled(matches) {
    handle_bundle_mode(
      matches, context, context_errors, partials, partial_errors, render_options,
    )
  } else {
    handle_single_template_mode(
      matches, context, context_errors, partials, partial_errors, render_options,
    )
  }
}