// src/lib/admin/staff-management/core.ts — mutations du plan SYS (spec §7).
// Fonctions DB injectables (testables sans session) ; les server actions de
// staff-actions.ts les enveloppent du gate staff:manage + de l'audit.
import "server-only";
import { and, eq, notExists, sql } from "drizzle-orm";
import { isGrantableSysPermission } from "@/lib/auth/sys-permissions";
import { staffPermission } from "@/lib/db/admin-schema";
import { member, session, user } from "@/lib/db/auth-schema";
import type { db as DbClient } from "@/lib/db/client";

type Db = typeof DbClient;

export interface StaffRow {
  id: string;
  name: string;
  email: string;
  twoFactorEnabled: boolean;
  permissions: string[];
}

export async function listStaff(db: Db): Promise<StaffRow[]> {
  const users = await db
    .select({
      id: user.id,
      name: user.name,
      email: user.email,
      twoFactorEnabled: user.twoFactorEnabled,
    })
    .from(user)
    .where(eq(user.role, "staff"));
  const grants = await db.select().from(staffPermission);
  const byUser = new Map<string, string[]>();
  for (const g of grants) {
    const list = byUser.get(g.userId) ?? [];
    list.push(g.permission);
    byUser.set(g.userId, list);
  }
  return users.map((u) => ({
    ...u,
    twoFactorEnabled: Boolean(u.twoFactorEnabled),
    permissions: (byUser.get(u.id) ?? []).sort(),
  }));
}

export async function promoteToStaff(
  db: Db,
  _actorId: string,
  email: string,
): Promise<{ ok: true; userId: string } | { ok: false; error: string }> {
  const [u] = await db
    .select({ id: user.id, role: user.role, emailVerified: user.emailVerified })
    .from(user)
    .where(eq(sql`lower(${user.email})`, email.trim().toLowerCase()))
    .limit(1);
  if (!u) return { ok: false, error: "Aucun utilisateur avec cet email" };
  if (u.role === "staff") return { ok: false, error: "Déjà staff" };
  if (u.role === "admin")
    return { ok: false, error: "Compte admin système — non promouvable" };
  // Compte vérifié requis avant promotion (spec §7).
  if (!u.emailVerified)
    return {
      ok: false,
      error: "Compte non vérifié — l'email doit être vérifié avant promotion",
    };
  // Population SYS orgless par construction (spec §2.bis). Pre-check en
  // fast-path (message précis) ; l'UPDATE ci-dessous reste la garde
  // réellement atomique contre le TOCTOU (membership créée entre ce SELECT
  // et l'UPDATE).
  const [m] = await db
    .select({ id: member.id })
    .from(member)
    .where(eq(member.userId, u.id))
    .limit(1);
  if (m)
    return {
      ok: false,
      error:
        "Ce compte appartient à une organisation — les opérateurs SYS sont orgless",
    };
  const updated = await db
    .update(user)
    .set({ role: "staff" })
    .where(
      and(
        eq(user.id, u.id),
        notExists(
          db
            .select({ one: sql`1` })
            .from(member)
            .where(eq(member.userId, u.id)),
        ),
      ),
    )
    .returning({ id: user.id });
  if (updated.length === 0)
    return {
      ok: false,
      error:
        "Ce compte appartient à une organisation — les opérateurs SYS sont orgless",
    };
  return { ok: true, userId: u.id };
}

export async function setStaffPermissions(
  db: Db,
  actorId: string,
  userId: string,
  perms: string[],
): Promise<{ ok: true } | { ok: false; error: string }> {
  const invalid = perms.filter((p) => !isGrantableSysPermission(p));
  if (invalid.length)
    return {
      ok: false,
      error: `Permissions non assignables: ${invalid.join(", ")}`,
    };
  const [u] = await db
    .select({ role: user.role })
    .from(user)
    .where(eq(user.id, userId))
    .limit(1);
  if (u?.role !== "staff")
    return { ok: false, error: "La cible n'est pas un staff" };
  await db.transaction(async (tx) => {
    await tx.delete(staffPermission).where(eq(staffPermission.userId, userId));
    if (perms.length) {
      await tx.insert(staffPermission).values(
        [...new Set(perms)].map((permission) => ({
          userId,
          permission,
          grantedBy: actorId,
        })),
      );
    }
  });
  return { ok: true };
}

export async function removeStaff(
  db: Db,
  _actorId: string,
  userId: string,
): Promise<{ ok: true } | { ok: false; error: string }> {
  const [u] = await db
    .select({ role: user.role })
    .from(user)
    .where(eq(user.id, userId))
    .limit(1);
  if (u?.role !== "staff")
    return { ok: false, error: "La cible n'est pas un staff" };
  // Transactionnel (spec §7) : pas d'état fantôme — rôle, grants et sessions
  // retombent ensemble.
  await db.transaction(async (tx) => {
    await tx.update(user).set({ role: "user" }).where(eq(user.id, userId));
    await tx.delete(staffPermission).where(eq(staffPermission.userId, userId));
    await tx.delete(session).where(eq(session.userId, userId));
  });
  return { ok: true };
}
