// The DID document -- what a DID resolves to, and where an account's PDS is
// written down.
//
// https://atproto.com/specs/did
//
// Deliberately partial, as every atproto implementation's is. A W3C DID
// document has a dozen more members and `serviceEndpoint` may be an object or
// an array; atproto uses four fields and always writes the endpoint as a
// string. Modelling the rest would be modelling a spec this protocol does not
// use, so unknown members are kept in `extra` and returned unchanged rather
// than described.
//
// The one thing this file exists for, past being a type: an id may be written
// RELATIVE (`#atproto_pds`) or ABSOLUTE (`did:plc:xyz#atproto_pds`), and both
// forms are in the wild. Every lookup here accepts either. A resolver that
// checked only one silently fails to find the PDS for half the accounts it
// sees, which is the kind of bug that looks like a network problem.

///|
/// The fragment identifying an account's personal data server.
pub const PDS_SERVICE_ID : String = "#atproto_pds"

///|
/// ... and the `type` it must carry.
pub const PDS_SERVICE_TYPE : String = "AtprotoPersonalDataServer"

///|
/// The fragment identifying an account's repository signing key.
pub const SIGNING_KEY_ID : String = "#atproto"

///|
pub(all) struct VerificationMethod {
  id : String
  type_ : String
  controller : String
  public_key_multibase : String?
  extra : Map[String, @data.LexValue]
} derive(Eq, Debug)

///|
pub(all) struct Service {
  id : String
  type_ : String
  service_endpoint : String
  extra : Map[String, @data.LexValue]
} derive(Eq, Debug)

///|
pub(all) struct DidDocument {
  id : @syntax.Did
  /// Other names for this subject. An atproto handle appears here as
  /// `at://alice.bsky.social`.
  also_known_as : Array[String]
  verification_method : Array[VerificationMethod]
  service : Array[Service]
  extra : Map[String, @data.LexValue]
} derive(Eq, Debug)

///|
/// The account's handle, from the first `at://` entry in `alsoKnownAs`.
///
/// This is a CLAIM, not a fact. The document says what handle the account wants
/// and anyone can write anything there; it is only true if resolving that
/// handle leads back to this DID. `verify_handle` below is that second half,
/// and `@client.resolve_identity` is what does both.
pub fn DidDocument::handle(self : Self) -> @syntax.Handle? {
  for name in self.also_known_as {
    if name.has_prefix("at://") {
      // A malformed entry is skipped rather than fatal: `alsoKnownAs` is a list
      // of claims from a third party, and one bad one must not hide a good one
      // further down.
      if handle_or_none(name[5:].to_owned()) is Some(handle) {
        return Some(handle)
      }
    }
  }
  None
}

///|
fn handle_or_none(text : String) -> @syntax.Handle? {
  Some(@syntax.Handle::parse(text)) catch {
    _ => None
  }
}

///|
/// Whether the document claims this handle -- the second half of bidirectional
/// verification.
///
/// Compared after normalizing, because `Handle::parse` lower-cases and a
/// document may not have.
pub fn DidDocument::verify_handle(self : Self, handle : @syntax.Handle) -> Bool {
  self.handle() is Some(claimed) && claimed == handle
}

///|
/// Where this account's repository lives. Every authenticated request goes
/// here, so a session that cannot find it has to fall back on the host it
/// logged in to.
pub fn DidDocument::pds_endpoint(self : Self) -> String? {
  self.service_endpoint(PDS_SERVICE_ID, type_=PDS_SERVICE_TYPE)
}

///|
/// An endpoint by service id, accepting the relative and absolute spellings of
/// the id, and optionally requiring a `type`.
///
/// The endpoint must be an `http`/`https` URL. That check is not decoration: a
/// DID document is fetched from a third party and its contents decide where
/// this library sends an access token, so a `file:` or `data:` endpoint is an
/// attack rather than a curiosity.
pub fn DidDocument::service_endpoint(
  self : Self,
  id : String,
  type_? : String,
) -> String? {
  let absolute = self.id.to_string() + id
  for entry in self.service {
    if entry.id != id && entry.id != absolute {
      continue
    }
    if type_ is Some(wanted) && entry.type_ != wanted {
      continue
    }
    if is_http_url(entry.service_endpoint) {
      return Some(entry.service_endpoint)
    }
  }
  None
}

///|
/// The repository signing key, as `publicKeyMultibase`. Only useful to a caller
/// verifying commits, which this library does not do -- exposed because the
/// document carries it and dropping it would be lossy.
pub fn DidDocument::signing_key(self : Self) -> VerificationMethod? {
  let absolute = self.id.to_string() + SIGNING_KEY_ID
  // Spelled `entry` because `method` is a reserved word.
  for entry in self.verification_method {
    if entry.id == SIGNING_KEY_ID || entry.id == absolute {
      return Some(entry)
    }
  }
  None
}

///|
fn is_http_url(endpoint : String) -> Bool {
  (endpoint.has_prefix("https://") || endpoint.has_prefix("http://")) &&
  endpoint.length() > 8
}

///|
/// Decodes a `didDoc`, which arrives as a lexicon `unknown` -- so it is a
/// `LexValue` until something asks for it as this.
pub fn DidDocument::from_lex(
  value : @data.LexValue,
  path? : String = "",
) -> DidDocument raise @data.DecodeError {
  let rest = @data.object_fields(value, path~)
  let id = @data.require_format(rest, "id", path~, @syntax.Did::parse)
  let also_known_as = @data.take_string_array(rest, "alsoKnownAs", path~).unwrap_or([],
  )
  let verification_method = @data.take_array(
    rest,
    "verificationMethod",
    path~,
    verification_method_of_lex,
  ).unwrap_or([])
  let service = @data.take_array(rest, "service", path~, service_of_lex).unwrap_or([],
  )
  { id, also_known_as, verification_method, service, extra: rest }
}

///|
fn verification_method_of_lex(
  value : @data.LexValue,
  path : String,
) -> VerificationMethod raise @data.DecodeError {
  let rest = @data.object_fields(value, path~)
  let id = @data.require_string(rest, "id", path~)
  let type_ = @data.require_string(rest, "type", path~)
  let controller = @data.require_string(rest, "controller", path~)
  let public_key_multibase = @data.take_string(rest, "publicKeyMultibase")
  { id, type_, controller, public_key_multibase, extra: rest }
}

///|
fn service_of_lex(
  value : @data.LexValue,
  path : String,
) -> Service raise @data.DecodeError {
  let rest = @data.object_fields(value, path~)
  let id = @data.require_string(rest, "id", path~)
  let type_ = @data.require_string(rest, "type", path~)
  // An object-or-array `serviceEndpoint` is legal W3C and unused by atproto, so
  // it is left in `extra` rather than modelled -- which makes
  // `service_endpoint` return None for it, the correct answer.
  let service_endpoint = @data.take_string(rest, "serviceEndpoint").unwrap_or(
    "",
  )
  { id, type_, service_endpoint, extra: rest }
}

///|
pub fn DidDocument::to_lex(self : Self) -> @data.LexValue {
  let out : Map[String, @data.LexValue] = Map([])
  out["id"] = Str(self.id.to_string())
  if self.also_known_as.length() > 0 {
    @data.put_string_array(out, "alsoKnownAs", Some(self.also_known_as))
  }
  if self.verification_method.length() > 0 {
    @data.put_array(
      out,
      "verificationMethod",
      Some(self.verification_method),
      verification_method_to_lex,
    )
  }
  if self.service.length() > 0 {
    @data.put_array(out, "service", Some(self.service), service_to_lex)
  }
  @data.merge_extra(out, self.extra)
}

///|
fn verification_method_to_lex(entry : VerificationMethod) -> @data.LexValue {
  let out : Map[String, @data.LexValue] = Map([])
  out["id"] = Str(entry.id)
  out["type"] = Str(entry.type_)
  out["controller"] = Str(entry.controller)
  @data.put_string(out, "publicKeyMultibase", entry.public_key_multibase)
  @data.merge_extra(out, entry.extra)
}

///|
fn service_to_lex(entry : Service) -> @data.LexValue {
  let out : Map[String, @data.LexValue] = Map([])
  out["id"] = Str(entry.id)
  out["type"] = Str(entry.type_)
  out["serviceEndpoint"] = Str(entry.service_endpoint)
  @data.merge_extra(out, entry.extra)
}