// link_parser.mbt — RFC 8288 `Link` header field parser.
//
// Grammar (RFC 8288 Section 3):
//
//     Link       = #link-value
//     link-value = "<" URI-Reference ">" *( OWS ";" OWS link-param )
//     link-param = token BWS [ "=" BWS ( token / quoted-string ) ]
//
// The `#link-value` rule is the RFC 7230 list rule: empty elements are
// allowed and ignored. Duplicate occurrences of the single-occurrence
// parameters (`rel`, `anchor`, `media`, `title`, `title*`, `type`) are
// ignored after the first per RFC 8288 Sections 3.2-3.4, but their names
// are recorded in `LinkHeaderParse::duplicates()` so that the audit layer
// can flag them.
//
// Targets may be IRIs (RFC 8288 Section 3.1 permits a target IRI that is
// converted to a URI); pure-ASCII targets are validated strictly as RFC 3986
// URI-references.
//
// Every public entry point returns `Result[T, LinkError]`; internal
// functions raise and the boundary converts to `Result`.

///|
/// The result of parsing a full `Link` header field value.
pub struct LinkHeaderParse {
  links : Array[WebLink]
  duplicates : Array[String]
}

///|
/// The parsed links, in input order.
pub fn LinkHeaderParse::links(self : LinkHeaderParse) -> Array[WebLink] {
  self.links
}

///|
/// The names of the single-occurrence parameters that appeared more than
/// once and whose later occurrences were ignored, in first-seen order.
pub fn LinkHeaderParse::duplicates(self : LinkHeaderParse) -> Array[String] {
  self.duplicates
}

///|
/// Parses a `Link` header field value into an array of links.
///
/// Errors: `Input::EmptyInput` (no link present), `Input::LimitExceeded`
/// (input larger than `max_input_bytes`), `Limit::LimitExceeded`, plus the
/// `Target`, `Parameter`, `Relation`, `QuotedString`, `ExtendedValue`,
/// `UriReference` and `Header` errors raised while parsing a link.
pub fn parse_link_header(
  input : String,
  limits : Limits,
) -> Result[Array[WebLink], LinkError] {
  match parse_link_header_detailed(input, limits) {
    Ok(p) => Ok(p.links)
    Err(e) => Err(e)
  }
}

///|
/// Parses a `Link` header field value, also returning the names of ignored
/// duplicate parameters (see `LinkHeaderParse`).
pub fn parse_link_header_detailed(
  input : String,
  limits : Limits,
) -> Result[LinkHeaderParse, LinkError] {
  let input_bytes = @utf8.encode(input)
  if input_bytes.length() > limits.max_input_bytes() {
    return Err(
      link_error(Input, LimitExceeded, "input exceeds max_input_bytes"),
    )
  }
  Ok(parse_links_from_cursor(Scanner::new(input), limits)) catch {
    e => Err(unwrap_link_error(e))
  }
}

///|
/// Parses a run of link-values from an already-created cursor. Shared by the
/// `Link` header parser (SP / HTAB separators only) and the
/// `application/linkset` text parser (newlines also allowed, RFC 9264
/// Section 4.1).
fn parse_links_from_cursor(
  cursor : Scanner,
  limits : Limits,
) -> LinkHeaderParse raise {
  cursor.skip_ows()
  if cursor.eof() {
    raise link_error(Input, EmptyInput, "empty link collection")
  }

  let links = Array::new()
  let duplicates = Array::new()

  // Skip leading empty list elements ("#link-value" allows them). A header
  // that is only empty elements carries no link at all.
  while cursor.consume_char(44) {
    cursor.skip_ows()
  }
  if cursor.eof() {
    raise link_error(Input, EmptyInput, "empty link collection")
  }

  let mut done = false
  while !done {
    let link = parse_link_value(cursor, limits, duplicates)
    links.push(link)
    if links.length() > limits.max_links() {
      raise link_error(
        Limit,
        LimitExceeded,
        "too many links (exceeds max_links)",
      )
    }
    cursor.skip_ows()
    if cursor.eof() {
      done = true
    } else if cursor.consume_char(44) {
      cursor.skip_ows()
      // Skip empty elements between links.
      while cursor.consume_char(44) {
        cursor.skip_ows()
      }
      if cursor.eof() {
        done = true
      }
    } else {
      raise link_error_at(
        Header,
        TrailingInput,
        cursor.position(),
        cursor.context_string(),
      )
    }
  }

  if links.is_empty() {
    raise link_error(Input, EmptyInput, "link collection contains no links")
  }
  { links, duplicates }
}

///|
/// Parses a single link-value starting at `<`, consuming the target and
/// all parameters. The cursor is left just after the last parameter.
fn parse_link_value(
  cursor : Scanner,
  limits : Limits,
  duplicates : Array[String],
) -> WebLink raise {
  // --- target ---------------------------------------------------------
  if !cursor.consume_char(60) {
    raise link_error_at(
      Target,
      ExpectedAngleBracket,
      cursor.position(),
      cursor.context_string(),
    )
  }
  let target_start = cursor.position()
  let mut done = false
  while !done {
    match cursor.peek_byte() {
      Some(62) => done = true
      Some(b) => {
        if b == 60 || b == 34 || b == 32 || is_bad_control(b) {
          raise link_error_at(
            Target,
            InvalidTarget,
            cursor.position(),
            "character not allowed in link target",
          )
        }
        cursor.pos = cursor.pos + 1
      }
      None =>
        raise link_error_at(
          Target,
          UnterminatedTarget,
          target_start,
          cursor.context_string(),
        )
    }
    if cursor.position() - target_start > limits.max_target_bytes() {
      raise link_error_at(
        Limit,
        LimitExceeded,
        target_start,
        "target exceeds max_target_bytes",
      )
    }
  }
  let target = cursor.take_string(target_start, cursor.position())
  cursor.pos = cursor.pos + 1 // consume ">"
  if !valid_target_string(target) {
    raise link_error_at(
      Target,
      InvalidTarget,
      target_start,
      cursor.context_string(),
    )
  }

  // --- parameters ------------------------------------------------------
  let mut relations : Array[RelationType] = Array::new()
  let mut anchor : String? = None
  let mut anchor_seen = false
  let hreflang : Array[String] = Array::new()
  let mut media : String? = None
  let mut media_seen = false
  let mut title : String? = None
  let mut title_seen = false
  let mut title_star : ExtendedValue? = None
  let mut title_star_seen = false
  let mut media_type : String? = None
  let mut type_seen = false
  let extensions : Array[LinkParameter] = Array::new()
  let mut rel_seen = false
  let mut param_count = 0

  let mut params_done = false
  while !params_done {
    cursor.skip_ows()
    if !cursor.consume_char(59) {
      params_done = true
    } else {
      cursor.skip_ows()
      if param_count >= limits.max_params_per_link() {
        raise link_error_at(
          Limit,
          LimitExceeded,
          cursor.position(),
          "too many parameters per link (exceeds max_params_per_link)",
        )
      }
      param_count = param_count + 1
      let param = match parse_link_param(cursor, limits) {
        Ok(p) => p
        Err(e) => raise e
      }
      // Dispatch on the parameter name. RFC 8288's grammar inherits ABNF
      // (RFC 5234), whose string literals are case-insensitive, so `REL`,
      // `Anchor`, `TITLE*` etc. are recognized here case-insensitively.
      // Extension parameter names keep their original case in the model.
      // Single-occurrence parameters keep their first value; later
      // occurrences are ignored and their names recorded in `duplicates`.
      // Unknown parameters are preserved as extension parameters.
      match ascii_lowercase(param.name) {
        "rel" =>
          if rel_seen {
            mark_duplicate(duplicates, "rel")
          } else {
            rel_seen = true
            let value = require_value(param, "rel")
            let rels = match parse_relation_list(value, limits) {
              Ok(x) => x
              Err(e) => raise e
            }
            relations = rels
          }
        "anchor" =>
          if anchor_seen {
            mark_duplicate(duplicates, "anchor")
          } else {
            anchor_seen = true
            let value = require_value(param, "anchor")
            if !valid_target_string(value) {
              raise link_error_at(
                Parameter,
                InvalidContextValue,
                param.value_start,
                "anchor is not a valid URI-reference",
              )
            }
            anchor = Some(value)
          }
        "hreflang" => {
          let value = require_value(param, "hreflang")
          if !valid_language_tag(value) {
            raise link_error_at(
              Parameter,
              InvalidLanguageTag,
              param.value_start,
              "hreflang is not a valid Language-Tag",
            )
          }
          hreflang.push(value)
        }
        "media" =>
          if media_seen {
            mark_duplicate(duplicates, "media")
          } else {
            media_seen = true
            media = Some(require_value(param, "media"))
          }
        "title" =>
          if title_seen {
            mark_duplicate(duplicates, "title")
          } else {
            title_seen = true
            title = Some(require_value(param, "title"))
          }
        "title*" =>
          if title_star_seen {
            mark_duplicate(duplicates, "title*")
          } else {
            title_star_seen = true
            match param.value {
              Extended(ev) => title_star = Some(ev)
              _ =>
                raise link_error_at(
                  Parameter,
                  InvalidParameter,
                  param.value_start,
                  "title* must carry an extended value",
                )
            }
          }
        "type" =>
          if type_seen {
            mark_duplicate(duplicates, "type")
          } else {
            type_seen = true
            let value = require_value(param, "type")
            if !validate_media_type(value, limits) {
              raise link_error_at(
                Parameter,
                InvalidMediaType,
                param.value_start,
                "type is not a valid media type",
              )
            }
            media_type = Some(value)
          }
        "rev" => {
          // Deprecated by RFC 8288 Section 3.3. The value has the same ABNF
          // as `rel`; it is validated and preserved as an extension
          // parameter so the audit layer can flag it.
          let value = require_value(param, "rev")
          match parse_relation_list(value, limits) {
            Ok(_) => ()
            Err(e) => raise e
          }
          extensions.push({
            name: "rev",
            value: Some(value),
            quoted: was_quoted(param.value),
          })
        }
        _ => extensions.push(extension_parameter(param))
      }
    }
  }

  if relations.is_empty() {
    raise link_error_at(
      LinkValue,
      InvalidRelation,
      target_start,
      "link-value is missing the required rel parameter",
    )
  }

  {
    target,
    relations,
    anchor,
    hreflang,
    media,
    title,
    title_star,
    media_type,
    extensions,
  }
}

///|
/// The value of a parameter as a plain string, or an error when the
/// parameter is a flag (`; foo`) or an extended value.
fn require_value(p : ParsedParameter, name : String) -> String raise {
  match p.value {
    Empty =>
      raise link_error_at(
        Parameter,
        InvalidParameter,
        p.value_start,
        "\{name} requires a value",
      )
    Token(v) => v
    Quoted(v) => v
    Extended(_) =>
      raise link_error_at(
        Parameter,
        InvalidParameter,
        p.value_start,
        "\{name} must not be an extended value",
      )
  }
}

///|
/// Converts a parsed parameter to a preserved extension parameter.
fn extension_parameter(p : ParsedParameter) -> LinkParameter {
  match p.value {
    Empty => { name: p.name, value: None, quoted: false }
    Token(v) => { name: p.name, value: Some(v), quoted: false }
    Quoted(v) => { name: p.name, value: Some(v), quoted: true }
    Extended(ev) =>
      { name: p.name, value: Some(serialize_extended_value(ev)), quoted: false }
  }
}

///|
/// Whether a raw value came from the quoted-string form.
fn was_quoted(v : RawParamValue) -> Bool {
  match v {
    Quoted(_) => true
    _ => false
  }
}

///|
/// Records a duplicate parameter name once.
fn mark_duplicate(duplicates : Array[String], name : String) -> Unit {
  let mut present = false
  for d in duplicates {
    if d == name {
      present = true
    }
  }
  if !present {
    duplicates.push(name)
  }
}

///|
/// Whether a string is acceptable as a link target or anchor value.
///
/// Pure-ASCII targets must parse strictly as RFC 3986 URI-references.
/// Targets containing non-ASCII bytes are treated as IRIs (RFC 8288
/// Section 3.1): every byte must be a valid UTF-8 continuation of the
/// string, ASCII bytes must be URI characters, and `%` must be followed by
/// two hex digits.
fn valid_target_string(target : String) -> Bool {
  let bytes = @utf8.encode(target)
  let mut non_ascii = false
  let mut i = 0
  while i < bytes.length() {
    let b = bytes[i]
    if b.to_int() >= 128 {
      non_ascii = true
      i = i + 1
      while i < bytes.length() &&
            bytes[i].to_int() >= 128 &&
            bytes[i].to_int() <= 191 {
        i = i + 1
      }
    } else if b == 37 {
      let h1 = if i + 1 < bytes.length() { bytes[i + 1] } else { 0 }
      let h2 = if i + 2 < bytes.length() { bytes[i + 2] } else { 0 }
      if !(is_hexdigit(h1) && is_hexdigit(h2)) {
        return false
      }
      i = i + 3
    } else if uri_pchar(b) || b == 47 || b == 63 || b == 35 {
      i = i + 1
    } else {
      return false
    }
  }
  if non_ascii {
    true
  } else {
    match parse_uri_reference(target) {
      Ok(_) => true
      Err(_) => false
    }
  }
}

///|
/// Whether a string is a valid media type (`type-name "/" subtype-name`
/// with optional RFC 7231 parameters).
fn validate_media_type(value : String, limits : Limits) -> Bool {
  let bytes = @utf8.encode(value)
  let len = bytes.length()
  let mut i = 0
  // type-name = 1*tchar
  let type_start = i
  while i < len && token_char(bytes[i]) {
    i = i + 1
  }
  if i == type_start || i >= len || bytes[i] != 47 {
    return false
  }
  i = i + 1
  // subtype-name = 1*tchar
  let sub_start = i
  while i < len && token_char(bytes[i]) {
    i = i + 1
  }
  if i == sub_start {
    return false
  }
  // optional parameters: *( OWS ";" OWS parameter )
  //   parameter = token "=" ( token / quoted-string )
  while i < len {
    if bytes[i] == 32 || bytes[i] == 9 {
      i = i + 1
    } else if bytes[i] == 59 {
      i = i + 1
      while i < len && (bytes[i] == 32 || bytes[i] == 9) {
        i = i + 1
      }
      let name_start = i
      while i < len && token_char(bytes[i]) {
        i = i + 1
      }
      if i == name_start {
        return false
      }
      while i < len && (bytes[i] == 32 || bytes[i] == 9) {
        i = i + 1
      }
      if i >= len || bytes[i] != 61 {
        return false
      }
      i = i + 1
      while i < len && (bytes[i] == 32 || bytes[i] == 9) {
        i = i + 1
      }
      if i < len && bytes[i] == 34 {
        // quoted-string value
        let sc = Scanner::new(value)
        sc.seek(i)
        let ok = try {
          let _q = parse_quoted_string(sc, limits)
          true
        } catch {
          _ => false
        }
        if !ok {
          return false
        }
        i = sc.position()
      } else {
        let val_start = i
        while i < len && token_char(bytes[i]) {
          i = i + 1
        }
        if i == val_start {
          return false
        }
      }
    } else {
      return false
    }
  }
  true
}