import { DateTime } from 'luxon'
import vine from '@vinejs/vine'
import { Exception } from '@adonisjs/core/exceptions'
import type { HttpContext } from '@adonisjs/core/http'
import db from '@adonisjs/lucid/services/db'

import Organization from '#models/organization'
import DemoCapturedEmail from '#models/demo_captured_email'
import RelanceTask from '#models/relance_task'
import CheckinTask from '#models/checkin_task'
import * as clock from '#services/clock'
import { tickAndDispatch } from '#services/demo/dispatch'

const tickValidator = vine.create({
  /** ISO 8601 — la cible vers laquelle avancer. */
  virtualNow: vine.string(),
})

function requireOrgId(auth: HttpContext['auth']): string {
  const user = auth.getUserOrFail()
  if (!user.organizationId) {
    throw new Exception('Aucune organisation rattachée', { status: 404, code: 'not_found' })
  }
  return user.organizationId
}

async function requireDemoOrg(orgId: string): Promise<Organization> {
  const org = await Organization.findOrFail(orgId)
  if (!org.demoMode) {
    throw new Exception('Mode démo désactivé pour cette organisation', {
      status: 403,
      code: 'demo_disabled',
    })
  }
  return org
}

/**
 * Endpoints du mode démo.
 *
 * Garde-fou de sécurité : tous les endpoints sauf `/start` et `/end`
 * vérifient `org.demoMode = true` avant d'opérer. La prod ne peut donc
 * jamais se retrouver à exécuter du code démo par accident, même si
 * un endpoint était appelé par erreur.
 */
export default class DemoController {
  /**
   * POST /api/v1/demo/start
   *
   * Active le mode démo sur l'org du user. virtual_now = maintenant.
   * Wipe les emails capturés précédents (reset propre).
   */
  async start({ auth, response }: HttpContext) {
    const orgId = requireOrgId(auth)

    await db.transaction(async (trx) => {
      const org = await Organization.findOrFail(orgId, { client: trx })
      org.useTransaction(trx)
      org.demoMode = true
      org.virtualNow = DateTime.utc()
      await org.save()

      // Reset l'inbox démo précédente
      await DemoCapturedEmail.query({ client: trx }).where('organization_id', orgId).delete()
    })
    // Force un reload du cache clock
    await clock.setVirtualNow(orgId, DateTime.utc())

    const org = await Organization.findOrFail(orgId)
    return response.json({
      data: {
        demoMode: org.demoMode,
        virtualNow: org.virtualNow?.toISO(),
        speedFactor: org.demoSpeedFactor,
      },
    })
  }

  /**
   * POST /api/v1/demo/end
   *
   * Désactive le mode démo. virtual_now=null → clock.now() retombe sur
   * Date.now() à la prochaine lecture.
   */
  async end({ auth, response }: HttpContext) {
    const orgId = requireOrgId(auth)
    await clock.setDemoMode(orgId, false)
    return response.json({ data: { demoMode: false } })
  }

  /**
   * POST /api/v1/demo/tick { virtualNow: ISO }
   *
   * Avance virtual_now à la cible et fire les tasks dues entre l'ancien
   * et le nouveau virtual_now. Retourne les events déclenchés.
   *
   * Le client appelle cet endpoint depuis sa boucle rAF (typiquement
   * toutes les 250ms quand il joue, avec virtualNow incrémenté de
   * `speed * elapsed`).
   */
  async tick({ auth, request, response }: HttpContext) {
    const orgId = requireOrgId(auth)
    await requireDemoOrg(orgId)

    const { virtualNow } = await request.validateUsing(tickValidator)
    const target = DateTime.fromISO(virtualNow, { zone: 'utc' })
    if (!target.isValid) {
      throw new Exception('virtualNow ISO invalide', { status: 422, code: 'invalid_iso' })
    }

    const fired = await tickAndDispatch(orgId, target)
    return response.json({
      data: {
        virtualNow: target.toISO(),
        firedEvents: fired,
      },
    })
  }

  /**
   * GET /api/v1/demo/inbox
   *
   * Liste des emails capturés pour l'org démo, plus récent en tête.
   * Le SPA lit cet endpoint pour la slide-over "boîte de réception".
   */
  async inbox({ auth, response }: HttpContext) {
    const orgId = requireOrgId(auth)
    await requireDemoOrg(orgId)

    const emails = await DemoCapturedEmail.query()
      .where('organization_id', orgId)
      .orderBy('sent_at', 'desc')
      .limit(50)

    return response.json({
      data: emails.map((e) => ({
        id: e.id,
        kind: e.kind,
        from: { email: e.fromEmail, name: e.fromName },
        to: { email: e.toEmail, name: e.toName },
        replyTo: e.replyTo,
        subject: e.subject,
        body: e.body,
        meta: e.meta,
        sentAt: e.sentAt.toISO(),
      })),
    })
  }

  /**
   * GET /api/v1/demo/state
   *
   * État courant : virtualNow, prochaine task à fire (pour que le SPA
   * sache quand auto-pause), nombre d'emails en inbox.
   * Lu au mount du SPA pour reprendre une démo en cours.
   */
  async state({ auth, response }: HttpContext) {
    const orgId = requireOrgId(auth)
    const org = await Organization.findOrFail(orgId)

    if (!org.demoMode) {
      return response.json({ data: { demoMode: false } })
    }

    // Prochaine task scheduled — relance ou checkin, la plus proche.
    const nextRelance = await RelanceTask.query()
      .where('organization_id', orgId)
      .where('status', 'scheduled')
      .orderBy('send_at', 'asc')
      .first()
    const nextCheckin = await CheckinTask.query()
      .where('organization_id', orgId)
      .where('status', 'scheduled')
      .orderBy('send_at', 'asc')
      .first()

    let nextEventAt: string | null = null
    if (nextRelance && nextCheckin) {
      nextEventAt =
        nextRelance.sendAt < nextCheckin.sendAt
          ? nextRelance.sendAt.toISO()
          : nextCheckin.sendAt.toISO()
    } else if (nextRelance) {
      nextEventAt = nextRelance.sendAt.toISO()
    } else if (nextCheckin) {
      nextEventAt = nextCheckin.sendAt.toISO()
    }

    const inboxCount = await DemoCapturedEmail.query()
      .where('organization_id', orgId)
      .count('* as total')
      .first()

    return response.json({
      data: {
        demoMode: true,
        virtualNow: org.virtualNow?.toISO(),
        speedFactor: org.demoSpeedFactor,
        nextEventAt,
        inboxCount: Number((inboxCount as unknown as { $extras: { total: string } })?.$extras?.total ?? 0),
      },
    })
  }
}
