import "server-only"

import { cache } from "react"
import { and, count, desc, eq, ilike, inArray, or } from "drizzle-orm"

import { db } from "@/shared/db"
import { roles, users, usersToRoles } from "@/shared/db/schema"

import type { PaginatedResult, UserRoleAssignment } from "./types"

const DEFAULT_PAGE_SIZE = 20
const MAX_PAGE_SIZE = 100

function clampPageSize(size?: number): number {
  if (!size || size <= 0) return DEFAULT_PAGE_SIZE
  return Math.min(size, MAX_PAGE_SIZE)
}

function clampPage(page?: number): number {
  if (!page || page <= 0) return 1
  return page
}

/**
 * Get the list of role names assigned to a user.
 */
export const getUserRoleNames = cache(async (userId: string): Promise<string[]> => {
  const rows = await db
    .select({ name: roles.name })
    .from(usersToRoles)
    .innerJoin(roles, eq(usersToRoles.roleId, roles.id))
    .where(eq(usersToRoles.userId, userId))
  return rows.map((r) => r.name)
})

/**
 * Replace the full set of roles assigned to a user.
 * Resolves role names to IDs and inserts them in a transaction.
 *
 * Only enabled roles (or system roles, which are always considered enabled)
 * can be assigned. Disabled roles are silently filtered out to keep
 * `users_to_roles` consistent with `resolvePermissions` (which also filters
 * disabled roles at login time).
 *
 * Pass an empty array to remove all roles from the user.
 */
export async function assignRolesToUser(
  userId: string,
  roleNames: string[]
): Promise<void> {
  // Verify the user exists
  const user = await db.query.users.findFirst({ where: eq(users.id, userId) })
  if (!user) throw new Error("User not found")

  // Resolve role names to role records. Only enabled roles are assignable;
  // disabled roles are filtered out to avoid data inconsistency.
  const roleRows =
    roleNames.length > 0
      ? await db
          .select({ id: roles.id, name: roles.name, isEnabled: roles.isEnabled })
          .from(roles)
          .where(inArray(roles.name, roleNames))
      : []

  // Validate all requested role names exist
  const foundNames = new Set(roleRows.map((r) => r.name))
  const missing = roleNames.filter((n) => !foundNames.has(n))
  if (missing.length > 0) {
    throw new Error(`Unknown roles: ${missing.join(", ")}`)
  }

  // P1-9 security fix: filter out disabled non-system roles
  const assignableRoles = roleRows.filter((r) => r.isEnabled || r.name === "admin")
  const roleIds = assignableRoles.map((r) => r.id)

  await db.transaction(async (tx) => {
    await tx.delete(usersToRoles).where(eq(usersToRoles.userId, userId))
    if (roleIds.length > 0) {
      await tx.insert(usersToRoles).values(
        roleIds.map((roleId) => ({ userId, roleId }))
      )
    }
  })
}

/**
 * List user-role assignments with optional filtering and pagination.
 * Each row includes the user's id, name, email, and the list of role names assigned.
 */
export async function getUserRoleAssignments(params?: {
  page?: number
  pageSize?: number
  search?: string
  role?: string
}): Promise<PaginatedResult<UserRoleAssignment>> {
  const page = clampPage(params?.page)
  const pageSize = clampPageSize(params?.pageSize)
  const offset = (page - 1) * pageSize

  const conditions = []
  if (params?.search) {
    const term = `%${params.search}%`
    conditions.push(or(ilike(users.name, term), ilike(users.email, term)))
  }

  // If filtering by role, join through users_to_roles
  const where = conditions.length ? and(...conditions) : undefined

  const [userRows, totalRows] = await Promise.all([
    db
      .select({
        id: users.id,
        name: users.name,
        email: users.email,
        createdAt: users.createdAt,
      })
      .from(users)
      .where(where)
      .orderBy(desc(users.createdAt))
      .limit(pageSize)
      .offset(offset),
    db.select({ value: count() }).from(users).where(where),
  ])

  const userIds = userRows.map((u) => u.id)
  if (userIds.length === 0) {
    return {
      items: [],
      page,
      pageSize,
      total: 0,
      totalPages: 0,
    }
  }

  // Fetch role assignments for these users in a single query
  const assignmentRows = await db
    .select({ userId: usersToRoles.userId, roleName: roles.name })
    .from(usersToRoles)
    .innerJoin(roles, eq(usersToRoles.roleId, roles.id))
    .where(inArray(usersToRoles.userId, userIds))

  const roleMap = new Map<string, string[]>()
  for (const row of assignmentRows) {
    const list = roleMap.get(row.userId) ?? []
    list.push(row.roleName)
    roleMap.set(row.userId, list)
  }

  const items: UserRoleAssignment[] = userRows.map((u) => ({
    userId: u.id,
    userName: u.name,
    userEmail: u.email,
    roleNames: roleMap.get(u.id) ?? [],
    createdAt: u.createdAt,
  }))

  // Apply role filter post-fetch if specified (simpler than a subquery here)
  const filtered = params?.role
    ? items.filter((i) => i.roleNames.includes(params.role ?? ""))
    : items

  const total = Number(totalRows[0]?.value ?? 0)

  return {
    items: filtered,
    page,
    pageSize,
    total,
    totalPages: Math.ceil(total / pageSize),
  }
}
