import type { OrganizationStatus, PermissionResourceType, PermissionRole, PrismaClient, PrincipalType } from "@prisma/client";
import { PrismaPrincipalResolver, type ActorInput, type PrincipalRef, type PrincipalResolution, type PrincipalResolver } from "./principals.js";
import { inactiveOrganizationReason } from "../org/status.js";

export type AuthorizationAction =
  | "project.read"
  | "project.edit"
  | "collaborator.manage"
  | "agent.trigger"
  | "agent.cancel"
  | "role.trigger";

export interface AuthorizationResource {
  readonly type: PermissionResourceType;
  readonly id: string;
}

export interface AuthorizationRequest {
  readonly actor: ActorInput;
  readonly action: AuthorizationAction;
  readonly resource: AuthorizationResource;
  readonly roleId?: string | undefined;
}

export interface MatchedPermissionGrant {
  readonly id: string;
  readonly principal: PrincipalRef;
  readonly role: PermissionRole;
}

export interface MatchedRoleTriggerGrant {
  readonly id: string;
  readonly principal: PrincipalRef;
}

export interface AuthorizationDecision {
  readonly allowed: boolean;
  readonly reason: string;
  readonly action: AuthorizationAction;
  readonly resource: AuthorizationResource;
  readonly actor: ActorInput;
  readonly organizationId: string;
  readonly actorUserId?: string | undefined;
  readonly principals: readonly PrincipalRef[];
  readonly requiredRole: PermissionRole;
  readonly effectiveRole?: PermissionRole | undefined;
  readonly matchedGrant?: MatchedPermissionGrant | undefined;
  readonly matchedRoleGrant?: MatchedRoleTriggerGrant | undefined;
}

export interface PermissionAuthorizer {
  can(req: AuthorizationRequest): Promise<AuthorizationDecision>;
}

export class PrismaPermissionAuthorizer implements PermissionAuthorizer {
  constructor(
    private readonly prisma: PrismaClient,
    private readonly resolver: PrincipalResolver = new PrismaPrincipalResolver(prisma),
  ) {}

  async can(req: AuthorizationRequest): Promise<AuthorizationDecision> {
    if (req.action === "role.trigger" && (req.roleId === undefined || req.roleId === "")) {
      throw new Error("role.trigger authorization requires roleId");
    }
    const organization = await this.organizationForResource(req.resource);
    const inactiveReason = inactiveOrganizationReason(organization.id, organization.status);
    if (inactiveReason !== undefined) {
      return this.decision(req, inactivePrincipalResolution(req.actor, organization.id), {
        allowed: false,
        reason: inactiveReason,
        requiredRole: defaultRequiredRole(req.action),
      });
    }
    const resolution = await this.resolver.resolveActor(req.actor, { organizationId: organization.id });
    const threshold = await this.requiredRole(req);
    if (threshold === "DISABLED") {
      return this.decision(req, resolution, {
        allowed: false,
        reason: "permission setting disables this action",
        requiredRole: defaultRequiredRole(req.action),
      });
    }

    const grant = await this.bestGrant(req.resource, resolution.principals, threshold);
    if (grant === undefined) {
      return this.decision(req, resolution, {
        allowed: false,
        reason: `no active ${threshold}+ grant for resolved principals`,
        requiredRole: threshold,
      });
    }

    if (req.action !== "role.trigger") {
      return this.decision(req, resolution, {
        allowed: true,
        reason: `granted by ${grant.principal.type}:${grant.principal.id} ${grant.role}`,
        requiredRole: threshold,
        effectiveRole: grant.role,
        matchedGrant: grant,
      });
    }

    const roleGate = await this.roleTriggerGrant(req.resource, req.roleId!, resolution.principals);
    if (roleGate.status === "open") {
      return this.decision(req, resolution, {
        allowed: true,
        reason: `role ${req.roleId} unconfigured on project (open)`,
        requiredRole: threshold,
        effectiveRole: grant.role,
        matchedGrant: grant,
      });
    }
    if (roleGate.grant !== undefined) {
      return this.decision(req, resolution, {
        allowed: true,
        reason: `role ${req.roleId} granted by ${roleGate.grant.principal.type}:${roleGate.grant.principal.id}`,
        requiredRole: threshold,
        effectiveRole: grant.role,
        matchedGrant: grant,
        matchedRoleGrant: roleGate.grant,
      });
    }

    return this.decision(req, resolution, {
      allowed: false,
      reason: `no active role ${req.roleId} grant for resolved principals`,
      requiredRole: threshold,
      effectiveRole: grant.role,
      matchedGrant: grant,
    });
  }

  private async requiredRole(req: AuthorizationRequest): Promise<PermissionRole | "DISABLED"> {
    let required = defaultRequiredRole(req.action);
    // ADR-0019 + PermissionGrant spec: agentTrigger and agentCancel are
    // distinct policy knobs ("谁能触发" ≠ "谁能取消"). Each can tighten the
    // default role to MANAGE_ONLY or DISABLE the action entirely. The default
    // role (trigger=EDIT, cancel=MANAGE) is the floor when no policy is set.
    if (req.resource.type === "PROJECT" && (req.action === "agent.trigger" || req.action === "role.trigger")) {
      const settings = await this.prisma.permissionSettings.findFirst({
        where: { resourceType: req.resource.type, resourceId: req.resource.id },
        select: { agentTrigger: true },
      });
      const policy = normalizePolicy(settings?.agentTrigger);
      if (policy === "DISABLED") return "DISABLED";
      if (policy === "MANAGE_ONLY") required = "MANAGE";
    } else if (req.resource.type === "PROJECT" && req.action === "agent.cancel") {
      const settings = await this.prisma.permissionSettings.findFirst({
        where: { resourceType: req.resource.type, resourceId: req.resource.id },
        select: { agentCancel: true },
      });
      const policy = normalizePolicy(settings?.agentCancel);
      if (policy === "DISABLED") return "DISABLED";
      if (policy === "MANAGE_ONLY") required = "MANAGE";
    }
    return required;
  }

  private async bestGrant(
    resource: AuthorizationResource,
    principals: readonly PrincipalRef[],
    threshold: PermissionRole,
  ): Promise<MatchedPermissionGrant | undefined> {
    const grants = await this.prisma.permissionGrant.findMany({
      where: {
        resourceType: resource.type,
        resourceId: resource.id,
        revokedAt: null,
        OR: principalWhere(principals),
      },
      select: { id: true, principalType: true, principalId: true, role: true },
    });
    const sufficient = grants
      .filter((grant) => roleRank(grant.role) >= roleRank(threshold))
      .sort((a, b) => roleRank(b.role) - roleRank(a.role))[0];
    if (sufficient === undefined) return undefined;
    return {
      id: sufficient.id,
      principal: { type: sufficient.principalType, id: sufficient.principalId },
      role: sufficient.role,
    };
  }

  private async roleTriggerGrant(
    resource: AuthorizationResource,
    roleId: string,
    principals: readonly PrincipalRef[],
  ): Promise<{ readonly status: "open" } | { readonly status: "configured"; readonly grant?: MatchedRoleTriggerGrant | undefined }> {
    if (resource.type !== "PROJECT") {
      return { status: "configured" };
    }
    const anyGrant = await this.prisma.roleTriggerGrant.findFirst({
      where: { projectId: resource.id, roleId },
      select: { id: true },
    });
    if (anyGrant === null) return { status: "open" };

    const grant = await this.prisma.roleTriggerGrant.findFirst({
      where: {
        projectId: resource.id,
        roleId,
        revokedAt: null,
        OR: principalWhere(principals),
      },
      select: { id: true, principalType: true, principalId: true },
    });
    if (grant === null) return { status: "configured" };
    return {
      status: "configured",
      grant: {
        id: grant.id,
        principal: { type: grant.principalType, id: grant.principalId },
      },
    };
  }

  private decision(
    req: AuthorizationRequest,
    resolution: PrincipalResolution,
    result: Omit<AuthorizationDecision, "action" | "resource" | "actor" | "organizationId" | "actorUserId" | "principals">,
  ): AuthorizationDecision {
    return {
      action: req.action,
      resource: req.resource,
      actor: req.actor,
      organizationId: resolution.organizationId,
      ...(resolution.userId !== undefined ? { actorUserId: resolution.userId } : {}),
      principals: resolution.principals,
      ...result,
    };
  }

  private async organizationForResource(
    resource: AuthorizationResource,
  ): Promise<{ readonly id: string; readonly status: OrganizationStatus }> {
    switch (resource.type) {
      case "PROJECT": {
        const project = await this.prisma.project.findUnique({
          where: { id: resource.id },
          select: { organization: { select: { id: true, status: true } } },
        });
        if (project === null) {
          throw new Error(`authorization resource missing: PROJECT ${resource.id}`);
        }
        return project.organization;
      }
      case "PROJECT_GROUP": {
        const binding = await this.prisma.projectGroupBinding.findFirst({
          where: { chatId: resource.id, archivedAt: null },
          select: { project: { select: { organization: { select: { id: true, status: true } } } } },
        });
        if (binding === null) {
          throw new Error(`authorization resource missing: PROJECT_GROUP ${resource.id}`);
        }
        return binding.project.organization;
      }
      case "ARTIFACT":
        throw new Error("authorization for ARTIFACT resources requires an organization resolver");
    }
  }
}

export function createPermissionAuthorizer(
  prisma: PrismaClient,
  resolver?: PrincipalResolver,
): PermissionAuthorizer {
  return new PrismaPermissionAuthorizer(prisma, resolver);
}

export function roleGrantsEdit(role: PermissionRole): boolean {
  return roleRank(role) >= roleRank("EDIT");
}

export function roleRank(role: PermissionRole): number {
  switch (role) {
    case "READ": return 1;
    case "EDIT": return 2;
    case "MANAGE": return 3;
  }
}

function defaultRequiredRole(action: AuthorizationAction): PermissionRole {
  switch (action) {
    case "project.read": return "READ";
    case "project.edit": return "EDIT";
    case "agent.trigger": return "EDIT";
    case "role.trigger": return "EDIT";
    case "collaborator.manage": return "MANAGE";
    case "agent.cancel": return "MANAGE";
  }
}

function normalizePolicy(policy: string | undefined): "ROLE" | "MANAGE_ONLY" | "DISABLED" {
  if (policy === undefined || policy.trim() === "") return "ROLE";
  const normalized = policy.trim().toUpperCase();
  if (normalized === "DISABLED" || normalized === "DENY" || normalized === "OFF") return "DISABLED";
  if (normalized === "MANAGE_ONLY") return "MANAGE_ONLY";
  return "ROLE";
}

function principalWhere(principals: readonly PrincipalRef[]): Array<{ principalType: PrincipalType; principalId: string }> {
  return principals.map((principal) => ({
    principalType: principal.type,
    principalId: principal.id,
  }));
}

function inactivePrincipalResolution(actor: ActorInput, organizationId: string): PrincipalResolution {
  const principals: PrincipalRef[] = [{ type: "USER", id: actor.feishuOpenId }];
  if (actor.chatId !== undefined && actor.chatId !== "") {
    principals.push({ type: "FEISHU_CHAT", id: actor.chatId });
  }
  return {
    actor,
    organizationId,
    principals,
    resolved: [],
  };
}
