import { VacationType, VacationStatus } from "@nexus/db";
import type { Prisma, PrismaClient } from "@nexus/db";
import { toIsoDate } from "@nexus/shared";
import { TRPCError } from "@trpc/server";
import {
  type EntitlementSnapshot,
  type ResourceHolidayContext,
  type SyncEntitlementDeps,
  syncEntitlement,
} from "./sync-entitlement.js";

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

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

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;
    };
  };
};

type VacationPreviewInput = {
  type: VacationType;
  startDate: Date;
  endDate: Date;
  isHalfDay: boolean;
  holidayContext: ResourceHolidayContext;
};

type VacationPreviewResult = {
  requestedDays: number;
  deductedDays: number;
  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 type ReadEntitlementBalanceDeps = SyncEntitlementDeps & {
  buildVacationPreview: (input: VacationPreviewInput) => VacationPreviewResult;
  parseVacationSnapshotDateList: (value: Prisma.JsonValue | null | undefined) => string[];
};

export type EntitlementBalanceInput = {
  resourceId: string;
  year: number;
};

export type EntitlementYearSummaryInput = {
  year: number;
  chapter?: string | undefined;
};

export type EntitlementYearSummaryDetailInput = EntitlementYearSummaryInput & {
  resourceName?: string | undefined;
};

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 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 hasPersistedHolidaySnapshot(vacation: {
  deductedDays: number | null;
  holidayCountryCode: string | null;
  holidayCountryName: string | null;
  holidayFederalState: string | null;
  holidayMetroCityName: string | null;
  holidayCalendarDates: Prisma.JsonValue | null;
  holidayLegacyPublicHolidayDates: 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}`,
  });
}

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

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 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 } : {}),
  };
}

async function readBalanceSnapshot(
  db: DbClient,
  input: EntitlementBalanceInput,
  deps: ReadEntitlementBalanceDeps,
) {
  const settings = await db.systemSettings.findUnique({ where: { id: "singleton" } });
  const defaultDays = settings?.vacationDefaultDays ?? 28;
  const entitlement = await syncEntitlement(db, input.resourceId, input.year, defaultDays, deps);

  const sickVacationsResult = await 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 +
      deps.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,
  };
}

async function readEntitlementVacationExplainability(
  db: DbClient,
  input: EntitlementBalanceInput,
  deps: ReadEntitlementBalanceDeps,
): 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 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<ResourceHolidayContext> | null = null;
      const getVacationHolidayContext = async () => {
        if (!vacationHolidayContextPromise) {
          vacationHolidayContextPromise = deps.loadResourceHolidayContext(
            db,
            input.resourceId,
            period.startDate,
            period.endDate,
          );
        }
        return vacationHolidayContextPromise;
      };
      const fallbackHolidayContext = await getVacationHolidayContext();
      const preview = deps.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(
                deps.parseVacationSnapshotDateList(vacation.holidayCalendarDates),
                period.startDate,
                period.endDate,
              ),
              publicHolidayStrings: filterIsoDatesToRange(
                deps.parseVacationSnapshotDateList(vacation.holidayLegacyPublicHolidayDates),
                period.startDate,
                period.endDate,
              ),
            }
          : fallbackHolidayContext,
      });
      const persistedDeductedDays = deps.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(
  db: DbClient,
  input: EntitlementYearSummaryInput,
  deps: ReadEntitlementBalanceDeps,
) {
  const settings = await db.systemSettings.findUnique({ where: { id: "singleton" } });
  const defaultDays = settings?.vacationDefaultDays ?? 28;

  const resources = await db.resource.findMany({
    where: {
      isActive: true,
      ...(input.chapter ? { chapter: input.chapter } : {}),
    },
    select: {
      id: true,
      displayName: true,
      eid: true,
      lcrCents: true,
      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(db, resource.id, input.year, defaultDays, deps);
      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,
        ),
      };
    }),
  );
}

export type EntitlementBalanceResult = Awaited<ReturnType<typeof readBalanceSnapshot>>;
export type EntitlementYearSummaryRow = Awaited<ReturnType<typeof readYearSummarySnapshot>>[number];

export async function getEntitlementBalance(
  db: DbClient,
  input: EntitlementBalanceInput,
  deps: ReadEntitlementBalanceDeps,
) {
  return readBalanceSnapshot(db, input, deps);
}

export async function getEntitlementBalanceDetail(
  db: DbClient,
  input: EntitlementBalanceInput,
  deps: ReadEntitlementBalanceDeps,
) {
  const [balance, vacations] = await Promise.all([
    readBalanceSnapshot(db, input, deps),
    readEntitlementVacationExplainability(db, input, deps),
  ]);
  const resource = await 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 getEntitlementSync(
  db: DbClient,
  input: EntitlementBalanceInput,
  deps: ReadEntitlementBalanceDeps,
): Promise<EntitlementSnapshot> {
  const settings = await db.systemSettings.findUnique({ where: { id: "singleton" } });
  const defaultDays = settings?.vacationDefaultDays ?? 28;
  return syncEntitlement(db, input.resourceId, input.year, defaultDays, deps);
}

export async function getEntitlementYearSummary(
  db: DbClient,
  input: EntitlementYearSummaryInput,
  deps: ReadEntitlementBalanceDeps,
) {
  return readYearSummarySnapshot(db, input, deps);
}

export async function getEntitlementYearSummaryDetail(
  db: DbClient,
  input: EntitlementYearSummaryDetailInput,
  deps: ReadEntitlementBalanceDeps,
) {
  const summaries = await readYearSummarySnapshot(
    db,
    {
      year: input.year,
      ...(input.chapter ? { chapter: input.chapter } : {}),
    },
    deps,
  );

  const needle = input.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: input.year,
      entitled: summary.entitledDays,
      carryover: summary.carryoverDays,
      used: summary.usedDays,
      pending: summary.pendingDays,
      remaining: summary.remainingDays,
    }));
}
