import { ForbiddenException, Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';

export type ProjectGovernanceRole = 'owner' | 'admin' | 'member' | 'viewer';

export interface ProjectRoleAssertion {
  actorId?: string;
  projectId?: string | null;
  allowedRoles: ProjectGovernanceRole[];
  permissions?: string[];
}

export interface GlobalPermissionAssertion {
  actorId?: string;
  permissions?: string[];
  requiredPermissions: string[];
}

export interface ProjectRoleDecision {
  actorId: string;
  projectId: string;
  role: ProjectGovernanceRole;
  via: 'system' | 'project_member';
}

export interface GlobalPermissionDecision {
  actorId: string;
  via: 'system' | 'permission';
}

const ROLE_RANK: Record<ProjectGovernanceRole, number> = {
  owner: 4,
  admin: 3,
  member: 2,
  viewer: 1,
};

@Injectable()
export class RbacService {
  constructor(private readonly prisma: PrismaService) {}

  async assertGlobalPermission(input: GlobalPermissionAssertion): Promise<GlobalPermissionDecision> {
    const actorId = input.actorId?.trim();
    if (!actorId) {
      throw new ForbiddenException('Missing actor scope');
    }
    if (input.permissions?.includes('*')) {
      return { actorId, via: 'system' };
    }
    if (hasAnyPermission(input.permissions ?? [], input.requiredPermissions)) {
      return { actorId, via: 'permission' };
    }
    throw new ForbiddenException('Insufficient global permission');
  }

  async assertProjectRole(input: ProjectRoleAssertion): Promise<ProjectRoleDecision> {
    const actorId = input.actorId?.trim();
    const projectId = input.projectId?.trim();
    if (!actorId || !projectId) {
      throw new ForbiddenException('Missing actor or project scope');
    }

    if (input.permissions?.includes('*')) {
      return { actorId, projectId, role: 'owner', via: 'system' };
    }

    const membership = await this.prisma.projectMember.findUnique({
      where: { projectId_userId: { projectId, userId: actorId } },
    });
    const role = normalizeProjectRole(membership?.role);
    if (!role || !hasRequiredRole(role, input.allowedRoles)) {
      throw new ForbiddenException('Insufficient project role');
    }

    return { actorId, projectId, role, via: 'project_member' };
  }
}

export function normalizeProjectRole(role: string | null | undefined): ProjectGovernanceRole | null {
  if (role === 'owner' || role === 'admin' || role === 'member' || role === 'viewer') return role;
  return null;
}

export function hasRequiredRole(role: ProjectGovernanceRole, allowedRoles: ProjectGovernanceRole[]): boolean {
  if (allowedRoles.length === 0) return false;
  const minimumRank = Math.min(...allowedRoles.map((allowedRole) => ROLE_RANK[allowedRole]));
  return ROLE_RANK[role] >= minimumRank;
}

export function hasAnyPermission(permissions: string[], requiredPermissions: string[]): boolean {
  if (requiredPermissions.length === 0) return false;
  const granted = new Set(permissions);
  return requiredPermissions.some((permission) => granted.has(permission));
}
