import { CreateRoleSchema, PermissionKey, UpdateRoleSchema } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { RESOURCE_BRIEF_SELECT } from "../db/selects.js";
import { emitRoleCreated, emitRoleDeleted, emitRoleUpdated } from "../sse/event-bus.js";
import type { TRPCContext } from "../trpc.js";
import { requirePermission } from "../trpc.js";
import {
  appendZeroAllocationCount,
  assertRoleNameAvailable,
  attachRolePlanningEntryCounts,
  attachSingleRolePlanningEntryCount,
  buildRoleCreateData,
  buildRoleUpdateData,
  buildRoleListWhere,
  findRoleByIdentifier,
} from "./role-support.js";

export const RoleListInputSchema = z.object({
  isActive: z.boolean().optional(),
  search: z.string().optional(),
});

export const RoleIdentifierInputSchema = z.object({
  identifier: z.string(),
});

export const ResolveRoleIdentifierInputSchema = z.object({
  identifier: z.string().trim().min(1),
});

export const RoleIdInputSchema = z.object({
  id: z.string(),
});

export const UpdateRoleProcedureInputSchema = z.object({
  id: z.string(),
  data: UpdateRoleSchema,
});

type RoleMutationContext = Pick<TRPCContext, "db" | "dbUser"> & {
  permissions: Set<PermissionKey>;
};

type RoleReadContext = Pick<TRPCContext, "db">;

export async function listRoles(
  ctx: RoleReadContext,
  input: z.infer<typeof RoleListInputSchema>,
) {
  const roles = await ctx.db.role.findMany({
    where: buildRoleListWhere(input),
    include: {
      _count: {
        select: { resourceRoles: true },
      },
    },
    orderBy: { name: "asc" },
  });

  return attachRolePlanningEntryCounts(ctx.db, roles);
}

export async function resolveRoleByIdentifier(
  ctx: RoleReadContext,
  input: z.infer<typeof ResolveRoleIdentifierInputSchema>,
) {
  const select = {
    id: true,
    name: true,
    color: true,
    isActive: true,
  } as const;

  return findRoleByIdentifier<{
    id: string;
    name: string;
    color: string | null;
    isActive: boolean;
  }>(ctx.db, input.identifier, select);
}

export async function getRoleByIdentifier(
  ctx: RoleReadContext,
  input: z.infer<typeof RoleIdentifierInputSchema>,
) {
  const select = {
    id: true,
    name: true,
    description: true,
    color: true,
    isActive: true,
    _count: { select: { resourceRoles: true } },
  } as const;

  const role = await findRoleByIdentifier<{
    id: string;
    name: string;
    description: string | null;
    color: string | null;
    isActive: boolean;
    _count: { resourceRoles: number };
  }>(ctx.db, input.identifier, select);

  return attachSingleRolePlanningEntryCount(ctx.db, role);
}

export async function getRoleById(
  ctx: RoleReadContext,
  input: z.infer<typeof RoleIdInputSchema>,
) {
  const role = await findUniqueOrThrow(
    ctx.db.role.findUnique({
      where: { id: input.id },
      include: {
        _count: { select: { resourceRoles: true } },
        resourceRoles: {
          include: {
            resource: { select: RESOURCE_BRIEF_SELECT },
          },
        },
      },
    }),
    "Role",
  );

  return attachSingleRolePlanningEntryCount(ctx.db, role);
}

export async function createRole(
  ctx: RoleMutationContext,
  input: z.infer<typeof CreateRoleSchema>,
) {
  requirePermission(ctx, PermissionKey.MANAGE_ROLES);
  await assertRoleNameAvailable(ctx.db, input.name);

  const role = await ctx.db.role.create({
    data: buildRoleCreateData(input),
    include: { _count: { select: { resourceRoles: true } } },
  });

  await ctx.db.auditLog.create({
    data: {
      entityType: "Role",
      entityId: role.id,
      action: "CREATE",
      changes: { after: role },
    },
  });

  emitRoleCreated({ id: role.id, name: role.name });

  return appendZeroAllocationCount(role);
}

export async function updateRole(
  ctx: RoleMutationContext,
  input: z.infer<typeof UpdateRoleProcedureInputSchema>,
) {
  requirePermission(ctx, PermissionKey.MANAGE_ROLES);
  const existing = await findUniqueOrThrow(
    ctx.db.role.findUnique({ where: { id: input.id } }),
    "Role",
  );

  if (input.data.name && input.data.name !== existing.name) {
    await assertRoleNameAvailable(ctx.db, input.data.name, input.id);
  }

  const updated = await ctx.db.role.update({
    where: { id: input.id },
    data: buildRoleUpdateData(input.data),
    include: { _count: { select: { resourceRoles: true } } },
  });

  await ctx.db.auditLog.create({
    data: {
      entityType: "Role",
      entityId: input.id,
      action: "UPDATE",
      changes: { before: existing, after: updated },
    },
  });

  emitRoleUpdated({ id: updated.id, name: updated.name });

  return attachSingleRolePlanningEntryCount(ctx.db, updated);
}

export async function deleteRole(
  ctx: RoleMutationContext,
  input: z.infer<typeof RoleIdInputSchema>,
) {
  requirePermission(ctx, PermissionKey.MANAGE_ROLES);
  const role = await findUniqueOrThrow(
    ctx.db.role.findUnique({
      where: { id: input.id },
      include: { _count: { select: { resourceRoles: true } } },
    }),
    "Role",
  );

  const roleWithCounts = await attachSingleRolePlanningEntryCount(ctx.db, role);

  if (
    roleWithCounts._count.resourceRoles > 0 ||
    roleWithCounts._count.allocations > 0
  ) {
    throw new TRPCError({
      code: "PRECONDITION_FAILED",
      message: `Cannot delete role assigned to ${roleWithCounts._count.resourceRoles} resource(s) and ${roleWithCounts._count.allocations} allocation(s). Deactivate it instead.`,
    });
  }

  await ctx.db.role.delete({ where: { id: input.id } });

  await ctx.db.auditLog.create({
    data: {
      entityType: "Role",
      entityId: input.id,
      action: "DELETE",
      changes: { before: role },
    },
  });

  emitRoleDeleted(input.id);

  return { success: true };
}

export async function deactivateRole(
  ctx: RoleMutationContext,
  input: z.infer<typeof RoleIdInputSchema>,
) {
  requirePermission(ctx, PermissionKey.MANAGE_ROLES);
  const role = await ctx.db.role.update({
    where: { id: input.id },
    data: { isActive: false },
    include: { _count: { select: { resourceRoles: true } } },
  });

  await ctx.db.auditLog.create({
    data: {
      entityType: "Role",
      entityId: input.id,
      action: "UPDATE",
      changes: { after: { isActive: false } },
    },
  });

  emitRoleUpdated({ id: role.id, isActive: false });

  return attachSingleRolePlanningEntryCount(ctx.db, role);
}
