use std::str::FromStr;

use serde::{
    de::{Deserializer, Error as DeserializeError, Visitor},
    Deserialize, Serialize, Serializer,
};

/// Various scopes that can be requested through IndieAuth.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Scope {
    /// Allows to create posts using Micropub.
    Create,
    /// Allows to edit posts using Micropub.
    Update,
    /// Allows to delete posts using Micropub.
    Delete,
    /// Allows to upload blobs to the media endpoint.
    Media,
    /// Allows to read feeds via Microsub.
    Read,
    /// Allows to manage follows via Microsub.
    Follow,
    /// Allows to mute and unmute users in feeds via Microsub.
    Mute,
    /// Allows to block and unblock users.
    Block,
    /// Allows to create and manage feeds via Microsub.
    Channels,
    /// Allows to request profile information (except email, see Email)
    Profile,
    /// Allows to receive email in the profile information.
    Email,
    /// Custom scope not included above.
    Custom(String),
}
impl Scope {
    /// Create a custom scope from a string slice.
    pub fn custom(scope: &str) -> Scope {
        Scope::Custom(scope.to_string())
    }
}

// TODO consider relying on serde_variant for these conversions
impl AsRef<str> for Scope {
    fn as_ref(&self) -> &str {
        use Scope::*;
        match self {
            Create => "create",
            Update => "update",
            Delete => "delete",
            Media => "media",
            Read => "read",
            Follow => "follow",
            Mute => "mute",
            Block => "block",
            Channels => "channels",
            Profile => "profile",
            Email => "email",
            Custom(s) => s.as_ref(),
        }
    }
}
impl From<&str> for Scope {
    fn from(scope: &str) -> Self {
        match scope {
            "create" => Scope::Create,
            "update" => Scope::Update,
            "delete" => Scope::Delete,
            "media" => Scope::Media,
            "read" => Scope::Read,
            "follow" => Scope::Follow,
            "mute" => Scope::Mute,
            "block" => Scope::Block,
            "channels" => Scope::Channels,
            "profile" => Scope::Profile,
            "email" => Scope::Email,
            other => Scope::custom(other),
        }
    }
}
impl FromStr for Scope {
    type Err = std::convert::Infallible;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(s.into())
    }
}

/// A list of scopes that serializes to a space-separated string instead of a list.
///
/// OAuth2 is weird, don't ask me why it's a thing.
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Scopes(Vec<Scope>);
impl Scopes {
    /// Create a list of scopes from a vector of scopes.
    pub fn new(scopes: Vec<Scope>) -> Self {
        Self(scopes)
    }
    /// Ensure a certain scope is listed in the scope list.
    pub fn has(&self, scope: &Scope) -> bool {
        self.0.iter().any(|s| s == scope)
    }
    /// Ensure all of the requested scopes are in the list.
    pub fn has_all(&self, scopes: &[Scope]) -> bool {
        scopes
            .iter()
            .map(|s1| self.iter().any(|s2| s1 == s2))
            .all(|s| s)
    }
    /// Transform this into an iterator over individual scopes.
    pub fn iter(&self) -> std::slice::Iter<'_, Scope> {
        self.0.iter()
    }

    /// Count scopes requested by the application.
    pub fn len(&self) -> usize {
        self.0.len()
    }

    /// See if the application requested any scopes.
    ///
    /// Some older applications forget to request scopes. This may be used to force a default scope.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}
impl AsRef<[Scope]> for Scopes {
    fn as_ref(&self) -> &[Scope] {
        self.0.as_ref()
    }
}

impl std::fmt::Display for Scopes {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut iter = self.0.iter().peekable();
        while let Some(scope) = iter.next() {
            f.write_str(scope.as_ref())?;
            if iter.peek().is_some() {
                f.write_str(" ")?;
            }
        }
        Ok(())
    }
}

impl FromStr for Scopes {
    type Err = std::convert::Infallible;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        Ok(Self(
            value
                .split_ascii_whitespace()
                .map(Scope::from)
                .collect::<Vec<Scope>>(),
        ))
    }
}
impl Serialize for Scopes {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}
struct ScopeVisitor;
#[allow(clippy::needless_lifetimes, reason = "serde idiom")]
impl<'de> Visitor<'de> for ScopeVisitor {
    type Value = Scopes;

    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
        formatter.write_str("a string of space-separated OAuth2 scopes")
    }

    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
    where
        E: DeserializeError,
    {
        Ok(Scopes::from_str(value).unwrap())
    }
}
impl<'de> Deserialize<'de> for Scopes {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_str(ScopeVisitor)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_serde_vec_scope() {
        let scopes = vec![
            Scope::Create,
            Scope::Update,
            Scope::Delete,
            Scope::Media,
            Scope::custom("kittybox_internal_access"),
        ];

        let scope_serialized = serde_json::to_value(Scopes::new(scopes.clone())).unwrap();
        let scope_str = scope_serialized.as_str().unwrap();
        assert_eq!(
            scope_str,
            "create update delete media kittybox_internal_access"
        );

        assert!(serde_json::from_value::<Scopes>(scope_serialized)
            .unwrap()
            .has_all(&scopes))
    }

    #[test]
    fn test_scope_has_all() {
        let scopes = Scopes(vec![Scope::Create, Scope::Update, Scope::custom("draft")]);

        assert!(scopes.has_all(&[Scope::Create, Scope::custom("draft")]));

        assert!(!scopes.has_all(&[Scope::Read, Scope::custom("kittybox_internal_access")]));
    }
}