use super::{ids::*, Organization, Project};
use crate::{
    database::redis::RedisPool,
    models::teams::{OrganizationPermissions, ProjectPermissions},
};
use dashmap::DashMap;
use futures::TryStreamExt;
use itertools::Itertools;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

const TEAMS_NAMESPACE: &str = "teams";

pub struct TeamBuilder {
    pub members: Vec<TeamMemberBuilder>,
}
pub struct TeamMemberBuilder {
    pub user_id: UserId,
    pub role: String,
    pub is_owner: bool,
    pub permissions: ProjectPermissions,
    pub organization_permissions: Option<OrganizationPermissions>,
    pub accepted: bool,
    pub payouts_split: Decimal,
    pub ordering: i64,
}

impl TeamBuilder {
    pub async fn insert(
        self,
        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
    ) -> Result<TeamId, super::DatabaseError> {
        let team_id = generate_team_id(transaction).await?;

        let team = Team { id: team_id };

        sqlx::query!(
            "
            INSERT INTO teams (id)
            VALUES ($1)
            ",
            team.id as TeamId,
        )
        .execute(&mut **transaction)
        .await?;

        let mut team_member_ids = Vec::new();
        for _ in self.members.iter() {
            team_member_ids.push(generate_team_member_id(transaction).await?.0);
        }
        let TeamBuilder { members } = self;
        let (
            team_ids,
            user_ids,
            roles,
            is_owners,
            permissions,
            organization_permissions,
            accepteds,
            payouts_splits,
            orderings,
        ): (
            Vec<_>,
            Vec<_>,
            Vec<_>,
            Vec<_>,
            Vec<_>,
            Vec<_>,
            Vec<_>,
            Vec<_>,
            Vec<_>,
        ) = members
            .into_iter()
            .map(|m| {
                (
                    team.id.0,
                    m.user_id.0,
                    m.role,
                    m.is_owner,
                    m.permissions.bits() as i64,
                    m.organization_permissions.map(|p| p.bits() as i64),
                    m.accepted,
                    m.payouts_split,
                    m.ordering,
                )
            })
            .multiunzip();
        sqlx::query!(
            "
            INSERT INTO team_members (id, team_id, user_id, role, is_owner, permissions, organization_permissions, accepted, payouts_split, ordering)
            SELECT * FROM UNNEST ($1::int8[], $2::int8[], $3::int8[], $4::varchar[], $5::bool[], $6::int8[], $7::int8[], $8::bool[], $9::numeric[], $10::int8[])
            ",
            &team_member_ids[..],
            &team_ids[..],
            &user_ids[..],
            &roles[..],
            &is_owners[..],
            &permissions[..],
            &organization_permissions[..] as &[Option<i64>],
            &accepteds[..],
            &payouts_splits[..],
            &orderings[..],
        )
        .execute(&mut **transaction)
        .await?;

        Ok(team_id)
    }
}

/// A team of users who control a project
pub struct Team {
    /// The id of the team
    pub id: TeamId,
}

#[derive(Deserialize, Serialize, Clone, Debug, Copy)]
pub enum TeamAssociationId {
    Project(ProjectId),
    Organization(OrganizationId),
}

impl Team {
    pub async fn get_association<'a, 'b, E>(
        id: TeamId,
        executor: E,
    ) -> Result<Option<TeamAssociationId>, super::DatabaseError>
    where
        E: sqlx::Executor<'a, Database = sqlx::Postgres>,
    {
        let result = sqlx::query!(
            "
            SELECT m.id AS pid, NULL AS oid
            FROM mods m
            WHERE m.team_id = $1
            
            UNION ALL
             
            SELECT NULL AS pid, o.id AS oid
            FROM organizations o
            WHERE o.team_id = $1    
            ",
            id as TeamId
        )
        .fetch_optional(executor)
        .await?;

        if let Some(t) = result {
            // Only one of project_id or organization_id will be set
            let mut team_association_id = None;
            if let Some(pid) = t.pid {
                team_association_id =
                    Some(TeamAssociationId::Project(ProjectId(pid)));
            }
            if let Some(oid) = t.oid {
                team_association_id =
                    Some(TeamAssociationId::Organization(OrganizationId(oid)));
            }
            return Ok(team_association_id);
        }
        Ok(None)
    }
}

/// A member of a team
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct TeamMember {
    pub id: TeamMemberId,
    pub team_id: TeamId,

    /// The ID of the user associated with the member
    pub user_id: UserId,
    pub role: String,
    pub is_owner: bool,

    // The permissions of the user in this project team
    // For an organization team, these are the fallback permissions for any project in the organization
    pub permissions: ProjectPermissions,

    // The permissions of the user in this organization team
    // For a project team, this is None
    pub organization_permissions: Option<OrganizationPermissions>,

    pub accepted: bool,
    pub payouts_split: Decimal,
    pub ordering: i64,
}

impl TeamMember {
    // Lists the full members of a team
    pub async fn get_from_team_full<'a, 'b, E>(
        id: TeamId,
        executor: E,
        redis: &RedisPool,
    ) -> Result<Vec<TeamMember>, super::DatabaseError>
    where
        E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
    {
        Self::get_from_team_full_many(&[id], executor, redis).await
    }

    pub async fn get_from_team_full_many<'a, E>(
        team_ids: &[TeamId],
        exec: E,
        redis: &RedisPool,
    ) -> Result<Vec<TeamMember>, super::DatabaseError>
    where
        E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
    {
        if team_ids.is_empty() {
            return Ok(Vec::new());
        }

        let val = redis.get_cached_keys(
            TEAMS_NAMESPACE,
            &team_ids.iter().map(|x| x.0).collect::<Vec<_>>(),
            |team_ids| async move {
                let teams = sqlx::query!(
                    "
                    SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions,
                    accepted, payouts_split,
                    ordering, user_id
                    FROM team_members
                    WHERE team_id = ANY($1)
                    ORDER BY team_id, ordering;
                    ",
                    &team_ids
                )
                    .fetch(exec)
                    .try_fold(DashMap::new(), |acc: DashMap<i64, Vec<TeamMember>>, m| {
                        let member = TeamMember {
                            id: TeamMemberId(m.id),
                            team_id: TeamId(m.team_id),
                            role: m.member_role,
                            is_owner: m.is_owner,
                            permissions: ProjectPermissions::from_bits(m.permissions as u64)
                                .unwrap_or_default(),
                            organization_permissions: m
                                .organization_permissions
                                .map(|p| OrganizationPermissions::from_bits(p as u64).unwrap_or_default()),
                            accepted: m.accepted,
                            user_id: UserId(m.user_id),
                            payouts_split: m.payouts_split,
                            ordering: m.ordering,
                        };

                        acc.entry(m.team_id)
                            .or_default()
                            .push(member);

                        async move { Ok(acc) }
                    })
                    .await?;

                Ok(teams)
            },
        ).await?;

        Ok(val.into_iter().flatten().collect())
    }

    pub async fn clear_cache(
        id: TeamId,
        redis: &RedisPool,
    ) -> Result<(), super::DatabaseError> {
        let mut redis = redis.connect().await?;
        redis.delete(TEAMS_NAMESPACE, id.0).await?;
        Ok(())
    }

    /// Gets a team member from a user id and team id.  Does not return pending members.
    pub async fn get_from_user_id<'a, 'b, E>(
        id: TeamId,
        user_id: UserId,
        executor: E,
    ) -> Result<Option<Self>, super::DatabaseError>
    where
        E: sqlx::Executor<'a, Database = sqlx::Postgres>,
    {
        Self::get_from_user_id_many(&[id], user_id, executor)
            .await
            .map(|x| x.into_iter().next())
    }

    /// Gets team members from user ids and team ids.  Does not return pending members.
    pub async fn get_from_user_id_many<'a, 'b, E>(
        team_ids: &[TeamId],
        user_id: UserId,
        executor: E,
    ) -> Result<Vec<Self>, super::DatabaseError>
    where
        E: sqlx::Executor<'a, Database = sqlx::Postgres>,
    {
        let team_ids_parsed: Vec<i64> = team_ids.iter().map(|x| x.0).collect();

        let team_members = sqlx::query!(
            "
            SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions,
            accepted, payouts_split, role,
            ordering, user_id
            FROM team_members
            WHERE (team_id = ANY($1) AND user_id = $2 AND accepted = TRUE)
            ORDER BY ordering
            ",
            &team_ids_parsed,
            user_id as UserId
        )
        .fetch(executor)
        .map_ok(|m| TeamMember {
            id: TeamMemberId(m.id),
            team_id: TeamId(m.team_id),
            user_id,
            role: m.role,
            is_owner: m.is_owner,
            permissions: ProjectPermissions::from_bits(m.permissions as u64)
                .unwrap_or_default(),
            organization_permissions: m
                .organization_permissions
                .map(|p| OrganizationPermissions::from_bits(p as u64).unwrap_or_default()),
            accepted: m.accepted,
            payouts_split: m.payouts_split,
            ordering: m.ordering,
        })
        .try_collect::<Vec<TeamMember>>()
        .await?;

        Ok(team_members)
    }

    /// Gets a team member from a user id and team id, including pending members.
    pub async fn get_from_user_id_pending<'a, 'b, E>(
        id: TeamId,
        user_id: UserId,
        executor: E,
    ) -> Result<Option<Self>, super::DatabaseError>
    where
        E: sqlx::Executor<'a, Database = sqlx::Postgres>,
    {
        let result = sqlx::query!(
            "
            SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions,
                accepted, payouts_split, role,
                ordering, user_id
                
            FROM team_members
            WHERE (team_id = $1 AND user_id = $2)
            ORDER BY ordering
            ",
            id as TeamId,
            user_id as UserId
        )
        .fetch_optional(executor)
        .await?;

        if let Some(m) = result {
            Ok(Some(TeamMember {
                id: TeamMemberId(m.id),
                team_id: id,
                user_id,
                role: m.role,
                is_owner: m.is_owner,
                permissions: ProjectPermissions::from_bits(
                    m.permissions as u64,
                )
                .unwrap_or_default(),
                organization_permissions: m.organization_permissions.map(|p| {
                    OrganizationPermissions::from_bits(p as u64)
                        .unwrap_or_default()
                }),
                accepted: m.accepted,
                payouts_split: m.payouts_split,
                ordering: m.ordering,
            }))
        } else {
            Ok(None)
        }
    }

    pub async fn insert(
        &self,
        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
    ) -> Result<(), sqlx::error::Error> {
        sqlx::query!(
            "
            INSERT INTO team_members (
                id, team_id, user_id, role, permissions, organization_permissions, is_owner, accepted, payouts_split
            )
            VALUES (
                $1, $2, $3, $4, $5, $6, $7, $8, $9
            )
            ",
            self.id as TeamMemberId,
            self.team_id as TeamId,
            self.user_id as UserId,
            self.role,
            self.permissions.bits() as i64,
            self.organization_permissions.map(|p| p.bits() as i64),
            self.is_owner,
            self.accepted,
            self.payouts_split
        )
        .execute(&mut **transaction)
        .await?;

        Ok(())
    }

    pub async fn delete<'a, 'b>(
        id: TeamId,
        user_id: UserId,
        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
    ) -> Result<(), super::DatabaseError> {
        sqlx::query!(
            "
            DELETE FROM team_members
            WHERE (team_id = $1 AND user_id = $2 AND NOT is_owner = TRUE)
            ",
            id as TeamId,
            user_id as UserId,
        )
        .execute(&mut **transaction)
        .await?;

        Ok(())
    }

    #[allow(clippy::too_many_arguments)]
    pub async fn edit_team_member(
        id: TeamId,
        user_id: UserId,
        new_permissions: Option<ProjectPermissions>,
        new_organization_permissions: Option<OrganizationPermissions>,
        new_role: Option<String>,
        new_accepted: Option<bool>,
        new_payouts_split: Option<Decimal>,
        new_ordering: Option<i64>,
        new_is_owner: Option<bool>,
        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
    ) -> Result<(), super::DatabaseError> {
        if let Some(permissions) = new_permissions {
            sqlx::query!(
                "
                UPDATE team_members
                SET permissions = $1
                WHERE (team_id = $2 AND user_id = $3)
                ",
                permissions.bits() as i64,
                id as TeamId,
                user_id as UserId,
            )
            .execute(&mut **transaction)
            .await?;
        }

        if let Some(organization_permissions) = new_organization_permissions {
            sqlx::query!(
                "
                UPDATE team_members
                SET organization_permissions = $1
                WHERE (team_id = $2 AND user_id = $3)
                ",
                organization_permissions.bits() as i64,
                id as TeamId,
                user_id as UserId,
            )
            .execute(&mut **transaction)
            .await?;
        }

        if let Some(role) = new_role {
            sqlx::query!(
                "
                UPDATE team_members
                SET role = $1
                WHERE (team_id = $2 AND user_id = $3)
                ",
                role,
                id as TeamId,
                user_id as UserId,
            )
            .execute(&mut **transaction)
            .await?;
        }

        if let Some(accepted) = new_accepted {
            if accepted {
                sqlx::query!(
                    "
                    UPDATE team_members
                    SET accepted = TRUE
                    WHERE (team_id = $1 AND user_id = $2)
                    ",
                    id as TeamId,
                    user_id as UserId,
                )
                .execute(&mut **transaction)
                .await?;
            }
        }

        if let Some(payouts_split) = new_payouts_split {
            sqlx::query!(
                "
                UPDATE team_members
                SET payouts_split = $1
                WHERE (team_id = $2 AND user_id = $3)
                ",
                payouts_split,
                id as TeamId,
                user_id as UserId,
            )
            .execute(&mut **transaction)
            .await?;
        }

        if let Some(ordering) = new_ordering {
            sqlx::query!(
                "
                UPDATE team_members
                SET ordering = $1
                WHERE (team_id = $2 AND user_id = $3)
                ",
                ordering,
                id as TeamId,
                user_id as UserId,
            )
            .execute(&mut **transaction)
            .await?;
        }

        if let Some(is_owner) = new_is_owner {
            sqlx::query!(
                "
                UPDATE team_members
                SET is_owner = $1
                WHERE (team_id = $2 AND user_id = $3)
                ",
                is_owner,
                id as TeamId,
                user_id as UserId,
            )
            .execute(&mut **transaction)
            .await?;
        }

        Ok(())
    }

    pub async fn get_from_user_id_project<'a, 'b, E>(
        id: ProjectId,
        user_id: UserId,
        allow_pending: bool,
        executor: E,
    ) -> Result<Option<Self>, super::DatabaseError>
    where
        E: sqlx::Executor<'a, Database = sqlx::Postgres>,
    {
        let accepted = if allow_pending {
            vec![true, false]
        } else {
            vec![true]
        };

        let result = sqlx::query!(
            "
            SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering
            FROM mods m
            INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = ANY($3)
            WHERE m.id = $1
            ",
            id as ProjectId,
            user_id as UserId,
            &accepted
        )
            .fetch_optional(executor)
            .await?;

        if let Some(m) = result {
            Ok(Some(TeamMember {
                id: TeamMemberId(m.id),
                team_id: TeamId(m.team_id),
                user_id,
                role: m.role,
                is_owner: m.is_owner,
                permissions: ProjectPermissions::from_bits(
                    m.permissions as u64,
                )
                .unwrap_or_default(),
                organization_permissions: m.organization_permissions.map(|p| {
                    OrganizationPermissions::from_bits(p as u64)
                        .unwrap_or_default()
                }),
                accepted: m.accepted,
                payouts_split: m.payouts_split,
                ordering: m.ordering,
            }))
        } else {
            Ok(None)
        }
    }

    pub async fn get_from_user_id_organization<'a, 'b, E>(
        id: OrganizationId,
        user_id: UserId,
        allow_pending: bool,
        executor: E,
    ) -> Result<Option<Self>, super::DatabaseError>
    where
        E: sqlx::Executor<'a, Database = sqlx::Postgres>,
    {
        let accepted = if allow_pending {
            vec![true, false]
        } else {
            vec![true]
        };
        let result = sqlx::query!(
            "
            SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering
            FROM organizations o
            INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = ANY($3)
            WHERE o.id = $1
            ",
            id as OrganizationId,
            user_id as UserId,
            &accepted
        )
            .fetch_optional(executor)
            .await?;

        if let Some(m) = result {
            Ok(Some(TeamMember {
                id: TeamMemberId(m.id),
                team_id: TeamId(m.team_id),
                user_id,
                role: m.role,
                is_owner: m.is_owner,
                permissions: ProjectPermissions::from_bits(
                    m.permissions as u64,
                )
                .unwrap_or_default(),
                organization_permissions: m.organization_permissions.map(|p| {
                    OrganizationPermissions::from_bits(p as u64)
                        .unwrap_or_default()
                }),
                accepted: m.accepted,
                payouts_split: m.payouts_split,
                ordering: m.ordering,
            }))
        } else {
            Ok(None)
        }
    }

    pub async fn get_from_user_id_version<'a, 'b, E>(
        id: VersionId,
        user_id: UserId,
        executor: E,
    ) -> Result<Option<Self>, super::DatabaseError>
    where
        E: sqlx::Executor<'a, Database = sqlx::Postgres>,
    {
        let result = sqlx::query!(
            "
            SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering, v.mod_id 
            FROM versions v
            INNER JOIN mods m ON m.id = v.mod_id
            INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 AND tm.accepted = TRUE
            WHERE v.id = $1
            ",
            id as VersionId,
            user_id as UserId
        )
            .fetch_optional(executor)
            .await?;

        if let Some(m) = result {
            Ok(Some(TeamMember {
                id: TeamMemberId(m.id),
                team_id: TeamId(m.team_id),
                user_id,
                role: m.role,
                is_owner: m.is_owner,
                permissions: ProjectPermissions::from_bits(
                    m.permissions as u64,
                )
                .unwrap_or_default(),
                organization_permissions: m.organization_permissions.map(|p| {
                    OrganizationPermissions::from_bits(p as u64)
                        .unwrap_or_default()
                }),
                accepted: m.accepted,
                payouts_split: m.payouts_split,
                ordering: m.ordering,
            }))
        } else {
            Ok(None)
        }
    }

    // Gets both required members for checking permissions of an action on a project
    // - project team member (a user's membership to a given project)
    // - organization team member (a user's membership to a given organization that owns a given project)
    pub async fn get_for_project_permissions<'a, 'b, E>(
        project: &Project,
        user_id: UserId,
        executor: E,
    ) -> Result<(Option<Self>, Option<Self>), super::DatabaseError>
    where
        E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
    {
        let project_team_member =
            Self::get_from_user_id(project.team_id, user_id, executor).await?;

        let organization =
            Organization::get_associated_organization_project_id(
                project.id, executor,
            )
            .await?;

        let organization_team_member = if let Some(organization) = &organization
        {
            Self::get_from_user_id(organization.team_id, user_id, executor)
                .await?
        } else {
            None
        };

        Ok((project_team_member, organization_team_member))
    }
}
