import { Injectable } from '@nestjs/common';
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
import type { MongoAbility } from '@casl/ability';

import { PrismaService } from '@/prisma/prisma.service';

/**
 * The application ability. Action is any string (see {@link Action}); subject
 * is a string such as 'Role', 'Permission' or the wildcard 'all'. We use string
 * subjects because the data layer is Prisma (no entity classes to detect).
 */
export type AppAbility = MongoAbility<[string, string]>;

/**
 * A user's resolved access: the names of the roles they hold and the flattened,
 * deduped list of permission names granted via those roles and any direct grants.
 */
export interface UserGrants {
  roles: string[];
  permissions: string[];
}

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

  /**
   * Resolve a user's role names and effective (flattened) permission names from
   * the database, skipping anything soft-deleted. This is the single source of
   * truth used both to build the ability (below) and to expose the caller's
   * access via the profile endpoint.
   */
  async getGrantsForUser(userId: number): Promise<UserGrants> {
    const user = await this.prisma.user.findFirst({
      where: { id: userId, deletedAt: null },
      select: {
        roles: {
          select: {
            role: {
              select: {
                name: true,
                deletedAt: true,
                permissions: {
                  select: {
                    permission: { select: { name: true, deletedAt: true } },
                  },
                },
              },
            },
          },
        },
        permissions: {
          select: {
            permission: { select: { name: true, deletedAt: true } },
          },
        },
      },
    });

    if (!user) {
      return { roles: [], permissions: [] };
    }

    const roles: string[] = [];
    const permissions = new Set<string>();

    // Permissions granted through the user's roles.
    for (const userRole of user.roles) {
      if (userRole.role.deletedAt) continue;
      roles.push(userRole.role.name);
      for (const rolePermission of userRole.role.permissions) {
        if (rolePermission.permission.deletedAt) continue;
        permissions.add(rolePermission.permission.name);
      }
    }

    // Permissions granted directly to the user.
    for (const userPermission of user.permissions) {
      if (userPermission.permission.deletedAt) continue;
      permissions.add(userPermission.permission.name);
    }

    return { roles, permissions: [...permissions] };
  }

  /**
   * Build the CASL ability for a user. Each permission name is parsed as
   * "<action> <subject>" (e.g. "read Role", "manage all"); a single-token name
   * applies to the 'all' subject.
   */
  async createForUser(userId: number): Promise<AppAbility> {
    const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);

    const { permissions } = await this.getGrantsForUser(userId);

    for (const name of permissions) {
      const [action, ...subjectParts] = name.trim().split(/\s+/);
      if (!action) continue;
      can(action, subjectParts.join(' ') || 'all');
    }

    return build();
  }
}
