// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use anyhow::Context as _;
use async_graphql::{Context, Description, Enum, InputObject, Object, ID};
use mas_storage::{
    job::{JobRepositoryExt, ProvisionUserJob, VerifyEmailJob},
    user::{UserEmailRepository, UserRepository},
    RepositoryAccess,
};

use crate::{
    model::{NodeType, User, UserEmail},
    state::ContextExt,
    UserId,
};

#[derive(Default)]
pub struct UserEmailMutations {
    _private: (),
}

/// The input for the `addEmail` mutation
#[derive(InputObject)]
struct AddEmailInput {
    /// The email address to add
    email: String,

    /// The ID of the user to add the email address to
    user_id: ID,

    /// Skip the email address verification. Only allowed for admins.
    skip_verification: Option<bool>,

    /// Skip the email address policy check. Only allowed for admins.
    skip_policy_check: Option<bool>,
}

/// The status of the `addEmail` mutation
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum AddEmailStatus {
    /// The email address was added
    Added,
    /// The email address already exists
    Exists,
    /// The email address is invalid
    Invalid,
    /// The email address is not allowed by the policy
    Denied,
}

/// The payload of the `addEmail` mutation
#[derive(Description)]
enum AddEmailPayload {
    Added(mas_data_model::UserEmail),
    Exists(mas_data_model::UserEmail),
    Invalid,
    Denied {
        violations: Vec<mas_policy::Violation>,
    },
}

#[Object(use_type_description)]
impl AddEmailPayload {
    /// Status of the operation
    async fn status(&self) -> AddEmailStatus {
        match self {
            AddEmailPayload::Added(_) => AddEmailStatus::Added,
            AddEmailPayload::Exists(_) => AddEmailStatus::Exists,
            AddEmailPayload::Invalid => AddEmailStatus::Invalid,
            AddEmailPayload::Denied { .. } => AddEmailStatus::Denied,
        }
    }

    /// The email address that was added
    async fn email(&self) -> Option<UserEmail> {
        match self {
            AddEmailPayload::Added(email) | AddEmailPayload::Exists(email) => {
                Some(UserEmail(email.clone()))
            }
            AddEmailPayload::Invalid | AddEmailPayload::Denied { .. } => None,
        }
    }

    /// The user to whom the email address was added
    async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
        let state = ctx.state();
        let mut repo = state.repository().await?;

        let user_id = match self {
            AddEmailPayload::Added(email) | AddEmailPayload::Exists(email) => email.user_id,
            AddEmailPayload::Invalid | AddEmailPayload::Denied { .. } => return Ok(None),
        };

        let user = repo
            .user()
            .lookup(user_id)
            .await?
            .context("User not found")?;

        Ok(Some(User(user)))
    }

    /// The list of policy violations if the email address was denied
    async fn violations(&self) -> Option<Vec<String>> {
        let AddEmailPayload::Denied { violations } = self else {
            return None;
        };

        let messages = violations.iter().map(|v| v.msg.clone()).collect();
        Some(messages)
    }
}

/// The input for the `sendVerificationEmail` mutation
#[derive(InputObject)]
struct SendVerificationEmailInput {
    /// The ID of the email address to verify
    user_email_id: ID,
}

/// The status of the `sendVerificationEmail` mutation
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum SendVerificationEmailStatus {
    /// The verification email was sent
    Sent,
    /// The email address is already verified
    AlreadyVerified,
}

/// The payload of the `sendVerificationEmail` mutation
#[derive(Description)]
enum SendVerificationEmailPayload {
    Sent(mas_data_model::UserEmail),
    AlreadyVerified(mas_data_model::UserEmail),
}

#[Object(use_type_description)]
impl SendVerificationEmailPayload {
    /// Status of the operation
    async fn status(&self) -> SendVerificationEmailStatus {
        match self {
            SendVerificationEmailPayload::Sent(_) => SendVerificationEmailStatus::Sent,
            SendVerificationEmailPayload::AlreadyVerified(_) => {
                SendVerificationEmailStatus::AlreadyVerified
            }
        }
    }

    /// The email address to which the verification email was sent
    async fn email(&self) -> UserEmail {
        match self {
            SendVerificationEmailPayload::Sent(email)
            | SendVerificationEmailPayload::AlreadyVerified(email) => UserEmail(email.clone()),
        }
    }

    /// The user to whom the email address belongs
    async fn user(&self, ctx: &Context<'_>) -> Result<User, async_graphql::Error> {
        let state = ctx.state();
        let mut repo = state.repository().await?;

        let user_id = match self {
            SendVerificationEmailPayload::Sent(email)
            | SendVerificationEmailPayload::AlreadyVerified(email) => email.user_id,
        };

        let user = repo
            .user()
            .lookup(user_id)
            .await?
            .context("User not found")?;

        Ok(User(user))
    }
}

/// The input for the `verifyEmail` mutation
#[derive(InputObject)]
struct VerifyEmailInput {
    /// The ID of the email address to verify
    user_email_id: ID,
    /// The verification code
    code: String,
}

/// The status of the `verifyEmail` mutation
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum VerifyEmailStatus {
    /// The email address was just verified
    Verified,
    /// The email address was already verified before
    AlreadyVerified,
    /// The verification code is invalid
    InvalidCode,
}

/// The payload of the `verifyEmail` mutation
#[derive(Description)]
enum VerifyEmailPayload {
    Verified(mas_data_model::UserEmail),
    AlreadyVerified(mas_data_model::UserEmail),
    InvalidCode,
}

#[Object(use_type_description)]
impl VerifyEmailPayload {
    /// Status of the operation
    async fn status(&self) -> VerifyEmailStatus {
        match self {
            VerifyEmailPayload::Verified(_) => VerifyEmailStatus::Verified,
            VerifyEmailPayload::AlreadyVerified(_) => VerifyEmailStatus::AlreadyVerified,
            VerifyEmailPayload::InvalidCode => VerifyEmailStatus::InvalidCode,
        }
    }

    /// The email address that was verified
    async fn email(&self) -> Option<UserEmail> {
        match self {
            VerifyEmailPayload::Verified(email) | VerifyEmailPayload::AlreadyVerified(email) => {
                Some(UserEmail(email.clone()))
            }
            VerifyEmailPayload::InvalidCode => None,
        }
    }

    /// The user to whom the email address belongs
    async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
        let state = ctx.state();
        let mut repo = state.repository().await?;

        let user_id = match self {
            VerifyEmailPayload::Verified(email) | VerifyEmailPayload::AlreadyVerified(email) => {
                email.user_id
            }
            VerifyEmailPayload::InvalidCode => return Ok(None),
        };

        let user = repo
            .user()
            .lookup(user_id)
            .await?
            .context("User not found")?;

        Ok(Some(User(user)))
    }
}

/// The input for the `removeEmail` mutation
#[derive(InputObject)]
struct RemoveEmailInput {
    /// The ID of the email address to remove
    user_email_id: ID,
}

/// The status of the `removeEmail` mutation
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum RemoveEmailStatus {
    /// The email address was removed
    Removed,

    /// Can't remove the primary email address
    Primary,

    /// The email address was not found
    NotFound,
}

/// The payload of the `removeEmail` mutation
#[derive(Description)]
enum RemoveEmailPayload {
    Removed(mas_data_model::UserEmail),
    Primary(mas_data_model::UserEmail),
    NotFound,
}

#[Object(use_type_description)]
impl RemoveEmailPayload {
    /// Status of the operation
    async fn status(&self) -> RemoveEmailStatus {
        match self {
            RemoveEmailPayload::Removed(_) => RemoveEmailStatus::Removed,
            RemoveEmailPayload::Primary(_) => RemoveEmailStatus::Primary,
            RemoveEmailPayload::NotFound => RemoveEmailStatus::NotFound,
        }
    }

    /// The email address that was removed
    async fn email(&self) -> Option<UserEmail> {
        match self {
            RemoveEmailPayload::Removed(email) | RemoveEmailPayload::Primary(email) => {
                Some(UserEmail(email.clone()))
            }
            RemoveEmailPayload::NotFound => None,
        }
    }

    /// The user to whom the email address belonged
    async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
        let state = ctx.state();
        let mut repo = state.repository().await?;

        let user_id = match self {
            RemoveEmailPayload::Removed(email) | RemoveEmailPayload::Primary(email) => {
                email.user_id
            }
            RemoveEmailPayload::NotFound => return Ok(None),
        };

        let user = repo
            .user()
            .lookup(user_id)
            .await?
            .context("User not found")?;

        Ok(Some(User(user)))
    }
}

/// The input for the `setPrimaryEmail` mutation
#[derive(InputObject)]
struct SetPrimaryEmailInput {
    /// The ID of the email address to set as primary
    user_email_id: ID,
}

/// The status of the `setPrimaryEmail` mutation
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum SetPrimaryEmailStatus {
    /// The email address was set as primary
    Set,
    /// The email address was not found
    NotFound,
    /// Can't make an unverified email address primary
    Unverified,
}

/// The payload of the `setPrimaryEmail` mutation
#[derive(Description)]
enum SetPrimaryEmailPayload {
    Set(mas_data_model::User),
    NotFound,
    Unverified,
}

#[Object(use_type_description)]
impl SetPrimaryEmailPayload {
    async fn status(&self) -> SetPrimaryEmailStatus {
        match self {
            SetPrimaryEmailPayload::Set(_) => SetPrimaryEmailStatus::Set,
            SetPrimaryEmailPayload::NotFound => SetPrimaryEmailStatus::NotFound,
            SetPrimaryEmailPayload::Unverified => SetPrimaryEmailStatus::Unverified,
        }
    }

    /// The user to whom the email address belongs
    async fn user(&self) -> Option<User> {
        match self {
            SetPrimaryEmailPayload::Set(user) => Some(User(user.clone())),
            SetPrimaryEmailPayload::NotFound | SetPrimaryEmailPayload::Unverified => None,
        }
    }
}

#[Object]
impl UserEmailMutations {
    /// Add an email address to the specified user
    async fn add_email(
        &self,
        ctx: &Context<'_>,
        input: AddEmailInput,
    ) -> Result<AddEmailPayload, async_graphql::Error> {
        let state = ctx.state();
        let id = NodeType::User.extract_ulid(&input.user_id)?;
        let requester = ctx.requester();

        if !requester.is_owner_or_admin(&UserId(id)) {
            return Err(async_graphql::Error::new("Unauthorized"));
        }

        // Only admins can skip validation
        if (input.skip_verification.is_some() || input.skip_policy_check.is_some())
            && !requester.is_admin()
        {
            return Err(async_graphql::Error::new("Unauthorized"));
        }

        let skip_verification = input.skip_verification.unwrap_or(false);
        let skip_policy_check = input.skip_policy_check.unwrap_or(false);

        let mut repo = state.repository().await?;

        let user = repo
            .user()
            .lookup(id)
            .await?
            .context("Failed to load user")?;

        // XXX: this logic should be extracted somewhere else, since most of it is
        // duplicated in mas_handlers

        // Validate the email address
        if input.email.parse::<lettre::Address>().is_err() {
            return Ok(AddEmailPayload::Invalid);
        }

        if !skip_policy_check {
            let mut policy = state.policy().await?;
            let res = policy.evaluate_email(&input.email).await?;
            if !res.valid() {
                return Ok(AddEmailPayload::Denied {
                    violations: res.violations,
                });
            }
        }

        // Find an existing email address
        let existing_user_email = repo.user_email().find(&user, &input.email).await?;
        let (added, mut user_email) = if let Some(user_email) = existing_user_email {
            (false, user_email)
        } else {
            let clock = state.clock();
            let mut rng = state.rng();

            let user_email = repo
                .user_email()
                .add(&mut rng, &clock, &user, input.email)
                .await?;

            (true, user_email)
        };

        // Schedule a job to verify the email address if needed
        if user_email.confirmed_at.is_none() {
            if skip_verification {
                user_email = repo
                    .user_email()
                    .mark_as_verified(&state.clock(), user_email)
                    .await?;
            } else {
                // TODO: figure out the locale
                repo.job()
                    .schedule_job(VerifyEmailJob::new(&user_email))
                    .await?;
            }
        }

        repo.save().await?;

        let payload = if added {
            AddEmailPayload::Added(user_email)
        } else {
            AddEmailPayload::Exists(user_email)
        };
        Ok(payload)
    }

    /// Send a verification code for an email address
    async fn send_verification_email(
        &self,
        ctx: &Context<'_>,
        input: SendVerificationEmailInput,
    ) -> Result<SendVerificationEmailPayload, async_graphql::Error> {
        let state = ctx.state();
        let user_email_id = NodeType::UserEmail.extract_ulid(&input.user_email_id)?;
        let requester = ctx.requester();

        let mut repo = state.repository().await?;

        let user_email = repo
            .user_email()
            .lookup(user_email_id)
            .await?
            .context("User email not found")?;

        if !requester.is_owner_or_admin(&user_email) {
            return Err(async_graphql::Error::new("User email not found"));
        }

        // Schedule a job to verify the email address if needed
        let needs_verification = user_email.confirmed_at.is_none();
        if needs_verification {
            // TODO: figure out the locale
            repo.job()
                .schedule_job(VerifyEmailJob::new(&user_email))
                .await?;
        }

        repo.save().await?;

        let payload = if needs_verification {
            SendVerificationEmailPayload::Sent(user_email)
        } else {
            SendVerificationEmailPayload::AlreadyVerified(user_email)
        };
        Ok(payload)
    }

    /// Submit a verification code for an email address
    async fn verify_email(
        &self,
        ctx: &Context<'_>,
        input: VerifyEmailInput,
    ) -> Result<VerifyEmailPayload, async_graphql::Error> {
        let state = ctx.state();
        let user_email_id = NodeType::UserEmail.extract_ulid(&input.user_email_id)?;
        let requester = ctx.requester();

        let clock = state.clock();
        let mut repo = state.repository().await?;

        let user_email = repo
            .user_email()
            .lookup(user_email_id)
            .await?
            .context("User email not found")?;

        if !requester.is_owner_or_admin(&user_email) {
            return Err(async_graphql::Error::new("User email not found"));
        }

        if user_email.confirmed_at.is_some() {
            // Just return the email address if it's already verified
            // XXX: should we return an error instead?
            return Ok(VerifyEmailPayload::AlreadyVerified(user_email));
        }

        // XXX: this logic should be extracted somewhere else, since most of it is
        // duplicated in mas_handlers

        // Find the verification code
        let verification = repo
            .user_email()
            .find_verification_code(&clock, &user_email, &input.code)
            .await?
            .filter(|v| v.is_valid());

        let Some(verification) = verification else {
            return Ok(VerifyEmailPayload::InvalidCode);
        };

        repo.user_email()
            .consume_verification_code(&clock, verification)
            .await?;

        let user = repo
            .user()
            .lookup(user_email.user_id)
            .await?
            .context("Failed to load user")?;

        // XXX: is this the right place to do this?
        if user.primary_user_email_id.is_none() {
            repo.user_email().set_as_primary(&user_email).await?;
        }

        let user_email = repo
            .user_email()
            .mark_as_verified(&clock, user_email)
            .await?;

        repo.job()
            .schedule_job(ProvisionUserJob::new(&user))
            .await?;

        repo.save().await?;

        Ok(VerifyEmailPayload::Verified(user_email))
    }

    /// Remove an email address
    async fn remove_email(
        &self,
        ctx: &Context<'_>,
        input: RemoveEmailInput,
    ) -> Result<RemoveEmailPayload, async_graphql::Error> {
        let state = ctx.state();
        let user_email_id = NodeType::UserEmail.extract_ulid(&input.user_email_id)?;
        let requester = ctx.requester();

        let mut repo = state.repository().await?;

        let user_email = repo.user_email().lookup(user_email_id).await?;
        let Some(user_email) = user_email else {
            return Ok(RemoveEmailPayload::NotFound);
        };

        if !requester.is_owner_or_admin(&user_email) {
            return Ok(RemoveEmailPayload::NotFound);
        }

        let user = repo
            .user()
            .lookup(user_email.user_id)
            .await?
            .context("Failed to load user")?;

        if user.primary_user_email_id == Some(user_email.id) {
            // Prevent removing the primary email address
            return Ok(RemoveEmailPayload::Primary(user_email));
        }

        repo.user_email().remove(user_email.clone()).await?;

        // Schedule a job to update the user
        repo.job()
            .schedule_job(ProvisionUserJob::new(&user))
            .await?;

        repo.save().await?;

        Ok(RemoveEmailPayload::Removed(user_email))
    }

    /// Set an email address as primary
    async fn set_primary_email(
        &self,
        ctx: &Context<'_>,
        input: SetPrimaryEmailInput,
    ) -> Result<SetPrimaryEmailPayload, async_graphql::Error> {
        let state = ctx.state();
        let user_email_id = NodeType::UserEmail.extract_ulid(&input.user_email_id)?;
        let requester = ctx.requester();

        let mut repo = state.repository().await?;

        let user_email = repo.user_email().lookup(user_email_id).await?;
        let Some(user_email) = user_email else {
            return Ok(SetPrimaryEmailPayload::NotFound);
        };

        if !requester.is_owner_or_admin(&user_email) {
            return Err(async_graphql::Error::new("Unauthorized"));
        }

        if user_email.confirmed_at.is_none() {
            return Ok(SetPrimaryEmailPayload::Unverified);
        }

        repo.user_email().set_as_primary(&user_email).await?;

        // The user primary email should already be up to date
        let user = repo
            .user()
            .lookup(user_email.user_id)
            .await?
            .context("Failed to load user")?;

        repo.save().await?;

        Ok(SetPrimaryEmailPayload::Set(user))
    }
}
