// commands.mbt — the concrete subcommand implementations of weblink-tool.
//
// Every command reads its input from `CliOptions` (the `--input` argument or
// the first positional), runs the matching library function, and writes a
// deterministic report to stdout. On error the CLI prints one `error:` line
// (stage, kind, byte offset and bounded context) and continues; it never
// aborts the process, because a parser toolkit must be safe to script.

///|
fn print_error(e : @weblink.LinkError) -> Unit {
  println(
    "error: \{e.stage().to_string()}::\{e.kind().to_string()} at byte \{e.offset().to_string()}: \{e.context()}",
  )
}

///|
fn require_input(o : CliOptions) -> String {
  let input = get_input(o)
  if input.length() == 0 {
    println(
      "error: no input; pass the document with --input  or as a positional",
    )
  }
  input
}

///|
/// `parse`: parse a Link header field value and pretty-print each link, or
/// emit machine-readable JSON with `--json`.
fn cmd_parse(o : CliOptions) -> Unit {
  let input = require_input(o)
  if input.length() == 0 {
    return
  }
  let limits = resolve_limits(o.limits_preset)
  match @weblink.parse_link_header(input, limits) {
    Ok(links) => {
      if o.json {
        println(links_json(links))
      } else {
        for i = 0; i < links.length(); i = i + 1 {
          println(render_link(i, links[i]))
        }
      }
      println("links: \{links.length().to_string()}")
    }
    Err(e) => print_error(e)
  }
}

///|
/// `validate`: report whether the input is a well-formed Link header value.
fn cmd_validate(o : CliOptions) -> Unit {
  let input = require_input(o)
  if input.length() == 0 {
    return
  }
  match @weblink.parse_link_header(input, resolve_limits(o.limits_preset)) {
    Ok(links) => println("valid: true (\{links.length().to_string()} links)")
    Err(e) => {
      println("valid: false")
      print_error(e)
    }
  }
}

///|
/// `canonicalize`: emit the canonical RFC 8288 Link header serialization of
/// the parsed input.
fn cmd_canonicalize(o : CliOptions) -> Unit {
  let input = require_input(o)
  if input.length() == 0 {
    return
  }
  let result = if o.limits_preset == "default" {
    @weblink.canonicalize_link_header(input)
  } else {
    @weblink.canonicalize_link_header_with_limits(
      input,
      resolve_limits(o.limits_preset),
    )
  }
  match result {
    Ok(canon) => println(canon)
    Err(e) => print_error(e)
  }
}

///|
/// `query`: find links matching `--rel`, `--type` and/or `--hreflang`
/// filters (combined with AND) and print their targets.
fn cmd_query(o : CliOptions) -> Unit {
  let input = require_input(o)
  if input.length() == 0 {
    return
  }
  if o.rel == "" && o.media_type == "" && o.hreflang == "" {
    println("error: query needs at least one of --rel, --type, --hreflang")
    return
  }
  match @weblink.parse_link_header(input, resolve_limits(o.limits_preset)) {
    Err(e) => print_error(e)
    Ok(links) => {
      let mut matches = links
      if o.rel != "" {
        matches = @weblink.filter_by_relation(matches, o.rel)
      }
      if o.media_type != "" {
        matches = @weblink.filter_by_type(matches, o.media_type)
      }
      if o.hreflang != "" {
        matches = @weblink.filter_by_hreflang(matches, o.hreflang)
      }
      if o.json {
        println(links_json(matches))
      } else {
        for l in matches {
          println(l.target())
        }
      }
      println("matches: \{matches.length().to_string()}")
    }
  }
}

///|
/// `to-linkset-json`: convert a Link header (or a linkset text document) to
/// an `application/linkset+json` document.
fn cmd_to_linkset_json(o : CliOptions) -> Unit {
  let input = require_input(o)
  if input.length() == 0 {
    return
  }
  let limits = resolve_limits(o.limits_preset)
  let result = if input.trim().has_prefix("{") {
    @weblink.linkset_text_to_linkset_json(input, limits)
  } else {
    @weblink.link_header_to_linkset_json(input, limits)
  }
  match result {
    Ok(json) => println(json)
    Err(e) => print_error(e)
  }
}

///|
/// `from-linkset-json`: convert an `application/linkset+json` document to a
/// canonical Link header value.
fn cmd_from_linkset_json(o : CliOptions) -> Unit {
  let input = require_input(o)
  if input.length() == 0 {
    return
  }
  match
    @weblink.linkset_json_to_link_header(input, resolve_limits(o.limits_preset)) {
    Ok(header) => println(header)
    Err(e) => print_error(e)
  }
}

///|
/// `to-linkset-text`: convert a Link header or a linkset JSON document to an
/// `application/linkset` text document.
fn cmd_to_linkset_text(o : CliOptions) -> Unit {
  let input = require_input(o)
  if input.length() == 0 {
    return
  }
  let limits = resolve_limits(o.limits_preset)
  let result = if input.trim().has_prefix("{") {
    @weblink.linkset_json_to_linkset_text(input, limits)
  } else {
    match @weblink.link_header_to_linkset(input, limits) {
      Ok(set) => Ok(@weblink.linkset_to_linkset_text(set))
      Err(e) => Err(e)
    }
  }
  match result {
    Ok(text) => println(text)
    Err(e) => print_error(e)
  }
}

///|
/// `relation`: query the offline IANA Link Relation Types registry snapshot.
fn cmd_relation(o : CliOptions) -> Unit {
  if o.count {
    println(
      "registered relations: \{@weblink.registered_relation_count().to_string()}",
    )
    return
  }
  if o.list {
    for info in @weblink.registered_relations() {
      println(info.name())
    }
    println(
      "registered relations: \{@weblink.registered_relation_count().to_string()}",
    )
    return
  }
  let name = if o.rel != "" {
    o.rel
  } else if o.positionals.length() > 0 {
    o.positionals[0]
  } else {
    println("error: relation needs a name, --count or --list")
    return
  }
  if @weblink.is_registered_relation(name) {
    match @weblink.relation_info(name) {
      Some(info) => {
        println("relation: \{info.name()}")
        println("registered: true")
        println("description: \{info.description()}")
        if info.reference().length() > 0 {
          println("reference: \{info.reference()}")
        }
        if info.notes().length() > 0 {
          println("notes: \{info.notes()}")
        }
      }
      None => println("relation: \{name}\nregistered: true")
    }
  } else {
    println("relation: \{name}")
    println("registered: false (not in the offline IANA snapshot)")
  }
}

///|
/// `audit`: run the deterministic audit over a parsed Link header value.
fn cmd_audit(o : CliOptions) -> Unit {
  let input = require_input(o)
  if input.length() == 0 {
    return
  }
  match
    @weblink.audit_link_header_with_limits(
      input,
      resolve_limits(o.limits_preset),
    ) {
    Err(e) => print_error(e)
    Ok(report) => {
      if o.json {
        println(audit_json(report))
        return
      }
      if report.is_clean() {
        println("audit: clean")
      } else {
        for issue in report.issues() {
          println(
            "\{issue.severity().to_string()} \{issue.code()} [link \{issue.link_index().to_string()}]: \{issue.message()}",
          )
        }
      }
      let mut info_count = 0
      let mut warning_count = 0
      let mut error_count = 0
      for issue in report.issues() {
        let sev = issue.severity().to_string()
        if sev == "info" {
          info_count = info_count + 1
        } else if sev == "warning" {
          warning_count = warning_count + 1
        } else {
          error_count = error_count + 1
        }
      }
      println(
        "summary: \{info_count.to_string()} info, \{warning_count.to_string()} warnings, \{error_count.to_string()} errors",
      )
    }
  }
}

///|
/// `stats`: library statistics — the offline registry size and the resource
/// limit presets.
fn cmd_stats(_o : CliOptions) -> Unit {
  println(version_banner())
  println(
    "registered relations (offline IANA snapshot): \{@weblink.registered_relation_count().to_string()}",
  )
  print_limits("default", @weblink.Limits::default())
  print_limits("strict", @weblink.Limits::strict())
  print_limits("permissive", @weblink.Limits::permissive())
}

///|
fn print_limits(label : String, l : @weblink.Limits) -> Unit {
  let parts : Array[String] = Array::new()
  parts.push(label + ": input=" + l.max_input_bytes().to_string())
  parts.push("links=" + l.max_links().to_string())
  parts.push("params=" + l.max_params_per_link().to_string())
  parts.push("rels=" + l.max_relations_per_link().to_string())
  parts.push("target=" + l.max_target_bytes().to_string())
  println(join(parts, " "))
}