import { eq, and } from 'drizzle-orm';
import { db } from '../db';
import { roleCapabilities } from './schema';
import type { UserRole } from '../auth/schema';
import type { CapabilityLevelValue } from './schema';

// Grants, keyed on role. Absence denies — see the table comment.

export type RoleGrant = {
  role: UserRole;
  capability: string;
  level: CapabilityLevelValue;
};

/** Every grant, for the settings UI. Ordered so the UI never has to sort. */
export async function getAllRoleGrants(): Promise<RoleGrant[]> {
  const rows = await db
    .select({
      role: roleCapabilities.role,
      capability: roleCapabilities.capability,
      level: roleCapabilities.level,
    })
    .from(roleCapabilities)
    .orderBy(roleCapabilities.role, roleCapabilities.capability);
  return rows;
}

/**
 * What one role may reach, as capability key → level.
 *
 * A Map rather than an array because every caller is asking "does this role hold X, and at what level",
 * and the hot path is a per-request lookup.
 */
export async function getRoleGrants(role: UserRole): Promise<Map<string, CapabilityLevelValue>> {
  const rows = await db
    .select({ capability: roleCapabilities.capability, level: roleCapabilities.level })
    .from(roleCapabilities)
    .where(eq(roleCapabilities.role, role));
  return new Map(rows.map((r) => [r.capability, r.level]));
}

/** Grant, or change the level of an existing grant. Upsert: the UI has one control, not two. */
export async function setRoleGrant(role: UserRole, capability: string, level: CapabilityLevelValue): Promise<void> {
  await db
    .insert(roleCapabilities)
    .values({ role, capability, level })
    .onConflictDoUpdate({
      target: [roleCapabilities.role, roleCapabilities.capability],
      set: { level, updatedAt: new Date() },
    });
}

/** Revoke. Deleting rather than writing a `none` level is what keeps "absence denies" the only rule. */
export async function revokeRoleGrant(role: UserRole, capability: string): Promise<void> {
  await db
    .delete(roleCapabilities)
    .where(and(eq(roleCapabilities.role, role), eq(roleCapabilities.capability, capability)));
}

/**
 * Replace every grant for one role in a single transaction.
 *
 * The UI saves a whole role at a time, and doing that as N upserts plus M deletes leaves a window where the
 * role holds a mixture of the old and new sets. That window is short and would almost never be observed —
 * which is exactly what would make the resulting bug impossible to reproduce. One transaction, one state.
 */
export async function replaceRoleGrants(
  role: UserRole,
  grants: { capability: string; level: CapabilityLevelValue }[],
): Promise<void> {
  await db.transaction(async (tx) => {
    await tx.delete(roleCapabilities).where(eq(roleCapabilities.role, role));
    if (grants.length > 0) {
      await tx.insert(roleCapabilities).values(grants.map((g) => ({ role, capability: g.capability, level: g.level })));
    }
  });
}
