import {chunk} from '@crumbs/core/collections'
import {pickTagEmoji} from '@crumbs/core/emojis'
import {and, asc, eq, inArray, isNull, lte, sql} from 'drizzle-orm'

import {db, schema} from '../db/client'
import {dispatchPushMessages, type PushMessage} from '../lib/push'
import {redis} from '../lib/redis'

const TICK_MS = 60_000
const LOCK_KEY = 'jobs:reminders:lock'
const LOCK_TTL_S = 55
const SEND_BATCH = 100
const DISPATCH_BATCH = 25
const QUIET_START_HOUR = 22
const QUIET_END_HOUR = 9

// materialize auto reminders for upcoming events and planned crumbs;
// the partial unique index dedups, reschedules update unsent rows
export const generateAutoReminders = async () => {
  await db.execute(sql`
    insert into reminders (user_id, crumb_id, remind_at, kind)
    select c.author_id, c.id, e.starts_at - interval '7 days', 'week_before'
    from crumbs c
    join events e on e.id = c.event_id
    where c.status <> 'done'
      and e.status = 'scheduled'
      and e.starts_at - interval '7 days' > now()
    on conflict (user_id, crumb_id, kind) where kind <> 'manual'
    do update set remind_at = excluded.remind_at
    where reminders.sent_at is null
  `)
  await db.execute(sql`
    insert into reminders (user_id, crumb_id, remind_at, kind)
    select c.author_id, c.id,
      coalesce(e.starts_at, c.planned_for) - interval '1 day', 'day_before'
    from crumbs c
    left join events e on e.id = c.event_id
    where c.status <> 'done'
      and (e.id is null or e.status = 'scheduled')
      -- planned_for only counts while the plan is live, matching upcomingDate
      and (e.id is not null or c.status = 'planned')
      and coalesce(e.starts_at, c.planned_for) - interval '1 day' > now()
    on conflict (user_id, crumb_id, kind) where kind <> 'manual'
    do update set remind_at = excluded.remind_at
    where reminders.sent_at is null
  `)
}

// drop unsent auto reminders whose basis no longer holds
// (plan cancelled, crumb done, event unscheduled)
export const pruneStaleReminders = async () => {
  await db.execute(sql`
    delete from reminders r
    using crumbs c
    left join events e on e.id = c.event_id
    where c.id = r.crumb_id
      and r.sent_at is null
      and r.kind <> 'manual'
      and (
        c.status = 'done'
        or (
          r.kind = 'week_before'
          and (e.id is null or e.status <> 'scheduled' or e.starts_at is null)
        )
        or (
          r.kind = 'day_before'
          and (
            coalesce(e.starts_at, c.planned_for) is null
            or (e.id is not null and e.status <> 'scheduled')
            or (e.id is null and c.status <> 'planned')
          )
        )
      )
  `)
}

const localHour = (timezone: string | null): number => {
  try {
    return Number(
      new Intl.DateTimeFormat('en-US', {
        hour: 'numeric',
        hourCycle: 'h23',
        timeZone: timezone ?? 'UTC',
      }).format(new Date()),
    )
  } catch {
    return 12
  }
}

const inQuietHours = (timezone: string | null): boolean => {
  const hour = localHour(timezone)
  return hour >= QUIET_START_HOUR || hour < QUIET_END_HOUR
}

const formatStart = (date: Date): string =>
  date.toLocaleDateString('en-US', {
    weekday: 'short',
    month: 'short',
    day: 'numeric',
  })

const reminderBody = (
  kind: 'manual' | 'week_before' | 'day_before',
  startsAt: Date | null,
): string => {
  if (kind === 'day_before') return 'Coming up tomorrow'
  if (kind === 'week_before' && startsAt)
    return `Coming up ${formatStart(startsAt)}`
  return 'Your reminder'
}

const sendDueReminders = async () => {
  const due = await db
    .select({
      id: schema.reminders.id,
      kind: schema.reminders.kind,
      userId: schema.reminders.userId,
      crumbId: schema.crumbs.id,
      crumbName: schema.crumbs.name,
      category: schema.places.category,
      startsAt: schema.events.startsAt,
    })
    .from(schema.reminders)
    .innerJoin(
      schema.crumbs,
      eq(schema.crumbs.id, schema.reminders.crumbId),
    )
    .leftJoin(
      schema.events,
      eq(schema.events.id, schema.crumbs.eventId),
    )
    .leftJoin(
      schema.places,
      eq(
        schema.places.id,
        sql`coalesce(${schema.crumbs.placeId}, ${schema.events.placeId})`,
      ),
    )
    .where(
      and(
        isNull(schema.reminders.sentAt),
        lte(schema.reminders.remindAt, new Date()),
      ),
    )
    .orderBy(asc(schema.reminders.remindAt))
    .limit(SEND_BATCH)
  if (!due.length) return

  const tokens = await db
    .select()
    .from(schema.pushTokens)
    .where(
      inArray(
        schema.pushTokens.userId,
        due.map((d) => d.userId),
      ),
    )
  const awakeTokensByUser = new Map<string, string[]>()
  const userHasTokens = new Set(tokens.map((t) => t.userId))
  for (const t of tokens) {
    if (inQuietHours(t.timezone)) continue
    const list = awakeTokensByUser.get(t.userId) ?? []
    list.push(t.token)
    awakeTokensByUser.set(t.userId, list)
  }

  const dispatches = due.flatMap((reminder) => {
    const deliverable = awakeTokensByUser.get(reminder.userId) ?? []
    if (userHasTokens.has(reminder.userId) && !deliverable.length)
      return [] // all devices in quiet hours — retry next tick
    return [
      {
        reminderId: reminder.id,
        messages: deliverable.map(
          (to): PushMessage => ({
            to,
            title: `${pickTagEmoji(reminder.category ?? '')} ${reminder.crumbName}`,
            body: reminderBody(reminder.kind, reminder.startsAt),
            data: {crumbId: reminder.crumbId},
          }),
        ),
      },
    ]
  })

  // mark sentAt per dispatched batch, not once at the end: a crash mid-run
  // must not resend pushes already delivered by earlier batches
  for (const batch of chunk(dispatches, DISPATCH_BATCH)) {
    await dispatchPushMessages(batch.flatMap((d) => d.messages))
    await db
      .update(schema.reminders)
      .set({sentAt: new Date()})
      .where(
        inArray(
          schema.reminders.id,
          batch.map((d) => d.reminderId),
        ),
      )
  }
}

const tick = async () => {
  const acquired = await redis.set(LOCK_KEY, '1', {
    NX: true,
    EX: LOCK_TTL_S,
  })
  if (!acquired) return
  try {
    await generateAutoReminders()
    await pruneStaleReminders()
    await sendDueReminders()
  } catch (err) {
    console.error('reminder tick failed', err)
  }
}

export const startReminderWorker = () => {
  void tick()
  return setInterval(() => void tick(), TICK_MS)
}
