import {
  combineDateAndTime,
  generateOccurrenceDates,
  type ScheduleDay,
  type ScheduleEntry,
  type WeekOfMonth,
} from './scheduleOccurrences'

export type EventRecurrenceType =
  | 'none'
  | 'daily'
  | 'weekly'
  | 'biweekly'
  | 'monthlyByDate'
  | 'monthlyByWeekday'
  | 'custom'

export type RecurrenceRuleFrequency =
  | 'daily'
  | 'weekly'
  | 'monthlyByDate'
  | 'monthlyByWeekday'

export interface RecurrenceRule {
  frequency: RecurrenceRuleFrequency
  weekday?: ScheduleDay | null
  weekOfMonth?: WeekOfMonth | null
  dayOfMonth?: number | null
}

// JS getDay(): 0 = Sunday ... 6 = Saturday
const WEEKDAY_FROM_INDEX: Record<number, ScheduleDay> = {
  0: 'sunday',
  1: 'monday',
  2: 'tuesday',
  3: 'wednesday',
  4: 'thursday',
  5: 'friday',
  6: 'saturday',
}

const WEEK_OF_MONTH_FROM_N: Record<number, WeekOfMonth> = {
  1: 'first',
  2: 'second',
  3: 'third',
  4: 'fourth',
}

const weekdayOf = (date: Date): ScheduleDay => WEEKDAY_FROM_INDEX[date.getDay()]

// Clamp 5th-occurrence dates (e.g. the 5th Monday of a 31-day month) to
// 'fourth'. Editors who really want "last" can pick it explicitly through
// a custom rule — auto-deriving to 'last' would surprise users whose event
// landed on a 5th occurrence only because that particular month happens to
// have one.
const weekOfMonthOf = (date: Date): WeekOfMonth => {
  const n = Math.ceil(date.getDate() / 7)
  return WEEK_OF_MONTH_FROM_N[n] ?? 'fourth'
}

/**
 * Map a single RecurrenceRule (custom-mode row) to a ScheduleEntry.
 * Returns null when a rule is missing fields its frequency requires —
 * callers drop those rows rather than throwing, so a half-filled row in
 * the admin doesn't explode the job.
 */
const ruleToEntry = (rule: RecurrenceRule): ScheduleEntry | null => {
  switch (rule.frequency) {
    case 'daily':
      return { frequency: 'daily' }
    case 'weekly':
      if (!rule.weekday) return null
      return { frequency: 'weekly', day: rule.weekday }
    case 'monthlyByDate':
      if (typeof rule.dayOfMonth !== 'number') return null
      return { frequency: 'monthlyByDate', dayOfMonth: rule.dayOfMonth }
    case 'monthlyByWeekday':
      if (!rule.weekday || !rule.weekOfMonth) return null
      return {
        frequency: 'monthlyByWeekday',
        day: rule.weekday,
        weekOfMonth: rule.weekOfMonth,
      }
    default:
      return null
  }
}

/**
 * Translate the Event doc's recurrence configuration to one or more
 * ScheduleEntry values. For shorthand modes (weekly/biweekly/monthlyByDate/
 * monthlyByWeekday) we derive the weekday/dayOfMonth/weekOfMonth from
 * `event.date` so the rule always stays in sync with the user-visible
 * Datum field. `custom` uses `recurrenceRules` verbatim.
 */
const entriesForEvent = (
  recurrenceType: EventRecurrenceType,
  recurrenceRules: RecurrenceRule[] | null | undefined,
  eventDate: Date,
): ScheduleEntry[] => {
  switch (recurrenceType) {
    case 'daily':
      return [{ frequency: 'daily' }]
    case 'weekly':
      return [{ frequency: 'weekly', day: weekdayOf(eventDate) }]
    case 'biweekly':
      return [
        {
          frequency: 'biweekly',
          day: weekdayOf(eventDate),
          biweeklyAnchor: eventDate,
        },
      ]
    case 'monthlyByDate':
      return [{ frequency: 'monthlyByDate', dayOfMonth: eventDate.getDate() }]
    case 'monthlyByWeekday':
      return [
        {
          frequency: 'monthlyByWeekday',
          day: weekdayOf(eventDate),
          weekOfMonth: weekOfMonthOf(eventDate),
        },
      ]
    case 'custom': {
      if (!Array.isArray(recurrenceRules)) return []
      const entries: ScheduleEntry[] = []
      for (const rule of recurrenceRules) {
        const entry = ruleToEntry(rule)
        if (entry) entries.push(entry)
      }
      return entries
    }
    default:
      return []
  }
}

/**
 * Materialise the set of future occurrences for an event, in local wall-
 * clock time copied from `event.date`. DST-safe by construction: we ask
 * scheduleOccurrences for calendar dates only (at local midnight) and
 * then stitch the original time-of-day onto each via combineDateAndTime.
 *
 * Returned dates are:
 *   - within [max(now, event.date), horizon] (caller supplies the horizon
 *     already clipped to event.endDate, if any)
 *   - sorted ascending
 *   - deduplicated (needed when a `custom` rule set has overlapping rules,
 *     e.g. "every Tuesday" and "3rd Tuesday")
 *
 * Returns an empty array for `recurrenceType === 'none'` — the caller
 * handles that case as a one-shot payload.create rather than going
 * through this path.
 */
export const expandEventOccurrences = (
  event: {
    date?: string | Date | null
    recurrenceType?: EventRecurrenceType | null
    recurrenceRules?: RecurrenceRule[] | null
  },
  { now, horizon }: { now: Date; horizon: Date },
): Date[] => {
  if (!event.date) return []
  const eventDate = event.date instanceof Date ? event.date : new Date(event.date)
  if (Number.isNaN(eventDate.getTime())) return []

  const recurrenceType = event.recurrenceType ?? 'none'
  if (recurrenceType === 'none') return []

  // Window lower bound: don't backfill occurrences before the event's own
  // start date, and don't return past dates relative to `now`.
  const after = new Date(Math.max(now.getTime(), eventDate.getTime()))
  if (after.getTime() > horizon.getTime()) return []

  const entries = entriesForEvent(recurrenceType, event.recurrenceRules, eventDate)
  if (entries.length === 0) return []

  const seen = new Set<number>()
  const out: Date[] = []
  for (const entry of entries) {
    const dates = generateOccurrenceDates(entry, after, horizon)
    for (const d of dates) {
      const stitched = combineDateAndTime(d, eventDate)
      const t = stitched.getTime()
      // Skip occurrences that, after time-of-day stitching, land before
      // the window's lower bound (e.g. today's entry at 09:00 when `now`
      // is 14:00) or past the horizon.
      if (t < after.getTime() || t > horizon.getTime()) continue
      if (seen.has(t)) continue
      seen.add(t)
      out.push(stitched)
    }
  }

  out.sort((a, b) => a.getTime() - b.getTime())
  return out
}
