import type { PayloadRequest, TaskConfig } from 'payload'
import type { Payload } from 'payload'
import {
  expandEventOccurrences,
  type EventRecurrenceType,
  type RecurrenceRule,
} from '@/jobs/lib/eventRecurrence'

/**
 * Materializes each Event's recurrence rule into `eventOccurrence` rows for
 * a rolling window.
 *
 * Two modes:
 *  - `replace` (default): wipe every future occurrence and regenerate from
 *    the current rule. Used by the Events `afterChange` hook, where the rule
 *    or date may have changed — wiping keeps rule edits and recurring→once
 *    transitions trivially correct.
 *  - `append`: never delete; only create occurrences whose slot doesn't yet
 *    exist. Used by the periodic backfill job, where the rule hasn't changed
 *    and we only want to extend the window forward without churning (and
 *    re-id'ing) occurrences that are already correct.
 *
 * Weekday and time-of-day come directly from `event.date`. Calendar-date
 * generation lives in scheduleOccurrences.ts (DST-safe by construction:
 * dates are built at local midnight and then stitched with the event's
 * HH:mm).
 *
 * Exposed both as a Payload Jobs Queue task (cron backfill) and as a direct
 * function called from the Events `afterChange` hook so edits are reflected
 * immediately without waiting for a queue worker.
 */

const DEFAULT_WEEKS_AHEAD = 8
const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000

type RegenerateMode = 'replace' | 'append'

type GenerateEventOccurrencesInput = {
  weeksAhead?: number
  eventId?: string
}

type GenerateEventOccurrencesOutput = {
  created: number
  deleted: number
  skipped: number
}

type EventLike = {
  id: string
  date?: string | null
  endDateTime?: string | null
  recurrenceType?: EventRecurrenceType | null
  recurrenceRules?: RecurrenceRule[] | null
  endDate?: string | null
}

// Treat endDate as inclusive of the whole local day.
const endOfLocalDay = (iso: string): Date => {
  const d = new Date(iso)
  return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999)
}

export const regenerateOccurrencesForEvent = async ({
  event,
  payload,
  req,
  weeksAhead = DEFAULT_WEEKS_AHEAD,
  now = new Date(),
  mode = 'replace',
}: {
  event: EventLike
  payload: Payload
  req?: PayloadRequest
  weeksAhead?: number
  now?: Date
  mode?: RegenerateMode
}): Promise<GenerateEventOccurrencesOutput> => {
  const horizon = new Date(now.getTime() + weeksAhead * MS_PER_WEEK)

  // Existing future occurrences. Used to (replace) snapshot cancellations so
  // they survive the wipe, and (append) dedup so we don't recreate slots that
  // already exist. Matching by ISO date works because regen derives
  // timestamps from the same event.date stepped by whole calendar days —
  // shifts in event.date intentionally drop stale cancellations.
  const existing = await payload.find({
    collection: 'eventOccurrence',
    where: {
      and: [
        { event: { equals: event.id } },
        { date: { greater_than_equal: now.toISOString() } },
      ],
    },
    depth: 0,
    limit: 1000,
    pagination: false,
    req,
  })
  const cancelledDates = new Set(
    existing.docs
      .filter((occ) => occ.cancelled === true && typeof occ.date === 'string')
      .map((occ) => new Date(occ.date as string).toISOString()),
  )
  const existingSlots = new Set(
    existing.docs
      .filter((occ) => typeof occ.date === 'string')
      .map((occ) => new Date(occ.date as string).toISOString()),
  )

  let deleted = 0
  let created = 0
  let skipped = 0

  if (mode === 'replace') {
    // Wipe future occurrences and rebuild from the current rule. Used by the
    // Event hook, where the rule/date may have changed.
    const wipe = await payload.delete({
      collection: 'eventOccurrence',
      where: {
        and: [
          { event: { equals: event.id } },
          { date: { greater_than_equal: now.toISOString() } },
        ],
      },
      req,
    })
    deleted = wipe.docs.length
  }

  if (!event.date) {
    return { created, deleted, skipped: skipped + 1 }
  }
  const eventDate = new Date(event.date)
  if (Number.isNaN(eventDate.getTime())) {
    return { created, deleted, skipped: skipped + 1 }
  }

  // Derive each occurrence's end from the event's duration (end - start),
  // re-anchored to the occurrence's own start. A missing or non-positive
  // duration yields no end on the occurrences.
  const eventEnd = event.endDateTime ? new Date(event.endDateTime) : null
  const durationMs =
    eventEnd && !Number.isNaN(eventEnd.getTime()) && eventEnd.getTime() > eventDate.getTime()
      ? eventEnd.getTime() - eventDate.getTime()
      : null
  const endFor = (start: Date): string | null =>
    durationMs === null ? null : new Date(start.getTime() + durationMs).toISOString()

  const recurrenceType = event.recurrenceType ?? 'none'

  if (recurrenceType === 'none') {
    const iso = eventDate.toISOString()
    if (eventDate.getTime() < now.getTime()) {
      return { created, deleted, skipped: skipped + 1 }
    }
    // In append mode the slot may already exist (not wiped) — leave it alone.
    if (mode === 'append' && existingSlots.has(iso)) {
      return { created, deleted, skipped: skipped + 1 }
    }
    await payload.create({
      collection: 'eventOccurrence',
      data: {
        event: event.id,
        date: iso,
        endDateTime: endFor(eventDate),
        cancelled: cancelledDates.has(iso),
        generated: true,
      },
      req,
    })
    return { created: created + 1, deleted, skipped }
  }

  const effectiveEnd = event.endDate
    ? new Date(Math.min(horizon.getTime(), endOfLocalDay(event.endDate).getTime()))
    : horizon

  const dates = expandEventOccurrences(event, { now, horizon: effectiveEnd })
  for (const date of dates) {
    const iso = date.toISOString()
    // In append mode the future occurrences were never wiped, so leave any
    // slot that already has an occurrence alone.
    if (mode === 'append' && existingSlots.has(iso)) {
      skipped += 1
      continue
    }
    await payload.create({
      collection: 'eventOccurrence',
      data: {
        event: event.id,
        date: iso,
        endDateTime: endFor(date),
        cancelled: cancelledDates.has(iso),
        generated: true,
      },
      req,
    })
    existingSlots.add(iso)
    created += 1
  }

  return { created, deleted, skipped }
}

export const generateEventOccurrencesTask: TaskConfig<{
  input: GenerateEventOccurrencesInput
  output: GenerateEventOccurrencesOutput
}> = {
  slug: 'generateEventOccurrences',
  label: 'Veranstaltungs-Termine erzeugen',
  inputSchema: [
    { name: 'weeksAhead', type: 'number', required: false },
    { name: 'eventId', type: 'text', required: false },
  ],
  outputSchema: [
    { name: 'created', type: 'number' },
    { name: 'deleted', type: 'number' },
    { name: 'skipped', type: 'number' },
  ],
  // Staggered 30 minutes after the Worship task to avoid DB contention.
  schedule: [
    {
      cron: '0 30 3 * * 1',
      queue: 'default',
    },
  ],
  handler: async ({ input, req }) => {
    const { payload } = req
    const weeksAhead = input?.weeksAhead ?? DEFAULT_WEEKS_AHEAD
    const now = new Date()

    const eventsResult = await payload.find({
      collection: 'event',
      depth: 0,
      limit: 1000,
      pagination: false,
      where: input?.eventId ? { id: { equals: input.eventId } } : undefined,
    })

    let created = 0
    let deleted = 0
    let skipped = 0

    for (const event of eventsResult.docs) {
      const result = await regenerateOccurrencesForEvent({
        event: event as EventLike,
        payload,
        weeksAhead,
        now,
        // Periodic backfill: only extend the window forward. The rule hasn't
        // changed here, so don't wipe/recreate occurrences that are already
        // correct (that only happens via the Event hook on an actual edit).
        mode: 'append',
      })
      created += result.created
      deleted += result.deleted
      skipped += result.skipped
    }

    payload.logger.info(
      { created, deleted, skipped, weeksAhead },
      'generateEventOccurrences finished',
    )

    return {
      output: { created, deleted, skipped },
    }
  },
}
