/// moon-release CLI
/// Automated release tool

///|
/// Version extracted from embedded moon.mod.json (generated at build time)
let version : String = extract_version(embedded_moon_mod_json)

///|
/// Maximum number of commits to display in verbose output
let max_verbose_commits : Int = 10

///|
/// Extract version from embedded moon.mod.json content
fn extract_version(json_str : String) -> String {
  // Use try? + to_option for consistent error handling
  match (try? @json.parse(json_str)).to_option() {
    Some(Object(obj)) =>
      match obj.get("version") {
        Some(String(v)) => v
        _ => "unknown"
      }
    _ => "unknown"
  }
}

///|
/// Exit with status code (wrapper to avoid MSVC C2371 conflict with stdlib exit)
extern "C" fn c_exit(status : Int) -> Unit = "moon_release_exit"

///|
/// Exit with error (non-zero status code)
/// Uses panic() to satisfy type checker since exit never returns
fn[T] exit_error() -> T {
  c_exit(1)
  panic()
}

// ============================================================================
// Output helpers for consistent error/warning formatting
// ============================================================================

///|
/// Output error message (supports JSON and text formats)
fn output_error(msg : String, json_output : Bool) -> Unit {
  if json_output {
    println("{\"error\": " + @util.json_string(msg) + "}")
  } else {
    println("Error: " + msg)
  }
}

///|
/// Output error message with hint (text mode only shows hint)
fn output_error_with_hint(
  msg : String,
  hint : String,
  json_output : Bool,
) -> Unit {
  if json_output {
    println("{\"error\": " + @util.json_string(msg) + "}")
  } else {
    println("Error: " + msg)
    println("Hint: " + hint)
  }
}

///|
/// Handle workflow error with automatic hint from error variant
fn handle_workflow_error(
  e : @workflow.WorkflowError,
  json_output : Bool,
  hint_override? : String = "",
) -> Unit {
  let hint = if hint_override.is_empty() { e.hint() } else { hint_override }
  if hint.is_empty() {
    output_error(e.to_string(), json_output)
  } else {
    output_error_with_hint(e.to_string(), hint, json_output)
  }
}

///|
/// Output warning message (suppressed in JSON mode)
fn output_warning(msg : String, json_output : Bool) -> Unit {
  if not(json_output) {
    println("Warning: " + msg)
  }
}

///|
/// Get dry-run flag from subcommand (combines global and subcommand flags)
fn get_dry_run_flag(subcmd : @clap.SimpleValue, global_dry_run : Bool) -> Bool {
  let subcmd_dry_run = match subcmd.get_flag("dry-run") {
    Some(v) => v
    None => false
  }
  global_dry_run || subcmd_dry_run
}

///|
/// Get output format from subcommand (returns true if JSON)
fn get_json_output(subcmd : @clap.SimpleValue) -> Bool {
  // Use try? + unwrap_or pattern for consistent error handling
  let output_format = (try? subcmd.get_one("output")).unwrap_or("text")
  output_format == "json"
}

///|
/// Print verbose header for command
fn print_verbose_header(
  cmd_name : String,
  config_path : String,
  dry_run : Bool,
  verbose : Bool,
  json_output : Bool,
) -> Unit {
  if verbose && not(json_output) {
    println("Running " + cmd_name + " command...")
    println("  Config: " + config_path)
    println("  Dry run: " + dry_run.to_string())
  }
}

///|
/// Create CLI parser
fn create_parser() -> @clap.Parser {
  @clap.Parser::new(
    prog="moon-release",
    description="Automated release management tool for MoonBit",
    args={
      "dry-run": @clap.Arg::flag(
        short='n',
        global=true,
        help="Show what would be done without making changes",
      ),
      "verbose": @clap.Arg::flag(
        short='v',
        global=true,
        help="Enable verbose output",
      ),
      "config": @clap.Arg::named(
        short='c',
        nargs=@clap.One,
        defaults=["release.json"],
        global=true,
        help="Path to config file",
      ),
    },
    subcmds={
      "update": @clap.SubCommand::new(
        help="Update version locally (no commit/push)",
        args={
          "dry-run": @clap.Arg::flag(
            short='n',
            help="Show what would be done without making changes",
          ),
          "bump": @clap.Arg::named(
            short='b',
            nargs=@clap.AtMost(1),
            help="Force a specific version bump (major, minor, patch)",
          ),
          "bump-all": @clap.Arg::flag(
            help="Force bump ALL packages including unchanged ones",
          ),
          "packages": @clap.Arg::flag(
            short='p',
            help="Update all packages in monorepo mode",
          ),
          "output": @clap.Arg::named(
            short='o',
            nargs=@clap.One,
            defaults=["text"],
            help="Output format (text, json)",
          ),
        },
      ),
      "release-pr": @clap.SubCommand::new(
        help="Create a release pull request",
        args={
          "dry-run": @clap.Arg::flag(
            short='n',
            help="Show what would be done without making changes",
          ),
          "bump": @clap.Arg::named(
            short='b',
            nargs=@clap.AtMost(1),
            help="Force a specific version bump (major, minor, patch)",
          ),
          "bump-all": @clap.Arg::flag(
            help="Force bump ALL packages including unchanged ones",
          ),
        },
      ),
      "release": @clap.SubCommand::new(
        help="Create a release (tag and GitHub release)",
        args={
          "dry-run": @clap.Arg::flag(
            short='n',
            help="Show what would be done without making changes",
          ),
          "bump": @clap.Arg::named(
            short='b',
            nargs=@clap.AtMost(1),
            help="Force a specific version bump (major, minor, patch)",
          ),
          "bump-all": @clap.Arg::flag(
            help="Force bump ALL packages including unchanged ones",
          ),
          "prerelease": @clap.Arg::named(
            short='p',
            nargs=@clap.AtMost(1),
            help="Create a prerelease with the given identifier (e.g., alpha, beta, rc)",
          ),
        },
      ),
      "init": @clap.SubCommand::new(help="Initialize release configuration", args={
        "force": @clap.Arg::flag(
          short='f',
          help="Overwrite existing configuration",
        ),
      }),
      "version": @clap.SubCommand::new(help="Show version information"),
      "check": @clap.SubCommand::new(
        help="Check the current state without making changes",
        args={
          "output": @clap.Arg::named(
            short='o',
            nargs=@clap.One,
            defaults=["text"],
            help="Output format (text, json)",
          ),
        },
      ),
      "set-version": @clap.SubCommand::new(
        help="Set the package version manually",
        args={
          "dry-run": @clap.Arg::flag(
            short='n',
            help="Show what would be done without making changes",
          ),
          "version": @clap.Arg::positional(
            nargs=@clap.One,
            help="The version to set (e.g., 1.2.3)",
          ),
        },
      ),
      "generate-completions": @clap.SubCommand::new(
        help="Generate shell completion script",
        args={
          "shell": @clap.Arg::positional(
            nargs=@clap.One,
            help="Shell to generate completions for (bash, zsh, fish)",
          ),
        },
      ),
      "generate-schema": @clap.SubCommand::new(
        help="Generate JSON Schema for release.json configuration",
      ),
    },
  )
}

///|
/// Main function (async)
async fn main {
  let parser = create_parser()
  let value = @clap.SimpleValue::new("moon-release")

  // Get command line arguments (skip program name)
  let all_args = @env.args()
  let args : Array[String] = []
  for i = 1; i < all_args.length(); i = i + 1 {
    args.push(all_args[i])
  }

  // Parse with try-catch-noraise pattern
  let help_message = try parser.parse(value, args[:]) catch {
    e => {
      println("Error: " + e.to_string())
      exit_error()
    }
  } noraise {
    r => r
  }

  // Display help message if returned and exit
  match help_message {
    Some(msg) => {
      println(msg)
      return
    }
    None => ()
  }

  // Global options
  let dry_run = match value.get_flag("dry-run") {
    Some(v) => v
    None => false
  }
  let verbose = match value.get_flag("verbose") {
    Some(v) => v
    None => false
  }
  // Use try? + unwrap_or pattern for consistent error handling
  let config_path = (try? value.get_one("config")).unwrap_or("release.json")

  // Process subcommand
  match value.subcmd {
    Some(subcmd) =>
      match subcmd.name {
        "update" => cmd_update(subcmd, dry_run, verbose, config_path)
        "release-pr" => cmd_release_pr(subcmd, dry_run, verbose, config_path)
        "release" => cmd_release(subcmd, dry_run, verbose, config_path)
        "init" => cmd_init(subcmd, dry_run, verbose)
        "version" => cmd_version()
        "check" => cmd_check(subcmd, dry_run, verbose, config_path)
        "set-version" => cmd_set_version(subcmd, dry_run, verbose)
        "generate-completions" => cmd_generate_completions(subcmd)
        "generate-schema" => cmd_generate_schema()
        _ => println("Unknown command: " + subcmd.name)
      }
    None => {
      println("moon-release v" + version)
      println("")
      println("Use --help for usage information")
    }
  }
}

///|
/// update command
async fn cmd_update(
  subcmd : @clap.SimpleValue,
  global_dry_run : Bool,
  verbose : Bool,
  config_path : String,
) -> Unit {
  let dry_run = get_dry_run_flag(subcmd, global_dry_run)
  let json_output = get_json_output(subcmd)

  print_verbose_header("update", config_path, dry_run, verbose, json_output)

  // Load config and repo
  let config = load_config(config_path, verbose~, json_output~)
  let repo = @git.Repository::current()

  // Dirty check
  try @workflow.check_working_directory(config, repo, verbose~) catch {
    e => {
      if json_output {
        output_error("uncommitted changes in working directory", json_output)
      } else {
        output_error(e.to_string(), json_output)
      }
      exit_error()
    }
  } noraise {
    _ => ()
  }

  // Parse bump options before monorepo branching
  let forced_bump = get_bump_type(subcmd)
  let cli_bump_all = match subcmd.get_flag("bump-all") {
    Some(v) => v
    None => false
  }
  let bump_all = config.bump_all_packages || cli_bump_all

  // Check if monorepo mode
  let packages_mode = match subcmd.get_flag("packages") {
    Some(v) => v
    None => false
  }

  // In monorepo mode, update each package separately with try-catch-noraise pattern
  if packages_mode || not(config.packages.is_empty()) {
    // Hint: --bump only controls bump type, not scope
    if forced_bump is Some(_) && not(bump_all) && not(json_output) {
      println(
        "Note: --bump applies only to packages with changes. Use --bump-all to include all packages.",
      )
    }
    try
      @workflow.run_update_packages(
        config,
        repo,
        dry_run,
        verbose,
        forced_bump~,
        bump_all~,
      )
    catch {
      e => {
        handle_workflow_error(e, json_output)
        exit_error()
      }
    } noraise {
      _ => ()
    }
    return
  }

  // Analyze version bump
  let analysis = try
    @workflow.analyze_version_bump(
      config,
      repo,
      forced_bump~,
      max_commits=config.max_analyze_commits,
    )
  catch {
    e => {
      handle_workflow_error(
        e,
        json_output,
        hint_override="Use Conventional Commits (feat:, fix:) or --bump to force a version bump",
      )
      exit_error()
    }
  } noraise {
    r => r
  }

  let current_version = analysis.current_version
  let new_version = analysis.new_version
  let commits = analysis.commits
  let bump_type = analysis.bump_type

  // Registry check (if enabled) with try-catch-noraise pattern
  if config.registry_check {
    // Get package name: try moon.mod.json first, then package.json
    let pkg_name = match (try? load_moon_mod("moon.mod.json")).to_option() {
      Some(m) => m.name
      None =>
        match
          (try? load_package_json_name("package.json"))
          .to_option()
          .bind(fn(x) { x }) {
          Some(name) => name
          None => {
            output_warning(
              "Registry check skipped: could not determine package name from moon.mod.json or package.json",
              json_output,
            )
            ""
          }
        }
    }
    if not(pkg_name.is_empty()) {
      let should_rel = try
        @workflow.should_release(new_version, pkg_name, verbose)
      catch {
        e => {
          output_warning("Registry check failed: " + e.to_string(), json_output)
          true // Continue release on error
        }
      } noraise {
        r => r
      }

      if not(should_rel) {
        output_error(
          "Version " +
          new_version.to_string() +
          " is not newer than registry version. Nothing to update.",
          json_output,
        )
        exit_error()
      }
    }
  }

  // Create context and run workflow
  let ctx = @workflow.ReleaseContext::new(
    config,
    repo,
    current_version,
    new_version,
    commits,
    dry_run~,
    verbose~,
  )

  // Run update
  try @workflow.run_update(ctx) catch {
    e => {
      handle_workflow_error(e, json_output)
      exit_error()
    }
  } noraise {
    _ =>
      if json_output {
        let bump_str = match bump_type {
          @semver.Major => "major"
          @semver.Minor => "minor"
          @semver.Patch => "patch"
        }
        let json_str = @util.build_json_object([
          ("old_version", @util.json_string(current_version.to_string())),
          ("new_version", @util.json_string(new_version.to_string())),
          ("bump_type", @util.json_string(bump_str)),
          ("commits_count", commits.length().to_string()),
          ("dry_run", dry_run.to_string()),
          (
            "updated_files",
            {
              let files : Array[String] = []
              if config.moon_publish {
                files.push("\"moon.mod.json\"")
              }
              if config.npm_publish {
                files.push("\"package.json\"")
              }
              "[" +
              files
              .iter()
              .fold(init="", fn(acc, s) {
                if acc.is_empty() {
                  s
                } else {
                  acc + "," + s
                }
              }) +
              "]"
            },
          ),
        ])
        println(json_str)
      }
  }
}

///|
/// Load config, validate publish settings, and check for dirty state.
/// Shared setup for release-pr and release commands.
async fn prepare_release_config(
  config_path : String,
  verbose : Bool,
  json_output : Bool,
) -> (@config.Config, @git.Repository) {
  let config = load_config(config_path, verbose~, json_output~)
  let repo = @git.Repository::current()

  try @workflow.validate_publish_config(config, verbose~) catch {
    e => {
      handle_workflow_error(e, json_output)
      exit_error()
    }
  } noraise {
    _ => ()
  }

  try @workflow.check_working_directory(config, repo, verbose~) catch {
    e => {
      handle_workflow_error(e, json_output)
      exit_error()
    }
  } noraise {
    _ => ()
  }

  (config, repo)
}

///|
/// Extract bump flags from subcmd (shared by release-pr and release commands).
fn get_bump_flags(
  subcmd : @clap.SimpleValue,
  config : @config.Config,
) -> (@semver.BumpType?, Bool) {
  let forced_bump = get_bump_type(subcmd)
  let cli_bump_all = match subcmd.get_flag("bump-all") {
    Some(v) => v
    None => false
  }
  let bump_all = config.bump_all_packages || cli_bump_all
  (forced_bump, bump_all)
}

///|
/// release-pr command
async fn cmd_release_pr(
  subcmd : @clap.SimpleValue,
  global_dry_run : Bool,
  verbose : Bool,
  config_path : String,
) -> Unit {
  let dry_run = get_dry_run_flag(subcmd, global_dry_run)
  let json_output = false // release-pr doesn't support JSON output

  print_verbose_header("release-pr", config_path, dry_run, verbose, json_output)

  let (config, repo) = prepare_release_config(config_path, verbose, json_output)
  let (forced_bump, bump_all) = get_bump_flags(subcmd, config)
  let is_monorepo = not(config.packages.is_empty())

  // Hint: --bump only controls bump type, not scope
  if is_monorepo && forced_bump is Some(_) && not(bump_all) {
    println(
      "Note: --bump applies only to packages with changes. Use --bump-all to include all packages.",
    )
  }

  // Get current version and new version
  let (current_version, new_version, commits) = if is_monorepo {
    // Monorepo: compute representative version from packages
    try
      @workflow.get_monorepo_representative_version(
        config,
        repo,
        forced_bump,
        verbose,
        bump_all~,
      )
    catch {
      @workflow.NoReleasableCommits(msg) => {
        println(msg)
        println(
          "Hint: Use Conventional Commits or --bump to force a version bump",
        )
        return
      }
      e => {
        handle_workflow_error(e, json_output)
        exit_error()
      }
    } noraise {
      r => r
    }
  } else {
    // Single package: analyze version bump
    let analysis = try
      @workflow.analyze_version_bump(
        config,
        repo,
        forced_bump~,
        max_commits=config.max_analyze_commits,
      )
    catch {
      @workflow.NoReleasableCommits(msg) => {
        println(msg)
        println(
          "Hint: Use Conventional Commits or --bump to force a version bump",
        )
        return
      }
      e => {
        handle_workflow_error(
          e,
          json_output,
          hint_override="Use Conventional Commits or --bump to force a version bump",
        )
        exit_error()
      }
    } noraise {
      r => r
    }
    (analysis.current_version, analysis.new_version, analysis.commits)
  }

  // Create context
  let ctx = @workflow.ReleaseContext::new(
    config,
    repo,
    current_version,
    new_version,
    commits,
    dry_run~,
    verbose~,
    forced_bump~,
    bump_all~,
  )

  // Get GitHub client
  let github = try @workflow.get_github_client() catch {
    e => {
      handle_workflow_error(e, json_output)
      exit_error()
    }
  } noraise {
    r => r
  }

  // Run workflow
  try @workflow.run_release_pr(ctx, github) catch {
    e => {
      handle_workflow_error(e, json_output)
      exit_error()
    }
  } noraise {
    _ => ()
  }
}

///|
/// release command
async fn cmd_release(
  subcmd : @clap.SimpleValue,
  global_dry_run : Bool,
  verbose : Bool,
  config_path : String,
) -> Unit {
  let dry_run = get_dry_run_flag(subcmd, global_dry_run)
  let json_output = false // release doesn't support JSON output

  print_verbose_header("release", config_path, dry_run, verbose, json_output)

  let (config, repo) = prepare_release_config(config_path, verbose, json_output)
  let (forced_bump, bump_all) = get_bump_flags(subcmd, config)
  let is_monorepo = not(config.packages.is_empty())

  // Get prerelease identifier with try? + to_option pattern
  let prerelease = (try? subcmd.get_one("prerelease")).to_option()

  // Validate prerelease identifier format
  match prerelease {
    Some(id) =>
      match @workflow.validate_prerelease_identifier(id) {
        Some(reason) => {
          output_error_with_hint(
            "Invalid prerelease identifier '" + id + "': " + reason,
            "Use alphanumeric characters, hyphens, and dots (e.g., alpha, beta.1, rc-1)",
            json_output,
          )
          exit_error()
        }
        None => ()
      }
    None => ()
  }

  // Check if prerelease is allowed in config
  if prerelease is Some(_) && not(config.allow_prerelease) {
    output_error_with_hint(
      "Prerelease is not allowed in configuration.", "Set 'allow_prerelease: true' in release.json to enable prereleases",
      json_output,
    )
    exit_error()
  }

  // Hint: --bump only controls bump type, not scope
  if is_monorepo && forced_bump is Some(_) && not(bump_all) {
    println(
      "Note: --bump applies only to packages with changes. Use --bump-all to include all packages.",
    )
  }

  // The release command uses the version already set in moon.mod.json
  // (by release-pr). No version bumping is needed — the file version IS
  // the target release version.
  let new_version = try @workflow.get_current_version(config) catch {
    e => {
      handle_workflow_error(e, json_output)
      exit_error()
    }
  } noraise {
    r => r
  }
  let current_version = new_version

  let commits = try
    @workflow.get_commits_since_last_tag(
      repo,
      pr_title=config.pr_title,
      max_commits=config.max_analyze_commits,
    )
  catch {
    e => {
      handle_workflow_error(
        e,
        json_output,
        hint_override="Create new commits or use --bump to force a version bump",
      )
      exit_error()
    }
  } noraise {
    r => r
  }

  // Apply prerelease if specified
  let new_version = match prerelease {
    Some(pre_str) =>
      match new_version.with_prerelease(pre_str) {
        Some(v) => v
        None => {
          output_error_with_hint(
            "Invalid prerelease identifier: " + pre_str,
            "Prerelease must contain only ASCII alphanumerics and hyphens [0-9A-Za-z-]",
            json_output,
          )
          exit_error()
        }
      }
    None => new_version
  }

  // Create context
  let ctx = @workflow.ReleaseContext::new(
    config,
    repo,
    current_version,
    new_version,
    commits,
    dry_run~,
    verbose~,
    forced_bump~,
    bump_all~,
  )

  // Get GitHub client
  let github = try @workflow.get_github_client() catch {
    e => {
      handle_workflow_error(e, json_output)
      exit_error()
    }
  } noraise {
    r => r
  }

  // Run workflow
  try @workflow.run_release(ctx, github) catch {
    e => {
      handle_workflow_error(e, json_output)
      exit_error()
    }
  } noraise {
    _ => ()
  }
}

///|
/// init command
async fn cmd_init(
  subcmd : @clap.SimpleValue,
  dry_run : Bool,
  _verbose : Bool,
) -> Unit {
  let force = match subcmd.get_flag("force") {
    Some(v) => v
    None => false
  }

  println("Initializing release configuration...")

  // Check if file already exists
  let file_exists = (try? @fs.read_file("release.json")) is Ok(_)

  if file_exists && not(force) {
    println("release.json already exists. Use --force to overwrite.")
    exit_error()
  }

  if dry_run {
    println("Would create: release.json")
    return
  }

  // Write default config as JSON with try-catch-noraise pattern
  let config_json = generate_default_config_json()

  try @fs.write_file("release.json", config_json, create=0o644) catch {
    e => {
      println("Error: Failed to write release.json: " + e.to_string())
      exit_error()
    }
  } noraise {
    _ => ()
  }

  let config = @config.Config::default()
  println("Created release.json with defaults:")
  println("  pr_title: " + config.pr_title)
  println("  allow_prerelease: " + config.allow_prerelease.to_string())
  println("  git_tag_enable: " + config.git_tag_enable.to_string())
  println("  git_release_enable: " + config.git_release_enable.to_string())
  println("  moon_publish: " + config.moon_publish.to_string())
  println("  moon_publish_frozen: " + config.moon_publish_frozen.to_string())
  println("  npm_publish: " + config.npm_publish.to_string())
  println("  semver_check: " + config.semver_check.to_string())
  println("  pr_branch_prefix: " + config.pr_branch_prefix)
  println("  base_branch: " + config.base_branch)
}

///|
/// Generate default config JSON
fn generate_default_config_json() -> String {
  let json =
    #|{
    #|  "pr_title": "chore: release v{{ version }}",
    #|  "pr_draft": false,
    #|  "pr_labels": [],
    #|  "pr_body": "## Release v{{ version }}\n\n{{ changelog }}\n\n---\n*This PR was automatically created by moon-release.*",
    #|  "pr_branch_prefix": "release/",
    #|  "base_branch": "main",
    #|  "allow_prerelease": false,
    #|  "git_tag_enable": true,
    #|  "git_tag_name": "v{{ version }}",
    #|  "git_release_enable": true,
    #|  "git_release_draft": false,
    #|  "git_release_name": "Release v{{ version }}",
    #|  "git_release_body": null,
    #|  "git_release_latest": true,
    #|  "moon_publish": true,
    #|  "moon_publish_frozen": false,
    #|  "moon_publish_timeout": 300,
    #|  "npm_publish": false,
    #|  "npm_publish_provenance": true,
    #|  "npm_publish_timeout": 300,
    #|  "semver_check": false,
    #|  "registry_check": false,
    #|  "allow_dirty": false,
    #|  "bump_all_packages": false,
    #|  "max_analyze_commits": null,
    #|  "custom_major_increment_regex": null,
    #|  "custom_minor_increment_regex": null,
    #|  "npm_build_command": null,
    #|  "packages": []
    #|}
  json
}

///|
/// version command
fn cmd_version() -> Unit {
  println("moon-release v" + version)
  println("")
  println("A release automation tool for MoonBit projects.")
  println("Similar to release-plz, but designed for MoonBit.")
  println("")
  println("Features:")
  println("  - Conventional Commits parsing")
  println("  - Semantic Versioning")
  println("  - moon.mod.json version management")
  println("  - Git tag creation")
  println("  - GitHub release creation")
  println("  - mooncakes.io publishing")
}

///|
/// set-version command
async fn cmd_set_version(
  subcmd : @clap.SimpleValue,
  global_dry_run : Bool,
  verbose : Bool,
) -> Unit {
  // Check subcommand flags as well
  let subcmd_dry_run = match subcmd.get_flag("dry-run") {
    Some(v) => v
    None => false
  }
  let dry_run = global_dry_run || subcmd_dry_run

  // Get version argument (positional argument)
  let version_str : String = if subcmd.positional_args.length() > 0 {
    subcmd.positional_args[0]
  } else {
    println("Error: Version argument is required")
    println("Usage: moon-release set-version ")
    exit_error()
  }

  // Parse version
  let new_version = match @semver.Version::parse(version_str) {
    Some(v) => v
    None => {
      println("Error: Invalid version format: " + version_str)
      println("Expected format: major.minor.patch (e.g., 1.2.3)")
      exit_error()
    }
  }

  if verbose {
    println("Setting version to: " + new_version.to_string())
  }

  // Read current moon.mod.json with try-catch-noraise pattern
  let moon_mod_content = try @fs.read_file("moon.mod.json").text() catch {
    e => {
      println("Error: Failed to read moon.mod.json: " + e.to_string())
      exit_error()
    }
  } noraise {
    r => r
  }

  // Get current version with try-catch-noraise pattern
  let moon_mod = try @moon_mod.parse(moon_mod_content) catch {
    e => {
      println("Error: Failed to parse moon.mod.json: " + e.to_string())
      exit_error()
    }
  } noraise {
    r => r
  }

  if dry_run {
    println("[dry-run] Would update moon.mod.json:")
    println("  Current version: " + moon_mod.version)
    println("  New version: " + new_version.to_string())
    return
  }

  // Update version with try-catch-noraise pattern
  let updated_content = try
    @moon_mod.update_version(moon_mod_content, new_version)
  catch {
    e => {
      println("Error: Failed to update version: " + e.to_string())
      exit_error()
    }
  } noraise {
    r => r
  }

  // Write file with try-catch-noraise pattern
  try @fs.write_file("moon.mod.json", updated_content, create=0o644) catch {
    e => {
      println("Error: Failed to write moon.mod.json: " + e.to_string())
      exit_error()
    }
  } noraise {
    _ => ()
  }

  println("Updated moon.mod.json:")
  println("  " + moon_mod.version + " -> " + new_version.to_string())
}

///|
/// check command
async fn cmd_check(
  subcmd : @clap.SimpleValue,
  _dry_run : Bool,
  verbose : Bool,
  config_path : String,
) -> Unit {
  // Use try? + unwrap_or pattern for consistent error handling
  let output_format = (try? subcmd.get_one("output")).unwrap_or("text")
  let json_output = output_format == "json"

  if not(json_output) {
    println("Checking current state...")
    println("")
    println("Analyzing commits since last tag...")
  }

  // Load config for custom bump rules
  let config = load_config(config_path, verbose~, json_output~)

  // Get Git repository
  let repo = @git.Repository::current()

  // Get latest tag
  let latest_tag = try @workflow.get_latest_tag(repo) catch {
    _ => None
  } noraise {
    r => r
  }

  if not(json_output) {
    match latest_tag {
      Some(tag) => println("Latest tag: " + tag)
      None => println("No tags found")
    }
  }

  // Get commits
  let commits = try
    @workflow.get_commits_since_last_tag(
      repo,
      pr_title=config.pr_title,
      max_commits=config.max_analyze_commits,
    )
  catch {
    e => {
      if json_output {
        println("{\"error\": " + @util.json_string(e.to_string()) + "}")
      } else {
        println("Error getting commits: " + e.to_string())
      }
      exit_error()
    }
  } noraise {
    r => r
  }

  // Determine bump type (uses custom bump rules from config if configured)
  let bump_type = if commits.length() > 0 {
    @semver.determine_bump_with_custom(
      commits.map(fn(c) { c.0 }),
      config.custom_major_increment_regex,
      config.custom_minor_increment_regex,
    )
  } else {
    None
  }

  // JSON output
  if json_output {
    // latest_tag
    let latest_tag_json = match latest_tag {
      Some(tag) => @util.json_string(tag)
      None => "null"
    }
    // suggested_bump
    let bump_json = match bump_type {
      Some(@semver.Major) => "\"major\""
      Some(@semver.Minor) => "\"minor\""
      Some(@semver.Patch) => "\"patch\""
      None => "null"
    }
    // commits array
    let commits_json_arr : Array[String] = []
    for i = 0; i < commits.length(); i = i + 1 {
      let (cc, hash) = commits[i]
      let scope_json = match cc.scope {
        Some(s) => @util.json_string(s)
        None => "null"
      }
      let commit_json = @util.build_json_object([
        ("hash", @util.json_string(hash)),
        ("type", @util.json_string(commit_type_to_string(cc.typ))),
        ("scope", scope_json),
        ("description", @util.json_string(cc.description)),
        ("breaking", cc.breaking.to_string()),
      ])
      commits_json_arr.push(commit_json)
    }
    let commits_json = "[" +
      commits_json_arr
      .iter()
      .fold(init="", fn(acc, s) {
        if acc.is_empty() {
          s
        } else {
          acc + "," + s
        }
      }) +
      "]"

    let json_str = @util.build_json_object([
      ("latest_tag", latest_tag_json),
      ("commits_count", commits.length().to_string()),
      ("suggested_bump", bump_json),
      ("commits", commits_json),
    ])
    println(json_str)
    return
  }

  // Text output
  println("Commits since last tag: \{commits.length()}")

  if verbose && commits.length() > 0 {
    println("")
    println("Commits:")
    for i = 0; i < commits.length() && i < max_verbose_commits; i = i + 1 {
      let (cc, hash) = commits[i]
      let typ_str = commit_type_to_string(cc.typ)
      let scope_str = match cc.scope {
        Some(s) => "(" + s + ")"
        None => ""
      }
      let breaking_str = if cc.breaking { "!" } else { "" }
      let short_hash = @workflow.short_hash(hash)
      println(
        "  \{short_hash} \{typ_str}\{scope_str}\{breaking_str}: \{cc.description}",
      )
    }
    if commits.length() > max_verbose_commits {
      println("  ... and \{commits.length() - max_verbose_commits} more")
    }
  }

  // Display bump type
  if commits.length() > 0 {
    println("")
    match bump_type {
      Some(@semver.Major) => println("Suggested bump: major (breaking changes)")
      Some(@semver.Minor) => println("Suggested bump: minor (new features)")
      Some(@semver.Patch) => println("Suggested bump: patch (bug fixes)")
      None => println("No releasable changes")
    }
  }

  println("")
  println("Status: Ready for release analysis")
  println("")
  println("To perform a release, run:")
  println("  moon-release update --dry-run  # Preview changes")
  println("  moon-release update            # Update files locally")
  println("  moon-release release-pr        # Create release PR")
  println("  moon-release release           # Create release")
}

///|
/// Convert commit type to string
fn commit_type_to_string(typ : @conventional.CommitType) -> String {
  match typ {
    @conventional.Feat => "feat"
    @conventional.Fix => "fix"
    @conventional.Docs => "docs"
    @conventional.Style => "style"
    @conventional.Refactor => "refactor"
    @conventional.Perf => "perf"
    @conventional.Test => "test"
    @conventional.Build => "build"
    @conventional.Ci => "ci"
    @conventional.Chore => "chore"
    @conventional.Revert => "revert"
    @conventional.Custom(s) => s
  }
}

// ======== Helper functions ========

///|
/// Load configuration with try-catch-noraise pattern
async fn load_config(
  path : String,
  verbose? : Bool = false,
  json_output? : Bool = false,
) -> @config.Config {
  // Check if file exists with try-catch-noraise pattern
  let content = try @fs.read_file(path).text() catch {
    e => {
      if verbose {
        output_warning(
          "Config file not found or unreadable (" +
          path +
          "): " +
          e.to_string() +
          ". Using default configuration.",
          json_output,
        )
      }
      return @config.Config::default()
    }
  } noraise {
    r => r
  }

  // Parse config - fail if file exists but is invalid
  let warnings : Array[String] = []
  let config = try @config.Config::parse(content, warnings~) catch {
    e => {
      output_error(
        "Failed to parse config file (" +
        path +
        "): " +
        e.to_string() +
        ". Fix the configuration file or delete it to use defaults.",
        json_output,
      )
      exit_error()
    }
  } noraise {
    r => r
  }

  // Display warnings (suppressed in JSON mode)
  for w in warnings {
    output_warning(w, json_output)
  }

  config
}

///|
/// Load moon.mod.json
async fn load_moon_mod(path : String) -> @moon_mod.MoonMod {
  let content = @fs.read_file(path).text()
  @moon_mod.parse(content)
}

///|
/// Read package name from package.json (best effort)
async fn load_package_json_name(path : String) -> String? {
  let content = @fs.read_file(path).text()
  let json = @json.parse(content)
  match json {
    Object(map) =>
      match map.get("name") {
        Some(String(name)) => Some(name)
        _ => None
      }
    _ => None
  }
}

///|
/// Get bump type
fn get_bump_type(subcmd : @clap.SimpleValue) -> @semver.BumpType? {
  match subcmd.args.get("bump") {
    Some(arr) if arr.length() > 0 =>
      match arr[0] {
        "major" => Some(@semver.Major)
        "minor" => Some(@semver.Minor)
        "patch" => Some(@semver.Patch)
        bump_value => {
          println(
            "Error: Invalid bump value '\{bump_value}'. Expected 'major', 'minor', or 'patch'.",
          )
          exit_error()
        }
      }
    _ => None
  }
}

///|
/// generate-schema command
fn cmd_generate_schema() -> Unit {
  let schema =
    #|{
    #|  "$schema": "http://json-schema.org/draft-07/schema#",
    #|  "title": "moon-release configuration",
    #|  "description": "Configuration file for moon-release, a release automation tool for MoonBit projects",
    #|  "type": "object",
    #|  "properties": {
    #|    "pr_title": {
    #|      "type": "string",
    #|      "description": "Title template for release PR. Use {{ version }} placeholder.",
    #|      "default": "chore: release v{{ version }}"
    #|    },
    #|    "pr_draft": {
    #|      "type": "boolean",
    #|      "description": "Create PR as draft",
    #|      "default": false
    #|    },
    #|    "pr_labels": {
    #|      "type": "array",
    #|      "items": { "type": "string" },
    #|      "description": "Labels to add to release PR",
    #|      "default": []
    #|    },
    #|    "pr_body": {
    #|      "type": "string",
    #|      "description": "Body template for release PR. Use {{ version }} placeholder.",
    #|      "default": "## Release v{{ version }}\n\n{{ changelog }}\n\n---\n*This PR was automatically created by moon-release.*"
    #|    },
    #|    "allow_prerelease": {
    #|      "type": "boolean",
    #|      "description": "Allow prerelease versions",
    #|      "default": false
    #|    },
    #|    "git_tag_enable": {
    #|      "type": "boolean",
    #|      "description": "Create git tag on release",
    #|      "default": true
    #|    },
    #|    "git_tag_name": {
    #|      "type": "string",
    #|      "description": "Tag name template. Use {{ version }} placeholder.",
    #|      "default": "v{{ version }}"
    #|    },
    #|    "git_release_enable": {
    #|      "type": "boolean",
    #|      "description": "Create GitHub release",
    #|      "default": true
    #|    },
    #|    "git_release_draft": {
    #|      "type": "boolean",
    #|      "description": "Create GitHub release as draft",
    #|      "default": false
    #|    },
    #|    "git_release_name": {
    #|      "type": "string",
    #|      "description": "Release name template. Use {{ version }} placeholder.",
    #|      "default": "Release v{{ version }}"
    #|    },
    #|    "git_release_body": {
    #|      "type": ["string", "null"],
    #|      "description": "Release body template. Use {{ version }} and {{ changelog }} placeholders. If null, uses auto-generated changelog.",
    #|      "default": null
    #|    },
    #|    "git_release_latest": {
    #|      "type": "boolean",
    #|      "description": "Mark the GitHub release as latest",
    #|      "default": true
    #|    },
    #|    "moon_publish": {
    #|      "type": "boolean",
    #|      "description": "Publish to mooncakes.io registry",
    #|      "default": true
    #|    },
    #|    "moon_publish_frozen": {
    #|      "type": "boolean",
    #|      "description": "Use --frozen flag when publishing to mooncakes.io",
    #|      "default": false
    #|    },
    #|    "moon_publish_timeout": {
    #|      "type": "integer",
    #|      "minimum": 0,
    #|      "description": "Timeout in seconds for moon publish command",
    #|      "default": 300
    #|    },
    #|    "npm_publish": {
    #|      "type": "boolean",
    #|      "description": "Publish to npm registry (uses Trusted Publishing)",
    #|      "default": false
    #|    },
    #|    "npm_build_command": {
    #|      "type": ["string", "null"],
    #|      "description": "Command to build JS target before npm publish. Default: moon build --target js",
    #|      "default": null
    #|    },
    #|    "npm_publish_provenance": {
    #|      "type": "boolean",
    #|      "description": "Use --provenance flag for npm publish (requires GitHub Actions with id-token: write). Set to false for local or non-GitHub CI publishing.",
    #|      "default": true
    #|    },
    #|    "npm_publish_timeout": {
    #|      "type": "integer",
    #|      "minimum": 0,
    #|      "description": "Timeout in seconds for npm publish command",
    #|      "default": 300
    #|    },
    #|    "semver_check": {
    #|      "type": "boolean",
    #|      "description": "Check semver compatibility before release",
    #|      "default": false
    #|    },
    #|    "pr_branch_prefix": {
    #|      "type": "string",
    #|      "description": "Prefix for release branch names",
    #|      "default": "release/"
    #|    },
    #|    "base_branch": {
    #|      "type": "string",
    #|      "description": "Base branch for release PRs",
    #|      "default": "main"
    #|    },
    #|    "packages": {
    #|      "type": "array",
    #|      "description": "Package configurations for monorepo mode",
    #|      "items": {
    #|        "type": "object",
    #|        "properties": {
    #|          "name": {
    #|            "type": "string",
    #|            "description": "Package name"
    #|          },
    #|          "path": {
    #|            "type": "string",
    #|            "description": "Path to package directory",
    #|            "default": "."
    #|          },
    #|          "moon_publish": {
    #|            "type": "boolean",
    #|            "description": "Publish this package to mooncakes.io",
    #|            "default": true
    #|          },
    #|          "npm_publish": {
    #|            "type": "boolean",
    #|            "description": "Publish this package to npm",
    #|            "default": false
    #|          },
    #|          "npm_build_command": {
    #|            "type": ["string", "null"],
    #|            "description": "Command to build JS target for this package",
    #|            "default": null
    #|          },
    #|          "version_group": {
    #|            "type": ["string", "null"],
    #|            "description": "Version group for synchronized versioning",
    #|            "default": null
    #|          }
    #|        },
    #|        "required": ["name"]
    #|      },
    #|      "default": []
    #|    },
    #|    "registry_check": {
    #|      "type": "boolean",
    #|      "description": "Check if version already exists in registry before release",
    #|      "default": false
    #|    },
    #|    "allow_dirty": {
    #|      "type": "boolean",
    #|      "description": "Allow release with uncommitted changes",
    #|      "default": false
    #|    },
    #|    "custom_major_increment_regex": {
    #|      "type": ["string", "null"],
    #|      "description": "Custom regex pattern for major version increment commits",
    #|      "default": null
    #|    },
    #|    "custom_minor_increment_regex": {
    #|      "type": ["string", "null"],
    #|      "description": "Custom regex pattern for minor version increment commits",
    #|      "default": null
    #|    },
    #|    "max_analyze_commits": {
    #|      "type": ["integer", "null"],
    #|      "minimum": 1,
    #|      "description": "Maximum number of commits to analyze for version determination",
    #|      "default": null
    #|    },
    #|    "bump_all_packages": {
    #|      "type": "boolean",
    #|      "description": "Bump all packages uniformly using the highest bump type from changed packages (fixed versioning for monorepo)",
    #|      "default": false
    #|    }
    #|  },
    #|  "additionalProperties": false
    #|}
  println(schema)
}