// OpenAPI security scheme objects (← FastAPI surfacing `OAuth2PasswordBearer`,
// `OAuth2AuthorizationCodeBearer`, `HTTPBearer`, `HTTPBasic`, `HTTPDigest`,
// `APIKeyHeader`, `OpenIdConnect`, etc. under `components/securitySchemes`). A
// `SecurityScheme` is declared on the `App` and emitted into the generated spec
// so the security layer is described to clients. Both spec shapes are produced:
// OpenAPI 3.x `securitySchemes`, and the Swagger 2.0 `securityDefinitions` shape
// (flat OAuth2 flows, no `http` type, and no way at all to say `openIdConnect`).

///|
/// A security scheme describing how a client authenticates. Mirrors the OpenAPI
/// scheme types: the two OAuth2 flows a browser or a service actually uses
/// (password and authorization code), HTTP bearer / basic / digest, an API key
/// in a header / query / cookie, and OpenID Connect discovery.
pub(all) enum SecurityScheme {
  OAuth2Password(token_url~ : String, scopes~ : Array[(String, String)])
  // ← FastAPI's `OAuth2AuthorizationCodeBearer`: the endpoint the browser is
  // sent to, the one the code is exchanged at, and — when the provider offers it
  // — where a token is refreshed. An empty `refresh_url` is left out.
  OAuth2Code(
    authorization_url~ : String,
    token_url~ : String,
    refresh_url~ : String,
    scopes~ : Array[(String, String)]
  )
  HttpBearer(bearer_format~ : String)
  ApiKeyHeader(name~ : String)
  ApiKeyQuery(name~ : String)
  ApiKeyCookie(name~ : String)
  HttpBasic
  // ← FastAPI's `HTTPDigest`. RFC 7616 negotiates through a `WWW-Authenticate`
  // challenge, and OpenAPI describes only the scheme name.
  HttpDigest
  // ← FastAPI's `OpenIdConnect`: one discovery document from which a client
  // reads every other parameter.
  OpenIdConnect(url~ : String)
}

///|
/// A security scheme as an application declares it: the `name` routes refer to
/// it by, the `kind` that shapes the emitted object, and the prose beside it
/// (← the `description=` every FastAPI security class takes).
struct DeclaredScheme {
  name : String
  kind : SecurityScheme
  description : String
}

///|
/// Build the security scheme that describes this password-bearer flow (← the
/// object FastAPI derives from `OAuth2PasswordBearer`). `scopes` are the
/// `(name, description)` pairs advertised in the OpenAPI document.
pub fn OAuth2PasswordBearer::scheme(
  self : OAuth2PasswordBearer,
  scopes? : Array[(String, String)] = [],
) -> SecurityScheme {
  OAuth2Password(token_url=self.token_url, scopes~)
}

///|
/// A `{name: description, ...}` JSON object from scope pairs.
fn scopes_object(scopes : Array[(String, String)]) -> Json {
  let m : Map[String, Json] = Map([])
  for pair in scopes {
    m[pair.0] = pair.1.to_json()
  }
  m.to_json()
}

///|
/// This scheme as an OpenAPI 3.0 / 3.1 security scheme object.
fn SecurityScheme::to_openapi_3x(self : SecurityScheme) -> Json {
  match self {
    OAuth2Password(token_url~, scopes~) => {
      let flow : Map[String, Json] = Map([
        ("tokenUrl", token_url.to_json()),
        ("scopes", scopes_object(scopes)),
      ])
      oauth2_json("password", flow)
    }
    OAuth2Code(authorization_url~, token_url~, refresh_url~, scopes~) => {
      let flow : Map[String, Json] = Map([
        ("authorizationUrl", authorization_url.to_json()),
        ("tokenUrl", token_url.to_json()),
      ])
      if refresh_url != "" {
        flow["refreshUrl"] = refresh_url.to_json()
      }
      flow["scopes"] = scopes_object(scopes)
      oauth2_json("authorizationCode", flow)
    }
    HttpBearer(bearer_format~) => {
      let m : Map[String, Json] = Map([
        ("type", "http".to_json()),
        ("scheme", "bearer".to_json()),
        ("bearerFormat", bearer_format.to_json()),
      ])
      m.to_json()
    }
    ApiKeyHeader(name~) => api_key_json("header", name)
    ApiKeyQuery(name~) => api_key_json("query", name)
    ApiKeyCookie(name~) => api_key_json("cookie", name)
    HttpBasic => http_scheme_json("basic")
    HttpDigest => http_scheme_json("digest")
    OpenIdConnect(url~) => {
      let m : Map[String, Json] = Map([
        ("type", "openIdConnect".to_json()),
        ("openIdConnectUrl", url.to_json()),
      ])
      m.to_json()
    }
  }
}

///|
/// An `oauth2` scheme object carrying one named 3.x flow.
fn oauth2_json(flow_name : String, flow : Map[String, Json]) -> Json {
  let flows : Map[String, Json] = Map([(flow_name, flow.to_json())])
  let m : Map[String, Json] = Map([
    ("type", "oauth2".to_json()),
    ("flows", flows.to_json()),
  ])
  m.to_json()
}

///|
/// An `{"type":"http","scheme":...}` object for the HTTP authentication schemes.
fn http_scheme_json(scheme : String) -> Json {
  let m : Map[String, Json] = Map([
    ("type", "http".to_json()),
    ("scheme", scheme.to_json()),
  ])
  m.to_json()
}

///|
/// An `apiKey` scheme object for `in`/`name`.
fn api_key_json(in_ : String, name : String) -> Json {
  let m : Map[String, Json] = Map([
    ("type", "apiKey".to_json()),
    ("in", in_.to_json()),
    ("name", name.to_json()),
  ])
  m.to_json()
}

///|
/// This scheme as a Swagger 2.0 security definition, or `None` when 2.0 cannot
/// express it. Swagger 2.0 has no `http` type: an HTTP bearer or digest is
/// expressed as an `apiKey` in the `Authorization` header (the standard 2.0
/// workaround), Basic uses the `basic` type, and OAuth2 flows are the flat 2.0
/// form (`flow` + URLs at top level, `accessCode` being 2.0's name for the
/// authorization-code flow). Swagger 2.0 `apiKey` only allows `header`/`query`,
/// so a cookie key maps to a header. OpenID Connect has no 2.0 equivalent at
/// all, and inventing one would describe an API that does not exist — so it is
/// left out of the 2.0 document.
fn SecurityScheme::to_openapi_20(self : SecurityScheme) -> Json? {
  match self {
    OAuth2Password(token_url~, scopes~) => {
      let m : Map[String, Json] = Map([
        ("type", "oauth2".to_json()),
        ("flow", "password".to_json()),
        ("tokenUrl", token_url.to_json()),
        ("scopes", scopes_object(scopes)),
      ])
      Some(m.to_json())
    }
    OAuth2Code(authorization_url~, token_url~, scopes~, ..) => {
      let m : Map[String, Json] = Map([
        ("type", "oauth2".to_json()),
        ("flow", "accessCode".to_json()),
        ("authorizationUrl", authorization_url.to_json()),
        ("tokenUrl", token_url.to_json()),
        ("scopes", scopes_object(scopes)),
      ])
      Some(m.to_json())
    }
    HttpBearer(_) => Some(api_key_json("header", "Authorization"))
    HttpDigest => Some(api_key_json("header", "Authorization"))
    ApiKeyHeader(name~) => Some(api_key_json("header", name))
    ApiKeyQuery(name~) => Some(api_key_json("query", name))
    ApiKeyCookie(name~) => Some(api_key_json("header", name))
    HttpBasic => {
      let m : Map[String, Json] = Map([("type", "basic".to_json())])
      Some(m.to_json())
    }
    OpenIdConnect(_) => None
  }
}

///|
/// The `{name: scheme, ...}` object for the app's declared schemes, rendered in
/// the shape `version` uses, each carrying its `description` when it has one. A
/// scheme the target version cannot express is omitted.
fn security_schemes_json(
  schemes : Array[DeclaredScheme],
  version : OpenApiVersion,
) -> Map[String, Json] {
  let m : Map[String, Json] = Map([])
  for d in schemes {
    let body = match version {
      Swagger20 => d.kind.to_openapi_20()
      _ => Some(d.kind.to_openapi_3x())
    }
    match body {
      Some(j) =>
        m[d.name] = if d.description != "" {
          with_description(j, d.description)
        } else {
          j
        }
      None => ()
    }
  }
  m
}