import { DateTime } from 'luxon'
import type { DateOverride, MinuteInterval, WeeklyRule } from '../../schemas/availability-schedule.schema.js'

// Pure slot computation (§8.1). No I/O, no Mongoose — takes the rules + the known
// busy intervals and returns free UTC slots. All DST/offset math goes through
// Luxon; there is no manual offset arithmetic anywhere. Unit-tested across a
// Europe/Copenhagen DST boundary.

export interface SlotComputeInput {
  durationMinutes: number
  slotIntervalMinutes: number
  bufferBeforeMinutes: number
  bufferAfterMinutes: number
  minimumNoticeMinutes: number
  maximumDaysInFuture: number
  scheduleTimezone: string // IANA — availability is authored in this zone
  weeklyRules: WeeklyRule[]
  dateOverrides: DateOverride[]
  // Already-busy UTC intervals (calendar free/busy + confirmed bookings + live holds).
  busy: Array<{ startUtc: Date; endUtc: Date }>
  now: Date
  fromUtc: Date // requested window start
  toUtc: Date // requested window end (exclusive)
}

export interface ComputedSlot {
  startUtc: Date
  endUtc: Date
}

export function computeSlots(input: SlotComputeInput): ComputedSlot[] {
  const {
    durationMinutes, slotIntervalMinutes, bufferBeforeMinutes, bufferAfterMinutes,
    minimumNoticeMinutes, maximumDaysInFuture, scheduleTimezone,
    weeklyRules, dateOverrides, busy, now, fromUtc, toUtc,
  } = input

  const zone = scheduleTimezone
  const earliest = new Date(Math.max(now.getTime() + minimumNoticeMinutes * 60_000, fromUtc.getTime()))
  const horizon = new Date(now.getTime() + maximumDaysInFuture * 86_400_000)
  const latest = new Date(Math.min(horizon.getTime(), toUtc.getTime()))
  if (earliest >= latest) return []

  const overridesByDate = new Map(dateOverrides.map((o) => [o.date, o]))

  // Walk local calendar dates in the schedule zone from `earliest` to `latest`.
  // Start one day early so a slot near a tz/day boundary isn't missed.
  let cursor = DateTime.fromJSDate(earliest, { zone }).startOf('day').minus({ days: 1 })
  const end = DateTime.fromJSDate(latest, { zone }).endOf('day')
  const slots: ComputedSlot[] = []

  while (cursor <= end) {
    const isoDate = cursor.toFormat('yyyy-MM-dd')
    const intervals = intervalsForDate(cursor, isoDate, weeklyRules, overridesByDate)

    for (const intv of intervals) {
      for (
        let m = intv.startMinute;
        m + durationMinutes <= intv.endMinute;
        m += slotIntervalMinutes
      ) {
        const hour = Math.floor(m / 60)
        const minute = m % 60
        const startLocal = cursor.set({ hour, minute, second: 0, millisecond: 0 })
        // Non-existent local time (spring-forward gap): Luxon silently shifts the
        // wall clock forward rather than flagging invalid, so detect the gap by a
        // round-trip check and skip — we never offer a slot at a time that didn't
        // exist locally.
        if (!startLocal.isValid || startLocal.hour !== hour || startLocal.minute !== minute) continue
        const startUtc = startLocal.toUTC().toJSDate()
        const endUtc = new Date(startUtc.getTime() + durationMinutes * 60_000)

        if (startUtc < earliest || startUtc >= latest) continue
        if (overlapsBusy(startUtc, endUtc, bufferBeforeMinutes, bufferAfterMinutes, busy)) continue

        slots.push({ startUtc, endUtc })
      }
    }
    cursor = cursor.plus({ days: 1 })
  }

  // De-dupe (a fall-back DST hour can yield two identical local→UTC mappings) and sort.
  const seen = new Set<number>()
  return slots
    .filter((s) => (seen.has(s.startUtc.getTime()) ? false : seen.add(s.startUtc.getTime())))
    .sort((a, b) => a.startUtc.getTime() - b.startUtc.getTime())
}

function intervalsForDate(
  date: DateTime,
  isoDate: string,
  weeklyRules: WeeklyRule[],
  overrides: Map<string, DateOverride>,
): MinuteInterval[] {
  const override = overrides.get(isoDate)
  if (override) {
    if (override.isUnavailable) return []
    return override.intervals ?? []
  }
  const jsDow = date.weekday % 7 // luxon 1=Mon..7=Sun → 0=Sun..6=Sat
  return weeklyRules.filter((r) => r.dayOfWeek === jsDow).flatMap((r) => r.intervals ?? [])
}

// A candidate conflicts if the slot, padded by its before/after buffers, overlaps
// any busy interval.
function overlapsBusy(
  startUtc: Date,
  endUtc: Date,
  bufferBefore: number,
  bufferAfter: number,
  busy: Array<{ startUtc: Date; endUtc: Date }>,
): boolean {
  const padStart = startUtc.getTime() - bufferBefore * 60_000
  const padEnd = endUtc.getTime() + bufferAfter * 60_000
  for (const b of busy) {
    if (padStart < b.endUtc.getTime() && padEnd > b.startUtc.getTime()) return true
  }
  return false
}
