import { Team, TeamMember, TeamMemberRole } from './team.model';
import {
  Resolver,
  ResolveField,
  Args,
  Parent,
  Query,
  Mutation,
  Int,
  Subscription,
  ID,
} from '@nestjs/graphql';
import { TeamService } from './team.service';
import { GqlAuthGuard } from '../guards/gql-auth.guard';
import { GqlUser } from '../decorators/gql-user.decorator';
import { UseGuards } from '@nestjs/common';
import { RequiresTeamRole } from './decorators/requires-team-role.decorator';
import { GqlTeamMemberGuard } from './guards/gql-team-member.guard';
import { PubSubService } from '../pubsub/pubsub.service';
import * as E from 'fp-ts/Either';
import { throwErr } from 'src/utils';
import { AuthUser } from 'src/types/AuthUser';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { SkipThrottle } from '@nestjs/throttler';

@UseGuards(GqlThrottlerGuard)
@Resolver(() => Team)
export class TeamResolver {
  constructor(
    private readonly teamService: TeamService,
    private readonly pubsub: PubSubService,
  ) {}

  // Field Resolvers
  // TODO: Deprecate this
  @ResolveField(() => [TeamMember], {
    description: 'Returns the list of members of a team',
    complexity: 10,
  })
  members(
    @Parent() team: Team,
    @Args({
      name: 'cursor',
      type: () => ID,
      description:
        'The ID of the last returned team member entry (used for pagination)',
      nullable: true,
    })
    cursor?: string,
  ): Promise<TeamMember[]> {
    return this.teamService.getMembersOfTeam(team.id, cursor ?? null);
  }

  @ResolveField(() => [TeamMember], {
    description: 'Returns the list of members of a team',
    complexity: 10,
  })
  teamMembers(@Parent() team: Team): Promise<TeamMember[]> {
    return this.teamService.getTeamMembers(team.id);
  }

  @ResolveField(() => TeamMemberRole, {
    description: 'The role of the current user in the team',
    nullable: true,
  })
  @UseGuards(GqlAuthGuard)
  myRole(
    @Parent() team: Team,
    @GqlUser() user: AuthUser,
  ): Promise<TeamMemberRole | null> {
    return this.teamService.getRoleOfUserInTeam(team.id, user.uid);
  }

  @ResolveField(() => Int, {
    description: 'The number of users with the OWNER role in the team',
  })
  ownersCount(@Parent() team: Team): Promise<number> {
    return this.teamService.getCountOfUsersWithRoleInTeam(
      team.id,
      TeamMemberRole.OWNER,
    );
  }

  @ResolveField(() => Int, {
    description: 'The number of users with the EDITOR role in the team',
  })
  editorsCount(@Parent() team: Team): Promise<number> {
    return this.teamService.getCountOfUsersWithRoleInTeam(
      team.id,
      TeamMemberRole.EDITOR,
    );
  }

  @ResolveField(() => Int, {
    description: 'The number of users with the VIEWER role in the team',
  })
  viewersCount(@Parent() team: Team): Promise<number> {
    return this.teamService.getCountOfUsersWithRoleInTeam(
      team.id,
      TeamMemberRole.VIEWER,
    );
  }

  // Query
  @Query(() => [Team], {
    description: 'List of teams that the executing user belongs to.',
  })
  @UseGuards(GqlAuthGuard)
  myTeams(
    @GqlUser() user: AuthUser,
    @Args({
      name: 'cursor',
      type: () => ID,
      description:
        'The ID of the last returned team entry (used for pagination)',
      nullable: true,
    })
    cursor?: string,
  ): Promise<Team[]> {
    return this.teamService.getTeamsOfUser(user.uid, cursor ?? null);
  }

  @Query(() => Team, {
    description: 'Returns the detail of the team with the given ID',
    nullable: true,
  })
  @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
  @RequiresTeamRole(
    TeamMemberRole.VIEWER,
    TeamMemberRole.EDITOR,
    TeamMemberRole.OWNER,
  )
  team(
    @Args({
      name: 'teamID',
      type: () => ID,
      description: 'ID of the team to check',
    })
    teamID: string,
  ): Promise<Team | null> {
    return this.teamService.getTeamWithID(teamID);
  }

  // Mutation
  @Mutation(() => Team, {
    description: 'Creates a team owned by the executing user',
  })
  @UseGuards(GqlAuthGuard)
  async createTeam(
    @GqlUser() user: AuthUser,
    @Args({ name: 'name', description: 'Displayed name of the team' })
    name: string,
  ): Promise<Team> {
    const team = await this.teamService.createTeam(name, user.uid);
    if (E.isLeft(team)) throwErr(team.left);
    return team.right;
  }

  @Mutation(() => Boolean, {
    description: 'Leaves a team the executing user is a part of',
  })
  @UseGuards(GqlAuthGuard)
  async leaveTeam(
    @GqlUser() user: AuthUser,
    @Args({
      name: 'teamID',
      description: 'ID of the Team to leave',
      type: () => ID,
    })
    teamID: string,
  ): Promise<boolean> {
    const isUserLeft = await this.teamService.leaveTeam(teamID, user.uid);
    if (E.isLeft(isUserLeft)) throwErr(isUserLeft.left);
    return isUserLeft.right;
  }

  @Mutation(() => Boolean, {
    description: 'Removes the team member from the team',
  })
  @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
  @RequiresTeamRole(TeamMemberRole.OWNER)
  async removeTeamMember(
    @GqlUser() _user: AuthUser,
    @Args({
      name: 'teamID',
      description: 'ID of the Team to remove user from',
      type: () => ID,
    })
    teamID: string,
    @Args({
      name: 'userUid',
      description: 'ID of the user to remove from the given team',
      type: () => ID,
    })
    userUid: string,
  ): Promise<boolean> {
    const isRemoved = await this.teamService.leaveTeam(teamID, userUid);
    if (E.isLeft(isRemoved)) throwErr(isRemoved.left);
    return isRemoved.right;
  }

  @Mutation(() => Team, {
    description: 'Renames a team',
  })
  @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
  @RequiresTeamRole(TeamMemberRole.OWNER)
  async renameTeam(
    @Args({ name: 'teamID', description: 'ID of the team', type: () => ID })
    teamID: string,
    @Args({ name: 'newName', description: 'The updated name of the team' })
    newName: string,
  ): Promise<Team> {
    const team = await this.teamService.renameTeam(teamID, newName);
    if (E.isLeft(team)) throwErr(team.left);
    return team.right;
  }

  @Mutation(() => Boolean, {
    description: 'Deletes the team',
  })
  @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
  @RequiresTeamRole(TeamMemberRole.OWNER)
  async deleteTeam(
    @Args({ name: 'teamID', description: 'ID of the team', type: () => ID })
    teamID: string,
  ): Promise<boolean> {
    const isDeleted = await this.teamService.deleteTeam(teamID);
    if (E.isLeft(isDeleted)) throwErr(isDeleted.left);
    return isDeleted.right;
  }

  @Mutation(() => TeamMember, {
    description: 'Update role of a team member the executing user owns',
  })
  @RequiresTeamRole(TeamMemberRole.OWNER)
  @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
  async updateTeamMemberRole(
    @Args({
      name: 'teamID',
      description: 'ID of the affected team',
      type: () => ID,
    })
    teamID: string,
    @Args({
      name: 'userUid',
      description: 'UID of the affected user',
      type: () => ID,
    })
    userUid: string,
    @Args({
      name: 'newRole',
      description: 'Updated role value',
      type: () => TeamMemberRole,
    })
    newRole: TeamMemberRole,
  ): Promise<TeamMember> {
    const teamMember = await this.teamService.updateTeamMemberRole(
      teamID,
      userUid,
      newRole,
    );
    if (E.isLeft(teamMember)) throwErr(teamMember.left);
    return teamMember.right;
  }

  // Subscriptions
  @Subscription(() => TeamMember, {
    description:
      'Listen to when a new team member being added to the team. The emitted value is the new team member added.',
    resolve: (value) => value,
  })
  @RequiresTeamRole(
    TeamMemberRole.OWNER,
    TeamMemberRole.EDITOR,
    TeamMemberRole.VIEWER,
  )
  @SkipThrottle()
  @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
  teamMemberAdded(
    @Args({
      name: 'teamID',
      description: 'ID of the team to listen to',
      type: () => ID,
    })
    teamID: string,
  ): AsyncIterator<TeamMember> {
    return this.pubsub.asyncIterator(`team/${teamID}/member_added`);
  }

  @Subscription(() => TeamMember, {
    description:
      'Listen to when a team member status has been updated. The emitted value is the new team member status',
    resolve: (value) => value,
  })
  @RequiresTeamRole(
    TeamMemberRole.OWNER,
    TeamMemberRole.EDITOR,
    TeamMemberRole.VIEWER,
  )
  @SkipThrottle()
  @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
  teamMemberUpdated(
    @Args({
      name: 'teamID',
      description: 'ID of the team to listen to',
      type: () => ID,
    })
    teamID: string,
  ): AsyncIterator<TeamMember> {
    return this.pubsub.asyncIterator(`team/${teamID}/member_updated`);
  }

  @Subscription(() => ID, {
    description:
      'Listen to when a team member has been removed. The emitted value is the uid of the user removed',
    resolve: (value) => value,
  })
  @RequiresTeamRole(
    TeamMemberRole.OWNER,
    TeamMemberRole.EDITOR,
    TeamMemberRole.VIEWER,
  )
  @SkipThrottle()
  @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
  teamMemberRemoved(
    @Args({
      name: 'teamID',
      description: 'ID of the team to listen to',
      type: () => ID,
    })
    teamID: string,
  ): AsyncIterator<string> {
    return this.pubsub.asyncIterator(`team/${teamID}/member_removed`);
  }
}
