import { eq, and, inArray, count, type Column } from 'drizzle-orm'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import type { PaginationInput } from '@lunarfront/shared/schemas'
import { permissions, roles, rolePermissions, userRoles } from '../db/schema/rbac.js'
import { SYSTEM_PERMISSIONS, DEFAULT_ROLES } from '../db/seeds/rbac.js'
import { ForbiddenError } from '../lib/errors.js'
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js'

export const RbacService = {
  /** Seed system permissions and sync role assignments for any new permissions */
  async seedPermissions(db: PostgresJsDatabase<any>) {
    const existing = await db.select({ slug: permissions.slug }).from(permissions)
    const existingSlugs = new Set(existing.map((p) => p.slug))

    const toInsert = SYSTEM_PERMISSIONS.filter((p) => !existingSlugs.has(p.slug))
    if (toInsert.length > 0) {
      await db.insert(permissions).values(toInsert)
    }

    // Sync system role permission assignments — ensures new permissions
    // (e.g., vault.view) get assigned to existing roles on restart
    const allPerms = await db.select().from(permissions)
    const permMap = new Map(allPerms.map((p) => [p.slug, p.id]))

    const systemRoles = await db.select().from(roles).where(eq(roles.isSystem, true))
    for (const role of systemRoles) {
      const roleDef = DEFAULT_ROLES.find((r) => r.slug === role.slug)
      if (!roleDef) continue

      const existingAssignments = await db.select({ permissionId: rolePermissions.permissionId })
        .from(rolePermissions).where(eq(rolePermissions.roleId, role.id))
      const assignedPermIds = new Set(existingAssignments.map((a) => a.permissionId))

      const missingPermIds = roleDef.permissions
        .map((slug) => permMap.get(slug))
        .filter((id): id is string => id !== undefined && !assignedPermIds.has(id))

      if (missingPermIds.length > 0) {
        await db.insert(rolePermissions).values(
          missingPermIds.map((permissionId) => ({ roleId: role.id, permissionId })),
        )
      }
    }
  },

  /** Seed default roles */
  async seedDefaultRoles(db: PostgresJsDatabase<any>) {
    const existingRoles = await db
      .select({ slug: roles.slug })
      .from(roles)
      .where(eq(roles.isSystem, true))

    if (existingRoles.length > 0) return // already seeded

    // Get all permission records for slug -> id mapping
    const allPerms = await db.select().from(permissions)
    const permMap = new Map(allPerms.map((p) => [p.slug, p.id]))

    for (const roleDef of DEFAULT_ROLES) {
      const [role] = await db
        .insert(roles)
        .values({
          name: roleDef.name,
          slug: roleDef.slug,
          description: roleDef.description,
          isSystem: true,
        })
        .returning()

      const permIds = roleDef.permissions
        .map((slug) => permMap.get(slug))
        .filter((id): id is string => id !== undefined)

      if (permIds.length > 0) {
        await db.insert(rolePermissions).values(
          permIds.map((permissionId) => ({
            roleId: role.id,
            permissionId,
          })),
        )
      }
    }
  },

  /** Get all permissions for a user (union of all role permissions) */
  async getUserPermissions(db: PostgresJsDatabase<any>, userId: string): Promise<string[]> {
    const userRoleRecords = await db
      .select({ roleId: userRoles.roleId })
      .from(userRoles)
      .where(eq(userRoles.userId, userId))

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

    const roleIds = userRoleRecords.map((r) => r.roleId)

    const rpRecords = await db
      .select({ permissionId: rolePermissions.permissionId })
      .from(rolePermissions)
      .where(inArray(rolePermissions.roleId, roleIds))

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

    const permIds = [...new Set(rpRecords.map((r) => r.permissionId))]

    const permRecords = await db
      .select({ slug: permissions.slug })
      .from(permissions)
      .where(inArray(permissions.id, permIds))

    return permRecords.map((p) => p.slug)
  },

  /** List all permissions */
  async listPermissions(db: PostgresJsDatabase<any>) {
    return db.select().from(permissions).orderBy(permissions.domain, permissions.action)
  },

  /** List roles */
  async listRoles(db: PostgresJsDatabase<any>, params: PaginationInput) {
    const baseWhere = eq(roles.isActive, true)

    const searchCondition = params.q
      ? buildSearchCondition(params.q, [roles.name, roles.slug])
      : undefined

    const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere

    const sortableColumns: Record<string, Column> = {
      name: roles.name,
      slug: roles.slug,
      created_at: roles.createdAt,
    }

    let query = db.select().from(roles).where(where).$dynamic()
    query = withSort(query, params.sort, params.order, sortableColumns, roles.name)
    query = withPagination(query, params.page, params.limit)

    const [data, [{ total }]] = await Promise.all([
      query,
      db.select({ total: count() }).from(roles).where(where),
    ])

    return paginatedResponse(data, total, params.page, params.limit)
  },

  /** Get role with its permissions */
  async getRoleWithPermissions(db: PostgresJsDatabase<any>, roleId: string) {
    const [role] = await db
      .select()
      .from(roles)
      .where(eq(roles.id, roleId))
      .limit(1)

    if (!role) return null

    const rp = await db
      .select({ permissionId: rolePermissions.permissionId, slug: permissions.slug })
      .from(rolePermissions)
      .innerJoin(permissions, eq(rolePermissions.permissionId, permissions.id))
      .where(eq(rolePermissions.roleId, roleId))

    return { ...role, permissions: rp.map((r) => r.slug) }
  },

  /** Create a custom role */
  async createRole(
    db: PostgresJsDatabase<any>,
    input: { name: string; slug: string; description?: string; permissionSlugs: string[] },
  ) {
    const [role] = await db
      .insert(roles)
      .values({
        name: input.name,
        slug: input.slug,
        description: input.description,
        isSystem: false,
      })
      .returning()

    await this.setRolePermissions(db, role.id, input.permissionSlugs)
    return this.getRoleWithPermissions(db, role.id)
  },

  /** Update role permissions (replace all) */
  async setRolePermissions(db: PostgresJsDatabase<any>, roleId: string, permissionSlugs: string[]) {
    // Delete existing
    await db.delete(rolePermissions).where(eq(rolePermissions.roleId, roleId))

    if (permissionSlugs.length === 0) return

    // Get permission IDs
    const perms = await db
      .select({ id: permissions.id, slug: permissions.slug })
      .from(permissions)
      .where(inArray(permissions.slug, permissionSlugs))

    if (perms.length > 0) {
      await db.insert(rolePermissions).values(
        perms.map((p) => ({ roleId, permissionId: p.id })),
      )
    }
  },

  /** Update a role */
  async updateRole(
    db: PostgresJsDatabase<any>,
    roleId: string,
    input: { name?: string; description?: string; permissionSlugs?: string[] },
  ) {
    if (input.name || input.description) {
      await db
        .update(roles)
        .set({
          ...(input.name ? { name: input.name } : {}),
          ...(input.description !== undefined ? { description: input.description } : {}),
          updatedAt: new Date(),
        })
        .where(eq(roles.id, roleId))
    }

    if (input.permissionSlugs) {
      await this.setRolePermissions(db, roleId, input.permissionSlugs)
    }

    return this.getRoleWithPermissions(db, roleId)
  },

  /** Delete a custom role */
  async deleteRole(db: PostgresJsDatabase<any>, roleId: string) {
    const [role] = await db
      .select()
      .from(roles)
      .where(eq(roles.id, roleId))
      .limit(1)

    if (!role) return null
    if (role.isSystem) throw new ForbiddenError('Cannot delete a system role')

    // Remove role permissions and user assignments
    await db.delete(rolePermissions).where(eq(rolePermissions.roleId, roleId))
    await db.delete(userRoles).where(eq(userRoles.roleId, roleId))

    const [deleted] = await db
      .delete(roles)
      .where(eq(roles.id, roleId))
      .returning()

    return deleted
  },

  /** Assign a role to a user */
  async assignRole(db: PostgresJsDatabase<any>, userId: string, roleId: string, assignedBy?: string) {
    const [existing] = await db
      .select()
      .from(userRoles)
      .where(and(eq(userRoles.userId, userId), eq(userRoles.roleId, roleId)))
      .limit(1)

    if (existing) return existing // already assigned

    const [assignment] = await db
      .insert(userRoles)
      .values({ userId, roleId, assignedBy })
      .returning()

    return assignment
  },

  /** Remove a role from a user */
  async removeRole(db: PostgresJsDatabase<any>, userId: string, roleId: string) {
    const [removed] = await db
      .delete(userRoles)
      .where(and(eq(userRoles.userId, userId), eq(userRoles.roleId, roleId)))
      .returning()

    return removed ?? null
  },

  /** Get roles assigned to a user */
  async getUserRoles(db: PostgresJsDatabase<any>, userId: string) {
    return db
      .select({
        id: roles.id,
        name: roles.name,
        slug: roles.slug,
        isSystem: roles.isSystem,
      })
      .from(userRoles)
      .innerJoin(roles, eq(userRoles.roleId, roles.id))
      .where(eq(userRoles.userId, userId))
  },
}
