import { VacationType, VacationStatus } from "@capakraken/db";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { RESOURCE_BRIEF_SELECT } from "../db/selects.js";
import { createAuditEntry } from "../lib/audit.js";
import { loadResourceHolidayContext } from "../lib/resource-holiday-context.js";
import { countCalendarDaysInPeriod, countVacationChargeableDays } from "../lib/vacation-day-count.js";
import {
  countVacationChargeableDaysFromSnapshot,
  parseVacationSnapshotDateList,
} from "../lib/vacation-deduction-snapshot.js";
import type { TRPCContext } from "../trpc.js";
import { buildVacationPreview } from "./vacation-read-support.js";

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

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

type EntitlementReadContext = Pick<TRPCContext, "db" | "dbUser">;
type EntitlementWriteContext = Pick<TRPCContext, "db" | "dbUser">;
type EntitlementVacationStatus = "APPROVED" | "PENDING";

type EntitlementVacationExplainability = {
  type: VacationType;
  status: EntitlementVacationStatus;
  startDate: string;
  endDate: string;
  isHalfDay: boolean;
  requestedDays: number;
  deductedDays: number;
  holidayCountryCode: string | null;
  holidayCountryName: string | null;
  holidayFederalState: string | null;
  holidayMetroCityName: string | null;
  holidayCalendarDates: string[];
  holidayLegacyPublicHolidayDates: string[];
  holidayDetails: Array<{
    date: string;
    source: "CALENDAR" | "LEGACY_PUBLIC_HOLIDAY" | "CALENDAR_AND_LEGACY";
  }>;
  holidayContext: {
    countryCode: string | null;
    countryName: string | null;
    federalState: string | null;
    metroCityName: string | null;
    sources: {
      hasCalendarHolidays: boolean;
      hasLegacyPublicHolidayEntries: boolean;
    };
  };
};

export const EntitlementBalanceInputSchema = z.object({
  resourceId: z.string(),
  year: z.number().int().min(2000).max(2100).default(new Date().getFullYear()),
});

export const EntitlementGetInputSchema = z.object({
  resourceId: z.string(),
  year: z.number().int(),
});

export const EntitlementSetInputSchema = z.object({
  resourceId: z.string(),
  year: z.number().int(),
  entitledDays: z.number().min(0).max(365),
});

export const EntitlementBulkSetInputSchema = z.object({
  year: z.number().int(),
  entitledDays: z.number().min(0).max(365),
  resourceIds: z.array(z.string()).optional(),
});

export const EntitlementYearSummaryInputSchema = z.object({
  year: z.number().int().min(2000).max(2100).default(new Date().getFullYear()),
  chapter: z.string().optional(),
});

export const EntitlementYearSummaryDetailInputSchema = EntitlementYearSummaryInputSchema.extend({
  resourceName: z.string().optional(),
});

function mapBalanceDetail(resource: {
  displayName: string;
  eid: string;
}, balance: {
  year: number;
  entitledDays: number;
  carryoverDays: number;
  usedDays: number;
  pendingDays: number;
  remainingDays: number;
  sickDays: number;
  deductionSummary?: {
    formula: string;
    approvedVacationCount: number;
    pendingVacationCount: number;
    approvedRequestedDays: number;
    pendingRequestedDays: number;
    approvedDeductedDays: number;
    pendingDeductedDays: number;
    excludedHolidayDates: string[];
    holidayBasisVariants: string[];
    sources: {
      hasCalendarHolidays: boolean;
      hasLegacyPublicHolidayEntries: boolean;
    };
  };
  vacations?: EntitlementVacationExplainability[];
}) {
  return {
    resource: resource.displayName,
    eid: resource.eid,
    year: balance.year,
    entitlement: balance.entitledDays,
    carryOver: balance.carryoverDays,
    taken: balance.usedDays,
    pending: balance.pendingDays,
    remaining: balance.remainingDays,
    sickDays: balance.sickDays,
    ...(balance.deductionSummary ? { deductionSummary: balance.deductionSummary } : {}),
    ...(balance.vacations ? { vacations: balance.vacations } : {}),
  };
}

function mapYearSummaryDetail(
  year: number,
  summaries: Array<{
    displayName: string;
    eid: string;
    chapter: string | null;
    countryCode: string | null;
    countryName: string | null;
    federalState: string | null;
    metroCityName: string | null;
    entitledDays: number;
    carryoverDays: number;
    usedDays: number;
    pendingDays: number;
    remainingDays: number;
  }>,
  resourceName?: string,
) {
  const needle = resourceName?.toLowerCase();

  return summaries
    .filter((summary) => {
      if (!needle) {
        return true;
      }
      return summary.displayName.toLowerCase().includes(needle)
        || summary.eid.toLowerCase().includes(needle);
    })
    .slice(0, 50)
    .map((summary) => ({
      resource: summary.displayName,
      eid: summary.eid,
      chapter: summary.chapter ?? null,
      countryCode: summary.countryCode ?? null,
      countryName: summary.countryName ?? null,
      federalState: summary.federalState ?? null,
      metroCityName: summary.metroCityName ?? null,
      year,
      entitled: summary.entitledDays,
      carryover: summary.carryoverDays,
      used: summary.usedDays,
      pending: summary.pendingDays,
      remaining: summary.remainingDays,
    }));
}

async function readBalanceSnapshot(
  ctx: EntitlementReadContext,
  input: z.infer<typeof EntitlementBalanceInputSchema>,
) {
  if (ctx.dbUser) {
    const allowedRoles = ["ADMIN", "MANAGER", "CONTROLLER"];
    if (!allowedRoles.includes(ctx.dbUser.systemRole)) {
      const resource = await ctx.db.resource.findUnique({
        where: { id: input.resourceId },
        select: { userId: true },
      });
      if (!resource || resource.userId !== ctx.dbUser.id) {
        throw new TRPCError({
          code: "FORBIDDEN",
          message: "You can only view your own vacation balance",
        });
      }
    }
  }

  const settings = await ctx.db.systemSettings.findUnique({ where: { id: "singleton" } });
  const defaultDays = settings?.vacationDefaultDays ?? 28;
  const entitlement = await syncEntitlement(ctx.db, input.resourceId, input.year, defaultDays);

  const sickVacationsResult = await ctx.db.vacation.findMany({
    where: {
      resourceId: input.resourceId,
      type: VacationType.SICK,
      status: VacationStatus.APPROVED,
      startDate: { lte: new Date(`${input.year}-12-31T00:00:00.000Z`) },
      endDate: { gte: new Date(`${input.year}-01-01T00:00:00.000Z`) },
    },
    select: { startDate: true, endDate: true, isHalfDay: true },
  });
  const sickVacations = Array.isArray(sickVacationsResult) ? sickVacationsResult : [];
  const sickDays = sickVacations.reduce(
    (sum, vacation) => sum + countCalendarDaysInPeriod(
      vacation,
      new Date(`${input.year}-01-01T00:00:00.000Z`),
      new Date(`${input.year}-12-31T00:00:00.000Z`),
    ),
    0,
  );

  return {
    year: input.year,
    resourceId: input.resourceId,
    entitledDays: entitlement.entitledDays,
    carryoverDays: entitlement.carryoverDays,
    usedDays: entitlement.usedDays,
    pendingDays: entitlement.pendingDays,
    remainingDays: Math.max(
      0,
      entitlement.entitledDays - entitlement.usedDays - entitlement.pendingDays,
    ),
    sickDays,
  };
}

function toIsoDate(value: Date): string {
  return value.toISOString().slice(0, 10);
}

function buildEntitlementHolidayDateUnion(vacations: EntitlementVacationExplainability[]): string[] {
  return [...new Set(vacations.flatMap((vacation) => vacation.holidayDetails.map((detail) => detail.date)))].sort();
}

function filterIsoDatesToRange(isoDates: string[], startDate: Date, endDate: Date): string[] {
  const startIso = toIsoDate(startDate);
  const endIso = toIsoDate(endDate);
  return isoDates.filter((isoDate) => isoDate >= startIso && isoDate <= endIso);
}

function clampVacationPeriodToYear(
  vacation: { startDate: Date; endDate: Date },
  yearStart: Date,
  yearEnd: Date,
): { startDate: Date; endDate: Date } {
  return {
    startDate: vacation.startDate > yearStart ? vacation.startDate : yearStart,
    endDate: vacation.endDate < yearEnd ? vacation.endDate : yearEnd,
  };
}

function formatEntitlementHolidayBasis(vacation: Pick<
  EntitlementVacationExplainability,
  "holidayCountryName" | "holidayCountryCode" | "holidayFederalState" | "holidayMetroCityName"
>): string {
  return [
    vacation.holidayCountryName ?? vacation.holidayCountryCode ?? null,
    vacation.holidayFederalState ?? null,
    vacation.holidayMetroCityName ?? null,
  ].filter((value): value is string => Boolean(value)).join(" / ");
}

function hasPersistedHolidaySnapshot(vacation: {
  deductedDays: number | null;
  holidayCountryCode: string | null;
  holidayCountryName: string | null;
  holidayFederalState: string | null;
  holidayMetroCityName: string | null;
  holidayCalendarDates: import("@capakraken/db").Prisma.JsonValue | null;
  holidayLegacyPublicHolidayDates: import("@capakraken/db").Prisma.JsonValue | null;
}): boolean {
  return vacation.deductedDays != null
    || vacation.holidayCountryCode != null
    || vacation.holidayCountryName != null
    || vacation.holidayFederalState != null
    || vacation.holidayMetroCityName != null
    || vacation.holidayCalendarDates != null
    || vacation.holidayLegacyPublicHolidayDates != null;
}

function mapEntitlementVacationStatus(status: VacationStatus): EntitlementVacationStatus {
  if (status === VacationStatus.APPROVED || status === VacationStatus.PENDING) {
    return status;
  }

  throw new TRPCError({
    code: "INTERNAL_SERVER_ERROR",
    message: `Unsupported entitlement vacation status: ${status}`,
  });
}

async function readEntitlementVacationExplainability(
  ctx: EntitlementReadContext,
  input: z.infer<typeof EntitlementBalanceInputSchema>,
): Promise<EntitlementVacationExplainability[]> {
  const yearStart = new Date(`${input.year}-01-01T00:00:00.000Z`);
  const yearEnd = new Date(`${input.year}-12-31T00:00:00.000Z`);

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

  return Promise.all(vacations.map(async (vacation) => {
    const period = clampVacationPeriodToYear(vacation, yearStart, yearEnd);
    let vacationHolidayContextPromise: Promise<Awaited<ReturnType<typeof loadResourceHolidayContext>>> | null = null;
    const getVacationHolidayContext = async () => {
      if (!vacationHolidayContextPromise) {
        vacationHolidayContextPromise = loadResourceHolidayContext(
          ctx.db,
          input.resourceId,
          period.startDate,
          period.endDate,
        );
      }
      return vacationHolidayContextPromise;
    };
    const fallbackHolidayContext = await getVacationHolidayContext();
    const preview = buildVacationPreview({
      type: vacation.type,
      startDate: period.startDate,
      endDate: period.endDate,
      isHalfDay: vacation.isHalfDay,
      holidayContext: hasPersistedHolidaySnapshot(vacation)
        ? {
            countryCode: vacation.holidayCountryCode ?? fallbackHolidayContext.countryCode ?? null,
            countryName: vacation.holidayCountryName ?? fallbackHolidayContext.countryName ?? null,
            federalState: vacation.holidayFederalState ?? fallbackHolidayContext.federalState ?? null,
            metroCityName: vacation.holidayMetroCityName ?? fallbackHolidayContext.metroCityName ?? null,
            calendarHolidayStrings: filterIsoDatesToRange(
              parseVacationSnapshotDateList(vacation.holidayCalendarDates),
              period.startDate,
              period.endDate,
            ),
            publicHolidayStrings: filterIsoDatesToRange(
              parseVacationSnapshotDateList(vacation.holidayLegacyPublicHolidayDates),
              period.startDate,
              period.endDate,
            ),
          }
        : fallbackHolidayContext,
    });
    const persistedDeductedDays = countVacationChargeableDaysFromSnapshot(vacation, yearStart, yearEnd);
    return {
      type: vacation.type,
      status: mapEntitlementVacationStatus(vacation.status),
      startDate: toIsoDate(vacation.startDate),
      endDate: toIsoDate(vacation.endDate),
      isHalfDay: vacation.isHalfDay,
      requestedDays: preview.requestedDays,
      deductedDays: persistedDeductedDays ?? preview.deductedDays,
      holidayCountryCode: preview.holidayContext.countryCode,
      holidayCountryName: preview.holidayContext.countryName,
      holidayFederalState: preview.holidayContext.federalState,
      holidayMetroCityName: preview.holidayContext.metroCityName,
      holidayCalendarDates: preview.holidayDetails
        .filter((detail) => detail.source === "CALENDAR" || detail.source === "CALENDAR_AND_LEGACY")
        .map((detail) => detail.date),
      holidayLegacyPublicHolidayDates: preview.holidayDetails
        .filter((detail) => detail.source === "LEGACY_PUBLIC_HOLIDAY" || detail.source === "CALENDAR_AND_LEGACY")
        .map((detail) => detail.date),
      holidayDetails: preview.holidayDetails,
      holidayContext: preview.holidayContext,
    };
  }));
}

async function readYearSummarySnapshot(
  ctx: Pick<TRPCContext, "db">,
  input: z.infer<typeof EntitlementYearSummaryInputSchema>,
) {
  const settings = await ctx.db.systemSettings.findUnique({ where: { id: "singleton" } });
  const defaultDays = settings?.vacationDefaultDays ?? 28;

  const resources = await ctx.db.resource.findMany({
    where: {
      isActive: true,
      ...(input.chapter ? { chapter: input.chapter } : {}),
    },
    select: {
      ...RESOURCE_BRIEF_SELECT,
      chapter: true,
      federalState: true,
      country: { select: { code: true, name: true } },
      metroCity: { select: { name: true } },
    },
    orderBy: [{ chapter: "asc" }, { displayName: "asc" }],
  });

  return Promise.all(
    resources.map(async (resource) => {
      const entitlement = await syncEntitlement(ctx.db, resource.id, input.year, defaultDays);
      return {
        resourceId: resource.id,
        displayName: resource.displayName,
        eid: resource.eid,
        chapter: resource.chapter,
        countryCode: resource.country?.code ?? null,
        countryName: resource.country?.name ?? null,
        federalState: resource.federalState ?? null,
        metroCityName: resource.metroCity?.name ?? null,
        entitledDays: entitlement.entitledDays,
        carryoverDays: entitlement.carryoverDays,
        usedDays: entitlement.usedDays,
        pendingDays: entitlement.pendingDays,
        remainingDays: Math.max(
          0,
          entitlement.entitledDays - entitlement.usedDays - entitlement.pendingDays,
        ),
      };
    }),
  );
}

/**
 * Get or create an entitlement record, applying carryover from previous year if needed.
 */
async function getOrCreateEntitlement(
  db: TRPCContext["db"],
  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;
}

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

async function calculateEntitlementVacationDays(
  yearStart: Date,
  yearEnd: Date,
  vacation: {
    startDate: Date;
    endDate: Date;
    isHalfDay: boolean;
    deductedDays: number | null;
    holidayCountryCode: string | null;
    holidayFederalState: string | null;
    holidayMetroCityName: string | null;
    holidayCalendarDates: import("@capakraken/db").Prisma.JsonValue | null;
    holidayLegacyPublicHolidayDates: import("@capakraken/db").Prisma.JsonValue | null;
  },
  getLegacyHolidayContext: () => Promise<Awaited<ReturnType<typeof loadResourceHolidayContext>>>,
): Promise<number> {
  const persistedDays = countVacationChargeableDaysFromSnapshot(vacation, yearStart, yearEnd);
  if (persistedDays !== null) {
    return persistedDays;
  }

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

/**
 * Recompute used/pending days from actual vacation records and update the cached values.
 */
async function syncEntitlement(
  db: TRPCContext["db"],
  resourceId: string,
  year: number,
  defaultDays: number,
  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,
      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<Awaited<ReturnType<typeof loadResourceHolidayContext>>> | null = null;
  const getLegacyHolidayContext = async () => {
    if (!legacyHolidayContextPromise) {
      legacyHolidayContextPromise = loadResourceHolidayContext(db, resourceId, yearStart, yearEnd);
    }
    return legacyHolidayContextPromise;
  };

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

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

export async function getEntitlementBalance(
  ctx: EntitlementReadContext,
  input: z.infer<typeof EntitlementBalanceInputSchema>,
) {
  return readBalanceSnapshot(ctx, input);
}

export async function getEntitlementBalanceDetail(
  ctx: EntitlementReadContext,
  input: z.infer<typeof EntitlementBalanceInputSchema>,
) {
  const [balance, vacations] = await Promise.all([
    readBalanceSnapshot(ctx, input),
    readEntitlementVacationExplainability(ctx, input),
  ]);
  const resource = await ctx.db.resource.findUnique({
    where: { id: input.resourceId },
    select: { displayName: true, eid: true },
  });

  if (!resource) {
    throw new TRPCError({
      code: "NOT_FOUND",
      message: "Resource not found",
    });
  }

  const approvedVacations = vacations.filter((vacation) => vacation.status === VacationStatus.APPROVED);
  const pendingVacations = vacations.filter((vacation) => vacation.status === VacationStatus.PENDING);

  return mapBalanceDetail(resource, {
    ...balance,
    deductionSummary: {
      formula: "remaining = entitlement - taken - pending",
      approvedVacationCount: approvedVacations.length,
      pendingVacationCount: pendingVacations.length,
      approvedRequestedDays: approvedVacations.reduce((sum, vacation) => sum + vacation.requestedDays, 0),
      pendingRequestedDays: pendingVacations.reduce((sum, vacation) => sum + vacation.requestedDays, 0),
      approvedDeductedDays: approvedVacations.reduce((sum, vacation) => sum + vacation.deductedDays, 0),
      pendingDeductedDays: pendingVacations.reduce((sum, vacation) => sum + vacation.deductedDays, 0),
      excludedHolidayDates: buildEntitlementHolidayDateUnion(vacations),
      holidayBasisVariants: [...new Set(vacations.map(formatEntitlementHolidayBasis).filter((value) => value.length > 0))],
      sources: {
        hasCalendarHolidays: vacations.some((vacation) => vacation.holidayContext.sources.hasCalendarHolidays),
        hasLegacyPublicHolidayEntries: vacations.some((vacation) => vacation.holidayContext.sources.hasLegacyPublicHolidayEntries),
      },
    },
    vacations,
  });
}

export async function getEntitlement(
  ctx: Pick<TRPCContext, "db">,
  input: z.infer<typeof EntitlementGetInputSchema>,
) {
  const settings = await ctx.db.systemSettings.findUnique({ where: { id: "singleton" } });
  const defaultDays = settings?.vacationDefaultDays ?? 28;
  return syncEntitlement(ctx.db, input.resourceId, input.year, defaultDays);
}

export async function setEntitlement(
  ctx: EntitlementWriteContext,
  input: z.infer<typeof EntitlementSetInputSchema>,
) {
  const existing = await ctx.db.vacationEntitlement.findUnique({
    where: { resourceId_year: { resourceId: input.resourceId, year: input.year } },
  });
  if (existing) {
    const updated = await ctx.db.vacationEntitlement.update({
      where: { id: existing.id },
      data: { entitledDays: input.entitledDays },
    });

    void createAuditEntry({
      db: ctx.db,
      entityType: "VacationEntitlement",
      entityId: updated.id,
      entityName: `Entitlement ${input.resourceId} / ${input.year}`,
      action: "UPDATE",
      ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
      before: existing as unknown as Record<string, unknown>,
      after: updated as unknown as Record<string, unknown>,
      source: "ui",
      summary: `Updated entitlement from ${existing.entitledDays} to ${input.entitledDays} days (${input.year})`,
    });

    return updated;
  }

  const created = await ctx.db.vacationEntitlement.create({
    data: {
      resourceId: input.resourceId,
      year: input.year,
      entitledDays: input.entitledDays,
      carryoverDays: 0,
      usedDays: 0,
      pendingDays: 0,
    },
  });

  void createAuditEntry({
    db: ctx.db,
    entityType: "VacationEntitlement",
    entityId: created.id,
    entityName: `Entitlement ${input.resourceId} / ${input.year}`,
    action: "CREATE",
    ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
    after: created as unknown as Record<string, unknown>,
    source: "ui",
    summary: `Set entitlement to ${input.entitledDays} days (${input.year})`,
  });

  return created;
}

export async function bulkSetEntitlements(
  ctx: EntitlementWriteContext,
  input: z.infer<typeof EntitlementBulkSetInputSchema>,
) {
  const resources = await ctx.db.resource.findMany({
    where: {
      isActive: true,
      ...(input.resourceIds ? { id: { in: input.resourceIds } } : {}),
    },
    select: { id: true },
  });

  let updated = 0;
  for (const resource of resources) {
    await ctx.db.vacationEntitlement.upsert({
      where: { resourceId_year: { resourceId: resource.id, year: input.year } },
      create: {
        resourceId: resource.id,
        year: input.year,
        entitledDays: input.entitledDays,
        carryoverDays: 0,
        usedDays: 0,
        pendingDays: 0,
      },
      update: { entitledDays: input.entitledDays },
    });
    updated++;
  }

  void createAuditEntry({
    db: ctx.db,
    entityType: "VacationEntitlement",
    entityId: `bulk-${input.year}`,
    entityName: `Bulk Entitlement ${input.year}`,
    action: "UPDATE",
    ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
    after: { year: input.year, entitledDays: input.entitledDays, resourceCount: updated } as unknown as Record<string, unknown>,
    source: "ui",
    summary: `Bulk set entitlement to ${input.entitledDays} days for ${updated} resources (${input.year})`,
  });

  return { updated };
}

export async function getEntitlementYearSummary(
  ctx: Pick<TRPCContext, "db">,
  input: z.infer<typeof EntitlementYearSummaryInputSchema>,
) {
  return readYearSummarySnapshot(ctx, input);
}

export async function getEntitlementYearSummaryDetail(
  ctx: Pick<TRPCContext, "db">,
  input: z.infer<typeof EntitlementYearSummaryDetailInputSchema>,
) {
  const summaries = await readYearSummarySnapshot(ctx, {
    year: input.year,
    ...(input.chapter ? { chapter: input.chapter } : {}),
  });

  return mapYearSummaryDetail(input.year, summaries, input.resourceName);
}
