import { countPlanningEntries } from "@capakraken/application";
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 {
  createTRPCRouter,
  managerProcedure,
  planningReadProcedure,
  protectedProcedure,
  requirePermission,
} from "../trpc.js";

async function loadRolePlanningEntryCounts(
  db: Pick<import("@capakraken/db").PrismaClient, "demandRequirement" | "assignment">,
  roleIds: string[],
) {
  const { countsByRoleId } = await countPlanningEntries(db, {
    roleIds,
  });

  return countsByRoleId;
}

async function attachPlanningEntryCounts<
  TRole extends {
    id: string;
    _count: { resourceRoles: number };
  },
>(
  db: Pick<import("@capakraken/db").PrismaClient, "demandRequirement" | "assignment">,
  roles: TRole[],
): Promise<Array<TRole & { _count: { resourceRoles: number; allocations: number } }>> {
  const countsByRoleId = await loadRolePlanningEntryCounts(
    db,
    roles.map((role) => role.id),
  );

  return roles.map((role) => ({
    ...role,
    _count: {
      ...role._count,
      allocations: countsByRoleId.get(role.id) ?? 0,
    },
  }));
}

async function attachSinglePlanningEntryCount<
  TRole extends {
    id: string;
    _count: { resourceRoles: number };
  },
>(
  db: Pick<import("@capakraken/db").PrismaClient, "demandRequirement" | "assignment">,
  role: TRole,
): Promise<TRole & { _count: { resourceRoles: number; allocations: number } }> {
  return (await attachPlanningEntryCounts(db, [role]))[0]!;
}

export const roleRouter = createTRPCRouter({
  list: planningReadProcedure
    .input(
      z.object({
        isActive: z.boolean().optional(),
        search: z.string().optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const roles = await ctx.db.role.findMany({
        where: {
          ...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
          ...(input.search
            ? { name: { contains: input.search, mode: "insensitive" as const } }
            : {}),
        },
        include: {
          _count: {
            select: { resourceRoles: true },
          },
        },
        orderBy: { name: "asc" },
      });

      return attachPlanningEntryCounts(ctx.db, roles);
    }),

  resolveByIdentifier: protectedProcedure
    .input(z.object({ identifier: z.string().trim().min(1) }))
    .query(async ({ ctx, input }) => {
      const identifier = input.identifier.trim();
      const select = {
        id: true,
        name: true,
        color: true,
        isActive: true,
      } as const;

      let role = await ctx.db.role.findUnique({
        where: { id: identifier },
        select,
      });
      if (!role) {
        role = await ctx.db.role.findUnique({
          where: { name: identifier },
          select,
        });
      }
      if (!role) {
        role = await ctx.db.role.findFirst({
          where: { name: { equals: identifier, mode: "insensitive" } },
          select,
        });
      }
      if (!role) {
        role = await ctx.db.role.findFirst({
          where: { name: { contains: identifier, mode: "insensitive" } },
          select,
        });
      }
      if (!role) {
        throw new TRPCError({ code: "NOT_FOUND", message: "Role not found" });
      }

      return role;
    }),

  getByIdentifier: planningReadProcedure
    .input(z.object({ identifier: z.string() }))
    .query(async ({ ctx, input }) => {
      const select = {
        id: true,
        name: true,
        description: true,
        color: true,
        isActive: true,
        _count: { select: { resourceRoles: true } },
      } as const;

      let role = await ctx.db.role.findUnique({
        where: { id: input.identifier },
        select,
      });
      if (!role) {
        role = await ctx.db.role.findUnique({
          where: { name: input.identifier },
          select,
        });
      }
      if (!role) {
        role = await ctx.db.role.findFirst({
          where: { name: { equals: input.identifier, mode: "insensitive" } },
          select,
        });
      }
      if (!role) {
        role = await ctx.db.role.findFirst({
          where: { name: { contains: input.identifier, mode: "insensitive" } },
          select,
        });
      }
      if (!role) {
        throw new TRPCError({ code: "NOT_FOUND", message: "Role not found" });
      }

      return attachSinglePlanningEntryCount(ctx.db, role);
    }),

  getById: planningReadProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      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 attachSinglePlanningEntryCount(ctx.db, role);
    }),

  create: managerProcedure
    .input(CreateRoleSchema)
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ROLES);
      const existing = await ctx.db.role.findUnique({ where: { name: input.name } });
      if (existing) {
        throw new TRPCError({ code: "CONFLICT", message: `Role "${input.name}" already exists` });
      }

      const role = await ctx.db.role.create({
        data: {
          name: input.name,
          description: input.description ?? null,
          color: input.color ?? null,
        },
        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 {
        ...role,
        _count: {
          ...role._count,
          allocations: 0,
        },
      };
    }),

  update: managerProcedure
    .input(z.object({ id: z.string(), data: UpdateRoleSchema }))
    .mutation(async ({ ctx, input }) => {
      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) {
        const nameConflict = await ctx.db.role.findUnique({ where: { name: input.data.name } });
        if (nameConflict) {
          throw new TRPCError({ code: "CONFLICT", message: `Role "${input.data.name}" already exists` });
        }
      }

      const updated = await ctx.db.role.update({
        where: { id: input.id },
        data: {
          ...(input.data.name !== undefined ? { name: input.data.name } : {}),
          ...(input.data.description !== undefined ? { description: input.data.description } : {}),
          ...(input.data.color !== undefined ? { color: input.data.color } : {}),
          ...(input.data.isActive !== undefined ? { isActive: input.data.isActive } : {}),
        },
        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 attachSinglePlanningEntryCount(ctx.db, updated);
    }),

  delete: managerProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      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 attachSinglePlanningEntryCount(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 };
    }),

  deactivate: managerProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      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 attachSinglePlanningEntryCount(ctx.db, role);
    }),
});
