// Handle -- the human-readable name for an account, and a real domain name.
//
// https://atproto.com/specs/handle
// Ported from @atproto/syntax packages/syntax/src/handle.ts.
//
// Two things about handles that the type cannot express and callers keep
// getting wrong:
//
//   - A handle is not an identity. It is a domain name pointing at a DID, and
//     it can be reassigned. Store the DID; display the handle. This is why
//     `AtIdentifier` exists and why nothing in this library keys anything by
//     handle.
//   - Validity is not resolvability, and neither is `is_valid_tld`. A handle
//     can be perfectly well-formed, have an allowed TLD, and belong to nobody.
//     Only resolution answers that, and resolution needs the network.

///|
/// The DNS cap, and the reason handles are bounded at all.
const HANDLE_MAX_LENGTH : Int = 253

///|
const HANDLE_MAX_LABEL_LENGTH : Int = 63

///|
/// What a PDS returns in place of a handle when it has one on file but could
/// not bidirectionally verify it against the DID document. It is deliberately
/// itself a syntactically valid handle, so it flows through code that does not
/// check -- which is precisely why code that displays a handle should.
pub const INVALID_HANDLE : String = "handle.invalid"

///|
/// TLDs a handle may not use. Policy, not syntax: these strings parse fine and
/// are refused for what they mean, so `parse` does not consult this list and
/// `is_valid_tld` is a separate question.
///
/// `.test` is deliberately absent -- it is allowed, for development.
pub let disallowed_tlds : Array[String] = [
  ".local", ".arpa", ".invalid", ".localhost", ".internal", ".example", ".alt",
  // Policy could conceivably change on .onion some day.
   ".onion",
]

///|
/// A syntactically valid handle. Always lower-case: `parse` normalizes, because
/// handles are domain names and DNS is case-insensitive, so treating
/// `Alice.BSky.social` and `alice.bsky.social` as different values would be a
/// bug waiting to be written.
pub struct Handle(String) derive(Eq, Debug)

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

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

///|
/// Trusts the caller, and does NOT normalize -- `unchecked` means unchecked.
pub fn Handle::unchecked(handle : String) -> Handle {
  Handle(handle)
}

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

///|
/// Lower-casing is safe to do before validating rather than after because the
/// character-set gate rejects everything outside ASCII, so no locale-dependent
/// case mapping can apply.
pub fn Handle::parse(handle : String) -> Handle raise SyntaxError {
  fn bad(reason : String) -> SyntaxError {
    SyntaxError(kind=Handle, input=handle, reason~)
  }

  guard all_chars(handle, is_dns_char) else {
    raise bad(
      "Disallowed characters in handle (ASCII letters, digits, dashes, periods only)",
    )
  }
  guard handle.length() <= HANDLE_MAX_LENGTH else {
    raise bad("Handle is too long (\{HANDLE_MAX_LENGTH} chars max)")
  }
  let labels = handle.split(".").collect()
  guard labels.length() >= 2 else {
    raise bad("Handle domain needs at least two parts")
  }
  for label in labels {
    guard label.length() > 0 else { raise bad("Handle parts can not be empty") }
    guard label.length() <= HANDLE_MAX_LABEL_LENGTH else {
      raise bad("Handle part too long (max \{HANDLE_MAX_LABEL_LENGTH} chars)")
    }
    guard !code_unit_is(label[0], '-') &&
      !code_unit_is(label[label.length() - 1], '-') else {
      raise bad("Handle parts can not start or end with hyphens")
    }
  }
  let tld = labels[labels.length() - 1]
  guard code_unit_is_ascii_alpha(tld[0]) else {
    raise bad("Handle final component (TLD) must start with ASCII letter")
  }
  Handle(handle.to_lower())
}

///|
/// Whether the handle's TLD is one atproto permits. Separate from `parse`
/// because it is policy: the list has changed and will change again, and a
/// handle that stops being allowed does not retroactively stop being
/// well-formed.
pub fn Handle::is_valid_tld(self : Self) -> Bool {
  for tld in disallowed_tlds {
    if self.0.has_suffix(tld) {
      return false
    }
  }
  true
}

///|
/// True for the `handle.invalid` sentinel a PDS returns when it could not
/// verify a handle against its DID document. Such a value is well-formed, so
/// nothing else in this package will flag it.
pub fn Handle::is_invalid_sentinel(self : Self) -> Bool {
  self.0 == INVALID_HANDLE
}