// cli.mbt — Command-line dispatch for the `disposition-tool` executable.
//
// The whole CLI lives in the library as pure functions that return JSON
// strings, so every command is unit-testable and the `cmd/disposition-tool`
// package is a thin wrapper that prints `run_cli`'s output. No command ever
// unwraps: every `Result` is rendered either as a success JSON object or as
// an error JSON object carrying the structured `DispositionError`.
//
// Every command's output is a single JSON object with an `"ok"` boolean and
// a `"command"` field:
//
//   - `parse `            the parsed model
//   - `serialize `        parse then canonical serialisation
//   - `canonicalize `     parse then canonicalise
//   - `resolve `          parse then resolve the download filename
//   - `sanitize  [profile]` sanitise a filename under a profile
//   - `generate  `   generate an attachment/inline value
//   - `audit `            parse and audit
//   - `limits [preset]`          show the resource limits of a preset
//   - `profiles`                 describe the sanitisation profiles
//   - `version`                  the library version string
//   - `help`                     this usage message

///|
/// Runs the CLI with the given arguments (excluding the program name) and
/// returns the JSON output. Never raises, never unwraps.
pub fn run_cli(args : Array[String]) -> String {
  if args.is_empty() {
    return json_error("no-command", "no command given; try 'help'")
  }
  let command = args[0].to_lower()
  match command {
    "parse" => cmd_parse(args[1:].to_owned())
    "serialize" => cmd_serialize(args[1:].to_owned())
    "canonicalize" => cmd_canonicalize(args[1:].to_owned())
    "resolve" => cmd_resolve(args[1:].to_owned())
    "sanitize" => cmd_sanitize(args[1:].to_owned())
    "generate" => cmd_generate(args[1:].to_owned())
    "audit" => cmd_audit(args[1:].to_owned())
    "limits" => cmd_limits(args[1:].to_owned())
    "profiles" => cmd_profiles()
    "version" => cmd_version()
    "help" => cmd_help()
    _ => json_error("unknown-command", "unknown command '\{command}'; try 'help'")
  }
}

fn cmd_parse(args : Array[String]) -> String {
  match arg_join(args) {
    Err(msg) => json_error("usage", msg)
    Ok(input) => {
      match parse_content_disposition(input) {
        Ok(cd) => {
          let sb = StringBuilder()
          sb.write_string("{\"ok\":true,\"command\":\"parse\",\"disposition_type\":")
          sb.write_string(json_string(cd.disposition_type().to_lower_name()))
          sb.write_string(",\"parameters\":[")
          for i = 0; i < cd.parameters().length(); i = i + 1 {
            if i > 0 {
              sb.write_string(",")
            }
            sb.write_string(parameter_json(cd.parameters()[i]))
          }
          sb.write_string("]}")
          sb.to_string()
        }
        Err(e) => json_error_result(e, "parse")
      }
    }
  }
}

fn cmd_serialize(args : Array[String]) -> String {
  match arg_join(args) {
    Err(msg) => json_error("usage", msg)
    Ok(input) => {
      match parse_content_disposition(input) {
        Ok(cd) => {
          match serialize_content_disposition(cd) {
            Ok(s) => json_ok_str("serialize", "value", s)
            Err(e) => json_error_result(e, "serialize")
          }
        }
        Err(e) => json_error_result(e, "serialize")
      }
    }
  }
}

fn cmd_canonicalize(args : Array[String]) -> String {
  match arg_join(args) {
    Err(msg) => json_error("usage", msg)
    Ok(input) => {
      match canonicalize_content_disposition(input) {
        Ok(s) => json_ok_str("canonicalize", "value", s)
        Err(e) => json_error_result(e, "canonicalize")
      }
    }
  }
}

fn cmd_resolve(args : Array[String]) -> String {
  match arg_join(args) {
    Err(msg) => json_error("usage", msg)
    Ok(input) => {
      match parse_content_disposition(input) {
        Ok(cd) => {
          match resolve_filename(cd) {
            Ok(sel) => {
              let sb = StringBuilder()
              sb.write_string("{\"ok\":true,\"command\":\"resolve\",\"filename\":")
              sb.write_string(json_string(sel.selected()))
              sb.write_string(",\"source\":")
              sb.write_string(json_string(filename_source_name(sel.source())))
              sb.write_string(",\"fallback\":")
              sb.write_string(json_bool(sel.fallback()))
              sb.write_string(",\"warnings\":[")
              write_string_array(sb, sel.warnings())
              sb.write_string("]}")
              sb.to_string()
            }
            Err(e) => json_error_result(e, "resolve")
          }
        }
        Err(e) => json_error_result(e, "resolve")
      }
    }
  }
}

fn cmd_sanitize(args : Array[String]) -> String {
  if args.is_empty() {
    return json_error("usage", "sanitize needs a filename")
  }
  let name = args[0]
  let policy = match profile_arg(args, 1) {
    Ok(p) => p
    Err(msg) => return json_error("usage", msg)
  }
  match sanitize_filename(name, policy) {
    Ok(r) => {
      let sb = StringBuilder()
      sb.write_string("{\"ok\":true,\"command\":\"sanitize\",\"original\":")
      sb.write_string(json_string(r.original()))
      sb.write_string(",\"safe\":")
      sb.write_string(json_string(r.safe()))
      sb.write_string(",\"changed\":")
      sb.write_string(json_bool(r.changed()))
      sb.write_string(",\"issues\":[")
      write_string_array(sb, r.issues())
      sb.write_string("]}")
      sb.to_string()
    }
    Err(e) => json_error_result(e, "sanitize")
  }
}

fn cmd_generate(args : Array[String]) -> String {
  if args.length() < 2 {
    return json_error("usage", "generate needs a disposition type and a filename")
  }
  let dtype = args[0].to_lower()
  let name = args[1]
  let result = match dtype {
    "attachment" => generate_attachment(name)
    "inline" => generate_inline(name)
    _ => return json_error("usage", "disposition type must be 'attachment' or 'inline'")
  }
  match result {
    Ok(s) => json_ok_str("generate", "value", s)
    Err(e) => json_error_result(e, "generate")
  }
}

fn cmd_audit(args : Array[String]) -> String {
  match arg_join(args) {
    Err(msg) => json_error("usage", msg)
    Ok(input) => {
      match audit_header(input) {
        Ok(report) => {
          let sb = StringBuilder()
          sb.write_string("{\"ok\":true,\"command\":\"audit\",\"issues\":[")
          for i = 0; i < report.issues().length(); i = i + 1 {
            if i > 0 {
              sb.write_string(",")
            }
            let issue = report.issues()[i]
            sb.write_string("{\"severity\":")
            sb.write_string(json_string(issue.severity().to_string()))
            sb.write_string(",\"kind\":")
            sb.write_string(json_string(issue.kind().to_string()))
            sb.write_string(",\"parameter\":")
            sb.write_string(json_string(issue.parameter()))
            sb.write_string(",\"message\":")
            sb.write_string(json_string(issue.message()))
            sb.write_string("}")
          }
          sb.write_string("]}")
          sb.to_string()
        }
        Err(e) => json_error_result(e, "audit")
      }
    }
  }
}

fn cmd_limits(args : Array[String]) -> String {
  let limits = if args.is_empty() {
    Limits::default()
  } else {
    match args[0].to_lower() {
      "strict" => Limits::strict()
      "permissive" => Limits::permissive()
      "default" => Limits::default()
      _ => return json_error("usage", "limits preset must be default, strict or permissive")
    }
  }
  let sb = StringBuilder()
  sb.write_string("{\"ok\":true,\"command\":\"limits\",\"preset\":")
  sb.write_string(json_string(if args.is_empty() { "default" } else { args[0].to_lower() }))
  sb.write_string(",\"max_input_bytes\":")
  sb.write_string(limits.max_input_bytes().to_string())
  sb.write_string(",\"max_parameters\":")
  sb.write_string(limits.max_parameters().to_string())
  sb.write_string(",\"max_parameter_name_bytes\":")
  sb.write_string(limits.max_parameter_name_bytes().to_string())
  sb.write_string(",\"max_parameter_value_bytes\":")
  sb.write_string(limits.max_parameter_value_bytes().to_string())
  sb.write_string(",\"max_filename_bytes\":")
  sb.write_string(limits.max_filename_bytes().to_string())
  sb.write_string(",\"max_extended_value_bytes\":")
  sb.write_string(limits.max_extended_value_bytes().to_string())
  sb.write_string(",\"max_context_bytes\":")
  sb.write_string(limits.max_context_bytes().to_string())
  sb.write_string("}")
  sb.to_string()
}

fn cmd_profiles() -> String {
  let sb = StringBuilder()
  sb.write_string("{\"ok\":true,\"command\":\"profiles\",\"profiles\":[")
  let names = ["portable", "windows-like", "posix-like"]
  for i = 0; i < names.length(); i = i + 1 {
    if i > 0 {
      sb.write_string(",")
    }
    sb.write_string("{\"name\":")
    sb.write_string(json_string(names[i]))
    sb.write_string(",\"max_length\":\"255\"")
    sb.write_string(",\"reserved_names_defused\":")
    sb.write_string(if names[i] == "posix-like" { "false" } else { "true" })
    sb.write_string("}")
  }
  sb.write_string("]}")
  sb.to_string()
}

fn cmd_version() -> String {
  "{\"ok\":true,\"command\":\"version\",\"version\":\{json_string(library_version())}}"
}

fn cmd_help() -> String {
  "{\"ok\":true,\"command\":\"help\",\"usage\":\"disposition-tool  [args...]\",\"commands\":[\"parse\",\"serialize\",\"canonicalize\",\"resolve\",\"sanitize\",\"generate\",\"audit\",\"limits\",\"profiles\",\"version\",\"help\"]}"
}

// --- helpers ---------------------------------------------------------------

// Joins arguments into one input string (the header value may contain
// spaces). Empty input is an error: a parse of "" would fail anyway, but
// failing here keeps the usage message clear.
fn arg_join(args : Array[String]) -> Result[String, String] {
  if args.is_empty() {
    return Err("this command needs a header value argument")
  }
  let sb = StringBuilder()
  for i = 0; i < args.length(); i = i + 1 {
    if i > 0 {
      sb.write_string(" ")
    }
    sb.write_string(args[i])
  }
  Ok(sb.to_string())
}

// The policy for a `sanitize` profile argument (index `i`), defaulting to
// portable.
fn profile_arg(args : Array[String], i : Int) -> Result[FilenamePolicy, String] {
  if i >= args.length() || args[i] == "" {
    return Ok(FilenamePolicy::portable())
  }
  match args[i].to_lower() {
    "portable" => Ok(FilenamePolicy::portable())
    "windows-like" => Ok(FilenamePolicy::windows_like())
    "posix-like" => Ok(FilenamePolicy::posix_like())
    _ => Err("profile must be portable, windows-like or posix-like")
  }
}

// The JSON object for one parameter.
fn parameter_json(p : DispositionParameter) -> String {
  let sb = StringBuilder()
  sb.write_string("{\"name\":")
  sb.write_string(json_string(p.name()))
  sb.write_string(",\"value\":")
  match p.value() {
    Token(v) => {
      sb.write_string(json_string(v))
      sb.write_string(",\"form\":\"token\"")
    }
    Quoted(v) => {
      sb.write_string(json_string(v))
      sb.write_string(",\"form\":\"quoted\"")
    }
    Extended(ev) => {
      sb.write_string(json_string(ev.value()))
      sb.write_string(",\"form\":\"extended\",\"charset\":")
      sb.write_string(json_string(ev.charset()))
      sb.write_string(",\"language\":")
      match ev.language() {
        Some(l) => sb.write_string(json_string(l))
        None => sb.write_string("null")
      }
    }
  }
  sb.write_string("}")
  sb.to_string()
}

// Renders a structured error as an error JSON object.
fn json_error_result(e : DispositionError, command : String) -> String {
  let sb = StringBuilder()
  sb.write_string("{\"ok\":false,\"command\":")
  sb.write_string(json_string(command))
  sb.write_string(",\"error\":{\"stage\":")
  sb.write_string(json_string(e.stage().to_string()))
  sb.write_string(",\"kind\":")
  sb.write_string(json_string(e.kind().to_string()))
  sb.write_string(",\"offset\":")
  sb.write_string(e.offset().to_string())
  sb.write_string(",\"context\":")
  sb.write_string(json_string(e.context()))
  sb.write_string("}}")
  sb.to_string()
}

// A generic error JSON object.
fn json_error(command : String, message : String) -> String {
  "{\"ok\":false,\"command\":\{json_string(command)},\"error\":\{json_string(message)}}"
}

// A success JSON object with one string field.
fn json_ok_str(command : String, key : String, value : String) -> String {
  "{\"ok\":true,\"command\":\{json_string(command)},\{json_string(key)}:\{json_string(value)}}"
}

// Writes `["a","b"]` (or `[]`) to the builder.
fn write_string_array(sb : StringBuilder, values : Array[String]) -> Unit {
  for i = 0; i < values.length(); i = i + 1 {
    if i > 0 {
      sb.write_string(",")
    }
    sb.write_string(json_string(values[i]))
  }
}

// A JSON string literal (UTF-8 passthrough, escaping quote, backslash and
// control characters).
fn json_string(s : String) -> String {
  let sb = StringBuilder()
  sb.write_string("\"")
  for ch in s {
    let v = ch.to_int()
    if ch == '"' {
      sb.write_string("\\\"")
    } else if ch == '\\' {
      sb.write_string("\\\\")
    } else if v == 0x08 {
      sb.write_string("\\b")
    } else if v == 0x0C {
      sb.write_string("\\f")
    } else if v == 0x0A {
      sb.write_string("\\n")
    } else if v == 0x0D {
      sb.write_string("\\r")
    } else if v == 0x09 {
      sb.write_string("\\t")
    } else if v < 0x20 {
      // Four lowercase hex digits: control characters only (v < 0x20), so
      // the two high nibbles are always "00".
      sb.write_string("\\u00")
      sb.write_char(hex_nibble(v >> 4))
      sb.write_char(hex_nibble(v & 0xF))
    } else {
      sb.write_char(ch)
    }
  }
  sb.write_string("\"")
  sb.to_string()
}

fn json_bool(b : Bool) -> String {
  if b {
    "true"
  } else {
    "false"
  }
}

// A single lowercase hex digit character (0-9 a-f) for a nibble 0-15.
fn hex_nibble(n : Int) -> Char {
  if n < 10 {
    ('0'.to_int() + n).to_char().unwrap()
  } else {
    ('a'.to_int() + n - 10).to_char().unwrap()
  }
}

// A stable programmatic name for a FilenameSource, shared with the tests.
fn filename_source_name(src : FilenameSource) -> String {
  match src {
    Filename => "filename"
    FilenameStar => "filename*"
  }
}