import {
  buildSplitAllocationReadModel,
  createAssignment,
  createDemandRequirement,
  deleteAllocationEntry,
  deleteAssignment,
  loadAllocationEntry,
  updateAllocationEntry,
  updateAssignment,
} from "@capakraken/application";
import type { PrismaClient } from "@capakraken/db";
import {
  AllocationStatus,
  CreateAllocationSchema,
  UpdateAllocationSchema,
} from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { ASSIGNMENT_INCLUDE, toIsoDate } from "./allocation-shared.js";
import {
  findAllocationEntryOrNull,
  toAssignmentUpdateInput,
  toDemandRequirementUpdateInput,
} from "./allocation-support.js";

type AllocationMutationDb = Pick<
  PrismaClient,
  "$transaction" | "assignment" | "demandRequirement" | "resource" | "auditLog"
>;

type CreateAllocationInput = z.infer<typeof CreateAllocationSchema>;
type UpdateAllocationInput = z.infer<typeof UpdateAllocationSchema>;

type EnsureAssignmentInput = {
  resourceId: string;
  projectId: string;
  startDate: Date;
  endDate: Date;
  hoursPerDay: number;
  role?: string | undefined;
};

export async function createAllocationReadModelEntry(
  tx: Parameters<typeof createAssignment>[0],
  input: CreateAllocationInput,
) {
  if (!input.resourceId) {
    const demandRequirement = await createDemandRequirement(
      tx 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,
    {
      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]!;
}

export async function ensureAssignmentRecord(
  db: Pick<PrismaClient, "$transaction" | "assignment" | "resource">,
  input: EnsureAssignmentInput,
) {
  const existing = (await 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 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 db.$transaction(async (tx) => updateAssignment(
      tx 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 } : {}),
      },
    ));

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

  const assignment = await db.$transaction(async (tx) => createAssignment(
    tx 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 } : {}),
    },
  ));

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

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

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

    return updatedAllocation;
  });

  return { existing, updated };
}

export async function deleteAssignmentWithAudit(
  db: AllocationMutationDb,
  id: string,
) {
  const existing = await findUniqueOrThrow(
    db.assignment.findUnique({
      where: { id },
      include: ASSIGNMENT_INCLUDE,
    }),
    "Assignment",
  );

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

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

  return existing;
}

export async function deleteAllocationWithAudit(
  db: AllocationMutationDb,
  id: string,
) {
  const existing = await loadAllocationEntry(db, id);

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

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

  return existing;
}

export async function batchDeleteAllocationsWithAudit(
  db: AllocationMutationDb,
  ids: string[],
) {
  const existing = (
    await Promise.all(ids.map(async (id) => findAllocationEntryOrNull(db, id)))
  ).filter((entry): entry is NonNullable<typeof entry> => Boolean(entry));

  await db.$transaction(async (tx) => {
    for (const allocation of existing) {
      await deleteAllocationEntry(
        tx as Parameters<typeof deleteAllocationEntry>[0],
        allocation,
      );
    }

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

  return existing;
}

export async function batchUpdateAllocationStatusWithAudit(
  db: AllocationMutationDb,
  input: {
    ids: string[];
    status: AllocationStatus;
  },
) {
  const updated = await db.$transaction(async (tx) => {
    const updatedAllocations = await Promise.all(
      input.ids.map(async (id) => (
        await updateAllocationEntry(
          tx as Parameters<typeof updateAllocationEntry>[0],
          {
            id,
            demandRequirementUpdate: { status: input.status },
            assignmentUpdate: { status: input.status },
          },
        )
      ).allocation),
    );

    return updatedAllocations;
  });

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

  return updated;
}
