import type { Prisma, PrismaClient, VacationStatus } from "@capakraken/db";
import { TRPCError } from "@trpc/server";

type DbClient = Pick<
  PrismaClient,
  "vacation" | "user" | "resource" | "notification"
>;

export type VacationChargeableInput = {
  resourceId: string;
  type: string;
  startDate: Date;
  endDate: Date;
  isHalfDay: boolean;
};

export type ApproveVacationDeps = {
  assertVacationApprovable: (status: VacationStatus) => void;
  assertVacationStillChargeable: (
    db: DbClient,
    vacation: VacationChargeableInput,
  ) => Promise<void>;
  buildVacationApprovalWriteData: (
    db: DbClient,
    vacation: VacationChargeableInput,
  ) => Promise<Record<string, unknown>>;
  checkVacationConflicts: (
    db: DbClient,
    vacationId: string,
    actorUserId?: string,
  ) => Promise<{ warnings: string[] }>;
  buildApprovedVacationUpdateData: (input: {
    deductionSnapshotWriteData: Record<string, unknown>;
    approvedById?: string | undefined;
    approvedAt: Date;
  }) => Prisma.VacationUncheckedUpdateInput;
};

export type ApproveVacationInput = {
  id: string;
  actorUserId?: string | undefined;
};

export type ApproveVacationResult = {
  vacation: Awaited<ReturnType<PrismaClient["vacation"]["update"]>>;
  existingStatus: VacationStatus;
  warnings: string[];
};

export async function approveVacation(
  db: DbClient,
  input: ApproveVacationInput,
  deps: ApproveVacationDeps,
): Promise<ApproveVacationResult> {
  const existing = await db.vacation.findUnique({ where: { id: input.id } });
  if (!existing) {
    throw new TRPCError({ code: "NOT_FOUND", message: "Vacation not found" });
  }

  deps.assertVacationApprovable(existing.status);

  await deps.assertVacationStillChargeable(db, {
    resourceId: existing.resourceId,
    type: existing.type,
    startDate: existing.startDate,
    endDate: existing.endDate,
    isHalfDay: existing.isHalfDay,
  });

  const deductionSnapshotWriteData = await deps.buildVacationApprovalWriteData(db, {
    resourceId: existing.resourceId,
    type: existing.type,
    startDate: existing.startDate,
    endDate: existing.endDate,
    isHalfDay: existing.isHalfDay,
  });

  const conflictResult = await deps.checkVacationConflicts(
    db,
    input.id,
    input.actorUserId,
  );

  const updated = await db.vacation.update({
    where: { id: input.id },
    data: deps.buildApprovedVacationUpdateData({
      deductionSnapshotWriteData,
      approvedById: input.actorUserId,
      approvedAt: new Date(),
    }),
  });

  return {
    vacation: updated,
    existingStatus: existing.status,
    warnings: conflictResult.warnings,
  };
}

export type BatchApproveVacationDeps = {
  assertVacationStillChargeable: (
    db: DbClient,
    vacation: VacationChargeableInput,
  ) => Promise<void>;
  buildVacationApprovalWriteData: (
    db: DbClient,
    vacation: VacationChargeableInput,
  ) => Promise<Record<string, unknown>>;
  checkBatchVacationConflicts: (
    db: DbClient,
    vacationIds: string[],
    actorUserId?: string,
  ) => Promise<Map<string, string[]>>;
  buildApprovedVacationUpdateData: (input: {
    deductionSnapshotWriteData: Record<string, unknown>;
    approvedById?: string | undefined;
    approvedAt: Date;
  }) => Prisma.VacationUncheckedUpdateInput;
};

export type BatchApproveVacationInput = {
  ids: string[];
  actorUserId?: string | undefined;
};

export type BatchApproveVacationResult = {
  approved: number;
  warnings: string[];
  updatedVacations: Array<{
    id: string;
    resourceId: string;
    status: VacationStatus;
    existingVacation: Awaited<ReturnType<PrismaClient["vacation"]["update"]>>;
  }>;
};

export async function batchApproveVacations(
  db: DbClient,
  input: BatchApproveVacationInput,
  deps: BatchApproveVacationDeps,
): Promise<BatchApproveVacationResult> {
  const vacations: Array<{
    id: string;
    resourceId: string;
    type: string;
    startDate: Date;
    endDate: Date;
    isHalfDay: boolean;
  }> = await db.vacation.findMany({
    where: { id: { in: input.ids }, status: "PENDING" },
    select: {
      id: true,
      resourceId: true,
      type: true,
      startDate: true,
      endDate: true,
      isHalfDay: true,
    },
  });

  for (const vacation of vacations) {
    await deps.assertVacationStillChargeable(db, vacation);
  }

  const conflictMap = await deps.checkBatchVacationConflicts(
    db,
    vacations.map((v) => v.id),
    input.actorUserId,
  );

  const updatedVacations: BatchApproveVacationResult["updatedVacations"] = [];

  for (const vacation of vacations) {
    const deductionSnapshotWriteData = await deps.buildVacationApprovalWriteData(
      db,
      vacation,
    );

    const updated = await db.vacation.update({
      where: { id: vacation.id },
      data: deps.buildApprovedVacationUpdateData({
        deductionSnapshotWriteData,
        approvedById: input.actorUserId,
        approvedAt: new Date(),
      }),
    });

    updatedVacations.push({
      id: updated.id,
      resourceId: updated.resourceId,
      status: updated.status,
      existingVacation: updated,
    });
  }

  const warnings: string[] = [];
  for (const [, vacationWarnings] of conflictMap) {
    warnings.push(...vacationWarnings);
  }

  return { approved: vacations.length, warnings, updatedVacations };
}
