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

/** Types that consume from annual leave balance */
const BALANCE_TYPES: VacationType[] = [VacationType.ANNUAL, VacationType.OTHER];

type DbClient = Pick<
  PrismaClient,
  "vacation" | "vacationEntitlement" | "systemSettings" | "resource"
>;

export type EntitlementSnapshot = {
  id: string;
  entitledDays: number;
  carryoverDays: number;
  usedDays: number;
  pendingDays: number;
};

type VacationSnapshotCarrier = {
  startDate: Date;
  endDate: Date;
  isHalfDay: boolean;
  deductedDays: number | null;
  holidayCountryCode: string | null;
  holidayFederalState: string | null;
  holidayMetroCityName: string | null;
  holidayCalendarDates: Prisma.JsonValue | null;
  holidayLegacyPublicHolidayDates: Prisma.JsonValue | null;
};

export type ResourceHolidayContext = {
  countryCode?: string | null;
  countryName?: string | null;
  federalState?: string | null;
  metroCityName?: string | null;
  calendarHolidayStrings: string[];
  publicHolidayStrings: string[];
};

export type SyncEntitlementDeps = {
  loadResourceHolidayContext: (
    db: DbClient,
    resourceId: string,
    periodStart: Date,
    periodEnd: Date,
  ) => Promise<ResourceHolidayContext>;
  countCalendarDaysInPeriod: (
    vacation: { startDate: Date; endDate: Date; isHalfDay: boolean },
    periodStart?: Date,
    periodEnd?: Date,
  ) => number;
  countVacationChargeableDays: (args: {
    vacation: { startDate: Date; endDate: Date; isHalfDay: boolean };
    periodStart?: Date;
    periodEnd?: Date;
    countryCode?: string | null;
    federalState?: string | null;
    metroCityName?: string | null;
    calendarHolidayStrings: string[];
    publicHolidayStrings: string[];
  }) => number;
  countVacationChargeableDaysFromSnapshot: (
    vacation: VacationSnapshotCarrier,
    periodStart?: Date,
    periodEnd?: Date,
  ) => number | null;
};

function calculateCarryoverDays(entitlement: {
  entitledDays: number;
  usedDays: number;
  pendingDays: number;
}): number {
  return Math.max(0, entitlement.entitledDays - entitlement.usedDays - entitlement.pendingDays);
}

async function getOrCreateEntitlement(
  db: DbClient,
  resourceId: string,
  year: number,
  defaultDays: number,
) {
  let entitlement = await db.vacationEntitlement.findUnique({
    where: { resourceId_year: { resourceId, year } },
  });

  if (!entitlement) {
    const prevYear = await db.vacationEntitlement.findUnique({
      where: { resourceId_year: { resourceId, year: year - 1 } },
    });

    const carryover = prevYear
      ? Math.max(0, prevYear.entitledDays - prevYear.usedDays - prevYear.pendingDays)
      : 0;

    entitlement = await db.vacationEntitlement.create({
      data: {
        resourceId,
        year,
        entitledDays: defaultDays + carryover,
        carryoverDays: carryover,
        usedDays: 0,
        pendingDays: 0,
      },
    });
  }

  return entitlement;
}

async function calculateEntitlementVacationDays(
  yearStart: Date,
  yearEnd: Date,
  vacation: VacationSnapshotCarrier,
  getLegacyHolidayContext: () => Promise<ResourceHolidayContext>,
  deps: SyncEntitlementDeps,
): Promise<number> {
  const persistedDays = deps.countVacationChargeableDaysFromSnapshot(vacation, yearStart, yearEnd);
  if (persistedDays !== null) {
    return persistedDays;
  }

  const holidayContext = await getLegacyHolidayContext();
  return deps.countVacationChargeableDays({
    vacation,
    periodStart: yearStart,
    periodEnd: yearEnd,
    countryCode: holidayContext.countryCode ?? null,
    federalState: holidayContext.federalState ?? null,
    metroCityName: holidayContext.metroCityName ?? null,
    calendarHolidayStrings: holidayContext.calendarHolidayStrings,
    publicHolidayStrings: holidayContext.publicHolidayStrings,
  });
}

/**
 * Recompute used/pending days from actual vacation records and update the cached values.
 */
export async function syncEntitlement(
  db: DbClient,
  resourceId: string,
  year: number,
  defaultDays: number,
  deps: SyncEntitlementDeps,
  visitedYears: Set<number> = new Set(),
): Promise<EntitlementSnapshot> {
  if (visitedYears.has(year)) {
    throw new TRPCError({
      code: "INTERNAL_SERVER_ERROR",
      message: `Detected recursive entitlement sync for year ${year}`,
    });
  }
  visitedYears.add(year);

  let previousYearEntitlement: EntitlementSnapshot | null = await db.vacationEntitlement.findUnique(
    {
      where: { resourceId_year: { resourceId, year: year - 1 } },
    },
  );

  if (previousYearEntitlement) {
    previousYearEntitlement = await syncEntitlement(
      db,
      resourceId,
      year - 1,
      defaultDays,
      deps,
      visitedYears,
    );
  }

  const entitlement = await getOrCreateEntitlement(db, resourceId, year, defaultDays);
  const carryoverDays = previousYearEntitlement
    ? calculateCarryoverDays(previousYearEntitlement)
    : 0;
  const expectedEntitledDays = defaultDays + carryoverDays;
  const entitlementWithCarryover =
    entitlement.carryoverDays !== carryoverDays || entitlement.entitledDays !== expectedEntitledDays
      ? await db.vacationEntitlement.update({
          where: { id: entitlement.id },
          data: {
            carryoverDays,
            entitledDays: expectedEntitledDays,
          },
        })
      : entitlement;

  const yearStart = new Date(`${year}-01-01T00:00:00.000Z`);
  const yearEnd = new Date(`${year}-12-31T00:00:00.000Z`);

  const vacations = await db.vacation.findMany({
    where: {
      resourceId,
      type: { in: BALANCE_TYPES },
      startDate: { lte: yearEnd },
      endDate: { gte: yearStart },
      status: { in: [VacationStatus.APPROVED, VacationStatus.PENDING] },
    },
    select: {
      startDate: true,
      endDate: true,
      status: true,
      isHalfDay: true,
      deductedDays: true,
      holidayCountryCode: true,
      holidayFederalState: true,
      holidayMetroCityName: true,
      holidayCalendarDates: true,
      holidayLegacyPublicHolidayDates: true,
    },
  });

  let usedDays = 0;
  let pendingDays = 0;
  let legacyHolidayContextPromise: Promise<ResourceHolidayContext> | null = null;
  const getLegacyHolidayContext = async () => {
    if (!legacyHolidayContextPromise) {
      legacyHolidayContextPromise = deps.loadResourceHolidayContext(
        db,
        resourceId,
        yearStart,
        yearEnd,
      );
    }
    return legacyHolidayContextPromise;
  };

  for (const vacation of vacations) {
    const days = await calculateEntitlementVacationDays(
      yearStart,
      yearEnd,
      vacation,
      getLegacyHolidayContext,
      deps,
    );
    if (vacation.status === VacationStatus.APPROVED) {
      usedDays += days;
    } else {
      pendingDays += days;
    }
  }

  return db.vacationEntitlement.update({
    where: { id: entitlementWithCarryover.id },
    data: { usedDays, pendingDays },
  });
}
