import { type Prisma, prisma } from "~/db.server";
import { createEnvironment } from "./organization.server";
import { customAlphabet } from "nanoid";
import { logger } from "~/services/logger.server";
import { rbac } from "~/services/rbac.server";

const tokenValueLength = 40;
const tokenGenerator = customAlphabet("123456789abcdefghijkmnopqrstuvwxyz", tokenValueLength);

export async function getTeamMembersAndInvites({
  userId,
  organizationId,
}: {
  userId: string;
  organizationId: string;
}) {
  const org = await prisma.organization.findFirst({
    where: { id: organizationId, members: { some: { userId } } },
    select: {
      members: {
        select: {
          id: true,
          role: true,
          user: {
            select: {
              id: true,
              name: true,
              email: true,
              avatarUrl: true,
            },
          },
        },
      },
      invites: {
        select: {
          id: true,
          email: true,
          updatedAt: true,
          inviter: {
            select: {
              id: true,
              name: true,
              email: true,
              avatarUrl: true,
            },
          },
        },
      },
    },
  });

  if (!org) {
    return null;
  }

  return { members: org.members, invites: org.invites };
}

export async function removeTeamMember({
  userId,
  slug,
  memberId,
}: {
  userId: string;
  slug: string;
  memberId: string;
}) {
  const org = await prisma.organization.findFirst({
    where: { slug, members: { some: { userId } } },
  });

  if (!org) {
    throw new Error("User does not have access to this organization");
  }

  // Scope the target to this org. A member id is a globally unique key, so
  // deleting by id alone would remove members of other orgs; bind it to the
  // resolved org and reject a foreign id.
  const member = await prisma.orgMember.findFirst({
    where: { id: memberId, organizationId: org.id },
    include: {
      organization: true,
      user: true,
    },
  });

  if (!member) {
    throw new Error("Member not found in this organization");
  }

  await prisma.orgMember.delete({ where: { id: member.id } });

  return member;
}

export async function inviteMembers({
  slug,
  emails,
  userId,
  rbacRoleId,
}: {
  slug: string;
  emails: string[];
  userId: string;
  /**
   * Optional RBAC role to attach to the invite. When set, accepted
   * invites trigger `rbac.setUserRole(rbacRoleId)` after the OrgMember
   * is created.
   *
   * `OrgMemberInvite.role` is still set if the plugin isn't installed.
   */
  rbacRoleId?: string | null;
}) {
  const org = await prisma.organization.findFirst({
    where: { slug, members: { some: { userId } } },
  });

  if (!org) {
    throw new Error("User does not have access to this organization");
  }

  const invites = [...new Set(emails)].map(
    (email) =>
      ({
        email,
        token: tokenGenerator(),
        organizationId: org.id,
        inviterId: userId,
        role: "MEMBER",
        rbacRoleId: rbacRoleId ?? null,
      }) satisfies Prisma.OrgMemberInviteCreateManyInput
  );

  await prisma.orgMemberInvite.createMany({
    data: invites,
  });

  return await prisma.orgMemberInvite.findMany({
    where: {
      organizationId: org.id,
      inviterId: userId,
      email: {
        in: emails,
      },
    },
    include: {
      organization: true,
      inviter: true,
    },
  });
}

export async function getInviteFromToken({ token }: { token: string }) {
  return await prisma.orgMemberInvite.findFirst({
    where: {
      token,
    },
    include: {
      organization: true,
      inviter: true,
    },
  });
}

export async function getUsersInvites({ email }: { email: string }) {
  return await prisma.orgMemberInvite.findMany({
    where: {
      email,
      organization: {
        deletedAt: null,
      },
    },
    include: {
      organization: true,
      inviter: true,
    },
  });
}

export async function acceptInvite({
  user,
  inviteId,
}: {
  user: { id: string; email: string };
  inviteId: string;
}) {
  const result = await prisma.$transaction(async (tx) => {
    // 1. Delete the invite and get the invite details
    const invite = await tx.orgMemberInvite.delete({
      where: {
        id: inviteId,
        email: user.email,
      },
      include: {
        organization: {
          include: {
            projects: true,
          },
        },
      },
    });

    // 2. Join the organization
    const member = await tx.orgMember.create({
      data: {
        organizationId: invite.organizationId,
        userId: user.id,
        role: invite.role,
      },
    });

    // 3. Create an environment for each project
    for (const project of invite.organization.projects) {
      await createEnvironment({
        organization: invite.organization,
        project,
        type: "DEVELOPMENT",
        // We set this true but no backfill (yet!?) so never used
        // for dev environments
        isBranchableEnvironment: true,
        member,
        prismaClient: tx,
      });
    }

    // 4. Check for other invites
    const remainingInvites = await tx.orgMemberInvite.findMany({
      where: {
        email: user.email,
      },
    });

    return {
      remainingInvites,
      organization: invite.organization,
      inviteRole: invite.role,
      rbacRoleId: invite.rbacRoleId,
    };
  });

  // If the invite carried an explicit RBAC role, assign it. Best-effort: the
  // invite is already consumed and membership created above, so a failure here
  // — a returned {ok:false} or a thrown error from the plugin — must not block
  // joining the org. Swallow and log either way; without the catch a plugin
  // throw escapes and turns the whole invite-accept into a 400.
  if (result.rbacRoleId) {
    try {
      const roleResult = await rbac.setUserRole({
        userId: user.id,
        organizationId: result.organization.id,
        roleId: result.rbacRoleId,
      });
      if (!roleResult.ok) {
        logger.error("acceptInvite: skipped RBAC role assignment", {
          organizationId: result.organization.id,
          userId: user.id,
          rbacRoleId: result.rbacRoleId,
          reason: roleResult.error,
        });
      }
    } catch (error) {
      logger.error("acceptInvite: RBAC role assignment threw", {
        organizationId: result.organization.id,
        userId: user.id,
        rbacRoleId: result.rbacRoleId,
        error:
          error instanceof Error
            ? { name: error.name, message: error.message, stack: error.stack }
            : String(error),
      });
    }
  }

  return { remainingInvites: result.remainingInvites, organization: result.organization };
}

export async function declineInvite({
  user,
  inviteId,
}: {
  user: { id: string; email: string };
  inviteId: string;
}) {
  return await prisma.$transaction(async (tx) => {
    //1. delete invite
    const declinedInvite = await prisma.orgMemberInvite.delete({
      where: {
        id: inviteId,
        email: user.email,
      },
      include: {
        organization: true,
      },
    });

    //2. check for other invites
    const remainingInvites = await prisma.orgMemberInvite.findMany({
      where: {
        email: user.email,
      },
    });

    return { remainingInvites, organization: declinedInvite.organization };
  });
}

export async function resendInvite({ inviteId, userId }: { inviteId: string; userId: string }) {
  return await prisma.orgMemberInvite.update({
    where: {
      id: inviteId,
      inviterId: userId,
    },
    data: {
      updatedAt: new Date(),
    },
    include: {
      inviter: true,
      organization: true,
    },
  });
}

export async function revokeInvite({
  userId,
  orgSlug,
  inviteId,
}: {
  userId: string;
  orgSlug: string;
  inviteId: string;
}) {
  const invite = await prisma.orgMemberInvite.findFirst({
    where: {
      id: inviteId,
      organization: {
        slug: orgSlug,
        members: {
          some: {
            userId,
          },
        },
      },
    },
    select: {
      id: true,
      email: true,
      organization: true,
    },
  });

  if (!invite) {
    throw new Error("Invite not found");
  }

  await prisma.orgMemberInvite.delete({
    where: {
      id: invite.id,
    },
  });

  return { email: invite.email, organization: invite.organization };
}
