///| 
/// Provider-factory seams for host composition.
///
/// A host supplies an opaque provider settings object and a generic OAuth
/// credential. Provider extensions own interpretation of those values:
/// defaults, endpoint/protocol selection, model capabilities, and credential
/// rebuilding stay behind this contract. The router only receives ready
/// `ProviderModelCatalog` values.

///|
pub(all) struct ProviderConfigSource {
  /// Provider-owned settings copied from a generic host configuration source.
  settings : Json?
  /// Canonical tagged credential. Hosts should use this field for all new
  /// composition; it represents the one active auth method for this provider.
  provider_credential : @oauth.ProviderCredential?
  /// Generic credential-store result. Provider extensions decide whether and
  /// how this credential is used (including canonical OAuth provider ids).
  ///
  /// Deprecated compatibility input. If it is supplied together with
  /// `api_key_credential`, composition fails rather than preferring OAuth.
  credential : @oauth.Credential?
  /// Generic API-key credential loaded by the host store. Providers validate
  /// and interpret the opaque secret; the host never parses it.
  ///
  /// Deprecated compatibility input; see `credential` above.
  api_key_credential : @oauth.ApiKeyCredential?
  /// Host-cached model-discovery records for this provider, opaque JSON the
  /// provider interprets with its own decoder. Hosts seed it from a previous
  /// `refresh` so the static `build` path can reconstruct a multi-slot catalog
  /// without a network request. Providers that do not support offline
  /// expansion ignore it; a value the provider cannot decode degrades to the
  /// uncached build and never fails composition.
  cached_models : Json?
  /// Canonical credential store the snapshot was read from. Providers that
  /// renew credentials during `refresh` (load-time OAuth renewal) persist the
  /// rotated record here; `None` keeps renewal memory-only.
  credential_store : &@oauth.ProviderCredentialStore?
}

///|
pub fn ProviderConfigSource::ProviderConfigSource(
  settings? : Json? = None,
  provider_credential? : @oauth.ProviderCredential? = None,
  credential? : @oauth.Credential? = None,
  api_key_credential? : @oauth.ApiKeyCredential? = None,
  cached_models? : Json? = None,
  credential_store? : &@oauth.ProviderCredentialStore? = None,
) -> ProviderConfigSource {
  {
    settings,
    provider_credential,
    credential,
    api_key_credential,
    cached_models,
    credential_store,
  }
}

///|
/// Resolve the canonical tagged credential for a provider. The legacy fields
/// remain source-compatible for existing hosts, but a host that exposes both
/// records is an observable composition failure: silently choosing OAuth would
/// make an explicit API-key login disappear after recomposition.
pub fn ProviderConfigSource::effective_credential(
  self : ProviderConfigSource,
  provider_id : String,
) -> @oauth.ProviderCredential? raise @posoco.CompositionError {
  match self.provider_credential {
    Some(value) => {
      if self.credential is Some(_) || self.api_key_credential is Some(_) {
        raise @posoco.CompositionError::ManifestSchemaError(
          manifest_id="posoco_ext_llm.credentials",
          detail="ambiguous credential for provider " + provider_id,
        )
      }
      Some(value)
    }
    None =>
      match (self.credential, self.api_key_credential) {
        (Some(_), Some(_)) =>
          raise @posoco.CompositionError::ManifestSchemaError(
            manifest_id="posoco_ext_llm.credentials",
            detail="ambiguous credential for provider " + provider_id,
          )
        (Some(value), None) => Some(@oauth.ProviderCredential::OAuth(value))
        (None, Some(value)) => Some(@oauth.ProviderCredential::ApiKey(value))
        (None, None) => None
      }
  }
}

///|
/// Return one opaque setting without imposing provider-specific names on the
/// host. A missing/non-object source simply has no setting; factories decide
/// which keys are required and raise a typed composition error for malformed
/// values.
pub fn ProviderConfigSource::setting(
  self : ProviderConfigSource,
  key : String,
) -> Json? raise @posoco.CompositionError {
  match self.settings {
    Some(Json::Object(fields)) => fields.get(key)
    Some(_) =>
      raise @posoco.CompositionError::ManifestSchemaError(
        manifest_id="posoco_ext_llm.provider_source",
        detail="provider settings must be a JSON object",
      )
    None => None
  }
}

///|
pub fn ProviderConfigSource::has_settings(self : ProviderConfigSource) -> Bool {
  match self.settings {
    Some(_) => true
    None => false
  }
}

///|
/// Result of asking a provider extension to resolve a catalog. Missing
/// credentials/settings are a normal unconfigured state; malformed configured
/// values remain composition failures and must not be silently skipped.
pub(all) enum ProviderBuildResult {
  Ready(ProviderModelCatalog)
  Unconfigured
}

///|
/// Public extension seam for provider-owned configuration and catalog
/// construction. Implementations never expose endpoint or protocol details to
/// the host/router.
pub(open) trait ProviderFactory {
  fn provider_id(Self) -> String
  /// Canonical key used by the generic credential store. It may differ from
  /// the user-facing provider id (for example OpenAI/Codex OAuth).
  fn credential_id(Self) -> String
  fn build(Self, source : ProviderConfigSource) -> ProviderBuildResult raise @posoco.CompositionError
}

///|
/// Optional provider-owned model discovery seam.
///
/// A provider implements this trait only when its authenticated endpoint can
/// return a live model catalog. Hosts must call `refresh` explicitly (for
/// example during first startup or immediately after a successful login); a
/// normal `ProviderFactory::build` remains a pure snapshot reconstruction and
/// never performs an implicit network request.  Transport and schema failures
/// are raised as `CompositionError` by the provider implementation, so a host
/// cannot accidentally fall back to a stale or static catalog.
pub(open) trait RefreshableProviderFactory {
  fn provider_id(Self) -> String
  async fn refresh(Self, source : ProviderConfigSource) -> ProviderBuildResult raise @posoco.CompositionError
}

///|
/// Optional OAuth contribution paired with a ProviderFactory. Keeping login
/// as a separate trait means non-OAuth providers do not implement fake login
/// paths or know about host interaction details.
pub(open) trait OAuthFactory {
  fn provider_id(Self) -> String
  fn credential_id(Self) -> String
  async fn login(Self, interaction : &@oauth.AuthInteraction) -> @oauth.Credential raise @oauth.OAuthError
}

///|
/// Provider-neutral authentication method advertised by a model catalog.
pub(all) enum AuthMethod {
  ApiKey
  OAuth
} derive(Eq, Debug)

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

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

///|
pub fn AuthMethod::to_id(self : AuthMethod) -> String {
  match self {
    ApiKey => "api_key"
    OAuth => "oauth"
  }
}

///|
/// Provider-owned API-key login contribution. The provider prompts and
/// validates its secret; the router only persists it and rebuilds slots.
pub(open) trait ApiKeyFactory {
  fn provider_id(Self) -> String
  fn credential_id(Self) -> String
  async fn login(Self, interaction : &@oauth.AuthPromptInteraction) -> @oauth.ApiKeyCredential raise @oauth.OAuthError
}