// DID -- Decentralized Identifier. The stable identity of an atproto account;
// the thing a handle resolves to and the thing every record's authorship is
// expressed in.
//
// https://atproto.com/specs/did
// Ported from @atproto/syntax packages/syntax/src/did.ts.
//
// Three deliberate non-checks, all of them upstream's:
//
//   - Percent-encoding is not validated. `%` is an allowed character and
//     `did:method:val%BB` is accepted, but so is `did:method:%zz`; only a
//     trailing `%` is rejected, because that one cannot begin an escape.
//   - Method-specific content is not interpreted. `did:plc:` identifiers have a
//     fixed shape and `did:web:` ones are hostnames, and neither is enforced
//     here -- a DID this library has never heard of must still round-trip.
//   - Query strings and fragments are not accepted. Those belong to "DID URIs",
//     which are a different grammar; a DID is only the identifier.

///|
/// The upstream cap. The spec's prose says 8 KB, but every implementation --
/// TypeScript, Rust -- enforces 2048, and interoperating with them beats
/// interoperating with the prose.
const DID_MAX_LENGTH : Int = 2048

///|
/// A syntactically valid DID.
///
/// Opaque: the only ways in are `parse`, which checks, and `unchecked`, which
/// says in its name that it does not.
pub struct Did(String) derive(Eq, Debug)

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

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

///|
/// Trusts the caller. For values that have already been validated -- read back
/// out of a database this library wrote, or decoded from a response that was
/// checked at the boundary -- and for tests.
pub fn Did::unchecked(did : String) -> Did {
  Did(did)
}

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

///|
/// The method: `plc` in `did:plc:7iza6de2dwap2sbkpav7c6c6`.
///
/// Spelled `method_name` because `method` is a reserved word.
///
/// Total, because a `Did` cannot exist without one.
pub fn Did::method_name(self : Self) -> String {
  let rest = self.0[4:].to_owned()
  match rest.split_once(":") {
    Some((name, _)) => name.to_owned()
    // Unreachable: `parse` rejects anything with fewer than three segments.
    None => rest
  }
}

///|
/// The order of these checks is upstream's, and it is observable: a caller sees
/// the message for the FIRST rule an input breaks, so reordering them changes
/// the reported reason for inputs that break several. `DID:method:val` must
/// report a missing prefix, not an uppercase method.
pub fn Did::parse(did : String) -> Did raise SyntaxError {
  fn bad(reason : String) -> SyntaxError {
    SyntaxError(kind=Did, input=did, reason~)
  }

  guard did.has_prefix("did:") else {
    raise bad("DID requires \"did:\" prefix")
  }
  guard did.length() <= DID_MAX_LENGTH else {
    raise bad("DID is too long (\{DID_MAX_LENGTH} chars max)")
  }
  guard !did.has_suffix(":") && !did.has_suffix("%") else {
    raise bad("DID can not end with \":\" or \"%\"")
  }
  guard all_chars(did, is_did_char) else {
    raise bad(
      "Disallowed characters in DID (ASCII letters, digits, and a couple other characters only)",
    )
  }
  let segments = did.split(":").collect()
  guard segments.length() >= 3 else {
    raise bad("DID requires prefix, method, and method-specific content")
  }
  // Spelled `method_name` because `method` is a reserved word.
  let method_name = segments[1].to_owned()
  // `all_chars` on the empty string is vacuously true, so the length check is
  // not redundant: `did::val` has an empty method and must be rejected.
  guard method_name.length() > 0 && all_chars(method_name, is_ascii_lower) else {
    raise bad("DID method must be lower-case letters")
  }
  Did(did)
}

///|
/// `[a-zA-Z0-9._:%-]`.
fn is_did_char(c : Char) -> Bool {
  is_ascii_alnum(c) || c == '.' || c == '_' || c == ':' || c == '%' || c == '-'
}