use super::ScimMail;
use super::ScimOauth2ClaimMapJoinChar;
use super::ScimSshPublicKey;
use crate::attribute::Attribute;
use crate::internal::UiHint;
use crate::v1::OutboundMessage;
use crypto_glue::s256::Sha256Output;
use scim_proto::{ScimEntry, ScimEntryHeader};
use serde::Serialize;
use serde_with::{base64, formats, hex::Hex, serde_as, skip_serializing_none};
use std::collections::{BTreeMap, BTreeSet};
use std::num::NonZeroU64;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use url::Url;
use utoipa::ToSchema;
use uuid::Uuid;

/// A strongly typed ScimEntry that is for transmission to clients. This uses
/// Kanidm internal strong types for values allowing direct serialisation and
/// transmission.
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ScimEntryKanidm {
    #[serde(flatten)]
    pub header: ScimEntryHeader,

    pub ext_access_check: Option<ScimEffectiveAccess>,
    #[serde(flatten)]
    pub attrs: BTreeMap<Attribute, ScimValueKanidm>,
}

impl ScimEntryKanidm {
    fn get_string_attr(&self, attr: &Attribute) -> Option<&String> {
        self.attrs.get(attr).and_then(|v| match v {
            ScimValueKanidm::String(s) => Some(s),
            _ => None,
        })
    }

    fn get_scim_refs_attr(&self, attr: &Attribute) -> Option<&Vec<ScimReference>> {
        let option = self.attrs.get(attr);
        option.and_then(|v| match v {
            ScimValueKanidm::EntryReferences(s) => Some(s),
            _ => None,
        })
    }
}

#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Clone, Debug, Default, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ScimListResponse {
    pub schemas: Vec<String>,
    pub total_results: u64,
    #[schema(value_type = u64)]
    pub items_per_page: Option<NonZeroU64>,
    #[schema(value_type = u64)]
    pub start_index: Option<NonZeroU64>,
    #[schema(value_type = Vec<ScimEntry>)]
    pub resources: Vec<ScimEntryKanidm>,
}

#[derive(Serialize, Debug, Clone, ToSchema)]
pub enum ScimAttributeEffectiveAccess {
    /// All attributes on the entry have this permission granted
    Grant,
    /// All attributes on the entry have this permission denied
    Deny,
    /// The following attributes on the entry have this permission granted
    Allow(BTreeSet<Attribute>),
}

impl ScimAttributeEffectiveAccess {
    /// Check if the effective access allows or denies this attribute
    pub fn check(&self, attr: &Attribute) -> bool {
        match self {
            Self::Grant => true,
            Self::Deny => false,
            Self::Allow(set) => set.contains(attr),
        }
    }

    /// Check if the effective access allows ANY of the attributes
    pub fn check_any(&self, attrs: &BTreeSet<Attribute>) -> bool {
        match self {
            Self::Grant => true,
            Self::Deny => false,
            Self::Allow(set) => attrs.intersection(set).next().is_some(),
        }
    }
}

#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimEffectiveAccess {
    /// The identity that inherits the effective permission
    pub ident: Uuid,
    /// If the ident may delete the target entry
    pub delete: bool,
    /// The set of effective access over search events
    pub search: ScimAttributeEffectiveAccess,
    /// The set of effective access over modify present events
    pub modify_present: ScimAttributeEffectiveAccess,
    /// The set of effective access over modify remove events
    pub modify_remove: ScimAttributeEffectiveAccess,
}

#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimAddress {
    pub formatted: String,
    pub street_address: String,
    pub locality: String,
    pub region: String,
    pub postal_code: String,
    pub country: String,
}

#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimApplicationPasswordReference {
    pub uuid: Uuid,
    pub application_uuid: Uuid,
    pub label: String,
}

#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimBinary {
    pub label: String,
    #[serde_as(as = "base64::Base64<base64::UrlSafe, formats::Unpadded>")]
    pub value: Vec<u8>,
}

#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimCertificate {
    #[serde_as(as = "Hex")]
    pub s256: Vec<u8>,
    #[serde_as(as = "base64::Base64<base64::UrlSafe, formats::Unpadded>")]
    pub der: Vec<u8>,
}

#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimAuditString {
    #[serde_as(as = "Rfc3339")]
    pub date_time: OffsetDateTime,
    pub value: String,
}

#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub enum ScimIntentTokenState {
    Valid,
    InProgress,
    Consumed,
}

#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimIntentToken {
    pub token_id: String,
    pub state: ScimIntentTokenState,
    #[serde_as(as = "Rfc3339")]
    pub expires: OffsetDateTime,
}

#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimKeyInternal {
    pub key_id: String,
    pub status: String,
    pub usage: String,
    #[serde_as(as = "Rfc3339")]
    pub valid_from: OffsetDateTime,
}

#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimAuthSession {
    pub id: Uuid,
    #[serde_as(as = "Option<Rfc3339>")]
    pub expires: Option<OffsetDateTime>,
    #[serde_as(as = "Option<Rfc3339>")]
    pub revoked: Option<OffsetDateTime>,
    #[serde_as(as = "Rfc3339")]
    pub issued_at: OffsetDateTime,
    pub issued_by: Uuid,
    pub credential_id: Uuid,
    pub auth_type: String,
    pub session_scope: String,
}

#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimOAuth2Session {
    pub id: Uuid,
    pub parent_id: Option<Uuid>,
    pub client_id: Uuid,
    #[serde_as(as = "Rfc3339")]
    pub issued_at: OffsetDateTime,
    #[serde_as(as = "Option<Rfc3339>")]
    pub expires: Option<OffsetDateTime>,
    #[serde_as(as = "Option<Rfc3339>")]
    pub revoked: Option<OffsetDateTime>,
}

#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimApiToken {
    pub id: Uuid,
    pub label: String,
    #[serde_as(as = "Option<Rfc3339>")]
    pub expires: Option<OffsetDateTime>,
    #[serde_as(as = "Rfc3339")]
    pub issued_at: OffsetDateTime,
    pub issued_by: Uuid,
    pub scope: String,
}

#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimOAuth2ScopeMap {
    pub group: String,
    pub group_uuid: Uuid,
    pub scopes: BTreeSet<String>,
}

#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimOAuth2ClaimMap {
    pub group: String,
    pub group_uuid: Uuid,
    pub claim: String,
    pub join_char: ScimOauth2ClaimMapJoinChar,
    pub values: BTreeSet<String>,
}

#[derive(Serialize, Debug, Clone, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimReference {
    pub uuid: Uuid,
    pub value: String,
}

/// This is a strongly typed ScimValue for Kanidm. It is for serialisation only
/// since on a deserialisation path we can not know the intent of the sender
/// to how we deserialise strings. Additionally during deserialisation we need
/// to accept optional or partial types too.
#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(untagged)]
pub enum ScimValueKanidm {
    Bool(bool),
    Uint32(u32),
    Int64(i64),
    Uint64(u64),
    Integer(i64),
    Decimal(f64),
    String(String),

    // Other strong outbound types.
    DateTime(#[serde_as(as = "Rfc3339")] OffsetDateTime),
    Reference(Url),
    Uuid(Uuid),
    EntryReference(ScimReference),
    EntryReferences(Vec<ScimReference>),

    ArrayString(Vec<String>),
    ArrayDateTime(#[serde_as(as = "Vec<Rfc3339>")] Vec<OffsetDateTime>),
    ArrayUuid(Vec<Uuid>),
    ArrayBinary(Vec<ScimBinary>),
    ArrayCertificate(Vec<ScimCertificate>),

    Address(Vec<ScimAddress>),
    Mail(Vec<ScimMail>),
    ApplicationPassword(Vec<ScimApplicationPasswordReference>),
    AuditString(Vec<ScimAuditString>),
    SshPublicKey(Vec<ScimSshPublicKey>),
    AuthSession(Vec<ScimAuthSession>),
    OAuth2Session(Vec<ScimOAuth2Session>),
    ApiToken(Vec<ScimApiToken>),
    IntentToken(Vec<ScimIntentToken>),
    OAuth2ScopeMap(Vec<ScimOAuth2ScopeMap>),
    OAuth2ClaimMap(Vec<ScimOAuth2ClaimMap>),
    KeyInternal(Vec<ScimKeyInternal>),
    UiHints(Vec<UiHint>),

    Message(OutboundMessage),

    #[schema(value_type = Vec<String>)]
    Sha256(#[serde_as(as = "Vec<Hex>")] Vec<Sha256Output>),
}

#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
pub struct ScimPerson {
    pub uuid: Uuid,
    pub name: String,
    pub displayname: String,
    pub spn: String,
    pub description: Option<String>,
    pub mails: Vec<ScimMail>,
    pub managed_by: Option<ScimReference>,
    pub groups: Vec<ScimReference>,
}

impl TryFrom<ScimEntryKanidm> for ScimPerson {
    type Error = ();

    fn try_from(scim_entry: ScimEntryKanidm) -> Result<Self, Self::Error> {
        let uuid = scim_entry.header.id;
        let name = scim_entry
            .get_string_attr(&Attribute::Name)
            .cloned()
            .ok_or(())?;
        let displayname = scim_entry
            .get_string_attr(&Attribute::DisplayName)
            .cloned()
            .ok_or(())?;
        let spn = scim_entry
            .get_string_attr(&Attribute::Spn)
            .cloned()
            .ok_or(())?;
        let description = scim_entry.get_string_attr(&Attribute::Description).cloned();

        let mails = scim_entry
            .attrs
            .get(&Attribute::Mail)
            .and_then(|v| match v {
                ScimValueKanidm::Mail(m) => Some(m.clone()),
                _ => None,
            })
            .unwrap_or_default();

        let groups = scim_entry
            .get_scim_refs_attr(&Attribute::DirectMemberOf)
            .cloned()
            .unwrap_or_default();

        let managed_by = scim_entry
            .attrs
            .get(&Attribute::EntryManagedBy)
            .and_then(|v| match v {
                ScimValueKanidm::EntryReference(v) => Some(v.clone()),
                _ => None,
            });

        Ok(ScimPerson {
            uuid,
            name,
            displayname,
            spn,
            description,
            mails,
            managed_by,
            groups,
        })
    }
}

#[serde_as]
#[derive(Serialize, Debug, Clone, ToSchema)]
pub struct ScimGroup {
    pub uuid: Uuid,
    pub name: String,
    pub description: Option<String>,
    pub members: Vec<ScimReference>,
}

impl TryFrom<ScimEntryKanidm> for ScimGroup {
    type Error = ();

    fn try_from(scim_entry: ScimEntryKanidm) -> Result<Self, Self::Error> {
        let uuid = scim_entry.header.id;
        let name = scim_entry
            .get_string_attr(&Attribute::Name)
            .cloned()
            .ok_or(())?;
        let description = scim_entry.get_string_attr(&Attribute::Description).cloned();
        let members = scim_entry
            .get_scim_refs_attr(&Attribute::Member)
            .cloned()
            .unwrap_or_default();

        Ok(ScimGroup {
            uuid,
            name,
            description,
            members,
        })
    }
}

impl From<bool> for ScimValueKanidm {
    fn from(b: bool) -> Self {
        Self::Bool(b)
    }
}

impl From<OffsetDateTime> for ScimValueKanidm {
    fn from(odt: OffsetDateTime) -> Self {
        Self::DateTime(odt)
    }
}

impl From<Vec<UiHint>> for ScimValueKanidm {
    fn from(set: Vec<UiHint>) -> Self {
        Self::UiHints(set)
    }
}

impl From<Vec<OffsetDateTime>> for ScimValueKanidm {
    fn from(set: Vec<OffsetDateTime>) -> Self {
        Self::ArrayDateTime(set)
    }
}

impl From<String> for ScimValueKanidm {
    fn from(s: String) -> Self {
        Self::String(s)
    }
}

impl From<&str> for ScimValueKanidm {
    fn from(s: &str) -> Self {
        Self::String(s.to_string())
    }
}

impl From<Vec<String>> for ScimValueKanidm {
    fn from(set: Vec<String>) -> Self {
        Self::ArrayString(set)
    }
}

impl From<Uuid> for ScimValueKanidm {
    fn from(u: Uuid) -> Self {
        Self::Uuid(u)
    }
}

impl From<Vec<Uuid>> for ScimValueKanidm {
    fn from(set: Vec<Uuid>) -> Self {
        Self::ArrayUuid(set)
    }
}

impl From<u32> for ScimValueKanidm {
    fn from(u: u32) -> Self {
        Self::Uint32(u)
    }
}

impl From<i64> for ScimValueKanidm {
    fn from(u: i64) -> Self {
        Self::Int64(u)
    }
}

impl From<u64> for ScimValueKanidm {
    fn from(u: u64) -> Self {
        Self::Uint64(u)
    }
}

impl From<Vec<ScimAddress>> for ScimValueKanidm {
    fn from(set: Vec<ScimAddress>) -> Self {
        Self::Address(set)
    }
}

impl From<Vec<ScimMail>> for ScimValueKanidm {
    fn from(set: Vec<ScimMail>) -> Self {
        Self::Mail(set)
    }
}

impl From<Vec<ScimApplicationPasswordReference>> for ScimValueKanidm {
    fn from(set: Vec<ScimApplicationPasswordReference>) -> Self {
        Self::ApplicationPassword(set)
    }
}

impl From<Vec<ScimAuditString>> for ScimValueKanidm {
    fn from(set: Vec<ScimAuditString>) -> Self {
        Self::AuditString(set)
    }
}

impl From<Vec<ScimBinary>> for ScimValueKanidm {
    fn from(set: Vec<ScimBinary>) -> Self {
        Self::ArrayBinary(set)
    }
}

impl From<Vec<ScimCertificate>> for ScimValueKanidm {
    fn from(set: Vec<ScimCertificate>) -> Self {
        Self::ArrayCertificate(set)
    }
}

impl From<Vec<ScimSshPublicKey>> for ScimValueKanidm {
    fn from(set: Vec<ScimSshPublicKey>) -> Self {
        Self::SshPublicKey(set)
    }
}

impl From<Vec<ScimAuthSession>> for ScimValueKanidm {
    fn from(set: Vec<ScimAuthSession>) -> Self {
        Self::AuthSession(set)
    }
}

impl From<Vec<ScimOAuth2Session>> for ScimValueKanidm {
    fn from(set: Vec<ScimOAuth2Session>) -> Self {
        Self::OAuth2Session(set)
    }
}

impl From<Vec<ScimApiToken>> for ScimValueKanidm {
    fn from(set: Vec<ScimApiToken>) -> Self {
        Self::ApiToken(set)
    }
}

impl From<Vec<ScimIntentToken>> for ScimValueKanidm {
    fn from(set: Vec<ScimIntentToken>) -> Self {
        Self::IntentToken(set)
    }
}

impl From<Vec<ScimOAuth2ScopeMap>> for ScimValueKanidm {
    fn from(set: Vec<ScimOAuth2ScopeMap>) -> Self {
        Self::OAuth2ScopeMap(set)
    }
}

impl From<Vec<ScimOAuth2ClaimMap>> for ScimValueKanidm {
    fn from(set: Vec<ScimOAuth2ClaimMap>) -> Self {
        Self::OAuth2ClaimMap(set)
    }
}

impl From<Vec<ScimKeyInternal>> for ScimValueKanidm {
    fn from(set: Vec<ScimKeyInternal>) -> Self {
        Self::KeyInternal(set)
    }
}

impl From<OutboundMessage> for ScimValueKanidm {
    fn from(message: OutboundMessage) -> Self {
        Self::Message(message)
    }
}

impl From<Vec<Sha256Output>> for ScimValueKanidm {
    fn from(set: Vec<Sha256Output>) -> Self {
        Self::Sha256(set)
    }
}
