// OAuth2 password bearer flow — FastAPI's `OAuth2PasswordBearer` + `Security`.
// A token endpoint reads an `OAuth2PasswordRequestForm` off the urlencoded body
// and issues an HS256 JWT; a protected route hands its request to a `Security`
// value, which pulls the `Authorization: Bearer ` header, verifies the
// JWT, checks the required scopes, and either yields the authenticated user or
// the 401/403 response to return. With no reflection there's no auto-injection,
// so the handler asks the `Security` explicitly and matches the `Result` — the
// same shape as axum extractors or Go middleware that stashes the claims.

///|
/// The identity a verified token carries, injected into a protected handler
/// (← the object FastAPI's `get_current_user` returns). `subject` is the `sub`
/// claim, `scopes` the granted scopes, and `claims` the whole verified payload
/// for anything else the handler needs (`exp`, custom claims).
pub(all) struct AuthenticatedUser {
  subject : String
  scopes : Array[String]
  claims : Map[String, Json]
}

///|
/// Whether this user was granted `scope`.
pub fn AuthenticatedUser::has_scope(
  self : AuthenticatedUser,
  scope : String,
) -> Bool {
  self.scopes.contains(scope)
}

///|
/// Read the `scopes` claim, accepting either a JSON array of strings or a single
/// space-delimited string (both are seen in the wild). Absent -> no scopes.
fn scopes_from_claims(claims : Map[String, Json]) -> Array[String] {
  match claims.get("scopes") {
    Some(Array(a)) => {
      let out : Array[String] = []
      for v in a {
        match v {
          String(s) => out.push(s)
          _ => ()
        }
      }
      out
    }
    Some(String(s)) => @ctx.split_scopes(s)
    _ => []
  }
}

///|
/// The `sub` claim as a string, `""` if absent or non-string.
fn subject_from_claims(claims : Map[String, Json]) -> String {
  match claims.get("sub") {
    Some(String(s)) => s
    _ => ""
  }
}

///|
/// An OAuth2 password-bearer security scheme (← FastAPI's `OAuth2PasswordBearer`).
/// Holds the `token_url` (the endpoint that issues tokens, surfaced to API docs)
/// and the shared HS256 `secret` used to verify presented tokens.
pub(all) struct OAuth2PasswordBearer {
  token_url : String
  secret : String
}

///|
/// Build a password-bearer scheme pointing at the token endpoint at `token_url`,
/// verifying tokens with `secret`.
pub fn OAuth2PasswordBearer::new(
  token_url : String,
  secret : String,
) -> OAuth2PasswordBearer {
  { token_url, secret, }
}

///|
/// A `401 Unauthorized` with a `WWW-Authenticate` challenge and a
/// `{"detail": ...}` body — FastAPI's response for a missing or invalid
/// credential. RFC 9110 §15.5.2 requires the challenge, so it is a parameter:
/// `Basic realm="…"` for Basic, `Bearer scope="…"` for a scoped bearer route.
pub fn unauthorized(
  detail : String,
  challenge~ : String = "Bearer",
) -> @moonasgi.Response {
  let body : Map[String, Json] = Map([("detail", Json(detail))])
  @moonasgi.Response::new(
    401,
    [("content-type", "application/json"), ("www-authenticate", challenge)],
    @moonjson.dump(Json(body)),
  )
}

///|
/// A `403 Forbidden` with a `{"detail": ...}` body — returned when the token is
/// valid but lacks a required scope. The token authenticated the caller, so this
/// is authorization, not authentication: 403, not 401.
pub fn forbidden(detail : String) -> @moonasgi.Response {
  let body : Map[String, Json] = Map([("detail", Json(detail))])
  @moonasgi.Response::new(
    403,
    [("content-type", "application/json")],
    @moonjson.dump(Json(body)),
  )
}

///|
/// Authenticate a request against this scheme and enforce `scopes`
/// (← FastAPI's `Security(get_current_user, scopes=[...])`). On success returns
/// the `AuthenticatedUser`; otherwise the response to return:
///
/// - no bearer token -> `401` `{"detail":"Not authenticated"}`
/// - malformed / bad-signature / expired / not-yet-valid token -> `401`
///   `{"detail":"Could not validate credentials"}`
/// - valid token missing a required scope -> `403` `{"detail":"Not enough permissions"}`
///
/// Both `401`s carry the challenge the required scopes describe — `Bearer` on
/// its own, or `Bearer scope="..."` — so a client learns what it should have
/// asked for.
///
/// `now_secs` is the verification time (Unix seconds), passed in so the check
/// stays pure and testable on every backend.
pub fn OAuth2PasswordBearer::authenticate(
  self : OAuth2PasswordBearer,
  ctx : @ctx.Context,
  now_secs : Int64,
  scopes~ : Array[String] = [],
) -> Result[AuthenticatedUser, @moonasgi.Response] {
  verify_bearer(ctx, self.secret, SecurityScopes::new(scopes~), now_secs, true)
}

// -- token issuance -----------------------------------------------------------

///|
/// Mint an HS256 access token for `subject` (← FastAPI's `create_access_token`).
/// Stamps `sub`, `iat` (= `now_secs`), `exp` (= `now_secs + expires_in_secs`),
/// and, when non-empty, `scopes`; `extra` merges in any further claims. Times are
/// Unix seconds. `secret` is the shared HS256 key.
///
/// `extra` adds claims. Where it names one the arguments already set — `sub`,
/// `iat`, `exp`, `scopes` — the arguments win and saying it twice aborts, because
/// a token whose subject is not the `subject` passed in is a mistake in the
/// program rather than a choice about configuration. `wins=Extra` hands the
/// decision to `extra`, and `clash` can silence it or take a callback.
pub fn create_access_token(
  subject : String,
  secret : String,
  now_secs : Int64,
  scopes~ : Array[String] = [],
  expires_in_secs~ : Int64 = 3600,
  extra~ : Map[String, Json] = Map([]),
  wins~ : @jwt.Wins = Base,
  clash~ : @jwt.OnClash[Map[String, Json]] = Panic,
) -> String {
  // `iat` / `exp` are RFC 7519 NumericDates — JSON numbers, so an off-the-shelf
  // verifier reads them (`Int64::to_json` would emit a string).
  let base : Map[String, Json] = Map([])
  base["sub"] = Json(subject)
  base["iat"] = Json(now_secs.to_double())
  base["exp"] = Json((now_secs + expires_in_secs).to_double())
  if scopes.length() > 0 {
    let arr : Array[Json] = []
    for s in scopes {
      arr.push(Json(s))
    }
    base["scopes"] = Json(arr)
  }
  let merged : Map[String, Json] = Map([])
  for k, v in base {
    merged[k] = v
  }
  let mut clashed = false
  for k, v in extra {
    if base.get(k) is Some(_) {
      clashed = true
      if wins is Base {
        continue
      }
    }
    merged[k] = v
  }
  let claims = if clashed {
    match clash {
      Ignore => merged
      Panic =>
        abort(
          "create_access_token: `extra` names a claim the arguments already set",
        )
      Handle(decide) => decide(base, merged)
    }
  } else {
    merged
  }
  @jwt.sign(claims, HS256, hs256(secret))
}

///|
/// The `200` token response body `{"access_token": ..., "token_type": "bearer"}`
/// — the OAuth2 password-grant reply FastAPI's token endpoint returns.
pub fn token_response(access_token : String) -> @moonasgi.Response {
  let body : Map[String, Json] = Map([
    ("access_token", Json(access_token)),
    ("token_type", Json("bearer")),
  ])
  @status.json(200, Json(body))
}