import {
  buildSplitAllocationReadModel,
  createAssignment,
  createDemandRequirement,
  deleteAllocationEntry,
  deleteAssignment,
  loadAllocationEntry,
  updateAllocationEntry,
  updateAssignment,
} from "@capakraken/application";
import {
  AllocationStatus,
  CreateAllocationSchema,
  CreateAssignmentSchema,
  PermissionKey,
  UpdateAllocationSchema,
  UpdateAssignmentSchema,
} from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import {
  emitAllocationCreated,
  emitAllocationDeleted,
  emitAllocationUpdated,
} from "../sse/event-bus.js";
import {
  checkBudgetThresholdsInBackground,
  dispatchAllocationWebhookInBackground,
  invalidateDashboardCacheInBackground,
} from "./allocation-effects.js";
import {
  ASSIGNMENT_INCLUDE,
  toIsoDate,
} from "./allocation-shared.js";
import {
  findAllocationEntryOrNull,
  toAssignmentUpdateInput,
  toDemandRequirementUpdateInput,
} from "./allocation-support.js";
import {
  managerProcedure,
  requirePermission,
} from "../trpc.js";

export const allocationAssignmentProcedures = {
  create: managerProcedure
    .input(CreateAllocationSchema)
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      const allocation = await ctx.db.$transaction(async (tx) => {
        if (!input.resourceId) {
          const demandRequirement = await createDemandRequirement(
            tx as unknown as Parameters<typeof createDemandRequirement>[0],
            {
              projectId: input.projectId,
              startDate: input.startDate,
              endDate: input.endDate,
              hoursPerDay: input.hoursPerDay,
              percentage: input.percentage,
              role: input.role,
              roleId: input.roleId,
              headcount: input.headcount,
              status: input.status,
              metadata: input.metadata,
            },
          );

          return buildSplitAllocationReadModel({
            demandRequirements: [demandRequirement],
            assignments: [],
          }).allocations[0]!;
        }

        const assignment = await createAssignment(
          tx as unknown as Parameters<typeof createAssignment>[0],
          {
            resourceId: input.resourceId,
            projectId: input.projectId,
            startDate: input.startDate,
            endDate: input.endDate,
            hoursPerDay: input.hoursPerDay,
            percentage: input.percentage,
            role: input.role,
            roleId: input.roleId,
            status: input.status,
            metadata: input.metadata,
          },
        );

        return buildSplitAllocationReadModel({
          demandRequirements: [],
          assignments: [assignment],
        }).allocations[0]!;
      });

      emitAllocationCreated({
        id: allocation.id,
        projectId: allocation.projectId,
        resourceId: allocation.resourceId,
      });
      dispatchAllocationWebhookInBackground(ctx.db, "allocation.created", {
        id: allocation.id,
        projectId: allocation.projectId,
        resourceId: allocation.resourceId,
      });
      invalidateDashboardCacheInBackground();
      checkBudgetThresholdsInBackground(ctx.db, allocation.projectId);
      return allocation;
    }),

  createAssignment: managerProcedure
    .input(CreateAssignmentSchema)
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);

      const assignment = await ctx.db.$transaction(async (tx) => {
        return createAssignment(
          tx as unknown as Parameters<typeof createAssignment>[0],
          input,
        );
      });

      emitAllocationCreated({
        id: assignment.id,
        projectId: assignment.projectId,
        resourceId: assignment.resourceId,
      });
      invalidateDashboardCacheInBackground();
      checkBudgetThresholdsInBackground(ctx.db, assignment.projectId);

      return assignment;
    }),

  ensureAssignment: managerProcedure
    .input(z.object({
      resourceId: z.string(),
      projectId: z.string(),
      startDate: z.coerce.date(),
      endDate: z.coerce.date(),
      hoursPerDay: z.number().min(0.5).max(24),
      role: z.string().optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);

      const existing = (await ctx.db.assignment.findMany({
        where: {
          resourceId: input.resourceId,
          projectId: input.projectId,
        },
        include: ASSIGNMENT_INCLUDE,
        orderBy: { startDate: "asc" },
      })).find((assignment) => (
        toIsoDate(assignment.startDate) === toIsoDate(input.startDate)
        && toIsoDate(assignment.endDate) === toIsoDate(input.endDate)
      ));

      if (existing) {
        if (existing.status !== AllocationStatus.CANCELLED) {
          throw new TRPCError({
            code: "CONFLICT",
            message: `An allocation already exists for this resource/project/dates with status ${existing.status}.`,
          });
        }

        const resource = await ctx.db.resource.findUnique({
          where: { id: input.resourceId },
          select: { lcrCents: true },
        });
        if (!resource) {
          throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" });
        }

        const updated = await ctx.db.$transaction(async (tx) => updateAssignment(
          tx as unknown as Parameters<typeof updateAssignment>[0],
          existing.id,
          {
            status: AllocationStatus.PROPOSED,
            hoursPerDay: input.hoursPerDay,
            percentage: (input.hoursPerDay / 8) * 100,
            dailyCostCents: Math.round(resource.lcrCents * input.hoursPerDay),
            ...(input.role ? { role: input.role } : {}),
          },
        ));

        emitAllocationUpdated({
          id: updated.id,
          projectId: updated.projectId,
          resourceId: updated.resourceId,
        });
        dispatchAllocationWebhookInBackground(ctx.db, "allocation.updated", {
          id: updated.id,
          projectId: updated.projectId,
          resourceId: updated.resourceId,
        });
        invalidateDashboardCacheInBackground();
        checkBudgetThresholdsInBackground(ctx.db, updated.projectId);

        return { assignment: updated, action: "reactivated" as const };
      }

      const assignment = await ctx.db.$transaction(async (tx) => createAssignment(
        tx as unknown as Parameters<typeof createAssignment>[0],
        {
          resourceId: input.resourceId,
          projectId: input.projectId,
          startDate: input.startDate,
          endDate: input.endDate,
          hoursPerDay: input.hoursPerDay,
          percentage: (input.hoursPerDay / 8) * 100,
          status: AllocationStatus.PROPOSED,
          metadata: {},
          ...(input.role ? { role: input.role } : {}),
        },
      ));

      emitAllocationCreated({
        id: assignment.id,
        projectId: assignment.projectId,
        resourceId: assignment.resourceId,
      });
      invalidateDashboardCacheInBackground();
      checkBudgetThresholdsInBackground(ctx.db, assignment.projectId);

      return { assignment, action: "created" as const };
    }),

  updateAssignment: managerProcedure
    .input(z.object({ id: z.string(), data: UpdateAssignmentSchema }))
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      const existing = await findUniqueOrThrow(
        ctx.db.assignment.findUnique({
          where: { id: input.id },
          select: { resourceId: true },
        }),
        "Assignment",
      );

      const updated = await ctx.db.$transaction(async (tx) => {
        return updateAssignment(
          tx as unknown as Parameters<typeof updateAssignment>[0],
          input.id,
          input.data,
        );
      });

      emitAllocationUpdated({
        id: updated.id,
        projectId: updated.projectId,
        resourceId: updated.resourceId,
        resourceIds: [existing.resourceId, updated.resourceId],
      });
      dispatchAllocationWebhookInBackground(ctx.db, "allocation.updated", {
        id: updated.id,
        projectId: updated.projectId,
        resourceId: updated.resourceId,
      });
      invalidateDashboardCacheInBackground();
      checkBudgetThresholdsInBackground(ctx.db, updated.projectId);

      return updated;
    }),

  update: managerProcedure
    .input(z.object({ id: z.string(), data: UpdateAllocationSchema }))
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      const existing = await loadAllocationEntry(ctx.db, input.id);

      const updated = await ctx.db.$transaction(async (tx) => {
        const { allocation: updatedAllocation } = await updateAllocationEntry(
          tx as unknown as Parameters<typeof updateAllocationEntry>[0],
          {
            id: input.id,
            demandRequirementUpdate:
              existing.kind === "assignment" ? {} : toDemandRequirementUpdateInput(input.data),
            assignmentUpdate:
              existing.kind === "demand" ? {} : toAssignmentUpdateInput(input.data),
          },
        );

        await tx.auditLog.create({
          data: {
            entityType: "Allocation",
            entityId: input.id,
            action: "UPDATE",
            changes: {
              before: existing.entry,
              after: updatedAllocation,
            } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
          },
        });

        return updatedAllocation;
      });

      emitAllocationUpdated({
        id: updated.id,
        projectId: updated.projectId,
        resourceId: updated.resourceId,
        resourceIds: [existing.entry.resourceId, updated.resourceId],
      });
      invalidateDashboardCacheInBackground();
      checkBudgetThresholdsInBackground(ctx.db, updated.projectId);

      return updated;
    }),

  deleteAssignment: managerProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);

      const existing = await findUniqueOrThrow(
        ctx.db.assignment.findUnique({
          where: { id: input.id },
          include: ASSIGNMENT_INCLUDE,
        }),
        "Assignment",
      );

      await ctx.db.$transaction(async (tx) => {
        await deleteAssignment(
          tx as unknown as Parameters<typeof deleteAssignment>[0],
          input.id,
        );

        await tx.auditLog.create({
          data: {
            entityType: "Assignment",
            entityId: input.id,
            action: "DELETE",
            changes: { before: existing } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
          },
        });
      });

      emitAllocationDeleted(existing.id, existing.projectId, existing.resourceId);
      invalidateDashboardCacheInBackground();
      checkBudgetThresholdsInBackground(ctx.db, existing.projectId);

      return { success: true };
    }),

  delete: managerProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      const existing = await loadAllocationEntry(ctx.db, input.id);

      await ctx.db.$transaction(async (tx) => {
        await deleteAllocationEntry(
          tx as unknown as Parameters<typeof deleteAllocationEntry>[0],
          existing,
        );

        await tx.auditLog.create({
          data: {
            entityType: "Allocation",
            entityId: input.id,
            action: "DELETE",
            changes: { before: existing.entry } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
          },
        });
      });

      emitAllocationDeleted(existing.entry.id, existing.projectId, existing.entry.resourceId);
      invalidateDashboardCacheInBackground();
      checkBudgetThresholdsInBackground(ctx.db, existing.projectId);

      return { success: true };
    }),

  batchDelete: managerProcedure
    .input(z.object({ ids: z.array(z.string()).min(1).max(100) }))
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      const existing = (
        await Promise.all(input.ids.map(async (id) => findAllocationEntryOrNull(ctx.db, id)))
      ).filter((entry): entry is NonNullable<typeof entry> => Boolean(entry));

      await ctx.db.$transaction(async (tx) => {
        for (const allocation of existing) {
          await deleteAllocationEntry(
            tx as unknown as Parameters<typeof deleteAllocationEntry>[0],
            allocation,
          );
        }
        await tx.auditLog.create({
          data: {
            entityType: "Allocation",
            entityId: input.ids.join(","),
            action: "DELETE",
            changes: {
              before: existing.map((allocation) => ({
                id: allocation.entry.id,
                projectId: allocation.projectId,
              })),
            } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
          },
        });
      });

      for (const allocation of existing) {
        emitAllocationDeleted(
          allocation.entry.id,
          allocation.projectId,
          allocation.entry.resourceId,
        );
      }
      invalidateDashboardCacheInBackground();

      const affectedProjectIds = [...new Set(existing.map((allocation) => allocation.projectId))];
      for (const projectId of affectedProjectIds) {
        checkBudgetThresholdsInBackground(ctx.db, projectId);
      }

      return { count: existing.length };
    }),

  batchUpdateStatus: managerProcedure
    .input(
      z.object({
        ids: z.array(z.string()).min(1).max(100),
        status: z.nativeEnum(AllocationStatus),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      const updated = await ctx.db.$transaction(async (tx) => {
        const updatedAllocations = await Promise.all(
          input.ids.map(async (id) =>
            (
              await updateAllocationEntry(
                tx as unknown as Parameters<typeof updateAllocationEntry>[0],
                {
                  id,
                  demandRequirementUpdate: { status: input.status },
                  assignmentUpdate: { status: input.status },
                },
              )
            ).allocation,
          ),
        );

        return updatedAllocations;
      });

      await ctx.db.auditLog.create({
        data: {
          entityType: "Allocation",
          entityId: input.ids.join(","),
          action: "UPDATE",
          changes: { after: { status: input.status, ids: input.ids } },
        },
      });

      for (const allocation of updated) {
        emitAllocationUpdated({
          id: allocation.id,
          projectId: allocation.projectId,
          resourceId: allocation.resourceId,
        });
      }
      invalidateDashboardCacheInBackground();

      const affectedProjectIds = [...new Set(updated.map((allocation) => allocation.projectId))];
      for (const projectId of affectedProjectIds) {
        checkBudgetThresholdsInBackground(ctx.db, projectId);
      }

      return { count: updated.length };
    }),
};
