use crate::models::{Owner, User};
use crate::util::errors::{BoxedAppError, custom};
use crates_io_encryption::TokenEncryption;
use crates_io_github::{GitHubAuth, GitHubClient, GitHubError};
use http::StatusCode;

/// Access rights to the crate (publishing and ownership management)
/// NOTE: The order of these variants matters!
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
pub enum Rights {
    None,
    Publish,
    Full,
}

impl Rights {
    /// Given this set of owners, determines the strongest rights the
    /// user has.
    ///
    /// Short-circuits on `Full` because you can't beat it. In practice, we'll always
    /// see `[user, user, user, ..., team, team, team]`, so we could shortcircuit on
    /// `Publish` as well, but this is a non-obvious invariant so we don't bother.
    /// Sweet free optimization if teams are proving burdensome to check.
    /// More than one team isn't really expected, though.
    pub async fn get(
        user: &User,
        gh_client: &dyn GitHubClient,
        owners: &[Owner],
        encryption: &TokenEncryption,
    ) -> Result<Self, BoxedAppError> {
        // Compute GitHub token once rather than every loop iteration.
        // If the user doesn't have an associated `oauth_github`, then they can't be associated
        // with any GitHub teams.
        let token = user
            .gh_encrypted_token
            .as_ref()
            .map(|gh_encrypted_token| encryption.decrypt(gh_encrypted_token))
            .transpose()
            .map_err(GitHubError::Other)?;
        let auth = token.map(GitHubAuth::bearer);

        let mut best = Self::None;
        for owner in owners {
            match *owner {
                Owner::User(ref other_user) => {
                    if other_user.id == user.id {
                        return Ok(Self::Full);
                    }
                }
                Owner::Team(ref team) => {
                    if let Some(ref auth) = auth {
                        // Phones home to GitHub to ask if this User is a member of the given team.
                        // Note that we're assuming that the given user is the one interested in
                        // the answer. If this is not the case, then we could accidentally leak
                        // private membership information here.
                        let is_team_member = match gh_client
                            .team_membership(team.org_id, team.github_id, &user.gh_login, auth)
                            .await
                        {
                            Ok(membership) => membership.is_some_and(|m| m.is_active()),
                            Err(GitHubError::Forbidden(_)) => {
                                let org_name = team
                                    .split_login()
                                    .map(|(_, org, _)| org.to_string())
                                    .unwrap_or_else(|| "unknown".to_string());

                                return Err(custom(
                                    StatusCode::FORBIDDEN,
                                    format!(
                                        "GitHub organization '{org_name}' has restricted OAuth \
                                         access. A '{org_name}' administrator must approve the \
                                         'crates.io' application in the organization's \
                                         'Third-party access' settings."
                                    ),
                                ));
                            }
                            Err(e) => return Err(e.into()),
                        };

                        if is_team_member {
                            best = Self::Publish;
                        }
                    }
                }
            }
        }
        Ok(best)
    }
}
