//! Normalized binding shapes, resolved-connection shim, and concrete
//! lease implementations used by Phase 2 provider runtimes.
//!
//! `NormalizedBackendKind` / `NormalizedAuthMethod` are typed sums over
//! per-provider enums declared in `providers/<p>/{backend,auth}.rs`.
//! `ResolvedConnection.shim_credential` is the **Phase 2-only** seam that
//! `build_client` reads to get the resolved secret. Phase 3 deletes this
//! field when `build_client` owns HTTP request assembly directly.

use std::sync::Arc;

use async_trait::async_trait;
use chrono::{DateTime, Utc};

use meerkat_core::{
    AuthError, AuthLease, AuthMetadata, AuthRefreshReason, BackendProfile, HttpAuthorizer,
    Provider, ResolvedAuthKind,
};

use meerkat_core::provider_matrix::anthropic::{AnthropicAuthMethod, AnthropicBackendKind};
use meerkat_core::provider_matrix::google::{GoogleAuthMethod, GoogleBackendKind};
use meerkat_core::provider_matrix::openai::{OpenAiAuthMethod, OpenAiBackendKind};
use meerkat_core::provider_matrix::self_hosted::{SelfHostedAuthMethod, SelfHostedBackendKind};

pub use crate::provider_runtime::catalog::ValidatedBinding;

/// Provider-tagged normalized backend kind. Each variant is produced by the
/// provider runtime catalog.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NormalizedBackendKind {
    OpenAi(OpenAiBackendKind),
    Anthropic(AnthropicBackendKind),
    Google(GoogleBackendKind),
    SelfHosted(SelfHostedBackendKind),
}

impl NormalizedBackendKind {
    /// The canonical wire string for this backend kind, delegating to the
    /// per-provider matrix enum's `as_str`. The matrix owns the literal; this
    /// is the typed-to-string projection used when materializing config.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::OpenAi(kind) => kind.as_str(),
            Self::Anthropic(kind) => kind.as_str(),
            Self::Google(kind) => kind.as_str(),
            Self::SelfHosted(kind) => kind.as_str(),
        }
    }

    /// The default API-key backend kind for a provider — the canonical backend
    /// used when no explicit backend hint is given (e.g. non-interactive API-key
    /// login). Total over Provider; `Other` has no default. Owned here (the typed
    /// matrix) so surfaces never hand-map a provider->backend-kind string with a
    /// fail-open arm.
    pub fn default_for_provider(provider: Provider) -> Option<Self> {
        match provider {
            Provider::Anthropic => Some(Self::Anthropic(AnthropicBackendKind::AnthropicApi)),
            Provider::OpenAI => Some(Self::OpenAi(OpenAiBackendKind::OpenAiApi)),
            Provider::Gemini => Some(Self::Google(GoogleBackendKind::GoogleGenAi)),
            Provider::SelfHosted => Some(Self::SelfHosted(SelfHostedBackendKind::SelfHosted)),
            Provider::Other => None,
        }
    }
}

/// The typed [`NormalizedBackendKind`] these OAuth login credentials target.
///
/// Lives here as a free function (not an inherent method on
/// [`meerkat_core::OAuthProviderIdentity`]) because the identity is owned by
/// `meerkat-core` while [`NormalizedBackendKind`] is owned here in
/// `meerkat-llm-core`: this is the lowest crate that can see both. Auth-core's
/// `oauth_provider_declaration` reads it from here rather than re-deriving the
/// mapping.
pub fn oauth_provider_backend_kind(
    id: meerkat_core::OAuthProviderIdentity,
) -> NormalizedBackendKind {
    use meerkat_core::OAuthProviderIdentity;
    match id {
        OAuthProviderIdentity::AnthropicClaudeAi
        | OAuthProviderIdentity::AnthropicConsoleApiKey => {
            NormalizedBackendKind::Anthropic(AnthropicBackendKind::AnthropicApi)
        }
        OAuthProviderIdentity::OpenAiChatGpt => {
            NormalizedBackendKind::OpenAi(OpenAiBackendKind::ChatGptBackend)
        }
        OAuthProviderIdentity::GoogleCodeAssist => {
            NormalizedBackendKind::Google(GoogleBackendKind::GoogleCodeAssist)
        }
    }
}

/// Provider-tagged normalized auth method.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NormalizedAuthMethod {
    OpenAi(OpenAiAuthMethod),
    Anthropic(AnthropicAuthMethod),
    Google(GoogleAuthMethod),
    SelfHosted(SelfHostedAuthMethod),
}

impl NormalizedAuthMethod {
    /// Build the provider-tagged normalized auth method from a resolved
    /// [`AuthProfile`](meerkat_core::AuthProfile), parsing the profile's
    /// declared `auth_method` string through the profile provider's typed
    /// matrix enum. Returns `None` for `Provider::Other` or an `auth_method`
    /// that is not a member of that provider's auth matrix.
    ///
    /// This is the single typed projection consumed by every surface
    /// (RPC/REST/CLI) that holds a resolved `AuthProfile`, replacing the
    /// per-surface inline `provider -> *AuthMethod::parse` copies.
    pub fn from_auth_profile(auth_profile: &meerkat_core::AuthProfile) -> Option<Self> {
        Self::parse_for_provider(auth_profile.provider, auth_profile.auth_method.as_str())
    }

    /// Parse a raw `auth_method` string through the given provider's typed
    /// matrix enum. Returns `None` for `Provider::Other` or a method that is
    /// not a member of that provider's auth matrix.
    ///
    /// This is the single auth-method-string ingress for every surface that
    /// holds a provider identity but no resolved `AuthProfile` (e.g. CLI
    /// non-interactive login).
    pub fn parse_for_provider(provider: Provider, raw: &str) -> Option<Self> {
        match provider {
            Provider::OpenAI => OpenAiAuthMethod::parse(raw).map(Self::OpenAi),
            Provider::Anthropic => AnthropicAuthMethod::parse(raw).map(Self::Anthropic),
            Provider::Gemini => GoogleAuthMethod::parse(raw).map(Self::Google),
            Provider::SelfHosted => SelfHostedAuthMethod::parse(raw).map(Self::SelfHosted),
            Provider::Other => None,
        }
    }

    /// The persisted credential mode this auth method stores in the
    /// `TokenStore`, or `None` for authorizer/ADC/SigV4-backed methods that
    /// hold no persisted secret.
    ///
    /// Delegates to the per-provider `*AuthMethod::persisted_auth_mode`, which
    /// is the typed owner of the auth-method -> persisted-mode mapping.
    pub fn persisted_auth_mode(self) -> Option<meerkat_core::auth::token_store::PersistedAuthMode> {
        match self {
            Self::OpenAi(method) => method.persisted_auth_mode(),
            Self::Anthropic(method) => method.persisted_auth_mode(),
            Self::Google(method) => method.persisted_auth_mode(),
            Self::SelfHosted(method) => method.persisted_auth_mode(),
        }
    }

    /// The canonical wire string for this auth method, delegating to the
    /// per-provider matrix enum's as_str (the matrix owns the literal).
    pub fn as_str(self) -> &'static str {
        match self {
            Self::OpenAi(m) => m.as_str(),
            Self::Anthropic(m) => m.as_str(),
            Self::Google(m) => m.as_str(),
            Self::SelfHosted(m) => m.as_str(),
        }
    }
}

// Plan §6.11 deleted the legacy marker enum. Credential material
// lives on `auth_lease` directly via the typed `ResolvedAuthKind`
// variants: `InlineSecret(Arc<String>)` for simple-secret flows
// (dogma §5 closure — replaces the prior `__secret__` magic-header
// convention), `DynamicAuthorizer` for authorizer-backed flows,
// `StaticHeaders` for multi-header wire-level envelopes, and `None`
// for authless transports. `build_client` reads
// `ResolvedConnection::resolved_secret()` /
// `resolved_authorizer()`.

/// A fully resolved connection carries the trait-object lease alongside
/// backend metadata.
#[derive(Clone)]
pub struct ResolvedConnection {
    pub provider: Provider,
    pub backend: NormalizedBackendKind,
    pub backend_profile: Arc<BackendProfile>,
    pub auth_lease: Arc<dyn AuthLease>,
}

impl std::fmt::Debug for ResolvedConnection {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ResolvedConnection")
            .field("provider", &self.provider)
            .field("backend", &self.backend)
            .field("backend_profile_id", &self.backend_profile.id)
            .finish()
    }
}

impl ResolvedConnection {
    /// Extract the resolved inline secret (api key, bearer token, OAuth
    /// access token) from the auth lease. Returns `None` for
    /// authorizer-backed leases. Plan §6.11 + dogma §5 closure:
    /// reads the typed `InlineSecret` variant of `ResolvedAuthKind`
    /// (replaces the prior `__secret__` synthetic-header convention).
    pub fn resolved_secret(&self) -> Option<String> {
        match self.auth_lease.kind() {
            meerkat_core::ResolvedAuthKind::InlineSecret(secret) => Some((**secret).clone()),
            _ => None,
        }
    }

    /// Extract the resolved dynamic authorizer (AWS SigV4, Google Auth,
    /// Azure AD, ExternalAuthorizer-backed) from the auth lease. Returns
    /// `None` for non-authorizer leases. Plan §6.11.
    pub fn resolved_authorizer(&self) -> Option<Arc<dyn HttpAuthorizer>> {
        match self.auth_lease.kind() {
            meerkat_core::ResolvedAuthKind::DynamicAuthorizer(auth) => Some(auth.clone()),
            _ => None,
        }
    }
}

// ---------------------------------------------------------------------
// Lease implementations
// ---------------------------------------------------------------------

/// Static lease holding pre-projected headers + metadata. Used for api_key
/// and static_bearer resolutions.
pub struct StaticLease {
    kind: ResolvedAuthKind,
    metadata: AuthMetadata,
    expires_at: Option<DateTime<Utc>>,
    source_label: String,
}

impl StaticLease {
    /// Construct a lease carrying pre-projected wire headers. Used by
    /// resolvers that know the full header set (future post-§6.12
    /// paths). For raw secrets (api keys / bearer tokens), callers
    /// should prefer [`StaticLease::inline_secret`].
    pub fn new(
        headers: Vec<(String, String)>,
        metadata: AuthMetadata,
        expires_at: Option<DateTime<Utc>>,
        source_label: impl Into<String>,
    ) -> Self {
        Self {
            kind: ResolvedAuthKind::StaticHeaders(headers),
            metadata,
            expires_at,
            source_label: source_label.into(),
        }
    }

    /// Construct a lease carrying a raw inline secret (api key, bearer
    /// token, OAuth access token). Plan §6.11 + dogma §5: the typed
    /// `ResolvedAuthKind::InlineSecret` variant replaces the earlier
    /// `StaticHeaders(vec![("__secret__", value)])` magic-string
    /// convention.
    pub fn inline_secret(
        secret: String,
        metadata: AuthMetadata,
        expires_at: Option<DateTime<Utc>>,
        source_label: impl Into<String>,
    ) -> Self {
        Self {
            kind: ResolvedAuthKind::InlineSecret(Arc::new(secret)),
            metadata,
            expires_at,
            source_label: source_label.into(),
        }
    }

    /// Construct a lease with no credential material (authorizer-backed
    /// flows where the runtime constructs the authorizer in
    /// `build_client`, not the resolver). Matches
    /// `ResolvedAuthKind::None`.
    pub fn empty_lease(metadata: AuthMetadata, source_label: impl Into<String>) -> Self {
        Self {
            kind: ResolvedAuthKind::None,
            metadata,
            expires_at: None,
            source_label: source_label.into(),
        }
    }
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl AuthLease for StaticLease {
    fn kind(&self) -> &ResolvedAuthKind {
        &self.kind
    }
    fn metadata(&self) -> &AuthMetadata {
        &self.metadata
    }
    fn expires_at(&self) -> Option<DateTime<Utc>> {
        self.expires_at
    }
    fn source_label(&self) -> &str {
        &self.source_label
    }
    async fn refresh(&self, _reason: AuthRefreshReason) -> Result<(), AuthError> {
        // StaticLease has no refresh semantics in Phase 2.
        Ok(())
    }
}

/// Dynamic lease wrapping a runtime authorizer. Phase 2 build_client does
/// not accept this shape — authorizer-backed flows use this directly
/// `build_client` returns DynamicAuthorizerNotYetSupportedInShimMode.
pub struct DynamicLease {
    authorizer: Arc<dyn HttpAuthorizer>,
    metadata: AuthMetadata,
    expires_at: Option<DateTime<Utc>>,
    source_label: String,
    kind: ResolvedAuthKind,
}

impl DynamicLease {
    pub fn new(
        authorizer: Arc<dyn HttpAuthorizer>,
        metadata: AuthMetadata,
        expires_at: Option<DateTime<Utc>>,
        source_label: impl Into<String>,
    ) -> Self {
        let kind = ResolvedAuthKind::DynamicAuthorizer(authorizer.clone());
        Self {
            authorizer,
            metadata,
            expires_at,
            source_label: source_label.into(),
            kind,
        }
    }

    /// Construct a dynamic lease whose freshness is projected from the
    /// underlying authorizer. This is for authorizer-backed flows such as
    /// Google ADC and Azure AD where the token is fetched lazily per request.
    pub fn from_authorizer(
        authorizer: Arc<dyn HttpAuthorizer>,
        metadata: AuthMetadata,
        source_label: impl Into<String>,
    ) -> Self {
        Self::new(authorizer, metadata, None, source_label)
    }

    pub fn authorizer(&self) -> &Arc<dyn HttpAuthorizer> {
        &self.authorizer
    }
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl AuthLease for DynamicLease {
    fn kind(&self) -> &ResolvedAuthKind {
        &self.kind
    }
    fn metadata(&self) -> &AuthMetadata {
        &self.metadata
    }
    fn expires_at(&self) -> Option<DateTime<Utc>> {
        self.expires_at.or_else(|| self.authorizer.expires_at())
    }
    fn source_label(&self) -> &str {
        &self.source_label
    }
    async fn refresh(&self, reason: AuthRefreshReason) -> Result<(), AuthError> {
        // A dynamic lease has no in-place refresh: the caller must re-resolve
        // the typed auth binding through the resolver. Surface that as the
        // typed `ResolveRequired`, not a generic `RefreshFailed` (no refresh
        // was attempted), so callers can branch on re-resolution explicitly.
        Err(AuthError::ResolveRequired(format!(
            "dynamic lease '{}' cannot refresh in place for reason {reason:?}; re-resolve the typed auth_binding",
            self.source_label
        )))
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
    use super::*;

    // Plan §6.11 deleted the §6.11 deletion artifact cleanup.

    #[tokio::test]
    async fn static_lease_satisfies_trait() {
        let lease: Arc<dyn AuthLease> = Arc::new(StaticLease::new(
            Vec::new(),
            AuthMetadata::default(),
            None,
            "test",
        ));
        assert!(matches!(lease.kind(), ResolvedAuthKind::StaticHeaders(_)));
        assert_eq!(lease.source_label(), "test");
        assert!(lease.refresh(AuthRefreshReason::Manual).await.is_ok());
    }

    #[tokio::test]
    async fn dynamic_lease_refresh_reports_unsupported_instead_of_success() {
        #[derive(Debug)]
        struct TestAuthorizer;

        #[async_trait::async_trait]
        impl HttpAuthorizer for TestAuthorizer {
            async fn authorize(
                &self,
                _req: &mut meerkat_core::auth::HttpAuthorizationRequest<'_>,
            ) -> Result<(), AuthError> {
                Ok(())
            }

            fn label(&self) -> &'static str {
                "test-authorizer"
            }
        }

        let lease: Arc<dyn AuthLease> = Arc::new(DynamicLease::new(
            Arc::new(TestAuthorizer),
            AuthMetadata::default(),
            None,
            "dynamic:test",
        ));
        let err = lease
            .refresh(AuthRefreshReason::Manual)
            .await
            .expect_err("dynamic refresh must not report success without work");
        assert!(matches!(err, AuthError::ResolveRequired(_)));
    }

    #[tokio::test]
    async fn dynamic_lease_projects_authorizer_freshness() {
        #[derive(Debug)]
        struct ExpiringAuthorizer {
            expires_at: DateTime<Utc>,
        }

        #[async_trait::async_trait]
        impl HttpAuthorizer for ExpiringAuthorizer {
            async fn authorize(
                &self,
                _req: &mut meerkat_core::auth::HttpAuthorizationRequest<'_>,
            ) -> Result<(), AuthError> {
                Ok(())
            }

            fn label(&self) -> &'static str {
                "expiring-authorizer"
            }

            fn expires_at(&self) -> Option<DateTime<Utc>> {
                Some(self.expires_at)
            }
        }

        let expires_at =
            chrono::TimeZone::with_ymd_and_hms(&chrono::Utc, 2026, 4, 28, 12, 0, 0).unwrap();
        let lease: Arc<dyn AuthLease> = Arc::new(DynamicLease::from_authorizer(
            Arc::new(ExpiringAuthorizer { expires_at }),
            AuthMetadata::default(),
            "dynamic:expiring",
        ));

        assert_eq!(lease.expires_at(), Some(expires_at));
    }

    #[test]
    fn default_backend_kind_per_provider_is_typed() {
        assert_eq!(
            NormalizedBackendKind::default_for_provider(Provider::Anthropic),
            Some(NormalizedBackendKind::Anthropic(
                AnthropicBackendKind::AnthropicApi
            ))
        );
        assert_eq!(
            NormalizedBackendKind::default_for_provider(Provider::OpenAI),
            Some(NormalizedBackendKind::OpenAi(OpenAiBackendKind::OpenAiApi))
        );
        assert_eq!(
            NormalizedBackendKind::default_for_provider(Provider::Gemini),
            Some(NormalizedBackendKind::Google(
                GoogleBackendKind::GoogleGenAi
            ))
        );
        assert_eq!(
            NormalizedBackendKind::default_for_provider(Provider::SelfHosted),
            Some(NormalizedBackendKind::SelfHosted(
                SelfHostedBackendKind::SelfHosted
            ))
        );
        assert_eq!(
            NormalizedBackendKind::default_for_provider(Provider::Other),
            None
        );
    }

    #[test]
    fn auth_method_as_str_delegates_to_matrix() {
        assert_eq!(
            NormalizedAuthMethod::Anthropic(AnthropicAuthMethod::ClaudeAiOauth).as_str(),
            "claude_ai_oauth"
        );
    }
}
