import { calculateAllocation, validateAvailability, checkDuplicateAssignment } from "@capakraken/engine";
import type { PrismaClient, Prisma } from "@capakraken/db";
import {
  type Allocation,
  type CreateAssignmentInput,
  type WeekdayAvailability,
} from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { listAssignmentBookings } from "./list-assignment-bookings.js";

type DbClient = PrismaClient | Prisma.TransactionClient;

export const ASSIGNMENT_RELATIONS_INCLUDE = {
  resource: { select: { id: true, displayName: true, eid: true, lcrCents: true } },
  project: { select: { id: true, name: true, shortCode: true } },
  roleEntity: { select: { id: true, name: true, color: true } },
  demandRequirement: {
    select: {
      id: true,
      projectId: true,
      startDate: true,
      endDate: true,
      hoursPerDay: true,
      percentage: true,
      role: true,
      roleId: true,
      headcount: true,
      status: true,
    },
  },
} as const;

export type AssignmentWithRelations = Prisma.AssignmentGetPayload<{
  include: typeof ASSIGNMENT_RELATIONS_INCLUDE;
}>;

async function getVacationDates(db: DbClient, resourceId: string, startDate: Date, endDate: Date) {
  const vacationDates: Date[] = [];

  try {
    const vacations = await db.vacation.findMany({
      where: {
        resourceId,
        status: "APPROVED",
        startDate: { lte: endDate },
        endDate: { gte: startDate },
      },
      select: { startDate: true, endDate: true },
    });

    for (const vacation of vacations) {
      const current = new Date(vacation.startDate);
      current.setHours(0, 0, 0, 0);
      const vacationEnd = new Date(vacation.endDate);
      vacationEnd.setHours(0, 0, 0, 0);

      while (current <= vacationEnd) {
        vacationDates.push(new Date(current));
        current.setDate(current.getDate() + 1);
      }
    }
  } catch {
    // Vacation persistence may not be available in all environments yet.
  }

  return vacationDates;
}

export async function createAssignment(
  db: DbClient,
  input: CreateAssignmentInput,
): Promise<AssignmentWithRelations> {
  const project = await db.project.findUnique({ where: { id: input.projectId } });
  if (!project) {
    throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
  }

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

  if (input.demandRequirementId) {
    const demandRequirement = await db.demandRequirement.findUnique({
      where: { id: input.demandRequirementId },
      select: { id: true, projectId: true },
    });

    if (!demandRequirement) {
      throw new TRPCError({ code: "NOT_FOUND", message: "Demand requirement not found" });
    }

    if (demandRequirement.projectId !== input.projectId) {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: "Demand requirement belongs to a different project",
      });
    }
  }

  const existingBookings = await listAssignmentBookings(
    db as unknown as Parameters<typeof listAssignmentBookings>[0],
    {
      resourceIds: [input.resourceId],
    },
  );

  const duplicateResult = checkDuplicateAssignment(
    input.resourceId,
    input.projectId,
    input.startDate,
    input.endDate,
    existingBookings.map(b => ({
      id: b.id,
      resourceId: b.resourceId ?? "",
      projectId: b.projectId,
      startDate: b.startDate,
      endDate: b.endDate,
      status: b.status,
    })),
  );

  if (duplicateResult.isDuplicate) {
    throw new TRPCError({
      code: "CONFLICT",
      message: duplicateResult.message ?? "Resource is already assigned to this project with overlapping dates",
    });
  }

  const availability = resource.availability as unknown as WeekdayAvailability;
  const availabilityWindows = existingBookings.map((booking) => ({
    startDate: booking.startDate,
    endDate: booking.endDate,
    hoursPerDay: booking.hoursPerDay,
    status: booking.status,
  })) as Pick<Allocation, "startDate" | "endDate" | "hoursPerDay" | "status">[];

  const availabilityResult = validateAvailability(
    input.startDate,
    input.endDate,
    input.hoursPerDay,
    availability,
    availabilityWindows,
  );

  if (!availabilityResult.valid && availabilityResult.totalConflictDays > 5) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: `Resource has availability conflicts on ${availabilityResult.totalConflictDays} days`,
    });
  }

  const vacationDates = await getVacationDates(
    db,
    input.resourceId,
    input.startDate,
    input.endDate,
  );
  const calculation = calculateAllocation({
    lcrCents: resource.lcrCents,
    hoursPerDay: input.hoursPerDay,
    startDate: input.startDate,
    endDate: input.endDate,
    availability,
    vacationDates,
  });

  const assignment = await db.assignment.create({
    data: {
      demandRequirementId: input.demandRequirementId ?? null,
      resourceId: input.resourceId,
      projectId: input.projectId,
      startDate: input.startDate,
      endDate: input.endDate,
      hoursPerDay: input.hoursPerDay,
      percentage: input.percentage,
      role: input.role ?? null,
      roleId: input.roleId ?? null,
      dailyCostCents: input.dailyCostCents ?? calculation.dailyCostCents,
      status: input.status,
      metadata: input.metadata as unknown as Prisma.InputJsonValue,
    },
    include: ASSIGNMENT_RELATIONS_INCLUDE,
  });

  await db.auditLog.create({
    data: {
      entityType: "Assignment",
      entityId: assignment.id,
      action: "CREATE",
      changes: { after: assignment } as unknown as Prisma.InputJsonValue,
    },
  });

  return assignment;
}
