import { getPublicHolidays, type AbsenceDay } from "@capakraken/shared";

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

type HolidayAvailabilityInput = {
  vacations: VacationLike[];
  periodStart: Date;
  periodEnd: Date;
  countryCode?: string | null | undefined;
  federalState?: string | null | undefined;
  metroCityName?: string | null | undefined;
  resolvedHolidayStrings?: string[] | undefined;
};

type HolidayAvailabilityResult = {
  absenceDateStrings: string[];
  publicHolidayStrings: string[];
  absenceDays: AbsenceDay[];
};

export type CalendarHoliday = {
  date: string;
  name: string;
  scope: "COUNTRY" | "STATE" | "CITY";
};

type CalendarScope = CalendarHoliday["scope"];

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

type HolidayCalendarRecord = {
  id: string;
  name: string;
  scopeType: CalendarScope;
  priority: number;
  createdAt?: Date;
  entries: HolidayCalendarEntryRecord[];
};

type HolidayResolverDb = {
  [key: string]: unknown;
  country?: {
    findUnique: (args: any) => any;
  };
  metroCity?: {
    findUnique: (args: any) => any;
  };
  holidayCalendar?: {
    findMany: (args: any) => any;
  };
};

type ResolvedHoliday = CalendarHoliday & {
  calendarName: string;
  priority: number;
  sourceType: "BUILTIN" | "CUSTOM";
};

export function asHolidayResolverDb(db: unknown): HolidayResolverDb {
  return db as HolidayResolverDb;
}

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

type CityHolidayRule = {
  countryCode: string;
  cityName: string;
  resolveDates: (year: number) => string[];
};

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

const SCOPE_WEIGHT: Record<CalendarScope, number> = {
  COUNTRY: 1,
  STATE: 2,
  CITY: 3,
};

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

function resolveCalendarEntries(
  calendars: HolidayCalendarRecord[],
  periodStart: Date,
  periodEnd: Date,
): ResolvedHoliday[] {
  const startYear = periodStart.getUTCFullYear();
  const endYear = periodEnd.getUTCFullYear();
  const startIso = toIsoDate(periodStart);
  const endIso = toIsoDate(periodEnd);
  const resolved = new Map<string, ResolvedHoliday>();

  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 key = toIsoDate(effectiveDate);

        if (key < startIso || key > endIso) {
          if (!entry.isRecurringAnnual) {
            break;
          }
          continue;
        }

        const candidate: ResolvedHoliday = {
          date: key,
          name: entry.name,
          scope: calendar.scopeType,
          calendarName: calendar.name,
          priority: calendar.priority,
          sourceType: "CUSTOM",
        };
        const existing = resolved.get(key);

        if (
          !existing
          || SCOPE_WEIGHT[candidate.scope] > SCOPE_WEIGHT[existing.scope]
          || (
            SCOPE_WEIGHT[candidate.scope] === SCOPE_WEIGHT[existing.scope]
            && candidate.priority > existing.priority
          )
          || (
            SCOPE_WEIGHT[candidate.scope] === SCOPE_WEIGHT[existing.scope]
            && candidate.priority === existing.priority
            && existing.sourceType === "BUILTIN"
          )
        ) {
          resolved.set(key, candidate);
        }

        if (!entry.isRecurringAnnual) {
          break;
        }
      }
    }
  }

  return [...resolved.values()].sort((left, right) => left.date.localeCompare(right.date));
}

function mergeResolvedHolidays(
  builtInHolidays: CalendarHoliday[],
  customHolidays: ResolvedHoliday[],
): ResolvedHoliday[] {
  const merged = new Map<string, ResolvedHoliday>();

  for (const holiday of builtInHolidays) {
    merged.set(holiday.date, {
      ...holiday,
      calendarName: "System",
      priority: Number.MIN_SAFE_INTEGER,
      sourceType: "BUILTIN",
    });
  }

  for (const holiday of customHolidays) {
    const existing = merged.get(holiday.date);
    if (
      !existing
      || SCOPE_WEIGHT[holiday.scope] > SCOPE_WEIGHT[existing.scope]
      || (
        SCOPE_WEIGHT[holiday.scope] === SCOPE_WEIGHT[existing.scope]
        && holiday.priority >= existing.priority
      )
    ) {
      merged.set(holiday.date, holiday);
    }
  }

  return [...merged.values()].sort((left, right) => left.date.localeCompare(right.date));
}

async function loadScopedHolidayCalendars(
  db: HolidayResolverDb,
  input: {
    countryId?: string | null | undefined;
    stateCode?: string | null | undefined;
    metroCityId?: string | null | undefined;
  },
): Promise<HolidayCalendarRecord[]> {
  if (!input.countryId || typeof db.holidayCalendar?.findMany !== "function") {
    return [];
  }

  const stateCode = normalizeStateCode(input.stateCode);
  const metroCityId = input.metroCityId?.trim() || null;

  return db.holidayCalendar.findMany({
    where: {
      isActive: true,
      countryId: input.countryId,
      OR: [
        { scopeType: "COUNTRY" },
        ...(stateCode ? [{ scopeType: "STATE" as const, stateCode }] : []),
        ...(metroCityId ? [{ scopeType: "CITY" as const, metroCityId }] : []),
      ],
    },
    include: { entries: true },
    orderBy: [{ priority: "asc" }, { createdAt: "asc" }],
  });
}

export function getCalendarHolidayStrings(
  periodStart: Date,
  periodEnd: Date,
  countryCode?: string | null,
  federalState?: string | null,
  metroCityName?: string | null,
): string[] {
  return getCalendarHolidays(
    periodStart,
    periodEnd,
    countryCode,
    federalState,
    metroCityName,
  ).map((holiday) => holiday.date);
}

export function getCalendarHolidays(
  periodStart: Date,
  periodEnd: Date,
  countryCode?: string | null,
  federalState?: string | null,
  metroCityName?: string | null,
): CalendarHoliday[] {
  const startYear = periodStart.getUTCFullYear();
  const endYear = periodEnd.getUTCFullYear();
  const holidays = new Map<string, CalendarHoliday>();

  if (countryCode === "DE") {
    for (let year = startYear; year <= endYear; year += 1) {
      for (const holiday of getPublicHolidays(year, federalState ?? undefined)) {
        if (holiday.date >= toIsoDate(periodStart) && holiday.date <= toIsoDate(periodEnd)) {
          holidays.set(holiday.date, {
            date: holiday.date,
            name: holiday.name,
            scope: holiday.federal ? "COUNTRY" : "STATE",
          });
        }
      }
    }
  }

  const normalizedCityName = normalizeCityName(metroCityName);
  if (countryCode && normalizedCityName) {
    for (const rule of CITY_HOLIDAY_RULES) {
      if (
        rule.countryCode === countryCode
        && normalizeCityName(rule.cityName) === normalizedCityName
      ) {
        for (let year = startYear; year <= endYear; year += 1) {
          for (const holidayDate of rule.resolveDates(year)) {
            if (holidayDate >= toIsoDate(periodStart) && holidayDate <= toIsoDate(periodEnd)) {
              holidays.set(holidayDate, {
                date: holidayDate,
                name: "Augsburger Friedensfest",
                scope: "CITY",
              });
            }
          }
        }
      }
    }
  }

  return [...holidays.values()].sort((left, right) => left.date.localeCompare(right.date));
}

export async function getResolvedCalendarHolidays(
  db: HolidayResolverDb,
  input: {
    periodStart: Date;
    periodEnd: Date;
    countryId?: string | null | undefined;
    countryCode?: string | null | undefined;
    federalState?: string | null | undefined;
    metroCityId?: string | null | undefined;
    metroCityName?: string | null | undefined;
  },
): Promise<ResolvedHoliday[]> {
  let countryCode = input.countryCode ?? null;
  if (!countryCode && input.countryId && typeof db.country?.findUnique === "function") {
    const country = await db.country.findUnique({
      where: { id: input.countryId },
      select: { code: true },
    });
    countryCode = country?.code ?? null;
  }

  let metroCityName = input.metroCityName ?? null;
  if (!metroCityName && input.metroCityId && typeof db.metroCity?.findUnique === "function") {
    const metroCity = await db.metroCity.findUnique({
      where: { id: input.metroCityId },
      select: { name: true },
    });
    metroCityName = metroCity?.name ?? null;
  }

  const builtIn = getCalendarHolidays(
    input.periodStart,
    input.periodEnd,
    countryCode,
    input.federalState,
    metroCityName,
  );
  const calendars = await loadScopedHolidayCalendars(db, {
    countryId: input.countryId,
    stateCode: input.federalState,
    metroCityId: input.metroCityId,
  });
  const custom = resolveCalendarEntries(calendars, input.periodStart, input.periodEnd);

  return mergeResolvedHolidays(builtIn, custom);
}

export async function getResolvedCalendarHolidayStrings(
  db: HolidayResolverDb,
  input: {
    periodStart: Date;
    periodEnd: Date;
    countryId?: string | null | undefined;
    countryCode?: string | null | undefined;
    federalState?: string | null | undefined;
    metroCityId?: string | null | undefined;
    metroCityName?: string | null | undefined;
  },
): Promise<string[]> {
  const holidays = await getResolvedCalendarHolidays(db, input);
  return holidays.map((holiday) => holiday.date);
}

export function collectHolidayAvailability(
  input: HolidayAvailabilityInput,
): HolidayAvailabilityResult {
  const periodStartIso = toIsoDate(input.periodStart);
  const periodEndIso = toIsoDate(input.periodEnd);
  const publicHolidaySet = new Set(
    input.resolvedHolidayStrings
      ? input.resolvedHolidayStrings.filter((date) => date >= periodStartIso && date <= periodEndIso)
      : getCalendarHolidayStrings(
        input.periodStart,
        input.periodEnd,
        input.countryCode,
        input.federalState,
        input.metroCityName,
      ),
  );
  const absenceDateSet = new Set<string>();
  const absenceDayMap = new Map<string, AbsenceDay>();

  for (const isoDate of publicHolidaySet) {
    absenceDayMap.set(isoDate, {
      date: new Date(`${isoDate}T00:00:00.000Z`),
      type: "PUBLIC_HOLIDAY",
    });
  }

  for (const vacation of input.vacations) {
    if (vacation.type !== "PUBLIC_HOLIDAY") {
      continue;
    }

    const overlapStart = new Date(
      Math.max(vacation.startDate.getTime(), input.periodStart.getTime()),
    );
    const overlapEnd = new Date(
      Math.min(vacation.endDate.getTime(), input.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);
      publicHolidaySet.add(isoDate);
      absenceDayMap.set(isoDate, {
        date: new Date(cursor),
        type: "PUBLIC_HOLIDAY",
        ...(vacation.isHalfDay ? { isHalfDay: true } : {}),
      });

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

  for (const vacation of input.vacations) {
    if (vacation.type === "PUBLIC_HOLIDAY") {
      continue;
    }

    const overlapStart = new Date(
      Math.max(vacation.startDate.getTime(), input.periodStart.getTime()),
    );
    const overlapEnd = new Date(
      Math.min(vacation.endDate.getTime(), input.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);

    const triggerType = vacation.type === "SICK" ? "SICK" : "VACATION";

    while (cursor <= end) {
      const isoDate = toIsoDate(cursor);
      if (!publicHolidaySet.has(isoDate)) {
        absenceDateSet.add(isoDate);
        absenceDayMap.set(isoDate, {
          date: new Date(cursor),
          type: triggerType,
          ...(vacation.isHalfDay ? { isHalfDay: true } : {}),
        });
      }

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

  return {
    absenceDateStrings: [...absenceDateSet].sort(),
    publicHolidayStrings: [...publicHolidaySet].sort(),
    absenceDays: [...absenceDayMap.values()],
  };
}
