// NSID -- Namespaced Identifier. The name of a Lexicon: a record collection
// (`app.bsky.feed.post`), an XRPC endpoint (`com.atproto.repo.createRecord`),
// or a type definition.
//
// https://atproto.com/specs/nsid
// Ported from @atproto/syntax packages/syntax/src/nsid.ts.
//
//   segment   = alpha *( alpha / number / "-" )
//   authority = segment *( delim segment )
//   name      = alpha *( alpha / number )
//   nsid      = authority delim name
//
// The authority is a domain name in REVERSE order, so `app.bsky.feed.post` is
// the `post` Lexicon published by whoever controls `bsky.app`. That reversal is
// the only surprising thing about NSIDs and is why `authority()` exists rather
// than leaving callers to slice and flip the string themselves.
//
// Note the name part is stricter than the authority parts: no hyphens, and no
// leading digit. A 2023 tightening of the spec, and the rule most likely to
// trip up a hand-written validator that treats every segment alike.

///|
/// 253 for the authority (a domain name), plus the separating dot, plus 63 for
/// the name.
const NSID_MAX_LENGTH : Int = 317

///|
const NSID_MAX_SEGMENT_LENGTH : Int = 63

///|
/// A syntactically valid NSID.
pub struct Nsid(String) derive(Eq, Debug)

///|
pub impl Show for Nsid with fn output(self, logger) {
  logger.write_string(self.0)
}

///|
pub fn Nsid::to_string(self : Self) -> String {
  self.0
}

///|
pub fn Nsid::unchecked(nsid : String) -> Nsid {
  Nsid(nsid)
}

///|
pub fn Nsid::is_valid(nsid : String) -> Bool {
  try {
    Nsid::parse(nsid) |> ignore
    true
  } catch {
    _ => false
  }
}

///|
pub fn Nsid::parse(nsid : String) -> Nsid raise SyntaxError {
  fn bad(reason : String) -> SyntaxError {
    SyntaxError(kind=Nsid, input=nsid, reason~)
  }

  guard nsid.length() <= NSID_MAX_LENGTH else {
    raise bad("NSID is too long (\{NSID_MAX_LENGTH} chars max)")
  }
  guard all_chars(nsid, is_dns_char) else {
    raise bad(
      "Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)",
    )
  }
  let segments = nsid.split(".").collect()
  guard segments.length() >= 3 else {
    raise bad("NSID needs at least three parts")
  }
  for segment in segments {
    guard segment.length() > 0 else { raise bad("NSID parts can not be empty") }
    guard segment.length() <= NSID_MAX_SEGMENT_LENGTH else {
      raise bad("NSID part too long (max \{NSID_MAX_SEGMENT_LENGTH} chars)")
    }
    guard !code_unit_is(segment[0], '-') &&
      !code_unit_is(segment[segment.length() - 1], '-') else {
      raise bad("NSID parts can not start or end with hyphen")
    }
  }
  guard !code_unit_is_ascii_digit(segments[0][0]) else {
    raise bad("NSID first part may not start with a digit")
  }
  // The name is the last segment, and its rule is not the segment rule: letters
  // and digits only -- no hyphen -- and no leading digit.
  let name = segments[segments.length() - 1]
  guard code_unit_is_ascii_alpha(name[0]) && !name.contains("-") else {
    raise bad(
      "NSID name part must be only letters and digits (and no leading digit)",
    )
  }
  Nsid(nsid)
}

///|
/// The publishing authority, as a domain name -- so `app.bsky.feed.post` has
/// authority `bsky.app`. The segments are reversed back into DNS order, which
/// is the form you would look up or check a certificate against.
pub fn Nsid::authority(self : Self) -> String {
  let segments = self.0.split(".").collect()
  let b = StringBuilder::new(size_hint=self.0.length())
  // Every segment but the last, in reverse.
  for i = segments.length() - 2; i >= 0; i = i - 1 {
    b.write_string(segments[i].to_owned())
    if i > 0 {
      b.write_char('.')
    }
  }
  b.to_string()
}

///|
/// The name part -- `post` in `app.bsky.feed.post`.
pub fn Nsid::name(self : Self) -> String {
  let segments = self.0.split(".").collect()
  segments[segments.length() - 1].to_owned()
}