use crate::{
    jwt::{sign_hs256_jwt, verify_hs256_jwt},
    AuthError, JwtClaims, Principal, PrincipalKind,
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

pub const IDENTITY_PROVIDER_RUNTIME_FEATURES: &[&str] = &[
    "local-users",
    "access-keys",
    "oidc",
    "ldap",
    "active-directory",
    "saml",
];

pub const IDENTITY_PROVIDER_ADMIN_SURFACES: &[&str] = &[
    "AuthStore::upsert_identity_provider",
    "AuthStore::identity_provider",
    "AuthStore::assume_role_with_web_identity",
];

pub const IDENTITY_PROVIDER_SECURITY_CONTROLS: &[&str] = &[
    "unknown provider rejection",
    "typed principal creation",
    "scoped session issuance",
    "expired assertion rejection",
    "disabled directory subject rejection",
    "invalid secret/signature rejection",
];

pub const IDENTITY_PROVIDER_OBSERVABILITY_FIELDS: &[&str] = &[
    "provider_id",
    "principal_kind",
    "principal_id",
    "tenant_id",
    "access_key_id",
    "scope",
    "expires_at_epoch_seconds",
];

pub const IDENTITY_PROVIDER_FAILURE_MODES: &[&str] = &[
    "UnknownIdentityProvider",
    "InvalidIdentityProviderToken",
    "UnknownDirectorySubject",
    "DisabledDirectorySubject",
    "WebIdentityTokenExpired",
    "WebIdentityIssuerMismatch",
    "WebIdentityAudienceMismatch",
    "WebIdentityKeyIdMismatch",
];

pub const IDENTITY_PROVIDER_VALIDATION_TESTS: &[&str] = &[
    "crates/bucketwarden-auth/tests/identity_access_keys.rs",
    "crates/bucketwarden-auth/tests/oidc_jwt_identity.rs",
    "crates/bucketwarden-auth/tests/sts_scoped_sessions.rs",
    "crates/bucketwarden-auth/tests/external_identity_providers.rs",
];

pub const IDENTITY_PROVIDER_CAVEATS: &[&str] = &[
    "Directory providers use deterministic in-process fixtures; production LDAP/AD network binding remains a deployment integration.",
    "SAML assertions use signed compact test assertions rather than XML canonicalization.",
    "OIDC/SAML provider keys are local HS256 fixtures until external JWKS/key rotation is added.",
];

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct IdentityProviderSupportReport {
    pub native_support_state: Vec<&'static str>,
    pub semantic_parity: &'static str,
    pub configuration_admin_surface: Vec<&'static str>,
    pub security_governance_impact: Vec<&'static str>,
    pub observability_evidence_fields: Vec<&'static str>,
    pub failure_modes: Vec<&'static str>,
    pub validation_test_coverage: Vec<&'static str>,
    pub product_specific_caveats: Vec<&'static str>,
}

impl IdentityProviderSupportReport {
    pub fn current() -> Self {
        Self {
            native_support_state: IDENTITY_PROVIDER_RUNTIME_FEATURES.to_vec(),
            semantic_parity: "All configured identity sources resolve to typed principals and scoped session credentials before authorization.",
            configuration_admin_surface: IDENTITY_PROVIDER_ADMIN_SURFACES.to_vec(),
            security_governance_impact: IDENTITY_PROVIDER_SECURITY_CONTROLS.to_vec(),
            observability_evidence_fields: IDENTITY_PROVIDER_OBSERVABILITY_FIELDS.to_vec(),
            failure_modes: IDENTITY_PROVIDER_FAILURE_MODES.to_vec(),
            validation_test_coverage: IDENTITY_PROVIDER_VALIDATION_TESTS.to_vec(),
            product_specific_caveats: IDENTITY_PROVIDER_CAVEATS.to_vec(),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum IdentityProvider {
    Oidc(OidcProvider),
    Ldap(DirectoryProvider),
    ActiveDirectory(DirectoryProvider),
    Saml(SamlProvider),
}

impl IdentityProvider {
    pub fn id(&self) -> &str {
        match self {
            Self::Oidc(provider) => &provider.provider_id,
            Self::Ldap(provider) | Self::ActiveDirectory(provider) => &provider.provider_id,
            Self::Saml(provider) => &provider.provider_id,
        }
    }

    pub fn verify_subject(&self, token: &str, now_epoch_seconds: u64) -> Result<String, AuthError> {
        match self {
            Self::Oidc(provider) => provider.verify_subject(token, now_epoch_seconds),
            Self::Ldap(provider) | Self::ActiveDirectory(provider) => {
                provider.verify_subject(token)
            }
            Self::Saml(provider) => provider.verify_subject(token, now_epoch_seconds),
        }
    }

    pub fn principal_id(&self, subject: &str) -> String {
        match self {
            Self::Oidc(provider) => provider.principal_id(subject),
            Self::Ldap(provider) | Self::ActiveDirectory(provider) => {
                provider.principal_id(subject)
            }
            Self::Saml(provider) => provider.principal_id(subject),
        }
    }

    pub fn parent_secret(&self) -> String {
        match self {
            Self::Oidc(provider) => provider.parent_secret(),
            Self::Ldap(provider) | Self::ActiveDirectory(provider) => provider.parent_secret(),
            Self::Saml(provider) => provider.parent_secret(),
        }
    }

    pub fn principal_kind(&self) -> PrincipalKind {
        match self {
            Self::Oidc(_) => PrincipalKind::FederatedWebIdentity,
            Self::Ldap(_) => PrincipalKind::LdapUser,
            Self::ActiveDirectory(_) => PrincipalKind::ActiveDirectoryUser,
            Self::Saml(_) => PrincipalKind::SamlSubject,
        }
    }

    pub fn principal(&self, subject: &str) -> Principal {
        Principal::with_kind(
            self.principal_id(subject),
            self.principal_kind(),
            crate::DEFAULT_TENANT_ID,
        )
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct OidcProvider {
    pub provider_id: String,
    pub issuer: String,
    pub audience: String,
    pub key_id: String,
    shared_secret: String,
    pub principal_prefix: String,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DirectoryUser {
    pub subject: String,
    shared_secret: String,
    pub enabled: bool,
}

impl DirectoryUser {
    pub fn active(subject: impl Into<String>, shared_secret: impl Into<String>) -> Self {
        Self {
            subject: subject.into(),
            shared_secret: shared_secret.into(),
            enabled: true,
        }
    }

    pub fn disabled(subject: impl Into<String>, shared_secret: impl Into<String>) -> Self {
        Self {
            enabled: false,
            ..Self::active(subject, shared_secret)
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DirectoryProvider {
    pub provider_id: String,
    pub authority: String,
    pub principal_prefix: String,
    users: BTreeMap<String, DirectoryUser>,
}

impl DirectoryProvider {
    pub fn ldap(
        provider_id: impl Into<String>,
        authority: impl Into<String>,
        principal_prefix: impl Into<String>,
    ) -> Self {
        Self::new(provider_id, authority, principal_prefix)
    }

    pub fn active_directory(
        provider_id: impl Into<String>,
        authority: impl Into<String>,
        principal_prefix: impl Into<String>,
    ) -> Self {
        Self::new(provider_id, authority, principal_prefix)
    }

    fn new(
        provider_id: impl Into<String>,
        authority: impl Into<String>,
        principal_prefix: impl Into<String>,
    ) -> Self {
        Self {
            provider_id: provider_id.into(),
            authority: authority.into(),
            principal_prefix: principal_prefix.into(),
            users: BTreeMap::new(),
        }
    }

    pub fn with_user(
        mut self,
        subject: impl Into<String>,
        shared_secret: impl Into<String>,
    ) -> Self {
        let user = DirectoryUser::active(subject, shared_secret);
        self.users.insert(user.subject.clone(), user);
        self
    }

    pub fn with_disabled_user(
        mut self,
        subject: impl Into<String>,
        shared_secret: impl Into<String>,
    ) -> Self {
        let user = DirectoryUser::disabled(subject, shared_secret);
        self.users.insert(user.subject.clone(), user);
        self
    }

    pub fn verify_subject(&self, token: &str) -> Result<String, AuthError> {
        let (subject, shared_secret) = token.split_once(':').ok_or_else(|| {
            AuthError::InvalidIdentityProviderToken("missing directory secret".into())
        })?;
        let user = self
            .users
            .get(subject)
            .ok_or_else(|| AuthError::UnknownDirectorySubject(subject.to_string()))?;
        if !user.enabled {
            return Err(AuthError::DisabledDirectorySubject(subject.to_string()));
        }
        if user.shared_secret != shared_secret {
            return Err(AuthError::InvalidIdentityProviderToken(
                "directory secret mismatch".into(),
            ));
        }
        Ok(subject.to_string())
    }

    pub fn principal_id(&self, subject: &str) -> String {
        format!("{}{}", self.principal_prefix, subject)
    }

    pub fn parent_secret(&self) -> String {
        format!("bucketwarden-directory-parent:{}", self.provider_id)
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SamlProvider {
    pub provider_id: String,
    pub issuer: String,
    pub audience: String,
    pub key_id: String,
    shared_secret: String,
    pub principal_prefix: String,
}

impl SamlProvider {
    pub fn hs256(
        provider_id: impl Into<String>,
        issuer: impl Into<String>,
        audience: impl Into<String>,
        key_id: impl Into<String>,
        shared_secret: impl Into<String>,
        principal_prefix: impl Into<String>,
    ) -> Self {
        Self {
            provider_id: provider_id.into(),
            issuer: issuer.into(),
            audience: audience.into(),
            key_id: key_id.into(),
            shared_secret: shared_secret.into(),
            principal_prefix: principal_prefix.into(),
        }
    }

    pub fn sign_assertion(
        &self,
        subject: impl Into<String>,
        expires_at_epoch_seconds: u64,
        issued_at_epoch_seconds: u64,
    ) -> Result<String, AuthError> {
        let token = sign_hs256_jwt(
            Some(&self.key_id),
            &JwtClaims {
                iss: self.issuer.clone(),
                sub: subject.into(),
                aud: vec![self.audience.clone()],
                exp: Some(expires_at_epoch_seconds),
                iat: Some(issued_at_epoch_seconds),
            },
            self.shared_secret.as_bytes(),
        )?;
        Ok(format!("saml:{token}"))
    }

    pub fn verify_subject(&self, token: &str, now_epoch_seconds: u64) -> Result<String, AuthError> {
        let token = token
            .strip_prefix("saml:")
            .ok_or_else(|| AuthError::InvalidIdentityProviderToken("missing saml prefix".into()))?;
        let (header, claims) = verify_hs256_jwt(token, self.shared_secret.as_bytes())?;
        if header.kid.as_deref() != Some(self.key_id.as_str()) {
            return Err(AuthError::WebIdentityKeyIdMismatch(
                header.kid.unwrap_or_default(),
            ));
        }
        if claims.iss != self.issuer {
            return Err(AuthError::WebIdentityIssuerMismatch(claims.iss));
        }
        if !claims.aud.iter().any(|audience| audience == &self.audience) {
            return Err(AuthError::WebIdentityAudienceMismatch(claims.aud.join(",")));
        }
        if claims
            .exp
            .is_some_and(|expires_at| now_epoch_seconds > expires_at)
        {
            return Err(AuthError::WebIdentityTokenExpired);
        }
        if claims.sub.is_empty() {
            return Err(AuthError::InvalidIdentityProviderToken(
                "missing saml subject".to_string(),
            ));
        }
        Ok(claims.sub)
    }

    pub fn principal_id(&self, subject: &str) -> String {
        format!("{}{}", self.principal_prefix, subject)
    }

    pub fn parent_secret(&self) -> String {
        format!("bucketwarden-saml-parent:{}", self.provider_id)
    }
}

impl OidcProvider {
    pub fn hs256(
        provider_id: impl Into<String>,
        issuer: impl Into<String>,
        audience: impl Into<String>,
        key_id: impl Into<String>,
        shared_secret: impl Into<String>,
        principal_prefix: impl Into<String>,
    ) -> Self {
        Self {
            provider_id: provider_id.into(),
            issuer: issuer.into(),
            audience: audience.into(),
            key_id: key_id.into(),
            shared_secret: shared_secret.into(),
            principal_prefix: principal_prefix.into(),
        }
    }

    pub fn verify_subject(&self, token: &str, now_epoch_seconds: u64) -> Result<String, AuthError> {
        let (header, claims) = verify_hs256_jwt(token, self.shared_secret.as_bytes())?;
        if header.kid.as_deref() != Some(self.key_id.as_str()) {
            return Err(AuthError::WebIdentityKeyIdMismatch(
                header.kid.unwrap_or_default(),
            ));
        }
        if claims.iss != self.issuer {
            return Err(AuthError::WebIdentityIssuerMismatch(claims.iss));
        }
        if !claims.aud.iter().any(|audience| audience == &self.audience) {
            return Err(AuthError::WebIdentityAudienceMismatch(claims.aud.join(",")));
        }
        if claims
            .exp
            .is_some_and(|expires_at| now_epoch_seconds > expires_at)
        {
            return Err(AuthError::WebIdentityTokenExpired);
        }
        if claims.sub.is_empty() {
            return Err(AuthError::InvalidWebIdentityToken(
                "missing subject".to_string(),
            ));
        }
        Ok(claims.sub)
    }

    pub fn principal_id(&self, subject: &str) -> String {
        format!("{}{}", self.principal_prefix, subject)
    }

    pub fn parent_secret(&self) -> String {
        format!("bucketwarden-web-identity-parent:{}", self.provider_id)
    }
}
