import type { Prisma } from '@prisma/client';
import { randomUUID } from 'node:crypto';
import prisma from '../config/db.js';
import { DEFAULT_ROLES, ORG_ADMIN_ROLE } from '../config/access.js';
import { encodeId } from '../utils/idCodec.js';
import { paginatedResponse, type Pagination } from '../utils/pagination.js';
import { roleLabel } from './session.model.js';

// Users and roles for /api/v1/admin/*, always scoped to the caller's organization.

const userInclude = {
  department: { select: { name: true } },
  team: { select: { name: true } },
  tenantRoles: { include: { role: { select: { id: true, name: true, label: true } } } },
  projectMemberships: { orderBy: { createdAt: 'asc' }, include: { project: { select: { id: true, code: true, name: true } } } },
} satisfies Prisma.UserInclude;

type TenantUserRecord = Prisma.UserGetPayload<{ include: typeof userInclude }>;

// The BlenAxis default tenant roles (shared rows). `admin` and `user` are platform roles, never tenant roles.
const DEFAULT_ROLE_NAMES = new Set(DEFAULT_ROLES.map((r) => r.name));

/** Invited = no Firebase account yet (hasn't accepted); deactivated users keep their records. */
export function userStatus(user: { isActive: boolean; firebaseUid: string | null }): 'active' | 'invited' | 'disabled' {
  if (!user.isActive) return 'disabled';
  return user.firebaseUid ? 'active' : 'invited';
}

export function userView(user: TenantUserRecord) {
  return {
    id: encodeId(user.id),
    name: user.name,
    email: user.email,
    phone: user.phone ?? undefined,
    jobTitle: user.jobTitle ?? undefined,
    departmentId: user.departmentId ? encodeId(user.departmentId) : undefined,
    teamId: user.teamId ? encodeId(user.teamId) : undefined,
    department: user.department?.name,
    team: user.team?.name,
    roleIds: user.tenantRoles.map(({ role }) => encodeId(role.id)),
    roles: user.tenantRoles.map(({ role }) => roleLabel(role)),
    status: userStatus(user),
    lastActiveAt: user.lastLoginAt?.toISOString(),
    invitedAt: user.invitedAt?.toISOString(),
    // Relative, so admins can copy it from the Users screen (the app adds its origin).
    inviteUrl: userStatus(user) === 'invited' && user.inviteToken ? `/invite/${user.inviteToken}` : undefined,
    projects: user.projectMemberships.map((m) => ({
      projectId: encodeId(m.project.id),
      projectCode: m.project.code,
      projectName: m.project.name,
      projectRole: m.projectRole,
      siteIds: m.siteIds.map(encodeId),
      since: m.createdAt.toISOString(),
    })),
  };
}

export interface UserFilters {
  search?: string;
  status?: string;
  departmentId?: number;
  roleId?: number;
}

// Status is derived (isActive + firebaseUid), not a column, so it can't go through
// Prisma's groupBy — each bucket gets its own count instead.
const STATUS_WHERE: Record<'active' | 'invited' | 'disabled', Prisma.UserWhereInput> = {
  active: { isActive: true, firebaseUid: { not: null } },
  invited: { isActive: true, firebaseUid: null },
  disabled: { isActive: false },
};

/**
 * GET /admin/users: one page of people, filtered on the server. `statusCounts` applies
 * every filter except status, so the status chips can show their counts.
 */
export async function listUsers(organizationId: number, filters: UserFilters, page: Pagination) {
  const where: Prisma.UserWhereInput = { organizationId };
  if (filters.search) {
    where.OR = [
      { name: { contains: filters.search, mode: 'insensitive' } },
      { email: { contains: filters.search, mode: 'insensitive' } },
      { jobTitle: { contains: filters.search, mode: 'insensitive' } },
    ];
  }
  if (filters.departmentId) where.departmentId = filters.departmentId;
  if (filters.roleId) where.tenantRoles = { some: { roleId: filters.roleId } };
  const status = filters.status && filters.status in STATUS_WHERE ? STATUS_WHERE[filters.status as keyof typeof STATUS_WHERE] : {};
  const filtered = { AND: [where, status] };

  const [users, total, active, invited, disabled] = await Promise.all([
    prisma.user.findMany({ where: filtered, orderBy: [{ name: 'asc' }, { id: 'asc' }], include: userInclude, skip: page.skip, take: page.take }),
    prisma.user.count({ where: filtered }),
    prisma.user.count({ where: { AND: [where, STATUS_WHERE.active] } }),
    prisma.user.count({ where: { AND: [where, STATUS_WHERE.invited] } }),
    prisma.user.count({ where: { AND: [where, STATUS_WHERE.disabled] } }),
  ]);
  return {
    ...paginatedResponse(users.map(userView), total, page),
    statusCounts: { all: active + invited + disabled, active, invited, disabled },
  };
}

/** GET /admin/users/options: everyone in the organization, lightly, for pickers and duplicate checks. */
export async function listUserOptions(organizationId: number, status?: string) {
  const users = await prisma.user.findMany({
    where: { organizationId, ...(status && status in STATUS_WHERE ? STATUS_WHERE[status as keyof typeof STATUS_WHERE] : {}) },
    orderBy: [{ name: 'asc' }, { id: 'asc' }],
    select: {
      id: true, name: true, email: true, jobTitle: true, isActive: true, firebaseUid: true,
      tenantRoles: { select: { role: { select: { name: true, label: true } } } },
    },
  });
  return users.map((u) => ({
    id: encodeId(u.id),
    name: u.name,
    email: u.email,
    ...(u.jobTitle ? { jobTitle: u.jobTitle } : {}),
    status: userStatus(u),
    roles: u.tenantRoles.map(({ role }) => roleLabel(role)),
  }));
}

export function findTenantUser(organizationId: number, id: number) {
  return prisma.user.findFirst({ where: { id, organizationId }, include: userInclude });
}

export { userInclude };

export const isOrgAdmin = (user: TenantUserRecord) =>
  user.tenantRoles.some(({ role }) => role.name === ORG_ADMIN_ROLE);

/** Active Organization Admins other than `exceptUserId`. */
export function countOtherActiveAdmins(organizationId: number, exceptUserId: number) {
  return prisma.user.count({
    where: {
      organizationId,
      isActive: true,
      // Someone still invited hasn't joined, so they don't count as an admin yet.
      firebaseUid: { not: null },
      id: { not: exceptUserId },
      tenantRoles: { some: { role: { name: ORG_ADMIN_ROLE } } },
    },
  });
}

export interface UserChanges {
  name?: string;
  phone?: string | null;
  jobTitle?: string | null;
  departmentId?: number | null;
  teamId?: number | null;
  roleIds?: number[];
}

export async function updateTenantUser(id: number, changes: UserChanges) {
  const { roleIds, ...data } = changes;
  await prisma.$transaction(async (tx) => {
    await tx.user.update({ where: { id }, data });
    if (roleIds) {
      await tx.userRole.deleteMany({ where: { userId: id } });
      await tx.userRole.createMany({ data: roleIds.map((roleId) => ({ userId: id, roleId })) });
    }
  });
  return prisma.user.findUniqueOrThrow({ where: { id }, include: userInclude });
}

export function clearInvite(id: number) {
  return prisma.user.update({ where: { id }, data: { inviteToken: null }, include: userInclude });
}

export function setUserActive(id: number, isActive: boolean) {
  return prisma.user.update({ where: { id }, data: { isActive }, include: userInclude });
}

// ── Roles & module access ──
// An organization sees the BlenAxis default roles (shared rows, permissions
// read-only) plus its own custom roles. Module access for a default role is
// per organization (role_module_overrides); custom roles keep it on the row.

const roleInclude = { permissions: { include: { permission: { select: { name: true } } } } } satisfies Prisma.RoleInclude;

export function tenantRoleWhere(organizationId: number, ids?: number[]): Prisma.RoleWhereInput {
  return {
    OR: [{ organizationId: null, name: { in: [...DEFAULT_ROLE_NAMES] } }, { organizationId }],
    ...(ids ? { id: { in: ids } } : {}),
  };
}

export async function listRoles(organizationId: number, ids?: number[]) {
  const [roles, overrides, counts] = await Promise.all([
    prisma.role.findMany({ where: tenantRoleWhere(organizationId, ids), orderBy: { id: 'asc' }, include: roleInclude }),
    prisma.roleModuleOverride.findMany({ where: { organizationId } }),
    prisma.userRole.groupBy({
      by: ['roleId'],
      where: { user: { organizationId, isActive: true } },
      _count: { _all: true },
    }),
  ]);
  const modules = new Map(overrides.map((o) => [o.roleId, o.modules]));
  const userCount = new Map(counts.map((c) => [c.roleId, c._count._all]));
  return roles.map((role) => ({
    id: encodeId(role.id),
    name: roleLabel(role),
    description: role.description ?? '',
    // Default roles ship with BlenAxis: read-only here, but they can be cloned.
    isSystem: role.organizationId === null,
    scope: role.scope,
    permissions: role.permissions.map(({ permission }) => permission.name),
    modules: modules.get(role.id) ?? role.modules,
    userCount: userCount.get(role.id) ?? 0,
    clonedFromId: role.clonedFromId ? encodeId(role.clonedFromId) : undefined,
  }));
}

export function countTenantRoles(organizationId: number, ids: number[]) {
  return prisma.role.count({ where: tenantRoleWhere(organizationId, ids) });
}

export function findRolesByIds(ids: number[]) {
  return prisma.role.findMany({ where: { id: { in: ids } }, select: { id: true, name: true } });
}

export function findTenantRole(organizationId: number, id: number) {
  return prisma.role.findFirst({ where: { ...tenantRoleWhere(organizationId), id }, include: roleInclude });
}

/** Role names are unique per organization, across default and custom roles (case-insensitive). */
export async function roleNameTaken(organizationId: number, name: string, exceptId?: number) {
  const roles = await prisma.role.findMany({ where: tenantRoleWhere(organizationId), select: { id: true, name: true, label: true } });
  return roles.some((r) => r.id !== exceptId && roleLabel(r).toLowerCase() === name.toLowerCase());
}

export interface RoleData {
  name: string;
  description: string;
  scope: 'all' | 'assigned';
  permissions: string[];
  modules: string[];
  clonedFromId?: number | null;
}

async function permissionIds(names: string[]) {
  const rows = await prisma.permission.findMany({ where: { name: { in: names } }, select: { id: true } });
  return rows.map((r) => ({ permissionId: r.id }));
}

export async function createCustomRole(organizationId: number, data: RoleData) {
  const role = await prisma.role.create({
    data: {
      // Internal key; the organization sees `label`.
      name: `org-${organizationId}-${randomUUID().slice(0, 8)}`,
      label: data.name,
      description: data.description,
      scope: data.scope,
      modules: data.modules,
      organizationId,
      clonedFromId: data.clonedFromId ?? null,
      permissions: { create: await permissionIds(data.permissions) },
    },
  });
  return role.id;
}

export async function updateCustomRole(id: number, data: RoleData) {
  const permissions = await permissionIds(data.permissions);
  await prisma.$transaction([
    prisma.rolePermission.deleteMany({ where: { roleId: id } }),
    prisma.role.update({
      where: { id },
      data: {
        label: data.name,
        description: data.description,
        scope: data.scope,
        modules: data.modules,
        permissions: { create: permissions },
      },
    }),
  ]);
}

/** Active and deactivated users of this organization holding the role. */
export function roleHolders(organizationId: number, roleId: number) {
  return prisma.user.findMany({ where: { organizationId, tenantRoles: { some: { roleId } } }, select: { id: true } });
}

/** Deletes a custom role, first moving its holders to `reassignToId` (keeping their other roles). */
export async function deleteCustomRole(id: number, holderIds: number[], reassignToId?: number) {
  await prisma.$transaction(async (tx) => {
    if (reassignToId) {
      await tx.userRole.createMany({
        data: holderIds.map((userId) => ({ userId, roleId: reassignToId })),
        skipDuplicates: true,
      });
    }
    await tx.role.delete({ where: { id } });
  });
}

/** PUT /admin/module-access: custom roles on the row, default roles as per-organization overrides. */
export async function setModuleAccess(organizationId: number, matrix: Map<number, string[]>) {
  const roles = await prisma.role.findMany({
    where: tenantRoleWhere(organizationId, [...matrix.keys()]),
    select: { id: true, name: true, organizationId: true },
  });
  await prisma.$transaction(
    roles
      // Organization Admin always has every subscribed module.
      .filter((role) => role.name !== ORG_ADMIN_ROLE)
      .map((role) => {
        const modules = matrix.get(role.id)!;
        return role.organizationId === organizationId
          ? prisma.role.update({ where: { id: role.id }, data: { modules } })
          : prisma.roleModuleOverride.upsert({
              where: { organizationId_roleId: { organizationId, roleId: role.id } },
              update: { modules },
              create: { organizationId, roleId: role.id, modules },
            });
      }),
  );
}

export function countCustomRoles(organizationId: number) {
  return prisma.role.count({ where: { organizationId } });
}
