import type { Prisma, PrismaClient, PrincipalType } from "@prisma/client";
import { lockActiveOrganization } from "../org/status.js";

export type ExternalPrincipalType = Extract<
  PrincipalType,
  "FEISHU_CHAT" | "FEISHU_DEPARTMENT" | "FEISHU_USER_GROUP"
>;

export interface ExternalPrincipalMembershipInput {
  readonly feishuOpenId: string;
  readonly displayName: string;
  readonly principalType: ExternalPrincipalType;
  readonly principalId: string;
}

export interface SyncExternalPrincipalMembershipsInput {
  readonly organizationId: string;
  readonly provider?: string | undefined;
  readonly providerTenantId?: string | undefined;
  readonly source: string;
  readonly replaceSource?: boolean | undefined;
  readonly memberships: readonly ExternalPrincipalMembershipInput[];
}

export interface SyncExternalPrincipalMembershipsResult {
  readonly synced: number;
  readonly revoked: number;
}

export async function syncExternalPrincipalMemberships(
  prisma: PrismaClient,
  input: SyncExternalPrincipalMembershipsInput,
): Promise<SyncExternalPrincipalMembershipsResult> {
  const syncedAt = new Date();
  const incomingKeys = new Set<string>();
  let synced = 0;
  const connection = await prisma.$transaction(async (tx) => {
    await lockActiveOrganization(tx, input.organizationId);
    return tx.externalDirectoryConnection.upsert({
      where: {
        organizationId_provider_source: {
          organizationId: input.organizationId,
          provider: input.provider ?? "FEISHU",
          source: input.source,
        },
      },
      update: {
        revokedAt: null,
        ...(input.providerTenantId !== undefined ? { providerTenantId: input.providerTenantId } : {}),
      },
      create: {
        organizationId: input.organizationId,
        provider: input.provider ?? "FEISHU",
        ...(input.providerTenantId !== undefined ? { providerTenantId: input.providerTenantId } : {}),
        source: input.source,
      },
      select: { id: true },
    });
  });

  // Each membership is a short lifecycle-serialized unit. A large directory
  // must not hold the Organization row lock (or one interactive transaction)
  // for the duration of the entire external sync.
  for (const item of input.memberships) {
    assertExternalPrincipalType(item.principalType);
    const userId = await prisma.$transaction(async (tx) => {
      await lockActiveOrganization(tx, input.organizationId);
      const user = await tx.user.upsert({
        where: { feishuOpenId: item.feishuOpenId },
        update: { displayName: item.displayName },
        create: {
          feishuOpenId: item.feishuOpenId,
          displayName: item.displayName,
          organizationMemberships: { create: { organizationId: input.organizationId, role: "MEMBER" } },
        },
      });
      await ensureOrganizationMembership(tx, input.organizationId, user.id);
      const existing = await tx.externalPrincipalMembership.findFirst({
        where: {
          userId: user.id,
          principalType: item.principalType,
          principalId: item.principalId,
          connectionId: connection.id,
          revokedAt: null,
        },
        select: { id: true },
      });
      if (existing === null) {
        await tx.externalPrincipalMembership.create({
          data: {
            userId: user.id,
            connectionId: connection.id,
            principalType: item.principalType,
            principalId: item.principalId,
            syncedAt,
          },
        });
      } else {
        await tx.externalPrincipalMembership.update({
          where: { id: existing.id },
          data: { syncedAt },
        });
      }
      return user.id;
    });
    incomingKeys.add(externalMembershipKey(userId, item.principalType, item.principalId, connection.id));
    synced++;
  }

  let revoked = 0;
  if (input.replaceSource === true) {
    revoked = await prisma.$transaction(async (tx) => {
      await lockActiveOrganization(tx, input.organizationId);
      const active = await tx.externalPrincipalMembership.findMany({
        where: { connectionId: connection.id, revokedAt: null },
        select: { id: true, userId: true, principalType: true, principalId: true, connectionId: true },
      });
      const revokeIds = active
        .filter((membership) => !incomingKeys.has(externalMembershipKey(
          membership.userId,
          membership.principalType,
          membership.principalId,
          membership.connectionId,
        )))
        .map((membership) => membership.id);
      if (revokeIds.length > 0) {
        const result = await tx.externalPrincipalMembership.updateMany({
          where: { id: { in: revokeIds } },
          data: { revokedAt: syncedAt },
        });
        return result.count;
      }
      return 0;
    });
  }

  return { synced, revoked };
}

async function ensureOrganizationMembership(
  prisma: PrismaClient | Prisma.TransactionClient,
  organizationId: string,
  userId: string,
): Promise<void> {
  const active = await prisma.organizationMembership.findFirst({
    where: { organizationId, userId, revokedAt: null },
    select: { id: true },
  });
  if (active !== null) return;
  await prisma.organizationMembership.create({
    data: { organizationId, userId, role: "MEMBER" },
  });
}

function externalMembershipKey(userId: string, type: PrincipalType, id: string, connectionId: string): string {
  return `${connectionId}:${userId}:${type}:${id}`;
}

function assertExternalPrincipalType(type: PrincipalType): asserts type is ExternalPrincipalType {
  if (type !== "FEISHU_CHAT" && type !== "FEISHU_DEPARTMENT" && type !== "FEISHU_USER_GROUP") {
    throw new Error(`unsupported external principal type: ${type}`);
  }
}
