import type { PayloadRequest, TaskConfig } from 'payload'
import type { Payload } from 'payload'

import {
  combineDateAndTime,
  generateOccurrenceDates,
  type ScheduleEntry,
} from '@/jobs/lib/scheduleOccurrences'

/**
 * Payload Jobs Queue task that materializes each Church's
 * `recurringSchedule` into real `Worship` documents for the coming weeks.
 *
 * Rationale: the rest of the app (MassTimesBlock, gemeinde pages,
 * /gottesdienst/{id} detail pages) all read from the Worship collection
 * via `fetchWorship`. By generating real documents instead of rendering
 * from the schedule on the fly, we keep a single source of truth and
 * avoid duplicating rendering logic. Generated docs are normal Worship
 * documents and can be edited by hand (cancel, change celebrant, etc.).
 *
 * Triggers:
 *  - `afterChange` hook on Church (see collections/Churches.ts) calls
 *    `regenerateMassesForChurch` directly so generated Worship docs appear
 *    immediately on save — same pattern as Events/eventOccurrence.
 *  - Own `schedule` entry below runs weekly to keep the rolling window
 *    populated
 *  - Manual: call `payload.jobs.queue({ task: 'generateRecurringMasses' })`
 */

// How far into the future we materialize documents on each run.
// The weekly cron keeps this rolling window populated.
const DEFAULT_WEEKS_AHEAD = 4
const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000

// Most recent Monday at local midnight (Monday itself if `d` is a Monday).
const startOfLastMonday = (d: Date): Date => {
  const date = new Date(d.getFullYear(), d.getMonth(), d.getDate())
  const daysSinceMonday = (date.getDay() + 6) % 7 // 0=Sun..6=Sat → Mon=0
  date.setDate(date.getDate() - daysSinceMonday)
  return date
}

type GenerateRecurringMassesInput = {
  weeksAhead?: number
  churchId?: string
}

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

type ChurchLike = {
  id: string
  recurringSchedule?: unknown[] | null
}

type RegenerateMode = 'replace' | 'append'

/**
 * Materializes a single Church's `recurringSchedule` into real `Worship`
 * documents for the rolling [last Monday, horizon] window.
 *
 * Two modes:
 *  - `replace` (default): wipe every future auto-generated (`generated: true`)
 *    Worship doc for this church and rebuild from the current schedule. Used
 *    by the Church `afterChange` hook, where the schedule may have *changed* —
 *    wiping is what stops a changed/cleared schedule from leaving stale or
 *    double masses behind. Manual cancellations on generated docs are
 *    snapshotted by date and re-applied so they survive the regen.
 *  - `append`: never delete; only create occurrences whose slot doesn't yet
 *    exist. Used by the periodic backfill job, where the schedule hasn't
 *    changed and we only want to extend the rolling window forward — no point
 *    churning (and re-id'ing) docs that are already correct.
 *
 * Hand-created Worship docs (generated !== true) are never deleted in either
 * mode, and never duplicated in `append` mode.
 */
export const regenerateMassesForChurch = async ({
  church,
  payload,
  req,
  weeksAhead = DEFAULT_WEEKS_AHEAD,
  now = new Date(),
  mode = 'replace',
}: {
  church: ChurchLike
  payload: Payload
  req?: PayloadRequest
  weeksAhead?: number
  now?: Date
  mode?: RegenerateMode
}): Promise<GenerateRecurringMassesOutput> => {
  const horizon = new Date(now.getTime() + weeksAhead * MS_PER_WEEK)

  // Start the window at the most recent Monday (local midnight) rather than
  // `now`, so masses earlier in the current week are still materialized.
  const windowStart = startOfLastMonday(now)

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

  // Existing Worship docs in the window. We use these to (replace) snapshot
  // cancellations so they survive the wipe, and (append) dedup so we don't
  // recreate slots that already exist.
  const existing = await payload.find({
    collection: 'worship',
    where: {
      and: [
        { location: { equals: church.id } },
        { date: { greater_than_equal: windowStart.toISOString() } },
      ],
    },
    depth: 0,
    limit: 1000,
    pagination: false,
    req,
  })
  const existingSlots = new Set(
    existing.docs
      .filter((doc) => typeof doc.date === 'string')
      .map((doc) => new Date(doc.date as string).toISOString()),
  )
  const cancelledDates = new Set(
    existing.docs
      .filter((doc) => doc.cancelled === true && typeof doc.date === 'string')
      .map((doc) => new Date(doc.date as string).toISOString()),
  )

  if (mode === 'replace') {
    // Wipe future auto-generated docs, then regenerate from the current
    // schedule. Hand-created docs (generated !== true) are untouched.
    const wipe = await payload.delete({
      collection: 'worship',
      where: {
        and: [
          { location: { equals: church.id } },
          { generated: { equals: true } },
          { date: { greater_than_equal: windowStart.toISOString() } },
        ],
      },
      req,
    })
    deleted = wipe.docs.length
  }

  const schedule = church.recurringSchedule
  if (!Array.isArray(schedule) || schedule.length === 0) {
    return { created, deleted, skipped }
  }

  for (const rawEntry of schedule) {
    const entry = rawEntry as ScheduleEntry & {
      time?: string | Date | null
      type?: 'MASS' | 'FAMILY' | 'WORD' | 'LANGUAGE' | 'OTHER'
      defaultCelebrant?: string | null
      defaultTitle?: string | null
      defaultDescription?: string | null
    }

    // Guard: required fields may be missing if an editor saved a
    // half-filled row. Skip rather than crash the whole run.
    if (!entry.time || !entry.type) {
      skipped += 1
      continue
    }

    // Resolve the entry's recurrence pattern (weekly / biweekly /
    // monthly Nth weekday) into concrete calendar dates in the
    // [now, horizon] window. Returns dates at midnight only — we
    // combine with the time-of-day below.
    const occurrenceDates = generateOccurrenceDates(entry, windowStart, horizon)
    if (occurrenceDates.length === 0) {
      skipped += 1
      continue
    }
    const timeSource = new Date(entry.time)

    for (const occurrenceDate of occurrenceDates) {
      // Build the real Worship.date as (calendar date) + (HH:mm from
      // the schedule) using local-time components. This is the step
      // that keeps DST transitions from shifting the wall-clock hour.
      const date = combineDateAndTime(occurrenceDate, timeSource)

      // Skip anything before the window start (last Monday).
      if (date.getTime() < windowStart.getTime()) {
        skipped += 1
        continue
      }

      const iso = date.toISOString()

      // In append mode the future docs were never wiped, so leave any slot
      // that already has a Worship doc (generated or hand-created) alone.
      if (mode === 'append' && existingSlots.has(iso)) {
        skipped += 1
        continue
      }

      // `generated: true` marks this as auto-created so a later run (or a
      // replace wipe) can target it without touching manual rows. Re-apply
      // any cancellation the editor had set on this slot before the wipe.
      await payload.create({
        collection: 'worship',
        data: {
          date: iso,
          location: church.id,
          type: entry.type,
          cancelled: cancelledDates.has(iso),
          title: entry.defaultTitle || undefined,
          celebrant: entry.defaultCelebrant || undefined,
          description: entry.defaultDescription || undefined,
          generated: true,
        },
        req,
      })

      // Track within-run so two schedule entries can't double-book a slot.
      existingSlots.add(iso)
      created += 1
    }
  }

  return { created, deleted, skipped }
}

export const generateRecurringMassesTask: TaskConfig<{
  input: GenerateRecurringMassesInput
  output: GenerateRecurringMassesOutput
}> = {
  slug: 'generateRecurringMasses',
  label: 'Wiederkehrende Messzeiten erzeugen',
  inputSchema: [
    {
      name: 'weeksAhead',
      type: 'number',
      required: false,
    },
    {
      name: 'churchId',
      type: 'text',
      required: false,
    },
  ],
  outputSchema: [
    { name: 'created', type: 'number' },
    { name: 'deleted', type: 'number' },
    { name: 'skipped', type: 'number' },
  ],
  // Weekly cron to keep the rolling window populated even if nobody
  // edits a schedule. Payload's 6-field cron format starts with seconds.
  // → every Monday at 03:00 server time.
  schedule: [
    {
      cron: '0 0 3 * * 1',
      queue: 'default',
    },
  ],
  handler: async ({ input, req }) => {
    const { payload } = req
    const weeksAhead = input?.weeksAhead ?? DEFAULT_WEEKS_AHEAD
    const now = new Date()

    // Scope to a single church when invoked with a churchId, otherwise
    // process every church in one run (weekly cron backfill).
    const churchesResult = await payload.find({
      collection: 'church',
      depth: 0,
      limit: 1000,
      pagination: false,
      where: input?.churchId
        ? { id: { equals: input.churchId } }
        : undefined,
    })

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

    for (const church of churchesResult.docs) {
      // Cast needed because payload-types only exposes recurringSchedule
      // on the full Church interface and payload.find returns a looser
      // shape at depth: 0.
      const result = await regenerateMassesForChurch({
        church: church as ChurchLike,
        payload,
        req,
        weeksAhead,
        now,
        // Periodic backfill: only extend the window forward. The schedule
        // hasn't changed here, so don't wipe/recreate docs that are already
        // correct (that only happens via the Church hook on an actual edit).
        mode: 'append',
      })
      created += result.created
      deleted += result.deleted
      skipped += result.skipped
    }

    // Counts surface on the Payload Jobs admin page as the task output.
    payload.logger.info(
      { created, deleted, skipped, weeksAhead },
      'generateRecurringMasses finished',
    )

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