import "server-only"

import { cache } from "react"
import { createId } from "@paralleldrive/cuid2"
import { count, eq, inArray } from "drizzle-orm"
import { db } from "@/shared/db"
import { rolePermissions, roles, usersToRoles } from "@/shared/db/schema"
import { isPermission } from "@/shared/lib/type-guards"
import type { Permission } from "@/shared/types/permissions"

import type {
  CreateRoleInput,
  RoleDetail,
  RoleRecord,
  RoleWithStats,
  UpdateRoleInput,
} from "./types"

/** The `admin` role name. This role is fully locked: no edits, no disable, no delete. */
export const ADMIN_ROLE_NAME = "admin"

function toRoleRecord(row: typeof roles.$inferSelect): RoleRecord {
  return {
    id: row.id,
    name: row.name,
    description: row.description,
    isSystem: row.isSystem,
    isEnabled: row.isEnabled,
    createdAt: row.createdAt,
    updatedAt: row.updatedAt,
  }
}

/**
 * List all roles with permission and user counts.
 * Results are ordered: system roles first (alphabetical), then custom roles (newest first).
 */
export const getRoles = cache(async (): Promise<RoleWithStats[]> => {
  const roleRows = await db.select().from(roles).orderBy(roles.name)

  if (roleRows.length === 0) return []

  const roleIds = roleRows.map((r) => r.id)

  const [permCounts, userCounts] = await Promise.all([
    db
      .select({ roleId: rolePermissions.roleId, count: count() })
      .from(rolePermissions)
      .where(inArray(rolePermissions.roleId, roleIds))
      .groupBy(rolePermissions.roleId),
    db
      .select({ roleId: usersToRoles.roleId, count: count() })
      .from(usersToRoles)
      .where(inArray(usersToRoles.roleId, roleIds))
      .groupBy(usersToRoles.roleId),
  ])

  const permMap = new Map(permCounts.map((r) => [r.roleId, Number(r.count)]))
  const userMap = new Map(userCounts.map((r) => [r.roleId, Number(r.count)]))

  return roleRows.map((row) => ({
    ...toRoleRecord(row),
    permissionCount: permMap.get(row.id) ?? 0,
    userCount: userMap.get(row.id) ?? 0,
  }))
})

/**
 * Get a single role by id, including its full permission list and user count.
 */
export const getRoleById = cache(async (id: string): Promise<RoleDetail | null> => {
  const roleRow = await db.query.roles.findFirst({ where: eq(roles.id, id) })
  if (!roleRow) return null

  const [permRows, userCountRows] = await Promise.all([
    db
      .select({ permission: rolePermissions.permission })
      .from(rolePermissions)
      .where(eq(rolePermissions.roleId, id)),
    db
      .select({ count: count() })
      .from(usersToRoles)
      .where(eq(usersToRoles.roleId, id)),
  ])

  return {
    ...toRoleRecord(roleRow),
    permissions: permRows
      .map((p) => (isPermission(p.permission) ? p.permission : null))
      .filter((p): p is Permission => p !== null),
    userCount: userCountRows[0] ? Number(userCountRows[0].count) : 0,
  }
})

/**
 * Get a single role by name.
 */
export const getRoleByName = cache(async (name: string): Promise<RoleRecord | null> => {
  const row = await db.query.roles.findFirst({ where: eq(roles.name, name) })
  return row ? toRoleRecord(row) : null
})

/**
 * Create a new custom role.
 * Throws if a role with the same name already exists.
 */
export async function createRole(input: CreateRoleInput): Promise<RoleRecord> {
  const existing = await db.query.roles.findFirst({ where: eq(roles.name, input.name) })
  if (existing) {
    throw new Error(`Role "${input.name}" already exists`)
  }

  const id = createId()
  await db.insert(roles).values({
    id,
    name: input.name,
    description: input.description ?? null,
    isSystem: false,
    isEnabled: true,
  })

  const row = await db.query.roles.findFirst({ where: eq(roles.id, id) })
  if (!row) throw new Error("Failed to create role")
  return toRoleRecord(row)
}

/**
 * Update a role's name and/or description.
 * The `admin` role's name cannot be changed.
 */
export async function updateRole(id: string, input: UpdateRoleInput): Promise<RoleRecord> {
  const existing = await db.query.roles.findFirst({ where: eq(roles.id, id) })
  if (!existing) throw new Error("Role not found")

  // Lock the admin role name
  if (existing.name === ADMIN_ROLE_NAME && input.name && input.name !== ADMIN_ROLE_NAME) {
    throw new Error("The admin role name cannot be changed")
  }

  const updateData: Partial<typeof roles.$inferInsert> = {}
  if (input.name !== undefined) updateData.name = input.name
  if (input.description !== undefined) updateData.description = input.description

  if (Object.keys(updateData).length === 0) {
    return toRoleRecord(existing)
  }

  await db.update(roles).set(updateData).where(eq(roles.id, id))

  const row = await db.query.roles.findFirst({ where: eq(roles.id, id) })
  if (!row) throw new Error("Role disappeared after update")
  return toRoleRecord(row)
}

/**
 * Delete a role.
 * System roles (including admin) cannot be deleted.
 */
export async function deleteRole(id: string): Promise<void> {
  const existing = await db.query.roles.findFirst({ where: eq(roles.id, id) })
  if (!existing) throw new Error("Role not found")
  if (existing.isSystem) {
    throw new Error("System roles cannot be deleted")
  }

  // role_permissions and users_to_roles cascade on delete via FK
  await db.delete(roles).where(eq(roles.id, id))
}

/**
 * Enable or disable a role.
 * The admin role cannot be disabled.
 */
export async function setRoleEnabled(id: string, enabled: boolean): Promise<RoleRecord> {
  const existing = await db.query.roles.findFirst({ where: eq(roles.id, id) })
  if (!existing) throw new Error("Role not found")
  if (existing.name === ADMIN_ROLE_NAME && !enabled) {
    throw new Error("The admin role cannot be disabled")
  }

  await db.update(roles).set({ isEnabled: enabled }).where(eq(roles.id, id))

  const row = await db.query.roles.findFirst({ where: eq(roles.id, id) })
  if (!row) throw new Error("Role disappeared after update")
  return toRoleRecord(row)
}

/**
 * Get the list of permission strings assigned to a role.
 */
export const getRolePermissions = cache(async (roleId: string): Promise<Permission[]> => {
  const rows = await db
    .select({ permission: rolePermissions.permission })
    .from(rolePermissions)
    .where(eq(rolePermissions.roleId, roleId))
  return rows
    .map((r) => (isPermission(r.permission) ? r.permission : null))
    .filter((p): p is Permission => p !== null)
})

/**
 * Replace the full permission list for a role.
 * Uses a transaction: delete all existing entries, then insert the new set.
 *
 * The `admin` role's permissions cannot be changed (it is fully locked).
 */
export async function setRolePermissions(
  roleId: string,
  permissions: Permission[]
): Promise<void> {
  const existing = await db.query.roles.findFirst({ where: eq(roles.id, roleId) })
  if (!existing) throw new Error("Role not found")
  if (existing.name === ADMIN_ROLE_NAME) {
    throw new Error("The admin role permissions cannot be modified")
  }

  const uniquePermissions = [...new Set(permissions)]

  await db.transaction(async (tx) => {
    await tx.delete(rolePermissions).where(eq(rolePermissions.roleId, roleId))
    if (uniquePermissions.length > 0) {
      await tx.insert(rolePermissions).values(
        uniquePermissions.map((p) => ({ roleId, permission: p }))
      )
    }
  })
}
