///|
/// Top-level Moon Doctor command.
pub(all) enum DoctorCommand {
  Check
  Rules
  Explain(String)
} derive(Debug, Eq)

///|
/// Request parsed from both modern subcommands and legacy check syntax.
pub(all) struct CommandRequest {
  command : DoctorCommand
  target : String
  format : OutputFormat
  strict : Bool
  help : Bool
  config_path : String?
  profile : String?
} derive(Debug, Eq)

///|
pub fn CommandRequest::default() -> CommandRequest {
  {
    command: Check,
    target: ".",
    format: Text,
    strict: false,
    help: false,
    config_path: None,
    profile: None,
  }
}

///|
fn command_format(value : String) -> OutputFormat? {
  match value {
    "text" => Some(Text)
    "json" => Some(Json)
    "sarif" => Some(Sarif)
    _ => None
  }
}

///|
fn replace_request(
  current : CommandRequest,
  command? : DoctorCommand,
  target? : String,
  format? : OutputFormat,
  strict? : Bool,
  help? : Bool,
  config_path? : String?,
  profile? : String?,
) -> CommandRequest {
  let next_command = match command {
    Some(value) => value
    None => current.command
  }
  let next_target = match target {
    Some(value) => value
    None => current.target
  }
  let next_format = match format {
    Some(value) => value
    None => current.format
  }
  let next_strict = match strict {
    Some(value) => value
    None => current.strict
  }
  let next_help = match help {
    Some(value) => value
    None => current.help
  }
  let next_config_path = match config_path {
    Some(value) => value
    None => current.config_path
  }
  let next_profile = match profile {
    Some(value) => value
    None => current.profile
  }
  {
    command: next_command,
    target: next_target,
    format: next_format,
    strict: next_strict,
    help: next_help,
    config_path: next_config_path,
    profile: next_profile,
  }
}

///|
fn parse_command_at(
  args : Array[String],
  index : Int,
  request : CommandRequest,
  command_seen : Bool,
) -> Result[CommandRequest, String] {
  if index >= args.length() {
    return Ok(request)
  }
  let token = args[index]
  match token {
    "--help" | "-h" =>
      parse_command_at(
        args,
        index + 1,
        replace_request(request, help=true),
        command_seen,
      )
    "--strict" =>
      parse_command_at(
        args,
        index + 1,
        replace_request(request, strict=true),
        command_seen,
      )
    "--format" =>
      if index + 1 >= args.length() {
        Err("--format requires text, json, or sarif")
      } else {
        match command_format(args[index + 1]) {
          Some(format) =>
            parse_command_at(
              args,
              index + 2,
              replace_request(request, format~),
              command_seen,
            )
          None => Err("unsupported format: " + args[index + 1])
        }
      }
    "--config" =>
      if index + 1 >= args.length() {
        Err("--config requires a path")
      } else {
        parse_command_at(
          args,
          index + 2,
          replace_request(request, config_path=Some(args[index + 1])),
          command_seen,
        )
      }
    "--profile" =>
      if index + 1 >= args.length() {
        Err("--profile requires default, github, mooncakes, or release")
      } else {
        parse_command_at(
          args,
          index + 2,
          replace_request(request, profile=Some(args[index + 1])),
          command_seen,
        )
      }
    "check" =>
      if command_seen {
        Err("command specified more than once")
      } else {
        parse_command_at(
          args,
          index + 1,
          replace_request(request, command=Check),
          true,
        )
      }
    "rules" =>
      if command_seen {
        Err("command specified more than once")
      } else {
        parse_command_at(
          args,
          index + 1,
          replace_request(request, command=Rules),
          true,
        )
      }
    "explain" =>
      if command_seen {
        Err("command specified more than once")
      } else if index + 1 >= args.length() {
        Err("explain requires a rule id")
      } else {
        parse_command_at(
          args,
          index + 2,
          replace_request(request, command=Explain(args[index + 1])),
          true,
        )
      }
    _ =>
      if token.has_prefix("-") {
        Err("unsupported option: " + token)
      } else if request.target == "." && request.command == Check {
        parse_command_at(
          args,
          index + 1,
          replace_request(request, target=token),
          command_seen,
        )
      } else {
        Err("unexpected argument: " + token)
      }
  }
}

///|
/// Parse process arguments. Omit a subcommand to preserve legacy check usage.
pub fn parse_command_request(
  args : Array[String],
) -> Result[CommandRequest, String] {
  parse_command_at(args, 1, CommandRequest::default(), false)
}

///|
pub fn command_usage() -> String {
  let usage =
    #|Usage: moon run cmd/main -- [check] [--format text|json|sarif] [--strict] [--config path] [--profile name] [path]
    #|       moon run cmd/main -- rules [--format text|json]
    #|       moon run cmd/main -- explain  [--format text|json]
    #|
    #|Commands:
    #|  check     Analyze a project. This is the default command.
    #|  rules     List the stable rule catalog.
    #|  explain   Show one rule's category, severity, rationale and remediation.
    #|
  usage
}

///|
pub fn render_rule_explanation(rule_id : String) -> String? {
  match find_rule_definition(rule_id) {
    Some(rule) =>
      Some(
        "Rule: " +
        rule.id +
        "\n" +
        "Category: " +
        rule.category.label() +
        "\n" +
        "Default severity: " +
        rule.default_severity.label() +
        "\n" +
        "Title: " +
        rule.title +
        "\n\n" +
        rule.description +
        "\n\nSuggested fix: " +
        rule.remediation +
        "\n",
      )
    None => None
  }
}

///|
pub fn render_rule_explanation_json(rule_id : String) -> String? {
  match find_rule_definition(rule_id) {
    Some(rule) =>
      Some(
        Json::object({
          "id": Json::string(rule.id),
          "category": Json::string(rule.category.label()),
          "default_severity": Json::string(rule.default_severity.label()),
          "title": Json::string(rule.title),
          "description": Json::string(rule.description),
          "remediation": Json::string(rule.remediation),
          "profiles": Json::array(rule.profiles.map(Json::string)),
        }).stringify(),
      )
    None => None
  }
}