/**
 * Calendar days, as `YYYY-MM-DD` strings.
 *
 * String in, string out. A date here is a day on a wall calendar in Bangkok,
 * not an instant, and the arithmetic never touches the machine's timezone. The
 * system this replaces computed dates in the process timezone and dated every
 * row a day early on a UTC host, permanently, because the unique index made the
 * mistake collide with itself instead of correcting it.
 */

export const TIME_ZONE = "Asia/Bangkok"

const ISO = /^\d{4}-\d{2}-\d{2}$/

export function isIsoDate(value: string): boolean {
  if (!ISO.test(value)) return false
  const [y, m, d] = value.split("-").map(Number)
  if (m! < 1 || m! > 12) return false
  return d! >= 1 && d! <= lastDayOfMonth(y!, m!)
}

/** The Bangkok calendar day an instant falls on. */
export function dayIn(instant: Date, timeZone = TIME_ZONE): string {
  return new Intl.DateTimeFormat("en-CA", {
    timeZone,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  }).format(instant)
}

export function lastDayOfMonth(year: number, month: number): number {
  return new Date(Date.UTC(year, month, 0)).getUTCDate()
}

/** 1 Monday to 7 Sunday, the ISO numbering the schedules are written in. */
export function isoDayOfWeek(date: string): number {
  const [y, m, d] = date.split("-").map(Number)
  const day = new Date(Date.UTC(y!, m! - 1, d!)).getUTCDay()
  return day === 0 ? 7 : day
}

export function addDays(date: string, days: number): string {
  const [y, m, d] = date.split("-").map(Number)
  const at = new Date(Date.UTC(y!, m! - 1, d! + days))
  return at.toISOString().slice(0, 10)
}

export function dateOf(year: number, month: number, day: number): string {
  const mm = String(month).padStart(2, "0")
  const dd = String(day).padStart(2, "0")
  return `${year}-${mm}-${dd}`
}

export function monthOf(date: string): { year: number; month: number } {
  const [y, m] = date.split("-").map(Number)
  return { year: y!, month: m! }
}

/** The month before, January stepping back into the previous December. */
export function previousMonth(of: { year: number; month: number }): {
  year: number
  month: number
} {
  return of.month === 1
    ? { year: of.year - 1, month: 12 }
    : { year: of.year, month: of.month - 1 }
}

/**
 * Each employee works one pattern or the other, and it decides whether a
 * Saturday instance exists for them at all.
 */
export type WorkPattern = "mon_fri" | "mon_sat"

/**
 * The week somebody works when nothing says otherwise.
 *
 * It matches the column default in `lib/db/schema.ts`, and it exists so the two
 * cannot drift apart again. They already did once: this was `mon_fri` in the
 * generator while the column said `mon_sat`, so a duty on a vacant or shared
 * position silently stopped producing work on Saturdays, on a week not one of
 * the 193 people has.
 */
export const DEFAULT_WORK_PATTERN: WorkPattern = "mon_sat"

export function isWorkingDay(
  date: string,
  pattern: WorkPattern,
  holidays: ReadonlySet<string>,
): boolean {
  if (holidays.has(date)) return false
  const day = isoDayOfWeek(date)
  if (day === 7) return false
  if (day === 6) return pattern === "mon_sat"
  return true
}

/**
 * The first working day on or after `date`. A duty fixed to the 15th does not
 * vanish because the 15th is a Sunday, it moves.
 *
 * Bounded at 30 days: a holiday calendar that swallows a month is a data
 * problem, and looping forever hides it.
 */
export function nextWorkingDay(
  date: string,
  pattern: WorkPattern,
  holidays: ReadonlySet<string>,
): string | null {
  let candidate = date
  for (let i = 0; i <= 30; i++) {
    if (isWorkingDay(candidate, pattern, holidays)) return candidate
    candidate = addDays(candidate, 1)
  }
  return null
}
