import type { PrismaClient } from '@prisma/client';
import type { IRbacDefinitionRepository } from '../repositories/rbac-definition.repository.js';
import {
  normalizeResource,
  isResourceBinding,
  isOperationBinding,
  type RbacSubject,
  type RbacRoleBinding,
} from '../validation/rbac-definition.schema.js';

export type RbacAction = 'view' | 'create' | 'delete' | 'edit' | 'run' | 'expose';

export interface ResourcePermission {
  role: string;
  resource: string;
  name?: string;
}

export interface OperationPermission {
  role: 'run';
  action: string;
}

export type Permission = ResourcePermission | OperationPermission;

export interface AllowedScope {
  wildcard: boolean;
  names: Set<string>;
}

/** Maps roles to the set of actions they grant. */
const ROLE_ACTIONS: Record<string, readonly RbacAction[]> = {
  edit: ['view', 'create', 'delete', 'edit', 'expose'],
  view: ['view'],
  create: ['create'],
  delete: ['delete'],
  run: ['run'],
  expose: ['expose', 'view'],
};

export class RbacService {
  constructor(
    private readonly rbacRepo: IRbacDefinitionRepository,
    private readonly prisma: PrismaClient,
  ) {}

  /**
   * Check whether a user is allowed to perform an action on a resource.
   * @param resourceName — optional specific resource name (e.g. 'my-ha').
   *   If provided, name-scoped bindings only match when their name equals this.
   *   If omitted (listing), name-scoped bindings still grant access.
   */
  async canAccess(userId: string, action: RbacAction, resource: string, resourceName?: string, serviceAccountName?: string): Promise<boolean> {
    const permissions = await this.getPermissions(userId, serviceAccountName);
    const normalized = normalizeResource(resource);

    for (const perm of permissions) {
      if (!('resource' in perm)) continue;
      const actions = ROLE_ACTIONS[perm.role];
      if (actions === undefined) continue;
      if (!actions.includes(action)) continue;
      const permResource = normalizeResource(perm.resource);
      if (permResource !== '*' && permResource !== normalized) continue;
      // Name-scoped check: if binding has a name AND caller specified a resourceName, must match
      if (perm.name !== undefined && resourceName !== undefined && perm.name !== resourceName) continue;
      return true;
    }

    return false;
  }

  /**
   * Check whether a user is allowed to perform a named operation.
   * Operations require an explicit 'run' role binding with a matching action.
   */
  async canRunOperation(userId: string, operation: string, serviceAccountName?: string): Promise<boolean> {
    const permissions = await this.getPermissions(userId, serviceAccountName);

    for (const perm of permissions) {
      if ('action' in perm && perm.role === 'run' && perm.action === operation) {
        return true;
      }
    }

    return false;
  }

  /**
   * Determine the set of resource names a user may access for a given action+resource.
   * Returns wildcard:true if any matching binding is unscoped (no name constraint).
   * Returns wildcard:false with a set of allowed names if all bindings are name-scoped.
   */
  async getAllowedScope(userId: string, action: RbacAction, resource: string, serviceAccountName?: string): Promise<AllowedScope> {
    const permissions = await this.getPermissions(userId, serviceAccountName);
    const normalized = normalizeResource(resource);
    const names = new Set<string>();

    for (const perm of permissions) {
      if (!('resource' in perm)) continue;
      const actions = ROLE_ACTIONS[perm.role];
      if (actions === undefined) continue;
      if (!actions.includes(action)) continue;
      const permResource = normalizeResource(perm.resource);
      if (permResource !== '*' && permResource !== normalized) continue;
      // Unscoped binding → wildcard access to this resource
      if (perm.name === undefined) return { wildcard: true, names: new Set() };
      names.add(perm.name);
    }

    return { wildcard: false, names };
  }

  /**
   * Collect all permissions for a user across all matching RbacDefinitions.
   */
  async getPermissions(userId: string, serviceAccountName?: string): Promise<Permission[]> {
    // 1. Resolve user email
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { email: true },
    });
    if (user === null && serviceAccountName === undefined) return [];

    // 2. Resolve group names the user belongs to
    let groupNames: string[] = [];
    if (user !== null) {
      const memberships = await this.prisma.groupMember.findMany({
        where: { userId },
        select: { group: { select: { name: true } } },
      });
      groupNames = memberships.map((m) => m.group.name);
    }

    // 3. Load all RbacDefinitions
    const definitions = await this.rbacRepo.findAll();

    // 4. Find definitions where user or service account is a subject
    const permissions: Permission[] = [];
    for (const def of definitions) {
      const subjects = def.subjects as RbacSubject[];
      const matched = subjects.some((s) => {
        if (s.kind === 'User') return user !== null && s.name === user.email;
        if (s.kind === 'Group') return groupNames.includes(s.name);
        if (s.kind === 'ServiceAccount') return serviceAccountName !== undefined && s.name === serviceAccountName;
        return false;
      });

      if (!matched) continue;

      // 5. Collect roleBindings
      const bindings = def.roleBindings as RbacRoleBinding[];
      for (const binding of bindings) {
        if (isResourceBinding(binding)) {
          const perm: ResourcePermission = { role: binding.role, resource: binding.resource };
          if (binding.name !== undefined) perm.name = binding.name;
          permissions.push(perm);
        } else if (isOperationBinding(binding)) {
          permissions.push({ role: 'run', action: binding.action });
        }
      }
    }

    return permissions;
  }
}
