use std::{fmt, str::FromStr};
use oauth2_types::scope::ScopeToken as StrScopeToken;
pub use oauth2_types::scope::{InvalidScope, Scope};
use crate::PrivString;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ScopeToken {
    Openid,
    Profile,
    Email,
    Address,
    Phone,
    OfflineAccess,
    MatrixApi(MatrixApiScopeToken),
    MatrixDevice(PrivString),
    Custom(PrivString),
}
impl ScopeToken {
    pub fn try_with_matrix_device(device_id: String) -> Result<Self, InvalidScope> {
        StrScopeToken::from_str(&device_id)?;
        Ok(Self::MatrixDevice(PrivString(device_id)))
    }
    #[must_use]
    pub fn matrix_device_id(&self) -> Option<&str> {
        match &self {
            Self::MatrixDevice(id) => Some(&id.0),
            _ => None,
        }
    }
}
impl fmt::Display for ScopeToken {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ScopeToken::Openid => write!(f, "openid"),
            ScopeToken::Profile => write!(f, "profile"),
            ScopeToken::Email => write!(f, "email"),
            ScopeToken::Address => write!(f, "address"),
            ScopeToken::Phone => write!(f, "phone"),
            ScopeToken::OfflineAccess => write!(f, "offline_access"),
            ScopeToken::MatrixApi(scope) => {
                write!(f, "urn:matrix:org.matrix.msc2967.client:api:{scope}")
            }
            ScopeToken::MatrixDevice(s) => {
                write!(f, "urn:matrix:org.matrix.msc2967.client:device:{}", s.0)
            }
            ScopeToken::Custom(s) => f.write_str(&s.0),
        }
    }
}
impl From<StrScopeToken> for ScopeToken {
    fn from(t: StrScopeToken) -> Self {
        match &*t {
            "openid" => Self::Openid,
            "profile" => Self::Profile,
            "email" => Self::Email,
            "address" => Self::Address,
            "phone" => Self::Phone,
            "offline_access" => Self::OfflineAccess,
            s => {
                if let Some(matrix_scope) =
                    s.strip_prefix("urn:matrix:org.matrix.msc2967.client:api:")
                {
                    Self::MatrixApi(
                        MatrixApiScopeToken::from_str(matrix_scope)
                            .expect("If the whole string is a valid scope, a substring is too"),
                    )
                } else if let Some(device_id) =
                    s.strip_prefix("urn:matrix:org.matrix.msc2967.client:device:")
                {
                    Self::MatrixDevice(PrivString(device_id.to_owned()))
                } else {
                    Self::Custom(PrivString(s.to_owned()))
                }
            }
        }
    }
}
impl From<ScopeToken> for StrScopeToken {
    fn from(t: ScopeToken) -> Self {
        let s = t.to_string();
        match StrScopeToken::from_str(&s) {
            Ok(t) => t,
            Err(_) => unreachable!(),
        }
    }
}
impl FromStr for ScopeToken {
    type Err = InvalidScope;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let t = StrScopeToken::from_str(s)?;
        Ok(t.into())
    }
}
pub trait ScopeExt {
    fn insert_token(&mut self, token: ScopeToken) -> bool;
    fn contains_token(&self, token: &ScopeToken) -> bool;
}
impl ScopeExt for Scope {
    fn insert_token(&mut self, token: ScopeToken) -> bool {
        self.insert(token.into())
    }
    fn contains_token(&self, token: &ScopeToken) -> bool {
        self.contains(&token.to_string())
    }
}
impl FromIterator<ScopeToken> for Scope {
    fn from_iter<T: IntoIterator<Item = ScopeToken>>(iter: T) -> Self {
        iter.into_iter().map(Into::<StrScopeToken>::into).collect()
    }
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum MatrixApiScopeToken {
    Full,
    Guest,
    Custom(PrivString),
}
impl fmt::Display for MatrixApiScopeToken {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Full => write!(f, "*"),
            Self::Guest => write!(f, "guest"),
            Self::Custom(s) => f.write_str(&s.0),
        }
    }
}
impl FromStr for MatrixApiScopeToken {
    type Err = InvalidScope;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        StrScopeToken::from_str(s)?;
        let t = match s {
            "*" => Self::Full,
            "guest" => Self::Guest,
            _ => Self::Custom(PrivString(s.to_owned())),
        };
        Ok(t)
    }
}
#[cfg(test)]
mod tests {
    use assert_matches::assert_matches;
    use super::*;
    #[test]
    fn parse_scope_token() {
        assert_eq!(ScopeToken::from_str("openid"), Ok(ScopeToken::Openid));
        let scope =
            ScopeToken::from_str("urn:matrix:org.matrix.msc2967.client:device:ABCDEFGHIJKL")
                .unwrap();
        assert_matches!(scope, ScopeToken::MatrixDevice(_));
        assert_eq!(scope.matrix_device_id(), Some("ABCDEFGHIJKL"));
        let scope = ScopeToken::from_str("urn:matrix:org.matrix.msc2967.client:api:*").unwrap();
        assert_eq!(scope, ScopeToken::MatrixApi(MatrixApiScopeToken::Full));
        let scope = ScopeToken::from_str("urn:matrix:org.matrix.msc2967.client:api:guest").unwrap();
        assert_eq!(scope, ScopeToken::MatrixApi(MatrixApiScopeToken::Guest));
        let scope =
            ScopeToken::from_str("urn:matrix:org.matrix.msc2967.client:api:my.custom.scope")
                .unwrap();
        let api_scope = assert_matches!(scope, ScopeToken::MatrixApi(s) => s);
        assert_matches!(api_scope, MatrixApiScopeToken::Custom(_));
        assert_eq!(api_scope.to_string(), "my.custom.scope");
        assert_eq!(ScopeToken::from_str("invalid\\scope"), Err(InvalidScope));
        assert_eq!(
            MatrixApiScopeToken::from_str("invalid\\scope"),
            Err(InvalidScope)
        );
    }
    #[test]
    fn display_scope_token() {
        let scope = ScopeToken::MatrixApi(MatrixApiScopeToken::Full);
        assert_eq!(
            scope.to_string(),
            "urn:matrix:org.matrix.msc2967.client:api:*"
        );
        let scope = ScopeToken::MatrixApi(MatrixApiScopeToken::Guest);
        assert_eq!(
            scope.to_string(),
            "urn:matrix:org.matrix.msc2967.client:api:guest"
        );
        let api_scope = MatrixApiScopeToken::from_str("my.custom.scope").unwrap();
        let scope = ScopeToken::MatrixApi(api_scope);
        assert_eq!(
            scope.to_string(),
            "urn:matrix:org.matrix.msc2967.client:api:my.custom.scope"
        );
    }
    #[test]
    fn parse_scope() {
        let scope = Scope::from_str("openid profile address").unwrap();
        assert_eq!(scope.len(), 3);
        assert!(scope.contains_token(&ScopeToken::Openid));
        assert!(scope.contains_token(&ScopeToken::Profile));
        assert!(scope.contains_token(&ScopeToken::Address));
        assert!(!scope.contains_token(&ScopeToken::OfflineAccess));
    }
    #[test]
    fn display_scope() {
        let mut scope: Scope = [ScopeToken::Profile].into_iter().collect();
        assert_eq!(scope.to_string(), "profile");
        scope.insert_token(ScopeToken::MatrixApi(MatrixApiScopeToken::Full));
        assert_eq!(
            scope.to_string(),
            "profile urn:matrix:org.matrix.msc2967.client:api:*"
        );
    }
}