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.
 *
 * Per event we wipe every future occurrence and regenerate from the current
 * rule. Occurrences are derivative pointers to the parent event, so there
 * is no editor-edited state to preserve — rule edits and recurring→once
 * transitions stay trivially 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 (weekly 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 GenerateEventOccurrencesInput = {
  weeksAhead?: number
  eventId?: string
}

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

type EventLike = {
  id: string
  date?: 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(),
}: {
  event: EventLike
  payload: Payload
  req?: PayloadRequest
  weeksAhead?: number
  now?: Date
}): Promise<GenerateEventOccurrencesOutput> => {
  const horizon = new Date(now.getTime() + weeksAhead * MS_PER_WEEK)

  // Snapshot cancelled dates so manual cancellations survive the wipe/regen.
  // Matching by ISO date works because regen derives timestamps from the same
  // event.date stepped by whole calendar days — shi)fts 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 wipe = await payload.delete({
    collection: 'eventOccurrence',
    where: {
      and: [
        { event: { equals: event.id } },
        { date: { greater_than_equal: now.toISOString() } },
      ],
    },
    req,
  })
  let deleted = wipe.docs.length
  let created = 0
  let skipped = 0

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

  const recurrenceType = event.recurrenceType ?? 'none'

  if (recurrenceType === 'none') {
    if (eventDate.getTime() < now.getTime()) {
      return { created, deleted, skipped: skipped + 1 }
    }
    await payload.create({
      collection: 'eventOccurrence',
      data: {
        event: event.id,
        date: eventDate.toISOString(),
        cancelled: cancelledDates.has(eventDate.toISOString()),
        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()
    await payload.create({
      collection: 'eventOccurrence',
      data: {
        event: event.id,
        date: iso,
        cancelled: cancelledDates.has(iso),
        generated: true,
      },
      req,
    })
    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,
      })
      created += result.created
      deleted += result.deleted
      skipped += result.skipped
    }

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

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