import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { OrganisationGroupType, OrganisationMemberRole, Prisma, TeamMemberRole } from '@prisma/client';
import { match } from 'ts-pattern';

import { IS_BILLING_ENABLED } from '../../constants/app';
import { LOWEST_ORGANISATION_ROLE, ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/organisations';
import { TEAM_INTERNAL_GROUPS } from '../../constants/teams';
import { generateDatabaseId } from '../../universal/id';
import { buildOrganisationWhereQuery } from '../../utils/organisations';
import { generateDefaultTeamSettings } from '../../utils/teams';

export type CreateTeamOptions = {
  /**
   * ID of the user creating the Team.
   */
  userId: number;

  /**
   * Name of the team to display.
   */
  teamName: string;

  /**
   * Unique URL of the team.
   *
   * Used as the URL path, example: https://documenso.com/t/{teamUrl}/settings
   */
  teamUrl: string;

  /**
   * ID of the organisation the team belongs to.
   */
  organisationId: string;

  /**
   * Whether to inherit all members from the organisation.
   */
  inheritMembers: boolean;

  /**
   * List of additional groups to attach to the team.
   */
  groups?: {
    id: string;
    role: TeamMemberRole;
  }[];
};

export const createTeam = async ({ userId, teamName, teamUrl, organisationId, inheritMembers }: CreateTeamOptions) => {
  const organisation = await prisma.organisation.findFirst({
    where: buildOrganisationWhereQuery({
      organisationId,
      userId,
      roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
    }),
    include: {
      groups: true,
      subscription: true,
      organisationClaim: true,
      owner: {
        select: {
          id: true,
          name: true,
          email: true,
        },
      },
    },
  });

  if (!organisation) {
    throw new AppError(AppErrorCode.NOT_FOUND, {
      message: 'Organisation not found.',
    });
  }

  // Validate they have enough team slots. 0 means they can create unlimited teams.
  if (organisation.organisationClaim.teamCount !== 0 && IS_BILLING_ENABLED()) {
    const teamCount = await prisma.team.count({
      where: {
        organisationId,
      },
    });

    if (teamCount >= organisation.organisationClaim.teamCount) {
      throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
        message: 'You have reached the maximum number of teams for your plan.',
      });
    }
  }

  // Inherit internal organisation groups to the team.
  // Organisation Admins/Mangers get assigned as team admins, members get assigned as team members.
  const internalOrganisationGroups = organisation.groups
    .filter((group) => {
      if (group.type !== OrganisationGroupType.INTERNAL_ORGANISATION) {
        return false;
      }

      // If we're inheriting members, allow all internal organisation groups.
      if (inheritMembers) {
        return true;
      }

      // Otherwise, only inherit organisation admins/managers.
      return (
        group.organisationRole === OrganisationMemberRole.ADMIN ||
        group.organisationRole === OrganisationMemberRole.MANAGER
      );
    })
    .map((group) =>
      match(group.organisationRole)
        .with(OrganisationMemberRole.ADMIN, OrganisationMemberRole.MANAGER, () => ({
          organisationGroupId: group.id,
          teamRole: TeamMemberRole.ADMIN,
        }))
        .with(OrganisationMemberRole.MEMBER, () => ({
          organisationGroupId: group.id,
          teamRole: TeamMemberRole.MEMBER,
        }))
        .exhaustive(),
    );

  await prisma
    .$transaction(
      async (tx) => {
        const teamSettings = await tx.teamGlobalSettings.create({
          data: {
            ...generateDefaultTeamSettings(),
            defaultRecipients: Prisma.DbNull,
            id: generateDatabaseId('team_setting'),
          },
        });

        const team = await tx.team.create({
          data: {
            name: teamName,
            url: teamUrl,
            organisationId,
            teamGlobalSettingsId: teamSettings.id,
            teamGroups: {
              createMany: {
                // Attach the internal organisation groups to the team.
                data: internalOrganisationGroups.map((group) => ({
                  ...group,
                  id: generateDatabaseId('team_group'),
                })),
              },
            },
          },
          include: {
            teamGroups: true,
          },
        });

        // Create the internal team groups.
        await Promise.all(
          TEAM_INTERNAL_GROUPS.map(async (teamGroup) =>
            tx.organisationGroup.create({
              data: {
                id: generateDatabaseId('org_group'),
                type: teamGroup.type,
                organisationRole: LOWEST_ORGANISATION_ROLE,
                organisationId,
                teamGroups: {
                  create: {
                    id: generateDatabaseId('team_group'),
                    teamId: team.id,
                    teamRole: teamGroup.teamRole,
                  },
                },
              },
            }),
          ),
        );
      },
      {
        timeout: 7500,
      },
    )
    .catch((err) => {
      if (err.code === 'P2002') {
        throw new AppError(AppErrorCode.ALREADY_EXISTS, {
          message: 'Team URL already exists',
        });
      }

      throw err;
    });
};
