/**
 * Organization membership management for org admin (ADR-0021).
 *
 * Role rules (product pin, not yet recorded in an ADR):
 * 1. Actor must be OWNER or ADMIN (enforced at HTTP layer).
 * 2. Only OWNER can grant/revoke OWNER or modify another OWNER.
 * 3. Cannot revoke or demote the last remaining OWNER.
 * 4. ADMIN cannot modify OWNER memberships.
 */
import type { OrganizationMemberRole, Prisma, PrismaClient } from "@prisma/client";
import { upsertScopedFeishuIdentityInTransaction } from "../feishu/identityNamespace.js";
import { lockActiveOrganization } from "./status.js";

export interface OrgMemberRow {
  readonly userId: string;
  readonly feishuOpenId: string | null;
  readonly identityStatus: "SCOPED" | "UNLINKED";
  readonly displayName: string;
  readonly avatarUrl: string | null;
  readonly role: OrganizationMemberRole;
  readonly createdAt: string;
}

export async function listOrgMembers(
  prisma: PrismaClient,
  organizationId: string,
): Promise<readonly OrgMemberRow[]> {
  const rows = await prisma.organizationMembership.findMany({
    where: { organizationId, revokedAt: null },
    select: {
      role: true,
      createdAt: true,
      user: {
        select: {
          id: true,
          displayName: true,
          avatarUrl: true,
          feishuIdentities: {
            where: { connection: { organizationId } },
            select: { openId: true },
            take: 1,
          },
        },
      },
    },
    orderBy: [{ role: "asc" }, { createdAt: "asc" }],
  });
  return rows.map((row) => ({
    userId: row.user.id,
    feishuOpenId: row.user.feishuIdentities[0]?.openId ?? null,
    identityStatus: row.user.feishuIdentities.length === 1 ? "SCOPED" : "UNLINKED",
    displayName: row.user.displayName,
    avatarUrl: row.user.avatarUrl,
    role: row.role,
    createdAt: row.createdAt.toISOString(),
  }));
}

export async function addOrgMember(
  prisma: PrismaClient,
  input: {
    readonly organizationId: string;
    readonly actorRole: OrganizationMemberRole;
    readonly feishuOpenId: string;
    readonly displayName?: string | undefined;
    readonly role: OrganizationMemberRole;
  },
): Promise<OrgMemberRow> {
  const openId = requireNonEmpty(input.feishuOpenId, "feishuOpenId");
  assertCanAssignRole(input.actorRole, input.role, /* targetIsOwner */ false);

  const { identity, membership } = await prisma.$transaction(async (tx) => {
    await lockActiveOrganization(tx, input.organizationId);
    const connection = await tx.organizationFeishuApplicationConnection.findUnique({
      where: { organizationId: input.organizationId },
      select: {
        id: true,
        status: true,
        activeSecretVersion: { select: { connectionId: true, retiredAt: true } },
      },
    });
    if (connection === null || connection.status !== "ACTIVE" || connection.activeSecretVersion === null ||
        connection.activeSecretVersion.connectionId !== connection.id ||
        connection.activeSecretVersion.retiredAt !== null) {
      throw new Error("active Feishu Application Connection not found");
    }
    const identity = await upsertScopedFeishuIdentityInTransaction(tx, {
      connectionId: connection.id,
      expectedOrganizationId: input.organizationId,
      openId,
      displayName: input.displayName?.trim() || openId,
    });
    const existing = await tx.organizationMembership.findFirst({
      where: { organizationId: input.organizationId, userId: identity.userId, revokedAt: null },
    });
    if (existing !== null) throw new Error(`user ${identity.userId} is already an active member`);
    const membership = await tx.organizationMembership.create({
      data: {
        organizationId: input.organizationId,
        userId: identity.userId,
        role: input.role,
      },
    });
    return { identity, membership };
  });

  return {
    userId: identity.userId,
    feishuOpenId: identity.openId,
    identityStatus: "SCOPED",
    displayName: identity.displayName,
    avatarUrl: identity.avatarUrl,
    role: membership.role,
    createdAt: membership.createdAt.toISOString(),
  };
}

export async function setOrgMemberRole(
  prisma: PrismaClient,
  input: {
    readonly organizationId: string;
    readonly actorUserId: string;
    readonly actorRole: OrganizationMemberRole;
    readonly targetUserId: string;
    readonly role: OrganizationMemberRole;
  },
): Promise<OrgMemberRow> {
  return prisma.$transaction(async (tx) => {
    await lockActiveOrganization(tx, input.organizationId);
    const membership = await tx.organizationMembership.findFirst({
      where: {
        organizationId: input.organizationId,
        userId: input.targetUserId,
        revokedAt: null,
      },
      select: {
        id: true,
        role: true,
        createdAt: true,
        user: {
          select: {
            id: true,
            displayName: true,
            avatarUrl: true,
            feishuIdentities: {
              where: { connection: { organizationId: input.organizationId } },
              select: { openId: true },
              take: 1,
            },
          },
        },
      },
    });
    if (membership === null) throw new Error(`member not found: ${input.targetUserId}`);

    assertCanAssignRole(input.actorRole, input.role, membership.role === "OWNER");
    if (membership.role === "OWNER" && input.actorRole !== "OWNER") {
      throw new Error("cannot modify OWNER membership as ADMIN");
    }
    if (membership.role === "OWNER" && input.role !== "OWNER") {
      await assertNotLastOwner(tx, input.organizationId, input.targetUserId);
    }

    const updated = await tx.organizationMembership.update({
      where: { id: membership.id },
      data: { role: input.role },
    });

    return {
      userId: membership.user.id,
      feishuOpenId: membership.user.feishuIdentities[0]?.openId ?? null,
      identityStatus: membership.user.feishuIdentities.length === 1 ? "SCOPED" : "UNLINKED",
      displayName: membership.user.displayName,
      avatarUrl: membership.user.avatarUrl,
      role: updated.role,
      createdAt: membership.createdAt.toISOString(),
    };
  });
}

export async function revokeOrgMember(
  prisma: PrismaClient,
  input: {
    readonly organizationId: string;
    readonly actorUserId: string;
    readonly actorRole: OrganizationMemberRole;
    readonly targetUserId: string;
  },
): Promise<{ readonly revoked: true; readonly userId: string }> {
  await prisma.$transaction(async (tx) => {
    await lockActiveOrganization(tx, input.organizationId);
    const membership = await tx.organizationMembership.findFirst({
      where: {
        organizationId: input.organizationId,
        userId: input.targetUserId,
        revokedAt: null,
      },
      select: { id: true, role: true },
    });
    if (membership === null) throw new Error(`member not found: ${input.targetUserId}`);
    if (membership.role === "OWNER" && input.actorRole !== "OWNER") {
      throw new Error("cannot revoke OWNER membership as ADMIN");
    }
    if (membership.role === "OWNER") {
      await assertNotLastOwner(tx, input.organizationId, input.targetUserId);
    }
    await tx.organizationMembership.update({
      where: { id: membership.id },
      data: { revokedAt: new Date() },
    });
  });
  return { revoked: true as const, userId: input.targetUserId };
}

function assertCanAssignRole(
  actorRole: OrganizationMemberRole,
  newRole: OrganizationMemberRole,
  targetIsOwner: boolean,
): void {
  if (newRole === "OWNER" && actorRole !== "OWNER") {
    throw new Error("only OWNER can grant OWNER role");
  }
  if (targetIsOwner && actorRole !== "OWNER") {
    throw new Error("cannot modify OWNER membership as ADMIN");
  }
}

async function assertNotLastOwner(
  prisma: PrismaClient | Prisma.TransactionClient,
  organizationId: string,
  targetUserId: string,
): Promise<void> {
  const owners = await prisma.organizationMembership.count({
    where: { organizationId, role: "OWNER", revokedAt: null },
  });
  if (owners <= 1) {
    throw new Error(`cannot revoke or demote last OWNER (${targetUserId})`);
  }
}

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