import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { GenericQueryCtx } from "convex/server";
import { DataModel, Id, Doc } from "./_generated/dataModel";

const PERMISSION_KEYS = [
  "manage_channels",
  "manage_roles",
  "manage_messages",
  "create_invite",
  "embed_links",
  "attach_files",
  "move_members",
  "mute_members",
  "manage_nicknames",
] as const;

export async function getRolesForUser(
  ctx: GenericQueryCtx<DataModel>,
  userId: Id<"userProfiles">
): Promise<Doc<"roles">[]> {
  const assignments = await ctx.db
    .query("userRoles")
    .withIndex("by_user", (q) => q.eq("userId", userId))
    .collect();

  const roles = await Promise.all(
    assignments.map((ur) => ctx.db.get(ur.roleId))
  );
  return roles.filter((r): r is Doc<"roles"> => r !== null);
}

// List all roles
export const list = query({
  args: {},
  returns: v.array(v.any()),
  handler: async (ctx) => {
    const roles = await ctx.db.query("roles").collect();
    return roles.sort((a, b) => (b.position || 0) - (a.position || 0));
  },
});

// Create new role
export const create = mutation({
  args: {
    name: v.optional(v.string()),
    color: v.optional(v.string()),
    permissions: v.optional(v.any()),
    position: v.optional(v.number()),
    isHoist: v.optional(v.boolean()),
  },
  returns: v.any(),
  handler: async (ctx, args) => {
    const id = await ctx.db.insert("roles", {
      name: args.name || "new role",
      color: args.color || "#99aab5",
      position: args.position || 0,
      permissions: args.permissions || {},
      isHoist: args.isHoist || false,
    });

    return await ctx.db.get(id);
  },
});

// Update role properties
export const update = mutation({
  args: {
    id: v.id("roles"),
    name: v.optional(v.string()),
    color: v.optional(v.string()),
    permissions: v.optional(v.any()),
    position: v.optional(v.number()),
    isHoist: v.optional(v.boolean()),
  },
  returns: v.any(),
  handler: async (ctx, args) => {
    const role = await ctx.db.get(args.id);
    if (!role) throw new Error("Role not found");
    // Owner is frozen — we can't let a client rename/recolour it
    // or strip its permissions, since that would defeat the
    // assign/unassign guards in one shot.
    if (role.name === "Owner") {
      throw new Error("The Owner role can't be edited.");
    }

    const { id, ...fields } = args;
    const updates: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(fields)) {
      if (value !== undefined) updates[key] = value;
    }

    if (Object.keys(updates).length > 0) {
      await ctx.db.patch(id, updates);
    }

    return await ctx.db.get(id);
  },
});

/**
 * Batch-reorder roles by passing a list of `{id, position}` pairs.
 * Used by the Roles & Permissions settings surface after a drag-
 * drop drop. Owner and @everyone are refused so their positions
 * stay pinned at the natural extremes of the list.
 */
export const reorder = mutation({
  args: {
    updates: v.array(
      v.object({
        id: v.id("roles"),
        position: v.number(),
      }),
    ),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    for (const u of args.updates) {
      const role = await ctx.db.get(u.id);
      if (!role) continue;
      if (role.name === "Owner" || role.name === "@everyone") continue;
      await ctx.db.patch(u.id, { position: u.position });
    }
    return null;
  },
});

// Delete role
export const remove = mutation({
  args: { id: v.id("roles") },
  returns: v.object({ success: v.boolean() }),
  handler: async (ctx, args) => {
    const role = await ctx.db.get(args.id);
    if (!role) throw new Error("Role not found");
    if (role.name === "Owner") {
      throw new Error("The Owner role can't be deleted.");
    }
    if (role.name === "@everyone") {
      throw new Error("The @everyone role can't be deleted.");
    }

    const assignments = await ctx.db
      .query("userRoles")
      .withIndex("by_role", (q) => q.eq("roleId", args.id))
      .collect();
    for (const a of assignments) {
      await ctx.db.delete(a._id);
    }

    await ctx.db.delete(args.id);
    return { success: true };
  },
});

// List members with roles
export const listMembers = query({
  args: {},
  returns: v.array(v.any()),
  handler: async (ctx) => {
    const users = await ctx.db.query("userProfiles").collect();

    return await Promise.all(
      users.map(async (user) => ({
        id: user._id,
        username: user.username,
        public_identity_key: user.publicIdentityKey,
        roles: await getRolesForUser(ctx, user._id),
      }))
    );
  },
});

// Assign role to user
export const assign = mutation({
  args: {
    roleId: v.id("roles"),
    userId: v.id("userProfiles"),
  },
  returns: v.object({ success: v.boolean() }),
  handler: async (ctx, args) => {
    // Owner is immutable — it's granted once during first-user
    // bootstrap (convex/auth.ts) and the app never reassigns it. The
    // UI already hides it from the ManageRoles checkbox list, but we
    // also reject it server-side so a crafted client can't sneak it
    // onto another user.
    const role = await ctx.db.get(args.roleId);
    if (role?.name === "Owner") {
      throw new Error("The Owner role can't be assigned.");
    }

    const existing = await ctx.db
      .query("userRoles")
      .withIndex("by_user_and_role", (q) =>
        q.eq("userId", args.userId).eq("roleId", args.roleId)
      )
      .unique();

    if (!existing) {
      await ctx.db.insert("userRoles", {
        userId: args.userId,
        roleId: args.roleId,
      });
    }

    return { success: true };
  },
});

// Remove role from user
export const unassign = mutation({
  args: {
    roleId: v.id("roles"),
    userId: v.id("userProfiles"),
  },
  returns: v.object({ success: v.boolean() }),
  handler: async (ctx, args) => {
    // Owner is immutable — see `assign` above. Removing it would
    // leave the server without a permission-bearing admin.
    const role = await ctx.db.get(args.roleId);
    if (role?.name === "Owner") {
      throw new Error("The Owner role can't be removed.");
    }

    const existing = await ctx.db
      .query("userRoles")
      .withIndex("by_user_and_role", (q) =>
        q.eq("userId", args.userId).eq("roleId", args.roleId)
      )
      .unique();

    if (existing) {
      await ctx.db.delete(existing._id);
    }

    return { success: true };
  },
});

// Get current user's aggregated permissions
export const getMyPermissions = query({
  args: { userId: v.id("userProfiles") },
  returns: v.object({
    manage_channels: v.boolean(),
    manage_roles: v.boolean(),
    manage_messages: v.boolean(),
    create_invite: v.boolean(),
    embed_links: v.boolean(),
    attach_files: v.boolean(),
    move_members: v.boolean(),
    mute_members: v.boolean(),
    manage_nicknames: v.boolean(),
  }),
  handler: async (ctx, args) => {
    const roles = await getRolesForUser(ctx, args.userId);

    const finalPerms: Record<string, boolean> = {};
    for (const key of PERMISSION_KEYS) {
      finalPerms[key] = roles.some(
        (role) => (role.permissions as Record<string, boolean>)?.[key]
      );
    }

    return finalPerms as {
      manage_channels: boolean;
      manage_roles: boolean;
      manage_messages: boolean;
      create_invite: boolean;
      embed_links: boolean;
      attach_files: boolean;
      move_members: boolean;
      mute_members: boolean;
      manage_nicknames: boolean;
    };
  },
});
