import type { PrismaClient, PrincipalType } from "@prisma/client";
import { scopedFeishuPrincipalId } from "../feishu/identityNamespace.js";

export interface PrincipalRef {
  readonly type: PrincipalType;
  readonly id: string;
}

export interface ActorInput {
  readonly feishuOpenId: string;
  readonly chatId?: string | undefined;
}

export interface PrincipalResolutionScope {
  readonly organizationId: string;
}

export type PrincipalSource =
  | "actor-user"
  | "context-chat"
  | "team-membership"
  | "external-membership"
  | "team-external-binding";

export interface ResolvedPrincipal {
  readonly principal: PrincipalRef;
  readonly source: PrincipalSource;
  readonly via?: PrincipalRef | undefined;
}

export interface PrincipalResolution {
  readonly actor: ActorInput;
  readonly organizationId: string;
  readonly userId?: string | undefined;
  readonly principals: readonly PrincipalRef[];
  readonly resolved: readonly ResolvedPrincipal[];
}

export interface PrincipalResolver {
  resolveActor(actor: ActorInput, scope: PrincipalResolutionScope): Promise<PrincipalResolution>;
}

export class PrismaPrincipalResolver implements PrincipalResolver {
  constructor(
    private readonly prisma: PrismaClient,
    private readonly allowLegacyIdentity = process.env["NODE_ENV"] !== "production",
  ) {}

  async resolveActor(actor: ActorInput, scope: PrincipalResolutionScope): Promise<PrincipalResolution> {
    const resolved = new PrincipalCollector();
    const scopedIdentity = await this.prisma.feishuUserIdentity.findFirst({
      where: {
        openId: actor.feishuOpenId,
        connection: {
          organizationId: scope.organizationId,
          status: "ACTIVE",
          activeSecretVersion: { retiredAt: null },
        },
      },
      select: { userId: true, connectionId: true },
    });
    // Explicit migration adapter for pre-ADR-0024 fixtures/data. Alpha Silo
    // ingress always has an ACTIVE connection-scoped identity.
    const user = scopedIdentity === null && this.allowLegacyIdentity
      ? await this.prisma.user.findUnique({
          where: { feishuOpenId: actor.feishuOpenId },
          select: { id: true },
        })
      : scopedIdentity === null ? null : { id: scopedIdentity.userId };
    const connection = await this.prisma.organizationFeishuApplicationConnection.findUnique({
      where: { organizationId: scope.organizationId },
      select: {
        id: true,
        status: true,
        activeSecretVersion: { select: { connectionId: true, retiredAt: true } },
      },
    });
    const activeConnectionId = scopedIdentity?.connectionId ?? (
      connection?.status === "ACTIVE" && connection.activeSecretVersion !== null &&
      connection.activeSecretVersion.connectionId === connection.id &&
      connection.activeSecretVersion.retiredAt === null
        ? connection.id
        : undefined
    );
    if (activeConnectionId === undefined && !this.allowLegacyIdentity) {
      throw new Error(`active Feishu Application Connection not found for organization ${scope.organizationId}`);
    }
    const userPrincipalId = activeConnectionId === undefined
      ? actor.feishuOpenId
      : scopedFeishuPrincipalId("USER", activeConnectionId, actor.feishuOpenId);
    resolved.add({ type: "USER", id: userPrincipalId }, "actor-user");
    if (actor.chatId !== undefined && actor.chatId !== "") {
      const chatPrincipalId = activeConnectionId === undefined
        ? actor.chatId
        : scopedFeishuPrincipalId("CHAT", activeConnectionId, actor.chatId);
      resolved.add({ type: "FEISHU_CHAT", id: chatPrincipalId }, "context-chat");
    }

    if (user !== null) {
      const memberships = await this.prisma.teamMembership.findMany({
        where: {
          userId: user.id,
          revokedAt: null,
          team: { organizationId: scope.organizationId, archivedAt: null },
        },
        select: { teamId: true },
      });
      for (const membership of memberships) {
        resolved.add({ type: "TEAM", id: membership.teamId }, "team-membership");
      }

      const externalMemberships = await this.prisma.externalPrincipalMembership.findMany({
        where: {
          userId: user.id,
          revokedAt: null,
          connection: {
            organizationId: scope.organizationId,
            revokedAt: null,
          },
        },
        select: { principalType: true, principalId: true },
      });
      for (const membership of externalMemberships) {
        resolved.add(
          { type: membership.principalType, id: membership.principalId },
          "external-membership",
        );
      }
    }

    const externalPrincipals = resolved.principals().filter((principal) => isExternalPrincipal(principal.type));
    if (externalPrincipals.length > 0) {
      const bindings = await this.prisma.teamExternalBinding.findMany({
        where: {
          revokedAt: null,
          team: { organizationId: scope.organizationId, archivedAt: null },
          OR: externalPrincipals.map((principal) => ({
            principalType: principal.type,
            principalId: principal.id,
          })),
        },
        select: { teamId: true, principalType: true, principalId: true },
      });
      for (const binding of bindings) {
        resolved.add(
          { type: "TEAM", id: binding.teamId },
          "team-external-binding",
          { type: binding.principalType, id: binding.principalId },
        );
      }
    }

    return {
      actor,
      organizationId: scope.organizationId,
      ...(user !== null ? { userId: user.id } : {}),
      principals: resolved.principals(),
      resolved: resolved.entries(),
    };
  }
}

export function principalKey(principal: PrincipalRef): string {
  return `${principal.type}:${principal.id}`;
}

function isExternalPrincipal(type: PrincipalType): boolean {
  return type === "FEISHU_CHAT" || type === "FEISHU_DEPARTMENT" || type === "FEISHU_USER_GROUP";
}

class PrincipalCollector {
  private readonly refs = new Map<string, PrincipalRef>();
  private readonly provenance: ResolvedPrincipal[] = [];

  add(principal: PrincipalRef, source: PrincipalSource, via?: PrincipalRef): void {
    const key = principalKey(principal);
    if (!this.refs.has(key)) {
      this.refs.set(key, principal);
    }
    this.provenance.push({
      principal,
      source,
      ...(via !== undefined ? { via } : {}),
    });
  }

  principals(): readonly PrincipalRef[] {
    return [...this.refs.values()];
  }

  entries(): readonly ResolvedPrincipal[] {
    return this.provenance;
  }
}
