import { DateTime } from 'luxon'
import db from '@adonisjs/lucid/services/db'
import RelanceTask from '#models/relance_task'
import CheckinTask from '#models/checkin_task'
import * as clock from '#services/clock'
import { sendRelanceJob } from '#jobs/send_relance_job'
import { sendCheckinJob } from '#jobs/send_checkin_job'

/**
 * Démo dispatch — quand le SPA tick l'horloge virtuelle, on cherche
 * les tasks dont la date d'envoi virtuelle est dépassée et on les
 * exécute synchronement (les handlers BullMQ sont appelés directement,
 * skippant Redis — propre, aucune dépendance externe en démo).
 *
 * Les emails sont capturés (cf. captureEmailIfDemo dans mail_dispatcher),
 * pas envoyés réellement.
 *
 * Idempotent : seules les tasks `scheduled` sont prises ; les déjà
 * traitées sont ignorées. Si on tick deux fois avec la même `targetVirtualNow`,
 * la deuxième tick ne fait rien.
 */

export type FiredEvent = {
  kind: 'relance' | 'checkin'
  taskId: string
  invoiceId: string
  /** Numéro facture pour le toast UI ("Relance F-2026-0042 envoyée"). */
  invoiceNumero: string
  /** ISO du DemoCapturedEmail créé — permet de l'afficher direct. */
  capturedEmailId: string | null
  /** Le moment virtuel où l'event s'est produit. */
  firedAt: string
}

/**
 * Avance virtual_now et fire les tasks dues. Retourne les events
 * déclenchés (à afficher en UI + auto-pause de l'horloge).
 *
 * Note : on récupère les tasks AVANT de bumper virtual_now, puis on
 * ajuste leur `sendAt` virtuel pour comparer correctement. Concrètement :
 * - relance task.sendAt et checkin task.sendAt sont déjà des dates
 *   absolues (basées sur invoice.dueDate qui ne bouge pas)
 * - on les compare donc à la cible (target virtualNow)
 *
 * Ordre d'exécution : par sendAt croissant — pour que les events
 * soient présentés au user dans l'ordre temporel.
 */
export async function tickAndDispatch(
  organizationId: string,
  targetVirtualNow: DateTime
): Promise<FiredEvent[]> {
  // Met à jour virtual_now AVANT de chercher les tasks → les handlers
  // qui appellent clock.now() à l'intérieur lisent la valeur cible.
  await clock.setVirtualNow(organizationId, targetVirtualNow)

  // Tasks "à envoyer" : status scheduled + sendAt <= cible.
  const dueRelances = await RelanceTask.query()
    .where('organization_id', organizationId)
    .where('status', 'scheduled')
    .where('send_at', '<=', targetVirtualNow.toJSDate())
    .preload('planStep')
    .orderBy('send_at', 'asc')

  const dueCheckins = await CheckinTask.query()
    .where('organization_id', organizationId)
    .where('status', 'scheduled')
    .where('send_at', '<=', targetVirtualNow.toJSDate())
    .orderBy('send_at', 'asc')

  // Merge ordered par sendAt pour un récit chronologique.
  type Pending =
    | { type: 'relance'; task: RelanceTask }
    | { type: 'checkin'; task: CheckinTask; plain: string | null }

  const pending: Pending[] = []
  for (const t of dueRelances) pending.push({ type: 'relance', task: t })
  for (const t of dueCheckins) pending.push({ type: 'checkin', task: t, plain: null })
  pending.sort((a, b) => a.task.sendAt.toMillis() - b.task.sendAt.toMillis())

  const fired: FiredEvent[] = []

  for (const p of pending) {
    if (p.type === 'relance') {
      // Le job a déjà la logique complète (mark sent, bump rubis, activity).
      await sendRelanceJob({ taskId: p.task.id })

      const invoice = await db
        .from('invoices')
        .where('id', p.task.invoiceId)
        .select('id', 'numero')
        .first()
      const captured = await db
        .from('demo_captured_emails')
        .where('organization_id', organizationId)
        .whereRaw(`(meta->>'invoiceId') = ?`, [p.task.invoiceId])
        .orderBy('sent_at', 'desc')
        .first()

      fired.push({
        kind: 'relance',
        taskId: p.task.id,
        invoiceId: p.task.invoiceId,
        invoiceNumero: invoice?.numero ?? '',
        capturedEmailId: captured?.id ?? null,
        firedAt: targetVirtualNow.toISO()!,
      })
    } else {
      // Pour le checkin job, on a besoin du plain token — pas stocké
      // (on a juste le hash). En démo on regénère un "plain" volatile
      // dérivé de l'ID — les liens paid/pending n'ont pas vraiment de
      // sens ici (l'utilisateur ne va pas cliquer dessus en démo).
      // On délègue quand même à sendCheckinJob qui appellera l'envoi
      // (capturé), puis update task.status.
      await sendCheckinJob({ taskId: p.task.id, plain: 'demo-' + p.task.id })

      const invoice = await db
        .from('invoices')
        .where('id', p.task.invoiceId)
        .select('id', 'numero')
        .first()
      const captured = await db
        .from('demo_captured_emails')
        .where('organization_id', organizationId)
        .whereRaw(`(meta->>'invoiceId') = ?`, [p.task.invoiceId])
        .orderBy('sent_at', 'desc')
        .first()

      fired.push({
        kind: 'checkin',
        taskId: p.task.id,
        invoiceId: p.task.invoiceId,
        invoiceNumero: invoice?.numero ?? '',
        capturedEmailId: captured?.id ?? null,
        firedAt: targetVirtualNow.toISO()!,
      })
    }
  }

  return fired
}
