// Command-line parsing.
//
// DEVIATION FROM THE PLAN: this is hand-rolled rather than built on
// `moonbitlang/core/argparse`. Three properties of the reference CLI's contract drove
// that:
//
//   * commander's `.allowUnknownOption()` silently drops unrecognised options;
//   * its variadic `` options take several values per occurrence;
//   * the leading run of non-option tokens is a `[target...]` positional that is
//     itself a command line (`node build/index.js --method ...`).
//
// Normalising all three would take a pre-pass as long as this file, after which
// argparse would only be contributing conflict checks. Worse, its `ArgError` is `priv`,
// so parse failures could not carry the reference's exact messages -- and those
// messages are part of the contract we are matching (see docs/reference-behaviour.md
// section 3). Hand-rolling keeps the error text under our control.

///|
/// Options that take a value.
let value_options : Array[String] = [
  "--catalog", "--config", "--server", "--method", "--tool-name", "--tool-arg", "--tool-args-json",
  "--uri", "--prompt-name", "--prompt-args", "--log-level", "--cwd", "--transport",
  "--server-url", "--header", "--metadata", "--tool-metadata", "--connect-timeout",
  "--format", "--client-config", "--client-id", "--client-secret", "--client-metadata-url",
  "--callback-url", "--wait-for-auth", "-e",
  // subscriptions/listen
   "--watch-resource", "--max-events", "--listen-timeout",
  // tasks extension
   "--task-id", "--input-responses", "--max-polls",
]

///|
/// Options that take several values per occurrence, mirroring commander's
/// ``: `--tool-arg a=1 b=2` sets both.
let variadic_options : Array[String] = [
  "--tool-arg", "--prompt-args", "--header", "--metadata", "--tool-metadata", "--watch-resource",
]

///|
/// Boolean flags.
let flag_options : Array[String] = [
  "--app-info", "--content-info", "--use-stored-auth", "--stored-auth-only", "--relogin",
  "--list-stored-auth", "--print-handoff", "--help", "-h", "--version", "-V",
  // subscriptions/listen
   "--watch-tools", "--watch-prompts", "--watch-resources", "--follow-task",
]

///|
/// Everything the CLI was asked to do.
pub struct Args {
  /// The ad-hoc target: a command line, or a URL.
  target : Array[String]
  /// Repeatable options, in order of appearance.
  multi : Map[String, Array[String]]
  /// Single-valued options; last occurrence wins, as commander does.
  single : Map[String, String]
  flags : Map[String, Bool]
}

///|
pub fn Args::flag(self : Self, name : String) -> Bool {
  self.flags.get(name).unwrap_or(false)
}

///|
pub fn Args::opt(self : Self, name : String) -> String? {
  self.single.get(name)
}

///|
pub fn Args::list(self : Self, name : String) -> Array[String] {
  self.multi.get(name).unwrap_or([])
}

///|
/// Raised for anything that makes the command line unusable. Maps to exit 1.
pub(all) suberror UsageError {
  UsageError(String)
} derive(Debug)

///|
/// See the note on `CatalogError`: `Show` is what makes the message survive being
/// caught as a generic `Error`.
pub impl Show for UsageError with fn output(self, logger) {
  let UsageError(m) = self
  logger.write_string(m)
}

///|
/// Parse argv (without the program name).
///
/// The target/option split follows the reference: with an explicit `--`, everything
/// BEFORE it is the target and everything after is options; otherwise the target is the
/// leading run of tokens that do not start with `-`. That ordering is what lets
/// `inspector-cli node server.js --method tools/list` work without quoting.
pub fn parse_args(argv : Array[String]) -> Args raise UsageError {
  let (target, rest) = split_target(argv)
  let multi : Map[String, Array[String]] = Map([])
  let single : Map[String, String] = Map([])
  let flags : Map[String, Bool] = Map([])
  let mut i = 0
  while i < rest.length() {
    let tok = rest[i]
    if !tok.has_prefix("-") {
      // A stray positional after the options began. commander would treat it as an
      // excess argument; we ignore it, as `.allowExcessArguments` does.
      i = i + 1
      continue
    }
    // Support --name=value as well as --name value.
    let (name, inline_value) = match tok.find("=") {
      Some(eq) if tok.has_prefix("--") =>
        (tok[:eq].to_owned(), Some(tok[eq + 1:].to_owned()))
      _ => (tok, None)
    }
    if flag_options.contains(name) {
      flags[name] = true
      i = i + 1
      continue
    }
    if !value_options.contains(name) {
      // Unknown option: drop it, and drop a following value-looking token, exactly
      // as commander's allowUnknownOption does.
      i = i + 1
      if inline_value is None &&
        i < rest.length() &&
        !rest[i].has_prefix("-") &&
        !is_known(rest[i]) {
        i = i + 1
      }
      continue
    }
    // Collect this option's value(s).
    let values : Array[String] = []
    match inline_value {
      Some(v) => {
        values.push(v)
        i = i + 1
      }
      None => {
        i = i + 1
        if variadic_options.contains(name) {
          // Consume until the next option token.
          while i < rest.length() && !rest[i].has_prefix("-") {
            values.push(rest[i])
            i = i + 1
          }
        } else if i < rest.length() {
          values.push(rest[i])
          i = i + 1
        }
      }
    }
    if values.is_empty() {
      raise UsageError("option '\{name}' requires a value")
    }
    if variadic_options.contains(name) || name == "-e" {
      let existing = multi.get(name).unwrap_or([])
      for v in values {
        existing.push(v)
      }
      multi[name] = existing
    } else {
      single[name] = values[values.length() - 1]
    }
  }
  { target, multi, single, flags }
}

///|
fn is_known(tok : String) -> Bool {
  value_options.contains(tok) || flag_options.contains(tok)
}

///|
/// Split argv into the ad-hoc target and the option tokens.
fn split_target(argv : Array[String]) -> (Array[String], Array[String]) {
  // An explicit `--` means: target first, options after. Note this is the reverse of
  // the usual shell convention, and matches the reference CLI.
  for i, tok in argv {
    if tok == "--" {
      return (argv[:i].to_owned(), argv[i + 1:].to_owned())
    }
  }
  let mut n = 0
  while n < argv.length() && !argv[n].has_prefix("-") {
    n = n + 1
  }
  (argv[:n].to_owned(), argv[n:].to_owned())
}

// --- Value coercion ----------------------------------------------------------

///|
/// Parse a `key=value` pair, JSON-coercing the value.
///
/// This is what makes `--tool-arg count=1` a number and `--tool-arg name=hello` a
/// string: the value is offered to the JSON parser first and falls back to a string.
pub fn parse_kv(pair : String) -> (String, Json) raise UsageError {
  guard pair.find("=") is Some(eq) else {
    raise UsageError("Invalid parameter format: \{pair}. Use key=value format.")
  }
  let key = pair[:eq].to_owned()
  let value = pair[eq + 1:].to_owned()
  if key == "" || value == "" {
    raise UsageError("Invalid parameter format: \{pair}. Use key=value format.")
  }
  let coerced = @json.parse(value) catch { _ => Json::string(value) }
  (key, coerced)
}

///|
/// Parse a `Name: Value` header pair. Split on the FIRST colon so a URL in the value
/// survives.
pub fn parse_header(pair : String) -> (String, String) raise UsageError {
  guard pair.find(":") is Some(colon) else {
    raise UsageError(
      "Invalid header format: \{pair}. Use \"HeaderName: Value\" format.",
    )
  }
  let name = pair[:colon].trim(chars=" ").to_owned()
  let value = pair[colon + 1:].trim(chars=" ").to_owned()
  if name == "" {
    raise UsageError(
      "Invalid header format: \{pair}. Use \"HeaderName: Value\" format.",
    )
  }
  (name, value)
}

///|
/// Collect repeated `key=value` options into a JSON object.
pub fn collect_kv(pairs : Array[String]) -> Map[String, Json] raise UsageError {
  let out : Map[String, Json] = Map([])
  for p in pairs {
    let (k, v) = parse_kv(p)
    out[k] = v
  }
  out
}

///|
/// Metadata values are carried as strings: a non-string coerced value is re-encoded
/// rather than stringified structurally, so `{"a":1}` stays JSON and not "[object ...]".
pub fn collect_meta(
  pairs : Array[String],
) -> Map[String, Json] raise UsageError {
  let out : Map[String, Json] = Map([])
  for p in pairs {
    let (k, v) = parse_kv(p)
    out[k] = match v {
      String(_) => v
      _ => Json::string(v.stringify())
    }
  }
  out
}