import { ConvexError, type Value } from "convex/values";
import { getAuthUserId, getAuthSessionId } from "@convex-dev/auth/server";
import type { Doc, Id } from "../_generated/dataModel";
import type { MutationCtx, QueryCtx } from "../_generated/server";

/**
 * Zero-trust access control at the function boundary. Every query returning
 * private data and every mutation calls one of these helpers; the sidebar
 * and buttons only mirror the result.
 */

export type Ctx = QueryCtx | MutationCtx;

export interface Actor {
  id: Id<"users">;
  user: Doc<"users">;
  role: Doc<"roles">;
  roleKey: string;
  fullName: string;
  isAdmin: boolean;
  perms: Set<string>;
  sessionId: Id<"authSessions"> | null;
}

export class AppError extends ConvexError<{ code: string; message: string; fields?: Record<string, string>; conflicts?: Value }> {
  constructor(message: string, code: "AUTH" | "PERMISSION" | "CONFLICT" | "VALIDATION" | "NOT_FOUND" | "UNKNOWN" = "UNKNOWN", extra: { fields?: Record<string, string>; conflicts?: Value } = {}) {
    super({ code, message, ...extra });
  }
}

/**
 * Mutation-side session invalidation (the Convex Auth helper of the same
 * name only runs inside actions). Deletes the auth session rows and their
 * refresh tokens; JWTs already issued expire on their own within the hour.
 */
export async function revokeUserSessions(ctx: MutationCtx, userId: Id<"users">, except: Id<"authSessions">[] = []): Promise<number> {
  const sessions = await ctx.db.query("authSessions").withIndex("userId", (q) => q.eq("userId", userId)).collect();
  let count = 0;
  for (const s of sessions) {
    if (except.includes(s._id)) continue;
    for (const t of await ctx.db.query("authRefreshTokens").withIndex("sessionId", (q) => q.eq("sessionId", s._id)).collect()) await ctx.db.delete(t._id);
    await ctx.db.delete(s._id);
    const meta = await ctx.db.query("sessionMeta").withIndex("by_session", (q) => q.eq("sessionId", s._id)).unique();
    if (meta && !meta.revokedAt) await ctx.db.patch(meta._id, { revokedAt: Date.now() });
    count++;
  }
  return count;
}

export function effectivePermissions(user: Doc<"users">, role: Doc<"roles">): Set<string> {
  if (role.key === "ADMIN") return new Set(["*"]);
  const set = new Set(role.permissions);
  for (const o of user.permissionOverrides ?? []) {
    if (o.granted) set.add(o.key);
    else set.delete(o.key);
  }
  return set;
}

/** Resolve the signed-in worker, or null when signed out / disabled. */
export async function currentActor(ctx: Ctx): Promise<Actor | null> {
  const userId = await getAuthUserId(ctx);
  if (!userId) return null;
  const user = await ctx.db.get(userId);
  if (!user || user.deletedAt || (user.status && user.status !== "ACTIVE") || !user.roleId) return null;
  const role = await ctx.db.get(user.roleId);
  if (!role) return null;
  const sessionId = await getAuthSessionId(ctx);
  return { id: user._id, user, role, roleKey: role.key, fullName: user.fullName ?? user.name ?? user.email ?? "Worker", isAdmin: role.key === "ADMIN", perms: effectivePermissions(user, role), sessionId };
}

export async function requireActor(ctx: Ctx): Promise<Actor> {
  const a = await currentActor(ctx);
  if (!a) throw new AppError("Your session has expired. Please sign in again.", "AUTH");
  return a;
}

export const can = (a: Actor | null, key: string) => !!a && (a.isAdmin || a.perms.has(key));
export const canAny = (a: Actor | null, ...keys: string[]) => keys.some((k) => can(a, k));

/** Any-of semantics: the actor needs at least one of the listed permissions. */
export async function assertPermission(ctx: Ctx, ...keys: string[]): Promise<Actor> {
  const a = await requireActor(ctx);
  if (!canAny(a, ...keys)) throw new AppError("You don't have permission to do this.", "PERMISSION");
  return a;
}

/** Lightweight actor shape for helpers that only need identity fields (audit, ledger). */
export const actorLite = (a: Actor | null) => (a ? { id: a.id, fullName: a.fullName, roleKey: a.roleKey, sessionId: a.sessionId } : null);
export type ActorLite = ReturnType<typeof actorLite>;
