///|
/// credential.mbt — OAuth credential types shared across all providers.

///|
/// A stored OAuth credential. All tokens are opaque strings to ext-oauth;
/// provider-specific fields (like OpenAI's account_id) go in `metadata`.
pub(all) struct Credential {
  access_token : String
  refresh_token : String
  expires_at : Int // epoch seconds; 0 = never expires
  token_type : String // usually "Bearer"
  metadata : Map[String, String]
}

///|
pub fn Credential::Credential(
  access_token~ : String,
  refresh_token~ : String,
  expires_at~ : Int,
  token_type? : String = "Bearer",
  metadata? : Map[String, String] = Map([]),
) -> Credential {
  { access_token, refresh_token, expires_at, token_type, metadata, }
}

///|
/// Check if the access token is expired (with a 60-second safety margin).
pub fn Credential::is_expired(self : Credential, now_epoch : Int) -> Bool {
  if self.expires_at == 0 {
    false
  } else {
    now_epoch >= self.expires_at - 60
  }
}

///|
/// Serialize to JSON for persistence.
pub fn Credential::to_json(self : Credential) -> Json {
  let meta_json : Map[String, Json] = Map([])
  for k, v in self.metadata {
    meta_json[k] = Json::string(v)
  }
  Json::object(
    Map([
      ("access_token", Json::string(self.access_token)),
      ("refresh_token", Json::string(self.refresh_token)),
      ("expires_at", Json::number(self.expires_at.to_double())),
      ("token_type", Json::string(self.token_type)),
      ("metadata", Json::object(meta_json)),
    ]),
  )
}

///|
/// Deserialize from JSON. Returns None on parse failure.
pub fn Credential::from_json(json : Json) -> Credential? {
  match json {
    Json::Object(map) => {
      let access = match map.get("access_token") {
        Some(Json::String(s)) => s
        _ => return None
      }
      let refresh = match map.get("refresh_token") {
        Some(Json::String(s)) => s
        _ => return None
      }
      let expires = match map.get("expires_at") {
        Some(Json::Number(n, ..)) => n.to_int()
        _ => 0
      }
      let token_type = match map.get("token_type") {
        Some(Json::String(s)) => s
        _ => "Bearer"
      }
      let metadata : Map[String, String] = Map([])
      match map.get("metadata") {
        Some(Json::Object(meta_map)) =>
          for k, v in meta_map {
            match v {
              Json::String(s) => metadata[k] = s
              _ => ()
            }
          }
        _ => ()
      }
      Some(
        Credential::Credential(
          access_token=access,
          refresh_token=refresh,
          expires_at=expires,
          token_type~,
          metadata~,
        ),
      )
    }
    _ => None
  }
}

///|
/// How to apply the credential to an HTTP request.
pub(all) enum AuthHeader {
  Bearer(String) // Authorization: Bearer 
  Header(Map[String, String]) // custom headers
} derive(Eq, Debug)

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

///|
pub extend AuthHeader with @moonbitlang/core/debug.Debug::{to_repr}

///|
/// Build a Bearer auth header from a credential.
pub fn Credential::to_auth_header(self : Credential) -> AuthHeader {
  Bearer(self.access_token)
}

///|
/// Read one provider-owned metadata value without exposing the map shape to a
/// host adapter.  Codex uses this accessor for its account id header.
pub fn Credential::metadata_value(self : Credential, key : String) -> String? {
  self.metadata.get(key)
}

///|
/// A provider-neutral stored API-key credential. The secret is opaque to the
/// host and is interpreted only by the provider extension that owns it.
pub(all) struct ApiKeyCredential {
  secret : String
  metadata : Map[String, String]
}

///|
pub fn ApiKeyCredential::ApiKeyCredential(
  secret~ : String,
  metadata? : Map[String, String] = Map([]),
) -> ApiKeyCredential {
  { secret, metadata, }
}

///|
/// The single active credential record for a provider.  A provider may expose
/// both OAuth and API-key login, but a host must persist exactly one selected
/// method under the provider's canonical id.  Keeping the method tag beside
/// the opaque payload prevents a host from silently preferring OAuth when both
/// legacy stores happen to contain a value.
pub(all) enum ProviderCredential {
  OAuth(Credential)
  ApiKey(ApiKeyCredential)
}

///|
/// Stable provider-neutral method id used by command payloads and storage
/// diagnostics.  This never includes credential material.
pub fn ProviderCredential::method_id(self : ProviderCredential) -> String {
  match self {
    OAuth(_) => "oauth"
    ApiKey(_) => "api_key"
  }
}

///|
/// Stable tagged persistence representation. The tag and payload live in one
/// record so a host cannot accidentally restore OAuth and API-key credentials
/// as two simultaneously active methods.
pub fn ProviderCredential::to_json(self : ProviderCredential) -> Json {
  match self {
    OAuth(credential) =>
      Json::object(
        Map([
          ("method", Json::string("oauth")),
          ("credential", credential.to_json()),
        ]),
      )
    ApiKey(credential) =>
      Json::object(
        Map([
          ("method", Json::string("api_key")),
          ("credential", credential.to_json()),
        ]),
      )
  }
}

///|
/// Decode the canonical tagged persistence shape. Provider-specific
/// validation remains in the provider extension; this only validates the
/// generic record envelope and opaque credential payload.
pub fn ProviderCredential::from_json(json : Json) -> ProviderCredential? {
  match json {
    Json::Object(map) => {
      let method_tag = match map.get("method") {
        Some(Json::String(value)) => value
        _ => return None
      }
      let payload = match map.get("credential") {
        Some(value) => value
        None => return None
      }
      match method_tag {
        "oauth" =>
          match Credential::from_json(payload) {
            Some(value) => Some(OAuth(value))
            None => None
          }
        "api_key" =>
          match ApiKeyCredential::from_json(payload) {
            Some(value) => Some(ApiKey(value))
            None => None
          }
        _ => None
      }
    }
    _ => None
  }
}

///|
/// Serialize without exposing provider-specific fields to the host.
pub fn ApiKeyCredential::to_json(self : ApiKeyCredential) -> Json {
  let meta_json : Map[String, Json] = Map([])
  for k, v in self.metadata {
    meta_json[k] = Json::string(v)
  }
  Json::object(
    Map([
      ("secret", Json::string(self.secret)),
      ("metadata", Json::object(meta_json)),
    ]),
  )
}

///|
/// Deserialize a generic API-key entry. Provider validation happens later in
/// its `ApiKeyFactory`; malformed generic shape is rejected here.
pub fn ApiKeyCredential::from_json(json : Json) -> ApiKeyCredential? {
  match json {
    Json::Object(map) => {
      let secret = match map.get("secret") {
        Some(Json::String(value)) =>
          if value.length() > 0 {
            value
          } else {
            return None
          }
        _ => return None
      }
      let metadata : Map[String, String] = Map([])
      match map.get("metadata") {
        Some(Json::Object(fields)) =>
          for key, value in fields {
            match value {
              Json::String(text) => metadata[key] = text
              _ => return None
            }
          }
        Some(_) => return None
        None => ()
      }
      Some(ApiKeyCredential::{ secret, metadata, })
    }
    _ => None
  }
}