import { VacationStatus } from "@capakraken/db";
import { getPublicHolidays, type WeekdayAvailability } from "@capakraken/shared";

const MILLISECONDS_PER_DAY = 86_400_000;

type CalendarScope = "COUNTRY" | "STATE" | "CITY";

type HolidayCalendarEntryRecord = {
  date: Date;
  isRecurringAnnual: boolean;
};

type HolidayCalendarRecord = {
  entries: HolidayCalendarEntryRecord[];
};

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

export type ResourceCapacityProfile = {
  id: string;
  availability: WeekdayAvailability;
  countryId: string | null | undefined;
  countryCode: string | null | undefined;
  federalState: string | null | undefined;
  metroCityId: string | null | undefined;
  metroCityName: string | null | undefined;
};

export type ResourceDailyAvailabilityContext = {
  absenceFractionsByDate: Map<string, number>;
  holidayDates: Set<string>;
  vacationFractionsByDate: Map<string, number>;
};

type ResourceCapacityDbClient = {
  holidayCalendar?: {
    findMany: (args: {
      where: Record<string, unknown>;
      include: { entries: true };
      orderBy: Array<Record<string, "asc" | "desc">>;
    }) => Promise<unknown[]>;
  };
  vacation?: {
    findMany: (args: {
      where: Record<string, unknown>;
      select: Record<string, boolean | Record<string, boolean>>;
    }) => Promise<unknown[]>;
  };
};

const DAY_KEYS: (keyof WeekdayAvailability)[] = [
  "sunday",
  "monday",
  "tuesday",
  "wednesday",
  "thursday",
  "friday",
  "saturday",
];

const CITY_HOLIDAY_RULES: Array<{
  countryCode: string;
  cityName: string;
  resolveDates: (year: number) => string[];
}> = [
  {
    countryCode: "DE",
    cityName: "Augsburg",
    resolveDates: (year) => [`${year}-08-08`],
  },
];

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

function normalizeCityName(cityName?: string | null): string | null {
  const normalized = cityName?.trim().toLowerCase();
  return normalized && normalized.length > 0 ? normalized : null;
}

function normalizeStateCode(stateCode?: string | null): string | null {
  const normalized = stateCode?.trim().toUpperCase();
  return normalized && normalized.length > 0 ? normalized : null;
}

export function getAvailabilityHoursForDate(
  availability: WeekdayAvailability,
  date: Date,
): number {
  const key = DAY_KEYS[date.getUTCDay()];
  return key ? (availability[key] ?? 0) : 0;
}

function listBuiltinHolidayDates(input: {
  periodStart: Date;
  periodEnd: Date;
  countryCode: string | null | undefined;
  federalState: string | null | undefined;
  metroCityName: string | null | undefined;
}): Set<string> {
  const dates = new Set<string>();
  const startIso = toIsoDate(input.periodStart);
  const endIso = toIsoDate(input.periodEnd);
  const startYear = input.periodStart.getUTCFullYear();
  const endYear = input.periodEnd.getUTCFullYear();

  if (input.countryCode === "DE") {
    for (let year = startYear; year <= endYear; year += 1) {
      for (const holiday of getPublicHolidays(year, input.federalState ?? undefined)) {
        if (holiday.date >= startIso && holiday.date <= endIso) {
          dates.add(holiday.date);
        }
      }
    }
  }

  const normalizedCityName = normalizeCityName(input.metroCityName);
  if (input.countryCode && normalizedCityName) {
    for (const rule of CITY_HOLIDAY_RULES) {
      if (
        rule.countryCode === input.countryCode
        && normalizeCityName(rule.cityName) === normalizedCityName
      ) {
        for (let year = startYear; year <= endYear; year += 1) {
          for (const date of rule.resolveDates(year)) {
            if (date >= startIso && date <= endIso) {
              dates.add(date);
            }
          }
        }
      }
    }
  }

  return dates;
}

function resolveCalendarEntryDates(
  calendars: HolidayCalendarRecord[],
  periodStart: Date,
  periodEnd: Date,
): Set<string> {
  const dates = new Set<string>();
  const startIso = toIsoDate(periodStart);
  const endIso = toIsoDate(periodEnd);
  const startYear = periodStart.getUTCFullYear();
  const endYear = periodEnd.getUTCFullYear();

  for (const calendar of calendars) {
    for (const entry of calendar.entries) {
      const baseDate = new Date(entry.date);
      for (let year = startYear; year <= endYear; year += 1) {
        const effectiveDate = entry.isRecurringAnnual
          ? new Date(Date.UTC(year, baseDate.getUTCMonth(), baseDate.getUTCDate()))
          : baseDate;
        const isoDate = toIsoDate(effectiveDate);
        if (isoDate >= startIso && isoDate <= endIso) {
          dates.add(isoDate);
        }
        if (!entry.isRecurringAnnual) {
          break;
        }
      }
    }
  }

  return dates;
}

async function loadCustomHolidayDates(
  db: ResourceCapacityDbClient,
  input: {
    periodStart: Date;
    periodEnd: Date;
    countryId: string | null | undefined;
    federalState: string | null | undefined;
    metroCityId: string | null | undefined;
  },
): Promise<Set<string>> {
  if (!input.countryId || typeof db.holidayCalendar?.findMany !== "function") {
    return new Set();
  }

  const stateCode = normalizeStateCode(input.federalState);
  const metroCityId = input.metroCityId?.trim() || null;
  const calendars = await db.holidayCalendar.findMany({
    where: {
      isActive: true,
      countryId: input.countryId,
      OR: [
        { scopeType: "COUNTRY" as CalendarScope },
        ...(stateCode ? [{ scopeType: "STATE" as CalendarScope, stateCode }] : []),
        ...(metroCityId ? [{ scopeType: "CITY" as CalendarScope, metroCityId }] : []),
      ],
    },
    include: { entries: true },
    orderBy: [{ priority: "asc" }, { createdAt: "asc" }],
  });

  return resolveCalendarEntryDates(
    calendars as HolidayCalendarRecord[],
    input.periodStart,
    input.periodEnd,
  );
}

function buildProfileKey(profile: ResourceCapacityProfile): string {
  return JSON.stringify({
    countryId: profile.countryId ?? null,
    countryCode: profile.countryCode ?? null,
    federalState: profile.federalState ?? null,
    metroCityId: profile.metroCityId ?? null,
    metroCityName: profile.metroCityName ?? null,
  });
}

export async function loadResourceDailyAvailabilityContexts(
  db: ResourceCapacityDbClient,
  resources: ResourceCapacityProfile[],
  periodStart: Date,
  periodEnd: Date,
): Promise<Map<string, ResourceDailyAvailabilityContext>> {
  const profileHolidayCache = new Map<string, Promise<Set<string>>>();
  const resourceIds = resources.map((resource) => resource.id);

  const vacations = resourceIds.length > 0 && typeof db.vacation?.findMany === "function"
    ? await db.vacation.findMany({
      where: {
        resourceId: { in: resourceIds },
        status: VacationStatus.APPROVED,
        startDate: { lte: periodEnd },
        endDate: { gte: periodStart },
      },
      select: {
        resourceId: true,
        startDate: true,
        endDate: true,
        type: true,
        isHalfDay: true,
      },
    })
    : [];

  const vacationsByResourceId = new Map<string, VacationRecord[]>();
  for (const vacation of vacations as VacationRecord[]) {
    const items = vacationsByResourceId.get(vacation.resourceId) ?? [];
    items.push(vacation);
    vacationsByResourceId.set(vacation.resourceId, items);
  }

  const contexts = new Map<string, ResourceDailyAvailabilityContext>();

  for (const resource of resources) {
    const profileKey = buildProfileKey(resource);
    const holidayPromise = profileHolidayCache.get(profileKey)
      ?? (async () => {
        const builtin = listBuiltinHolidayDates({
          periodStart,
          periodEnd,
          countryCode: resource.countryCode,
          federalState: resource.federalState,
          metroCityName: resource.metroCityName,
        });
        const custom = await loadCustomHolidayDates(db, {
          periodStart,
          periodEnd,
          countryId: resource.countryId,
          federalState: resource.federalState,
          metroCityId: resource.metroCityId,
        });
        return new Set([...builtin, ...custom]);
      })();

    if (!profileHolidayCache.has(profileKey)) {
      profileHolidayCache.set(profileKey, holidayPromise);
    }

    const holidayDates = new Set(await holidayPromise);
    const absenceFractionsByDate = new Map<string, number>();
    const vacationFractionsByDate = new Map<string, number>();
    const resourceVacations = vacationsByResourceId.get(resource.id) ?? [];

    for (const vacation of resourceVacations) {
      const overlapStart = new Date(Math.max(vacation.startDate.getTime(), periodStart.getTime()));
      const overlapEnd = new Date(Math.min(vacation.endDate.getTime(), periodEnd.getTime()));
      if (overlapStart > overlapEnd) {
        continue;
      }

      const cursor = new Date(overlapStart);
      cursor.setUTCHours(0, 0, 0, 0);
      const end = new Date(overlapEnd);
      end.setUTCHours(0, 0, 0, 0);

      while (cursor <= end) {
        const isoDate = toIsoDate(cursor);
        const fraction = vacation.isHalfDay ? 0.5 : 1;

        if (vacation.type === "PUBLIC_HOLIDAY") {
          holidayDates.add(isoDate);
        }

        if (vacation.type !== "PUBLIC_HOLIDAY") {
          const existingVacation = vacationFractionsByDate.get(isoDate) ?? 0;
          vacationFractionsByDate.set(isoDate, Math.max(existingVacation, fraction));
        }

        const existing = absenceFractionsByDate.get(isoDate) ?? 0;
        if (vacation.type === "PUBLIC_HOLIDAY" || !holidayDates.has(isoDate)) {
          absenceFractionsByDate.set(isoDate, Math.max(existing, fraction));
        }

        cursor.setUTCDate(cursor.getUTCDate() + 1);
      }
    }

    for (const isoDate of holidayDates) {
      const existing = absenceFractionsByDate.get(isoDate) ?? 0;
      absenceFractionsByDate.set(isoDate, Math.max(existing, 1));
    }

    contexts.set(resource.id, {
      absenceFractionsByDate,
      holidayDates,
      vacationFractionsByDate,
    });
  }

  return contexts;
}

function calculateDayAvailabilityFraction(
  context: ResourceDailyAvailabilityContext | undefined,
  isoDate: string,
): number {
  const fraction = context?.absenceFractionsByDate.get(isoDate) ?? 0;
  return Math.max(0, 1 - fraction);
}

export function calculateEffectiveDayAvailability(input: {
  availability: WeekdayAvailability;
  date: Date;
  context: ResourceDailyAvailabilityContext | undefined;
}): number {
  const baseHours = getAvailabilityHoursForDate(input.availability, input.date);
  if (baseHours <= 0) {
    return 0;
  }

  return baseHours * calculateDayAvailabilityFraction(input.context, toIsoDate(input.date));
}

export function calculateEffectiveAvailableHours(input: {
  availability: WeekdayAvailability;
  periodStart: Date;
  periodEnd: Date;
  context: ResourceDailyAvailabilityContext | undefined;
}): number {
  let hours = 0;
  const cursor = new Date(input.periodStart);
  cursor.setUTCHours(0, 0, 0, 0);
  const end = new Date(input.periodEnd);
  end.setUTCHours(0, 0, 0, 0);

  while (cursor <= end) {
    hours += calculateEffectiveDayAvailability({
      availability: input.availability,
      date: cursor,
      context: input.context,
    });
    cursor.setUTCDate(cursor.getUTCDate() + 1);
  }

  return hours;
}

export function countEffectiveWorkingDays(input: {
  availability: WeekdayAvailability;
  periodStart: Date;
  periodEnd: Date;
  context: ResourceDailyAvailabilityContext | undefined;
}): number {
  let days = 0;
  const cursor = new Date(input.periodStart);
  cursor.setUTCHours(0, 0, 0, 0);
  const end = new Date(input.periodEnd);
  end.setUTCHours(0, 0, 0, 0);

  while (cursor <= end) {
    if (calculateEffectiveDayAvailability({
      availability: input.availability,
      date: cursor,
      context: input.context,
    }) > 0) {
      days += 1;
    }
    cursor.setUTCDate(cursor.getUTCDate() + 1);
  }

  return days;
}

export function calculateEffectiveBookedHours(input: {
  availability: WeekdayAvailability;
  startDate: Date;
  endDate: Date;
  hoursPerDay: number;
  periodStart: Date;
  periodEnd: Date;
  context: ResourceDailyAvailabilityContext | undefined;
}): number {
  const overlapStart = new Date(Math.max(input.startDate.getTime(), input.periodStart.getTime()));
  const overlapEnd = new Date(Math.min(input.endDate.getTime(), input.periodEnd.getTime()));

  if (overlapStart > overlapEnd) {
    return 0;
  }

  let hours = 0;
  const cursor = new Date(overlapStart);
  cursor.setUTCHours(0, 0, 0, 0);
  const end = new Date(overlapEnd);
  end.setUTCHours(0, 0, 0, 0);

  while (cursor <= end) {
    const dayBaseHours = getAvailabilityHoursForDate(input.availability, cursor);
    if (dayBaseHours > 0) {
      hours += input.hoursPerDay * calculateDayAvailabilityFraction(input.context, toIsoDate(cursor));
    }
    cursor.setUTCDate(cursor.getUTCDate() + 1);
  }

  return hours;
}

export function calculateEffectiveAllocationHours(input: {
  availability: WeekdayAvailability;
  startDate: Date;
  endDate: Date;
  hoursPerDay: number;
  periodStart: Date;
  periodEnd: Date;
  context: ResourceDailyAvailabilityContext | undefined;
}): number {
  return calculateEffectiveBookedHours(input);
}

export function calculateEffectiveAllocationCostCents(input: {
  availability: WeekdayAvailability;
  startDate: Date;
  endDate: Date;
  dailyCostCents: number;
  periodStart: Date;
  periodEnd: Date;
  context: ResourceDailyAvailabilityContext | undefined;
}): number {
  let costCents = 0;
  const overlapStart = new Date(
    Math.max(input.startDate.getTime(), input.periodStart.getTime()),
  );
  const overlapEnd = new Date(
    Math.min(input.endDate.getTime(), input.periodEnd.getTime()),
  );

  if (overlapStart > overlapEnd) {
    return 0;
  }

  const cursor = new Date(overlapStart);
  cursor.setUTCHours(0, 0, 0, 0);
  const end = new Date(overlapEnd);
  end.setUTCHours(0, 0, 0, 0);

  while (cursor <= end) {
    const baseHours = getAvailabilityHoursForDate(input.availability, cursor);
    if (baseHours > 0) {
      costCents += input.dailyCostCents * calculateDayAvailabilityFraction(input.context, toIsoDate(cursor));
    }
    cursor.setUTCDate(cursor.getUTCDate() + 1);
  }

  return Math.round(costCents);
}

export function enumerateIsoDates(
  periodStart: Date,
  periodEnd: Date,
): string[] {
  const dates: string[] = [];
  const cursor = new Date(periodStart);
  cursor.setUTCHours(0, 0, 0, 0);
  const end = new Date(periodEnd);
  end.setUTCHours(0, 0, 0, 0);

  while (cursor <= end) {
    dates.push(toIsoDate(cursor));
    cursor.setTime(cursor.getTime() + MILLISECONDS_PER_DAY);
  }

  return dates;
}
