import type { PrismaClient } from "@prisma/client";
import {
  requireActiveFeishuApplicationConnectionInTransaction,
  scopedFeishuPrincipalId,
} from "./identityNamespace.js";
import { lockActiveOrganization } from "../org/status.js";

export type FeishuBindingLifecycleReason = "chat_dissolved" | "bot_removed";

/**
 * Archive a project's active Feishu chat binding after Feishu has notified this
 * bot that the chat dissolved or that the bot was removed. The event can be
 * redelivered; only the first delivery changes state and writes an audit row.
 */
export async function archiveFeishuBindingForLifecycleEvent(
  prisma: PrismaClient,
  input: {
    readonly chatId: string;
    readonly eventId: string;
    readonly reason: FeishuBindingLifecycleReason;
  },
): Promise<{ readonly archived: boolean; readonly projectId?: string | undefined }> {
  const chatId = requireNonEmpty(input.chatId, "Feishu chat id");
  const eventId = requireNonEmpty(input.eventId, "Feishu event id");
  return prisma.$transaction(async (tx) => {
    // Feishu WS delivery is at-least-once. Persist the receipt in the same
    // transaction as the archive so a failed archive remains eligible for a
    // retry, while an old redelivery cannot archive a later re-binding.
    const receipt = await tx.feishuEventReceipt.createMany({
      data: {
        eventId,
        eventType: lifecycleEventType(input.reason),
      },
      skipDuplicates: true,
    });
    if (receipt.count === 0) return { archived: false };

    const binding = await tx.projectGroupBinding.findFirst({
      where: { chatId, archivedAt: null },
      select: { id: true, projectId: true, project: { select: { organizationId: true } } },
    });
    if (binding === null) return { archived: false };

    await lockActiveOrganization(tx, binding.project.organizationId);
    const connection = await tx.organizationFeishuApplicationConnection.findUnique({
      where: { organizationId: binding.project.organizationId },
      select: { id: true },
    });
    if (connection === null) {
      throw new Error(`Feishu application connection not found for organization ${binding.project.organizationId}`);
    }
    const activeConnection = await requireActiveFeishuApplicationConnectionInTransaction(tx, connection.id);
    if (activeConnection.organizationId !== binding.project.organizationId) {
      throw new Error("Feishu application connection Organization scope mismatch");
    }
    const now = new Date();
    const archived = await tx.projectGroupBinding.updateMany({
      where: { id: binding.id, archivedAt: null },
      data: { archivedAt: now },
    });
    if (archived.count === 0) return { archived: false };

    await tx.permissionGrant.updateMany({
      where: {
        resourceType: "PROJECT",
        resourceId: binding.projectId,
        principalType: "FEISHU_CHAT",
        // Bindings created before ADR-0024 used the raw chat id. Retire only
        // that exact legacy principal alongside the current scoped principal.
        principalId: {
          in: [scopedFeishuPrincipalId("CHAT", connection.id, chatId), chatId],
        },
        revokedAt: null,
      },
      data: { revokedAt: now },
    });
    await tx.auditEntry.create({
      data: {
        organizationId: binding.project.organizationId,
        projectId: binding.projectId,
        action: "project.chat_binding_archived",
        metadata: {
          chatId,
          reason: input.reason,
          eventId,
        },
      },
    });
    return { archived: true, projectId: binding.projectId };
  });
}

function lifecycleEventType(reason: FeishuBindingLifecycleReason): string {
  return reason === "chat_dissolved" ? "im.chat.disbanded_v1" : "im.chat.member.bot.deleted_v1";
}

function requireNonEmpty(value: string, label: string): string {
  const trimmed = value.trim();
  if (trimmed === "") throw new Error(`${label} is required`);
  return trimmed;
}
