/**
 * SchedulerService — executa agendamentos de mensagens ricos.
 *
 * Tipos suportados:
 *   ONCE       — executa uma vez em scheduledAt
 *   RECURRING  — cron expression (cronExpr)
 *   EVENT      — dispara por evento (BIRTHDAY, ANNIVERSARY, SIGNUP_DAYS)
 *                verificado diariamente às 08:00 contra dados dos contatos
 *
 * O serviço verifica a cada minuto agendamentos ONCE/RECURRING vencidos.
 * Eventos são checados uma vez por dia via job separado.
 *
 * Para enviar, chama o socket Baileys diretamente via WhatsAppConnectionManager
 * (sem passar pelo HTTP — evita latência e auth overhead).
 */
import { schedule as cronSchedule } from 'node-cron'
import { Cron } from 'croner'
import { prisma } from '../../infra/database/prisma'
import { logger } from '../../config/logger'
import type { WhatsAppConnectionManager } from '../whatsapp/connection/WhatsAppConnectionManager'

type Recipient = { jid: string; name?: string; birthday?: string; anniversary?: string; signupDate?: string }

export class SchedulerService {
  private manager: WhatsAppConnectionManager
  private minuteJob: ReturnType<typeof cronSchedule> | null = null
  private eventJob:  ReturnType<typeof cronSchedule> | null = null

  constructor(manager: WhatsAppConnectionManager) {
    this.manager = manager
  }

  start() {
    // Tick a cada minuto — processa ONCE e RECURRING vencidos
    this.minuteJob = cronSchedule('* * * * *', () => {
      this.processTimeBased().catch((err) =>
        logger.error({ err }, '[Scheduler] Erro no tick de minuto')
      )
    })

    // Tick diário às 08:00 — processa agendamentos por EVENTO
    this.eventJob = cronSchedule('0 8 * * *', () => {
      this.processEventBased().catch((err) =>
        logger.error({ err }, '[Scheduler] Erro no tick de evento')
      )
    })

    logger.info('[Scheduler] Iniciado (minuto + evento diário)')
  }

  stop() {
    this.minuteJob?.stop()
    this.eventJob?.stop()
  }

  // ── Processa ONCE e RECURRING ──────────────────────────────────────────────

  private async processTimeBased() {
    const now = new Date()

    const schedules = await prisma.scheduledMessage.findMany({
      where: {
        status: 'ACTIVE',
        scheduleType: { in: ['ONCE', 'RECURRING'] },
        OR: [
          { nextRunAt: { lte: now } },
          { nextRunAt: null, scheduledAt: { lte: now }, scheduleType: 'ONCE' },
        ],
      },
    })

    for (const sched of schedules) {
      await this.executeSchedule(sched)
    }
  }

  // ── Processa EVENT (birthday, anniversary, signup) ─────────────────────────

  private async processEventBased() {
    const today = new Date()
    const mm = String(today.getMonth() + 1).padStart(2, '0')
    const dd = String(today.getDate()).padStart(2, '0')
    const todayMmDd = `${mm}-${dd}`

    const schedules = await prisma.scheduledMessage.findMany({
      where: { status: 'ACTIVE', scheduleType: 'EVENT' },
    })

    for (const sched of schedules) {
      const recipients = sched.recipients as Recipient[]
      const matched: Recipient[] = []

      for (const r of recipients) {
        if (!r.jid) continue

        if (sched.eventType === 'BIRTHDAY' && r.birthday) {
          // birthday format: "YYYY-MM-DD" or "MM-DD"
          const parts = r.birthday.split('-')
          const bMmDd = parts.length >= 3 ? `${parts[1]}-${parts[2]}` : r.birthday
          if (bMmDd === todayMmDd) matched.push(r)
        } else if (sched.eventType === 'ANNIVERSARY' && r.anniversary) {
          const parts = r.anniversary.split('-')
          const aMmDd = parts.length >= 3 ? `${parts[1]}-${parts[2]}` : r.anniversary
          if (aMmDd === todayMmDd) matched.push(r)
        } else if (sched.eventType === 'SIGNUP_DAYS' && r.signupDate) {
          // Dispara N dias após o signup — N fica em cronExpr como "30"
          const daysAfter = parseInt(sched.cronExpr ?? '0', 10)
          const signup = new Date(r.signupDate)
          const diffDays = Math.floor((today.getTime() - signup.getTime()) / 86_400_000)
          if (diffDays === daysAfter) matched.push(r)
        }
      }

      if (matched.length > 0) {
        await this.executeSchedule(sched, matched)
      }
    }
  }

  // ── Executa um agendamento ─────────────────────────────────────────────────

  private async executeSchedule(sched: any, overrideRecipients?: Recipient[]) {
    const recipients: Recipient[] = overrideRecipients ?? (sched.recipients as Recipient[])
    const sock = this.manager.getSocket(sched.instanceId)

    if (!sock) {
      logger.warn({ scheduleId: sched.id }, '[Scheduler] Socket não disponível — adiando')
      return
    }

    let sentCount = 0
    let failedCount = 0
    const errors: string[] = []

    for (const recipient of recipients) {
      if (!recipient.jid) continue
      try {
        const payload = this.buildPayload(sched.payload as any, recipient)
        await sock.sendMessage(recipient.jid, payload)
        sentCount++
        // Pequeno delay anti-ban
        await new Promise((r) => setTimeout(r, 800 + Math.random() * 400))
      } catch (err: any) {
        failedCount++
        errors.push(`${recipient.jid}: ${err?.message ?? err}`)
        logger.warn({ err, jid: recipient.jid, scheduleId: sched.id }, '[Scheduler] Falha ao enviar')
      }
    }

    // Persiste log
    await prisma.scheduledMessageLog.create({
      data: {
        scheduleId:  sched.id,
        status:      failedCount === 0 ? 'SUCCESS' : sentCount === 0 ? 'FAILED' : 'PARTIAL',
        sentCount,
        failedCount,
        error:       errors.length ? errors.join('\n') : null,
      },
    })

    // Calcula próximo nextRunAt ou marca como DONE
    if (sched.scheduleType === 'ONCE') {
      await prisma.scheduledMessage.update({
        where: { id: sched.id },
        data: {
          status:    'DONE',
          lastRunAt: new Date(),
          runCount:  { increment: 1 },
          nextRunAt: null,
        },
      })
    } else if (sched.scheduleType === 'RECURRING' && sched.cronExpr) {
      const next = this.nextCronDate(sched.cronExpr)
      await prisma.scheduledMessage.update({
        where: { id: sched.id },
        data: {
          lastRunAt: new Date(),
          nextRunAt: next,
          runCount:  { increment: 1 },
        },
      })
    } else {
      // EVENT — apenas registra execução
      await prisma.scheduledMessage.update({
        where: { id: sched.id },
        data: { lastRunAt: new Date(), runCount: { increment: 1 } },
      })
    }

    logger.info({ scheduleId: sched.id, sentCount, failedCount }, '[Scheduler] Agendamento executado')
  }

  // ── Monta payload Baileys com substituição de variáveis ────────────────────
  // Variáveis: {{name}}, {{birthday}}, etc.

  private buildPayload(raw: any, recipient: Recipient): any {
    const replace = (str: string) =>
      str
        .replace(/\{\{name\}\}/gi,        recipient.name        ?? '')
        .replace(/\{\{birthday\}\}/gi,    recipient.birthday    ?? '')
        .replace(/\{\{anniversary\}\}/gi, recipient.anniversary ?? '')

    const recurse = (node: any): any => {
      if (typeof node === 'string') return replace(node)
      if (Array.isArray(node))       return node.map(recurse)
      if (node && typeof node === 'object') {
        const out: any = {}
        for (const k of Object.keys(node)) out[k] = recurse(node[k])
        return out
      }
      return node
    }

    return recurse(raw)
  }

  // ── Calcula próxima data de um cron ───────────────────────────────────────

  private nextCronDate(expr: string): Date | null {
    try {
      const job = new Cron(expr, { paused: true })
      return job.nextRun() ?? null
    } catch {
      return null
    }
  }
}
