// The two documents an OAuth client reads before it can ask for a token: the
// protected resource metadata of RFC 9728, which says who vouches for this
// resource, and the authorization server metadata of RFC 8414 (or OpenID
// Connect Discovery), which says where to register, to authorize and to get a
// token.
//
// Both are read leniently: a field the document leaves out is a capability the
// server does not advertise, and only the fields a flow cannot go on without
// are required.

///|
/// Why an OAuth client could not go on, in words a person reads.
pub(all) suberror Refused {
  Refused(message~ : String)
}

///|
/// What a protected resource says about itself (RFC 9728): the identifier
/// tokens for it must be minted for, and the authorization servers that mint
/// them.
#derive(Debug, Eq)
pub(all) struct Resource {
  resource : String
  authorization_servers : Array[String]
  scopes_supported : Array[String]?
}

///|
/// What an authorization server says it can do (RFC 8414 and OpenID Connect
/// Discovery). A field the document leaves out keeps the default the metadata
/// specification gives it, except where the default is "not offered".
#derive(Debug, Eq)
pub(all) struct Server {
  issuer : String
  authorization_endpoint : String?
  token_endpoint : String
  registration_endpoint : String?
  scopes_supported : Array[String]?
  response_types_supported : Array[String]
  grant_types_supported : Array[String]
  token_endpoint_auth_methods_supported : Array[String]
  token_endpoint_auth_signing_alg_values_supported : Array[String]
  code_challenge_methods_supported : Array[String]?
  client_id_metadata_document_supported : Bool
}

///|
/// The `Bearer` challenge of a `401` or `403`: where the resource metadata is,
/// which scope would satisfy it, and what went wrong.
#derive(Debug, Eq)
pub(all) struct Challenge {
  resource_metadata : String?
  scope : String?
  error : String?
  error_description : String?
}

///|
/// Read a JSON object, or refuse.
fn object_of(json : Json) -> Map[String, Json] raise Refused {
  match json {
    Object(fields) => fields
    _ => raise Refused(message="expected a JSON object")
  }
}

///|
/// A string field, or `None` when it is absent or another type.
fn text_field(fields : Map[String, Json], key : String) -> String? {
  match fields.get(key) {
    Some(String(value)) => Some(value)
    _ => None
  }
}

///|
/// A string field a document cannot do without.
fn required_text(
  fields : Map[String, Json],
  key : String,
  of : String,
) -> String raise Refused {
  match text_field(fields, key) {
    Some(value) => value
    None => raise Refused(message="\{of} has no \{key}")
  }
}

///|
/// A field that is a list of strings: absent and not-a-list both read as empty.
fn text_list(fields : Map[String, Json], key : String) -> Array[String] {
  match fields.get(key) {
    Some(Array(items)) =>
      items.filter_map(item => {
        match item {
          String(text) => Some(text)
          _ => None
        }
      })
    _ => []
  }
}

///|
/// Read protected resource metadata.
pub fn Resource::parse(json : Json) -> Resource raise Refused {
  let fields = object_of(json)
  {
    resource: required_text(fields, "resource", "protected resource metadata"),
    authorization_servers: text_list(fields, "authorization_servers"),
    scopes_supported: match fields.get("scopes_supported") {
      Some(Array(_)) => Some(text_list(fields, "scopes_supported"))
      _ => None
    },
  }
}

///|
/// Read authorization server metadata. `issuer` and `token_endpoint` are the
/// two a client cannot work without.
pub fn Server::parse(json : Json) -> Server raise Refused {
  let fields = object_of(json)
  let authorization_endpoint = text_field(fields, "authorization_endpoint")
  let response_types = text_list(fields, "response_types_supported")
  let code_challenges = match fields.get("code_challenge_methods_supported") {
    Some(Array(_)) =>
      Some(text_list(fields, "code_challenge_methods_supported"))
    _ => None
  }
  {
    issuer: required_text(fields, "issuer", "authorization server metadata"),
    authorization_endpoint,
    token_endpoint: required_text(
      fields, "token_endpoint", "authorization server metadata",
    ),
    registration_endpoint: text_field(fields, "registration_endpoint"),
    scopes_supported: match fields.get("scopes_supported") {
      Some(Array(_)) => Some(text_list(fields, "scopes_supported"))
      _ => None
    },
    response_types_supported: if response_types.is_empty() {
      ["code"]
    } else {
      response_types
    },
    grant_types_supported: match fields.get("grant_types_supported") {
      Some(Array(_)) => text_list(fields, "grant_types_supported")
      _ => ["authorization_code", "implicit"]
    },
    token_endpoint_auth_methods_supported: match
      fields.get("token_endpoint_auth_methods_supported") {
      Some(Array(_)) =>
        text_list(fields, "token_endpoint_auth_methods_supported")
      // RFC 8414 §2: the default is client_secret_basic.
      _ => ["client_secret_basic"]
    },
    token_endpoint_auth_signing_alg_values_supported: text_list(
      fields, "token_endpoint_auth_signing_alg_values_supported",
    ),
    code_challenge_methods_supported: code_challenges,
    client_id_metadata_document_supported: match
      fields.get("client_id_metadata_document_supported") {
      Some(True) => true
      _ => false
    },
  }
}

///|
/// The documents to try for protected resource metadata, in the order the
/// specification asks for: the one a challenge pointed at, else the path-aware
/// location, else the root. A server that publishes the path-aware location is
/// not asked for the root one — a client that asks both is told apart by that.
pub fn Resource::probes(
  resource_url : String,
  hinted? : String,
) -> Array[String] {
  match hinted {
    Some(url) => [url]
    None => {
      let origin = origin_of(resource_url)
      let path = trimmed_path_of(resource_url)
      let pathbased = "\{origin}/.well-known/oauth-protected-resource\{path}"
      if path.is_empty() {
        [origin + "/.well-known/oauth-protected-resource"]
      } else {
        [pathbased, origin + "/.well-known/oauth-protected-resource"]
      }
    }
  }
}

///|
/// The documents to try for authorization server metadata, in the order RFC
/// 8414 and OpenID Connect Discovery put them: the path inserted after the
/// well-known prefix, then the two OpenID spellings.
pub fn Server::probes(authorization_server : String) -> Array[String] {
  let origin = origin_of(authorization_server)
  let path = trimmed_path_of(authorization_server)
  if path.is_empty() {
    [
      "\{origin}/.well-known/oauth-authorization-server",
      "\{origin}/.well-known/openid-configuration",
    ]
  } else {
    [
      "\{origin}/.well-known/oauth-authorization-server\{path}",
      "\{origin}/.well-known/openid-configuration\{path}",
      "\{origin}\{path}/.well-known/openid-configuration",
    ]
  }
}

///|
/// The endpoints a 2025-03-26 server has when it publishes no metadata at all:
/// the three OAuth paths at the server's own root (MCP's authorization section
/// before it named RFC 8414).
pub fn Server::at_root(base : String) -> Server {
  let origin = origin_of(base)
  {
    issuer: origin,
    authorization_endpoint: Some(origin + "/authorize"),
    token_endpoint: origin + "/token",
    registration_endpoint: Some(origin + "/register"),
    scopes_supported: None,
    response_types_supported: ["code"],
    grant_types_supported: ["authorization_code", "refresh_token"],
    token_endpoint_auth_methods_supported: ["client_secret_basic"],
    token_endpoint_auth_signing_alg_values_supported: [],
    code_challenge_methods_supported: Some(["S256"]),
    client_id_metadata_document_supported: false,
  }
}

///|
/// Whether this server can run an authorization-code flow with PKCE.
pub fn Server::serves_authorization_code(self : Server) -> Bool {
  if !self.response_types_supported.contains("code") {
    return false
  }
  match self.code_challenge_methods_supported {
    Some(methods) => methods.contains("S256")
    None => true
  }
}

///|
/// Whether this server offers the `grant`.
pub fn Server::offers(self : Server, grant : String) -> Bool {
  self.grant_types_supported.contains(grant)
}

///|
/// Read the `Bearer` challenge out of a `WWW-Authenticate` header. Parameters
/// are `name=value` with the value optionally quoted; a name repeated keeps the
/// first, which is what a client needs.
pub fn Challenge::parse(header : String) -> Challenge? {
  let rest = match header.to_lower().find("bearer") {
    Some(at) => header.exact_view(start=at + 6).to_owned()
    None => return None
  }
  let fields : Map[String, String] = Map([])
  let mut at = 0
  let text = rest
  while at < text.length() {
    // Skip what separates parameters: spaces and commas.
    while at < text.length() &&
          (text[at] == ' ' || text[at] == ',' || text[at] == '\t') {
      at += 1
    }
    guard at < text.length() else { break }
    let name_start = at
    while at < text.length() && text[at] != '=' && text[at] != ',' {
      at += 1
    }
    guard at < text.length() && text[at] == '=' else { break }
    let name = text
      .exact_view(start=name_start, end=at)
      .to_owned()
      .trim(chars=" \t")
      .to_owned()
      .to_lower()
    at += 1
    let value = if at < text.length() && text[at] == '"' {
      at += 1
      let start = at
      while at < text.length() && text[at] != '"' {
        at += 1
      }
      let value = text.exact_view(start~, end=at).to_owned()
      if at < text.length() {
        at += 1
      }
      value
    } else {
      let start = at
      while at < text.length() && text[at] != ',' && text[at] != ' ' {
        at += 1
      }
      text.exact_view(start~, end=at).to_owned()
    }
    if !fields.contains(name) {
      fields[name] = value
    }
  }
  Some({
    resource_metadata: fields.get("resource_metadata"),
    scope: fields.get("scope"),
    error: fields.get("error"),
    error_description: fields.get("error_description"),
  })
}

///|
/// The `error` and `error_description` of an OAuth error body, in one line.
fn refused_text(fields : Map[String, Json], what : String) -> String {
  let error = text_field(fields, "error").unwrap_or("error")
  match text_field(fields, "error_description") {
    Some(why) => "\{what}: \{error}: \{why}"
    None => "\{what}: \{error}"
  }
}

///|
pub extend Resource with Debug::{to_repr}

///|
pub extend Resource with Eq::{not_equal, equal}

///|
pub extend Server with Debug::{to_repr}

///|
pub extend Server with Eq::{not_equal, equal}

///|
pub extend Challenge with Debug::{to_repr}

///|
pub extend Challenge with Eq::{not_equal, equal}