import { ConvexError, v } from "convex/values"
import { paginationOptsValidator } from "convex/server"

import { api } from "./_generated/api"
import type { Doc, Id } from "./_generated/dataModel"
import type { MutationCtx } from "./_generated/server"
import { mutation, query } from "./_generated/server"
import { requireInternal } from "./rbac"
import {
  evaluateAutomationConditions,
  type AutomationConditionGroup,
  type AutomationTrigger,
  type TicketForAutomation,
} from "./automationsEngine"
import { getTemplateByKey, normalizeFormTemplateKey } from "./ticketFormTemplates"
import { TICKET_FORM_CONFIG } from "./ticketForms.config"
import type { AutomationEmailProps } from "./reactEmail"
import { buildBaseUrl } from "./url"
import { applyChecklistTemplateToItems, type TicketChecklistItem } from "./ticketChecklist"

type AutomationEmailTarget = "AUTO" | "PORTAL" | "STAFF"

type AutomationEmailRecipient =
  | { type: "REQUESTER" }
  | { type: "ASSIGNEE" }
  | { type: "USER"; userId: Id<"users"> }
  | { type: "EMAIL"; email: string }

type AutomationAction =
  | { type: "SET_PRIORITY"; priority: string }
  | { type: "MOVE_QUEUE"; queueId: Id<"queues"> }
  | { type: "ASSIGN_TO"; assigneeId: Id<"users"> }
  | { type: "SET_FORM_TEMPLATE"; formTemplate: string | null }
  | { type: "SET_CHAT_ENABLED"; enabled: boolean }
  | { type: "ADD_INTERNAL_COMMENT"; body: string }
  | { type: "APPLY_CHECKLIST_TEMPLATE"; templateId: Id<"ticketChecklistTemplates"> }
  | {
      type: "SEND_EMAIL"
      recipients: AutomationEmailRecipient[]
      subject: string
      message: string
      ctaTarget?: AutomationEmailTarget
      ctaLabel?: string
    }

type AutomationRunStatus = "SUCCESS" | "SKIPPED" | "ERROR"

type AppliedAction = { type: string; details?: Record<string, unknown> }

const TRIGGERS: AutomationTrigger[] = [
  "TICKET_CREATED",
  "STATUS_CHANGED",
  "PRIORITY_CHANGED",
  "QUEUE_CHANGED",
  "COMMENT_ADDED",
  "TICKET_RESOLVED",
]
const TIMINGS = ["IMMEDIATE", "DELAYED"] as const

const VISIT_QUEUE_KEYWORDS = ["visita", "visitas", "in loco", "laboratório", "laboratorio", "lab"]

function normalizeTrigger(input: string): AutomationTrigger {
  const normalized = input.trim().toUpperCase()
  const trigger = TRIGGERS.find((t) => t === normalized)
  if (!trigger) {
    throw new ConvexError("Gatilho inválido")
  }
  return trigger
}

function normalizeTiming(input: string): (typeof TIMINGS)[number] {
  const normalized = input.trim().toUpperCase()
  if (!TIMINGS.includes(normalized as (typeof TIMINGS)[number])) {
    throw new ConvexError("Agendamento inválido")
  }
  return normalized as (typeof TIMINGS)[number]
}

function parseConditions(raw: unknown): AutomationConditionGroup | null {
  if (!raw) return null
  if (typeof raw !== "object" || Array.isArray(raw)) {
    throw new ConvexError("Condições inválidas")
  }
  const group = raw as Partial<AutomationConditionGroup>
  if (!group.op || (group.op !== "AND" && group.op !== "OR")) {
    throw new ConvexError("Operador de condição inválido")
  }
  if (!Array.isArray(group.conditions)) {
    throw new ConvexError("Condições inválidas")
  }
  return {
    op: group.op,
    conditions: group.conditions as AutomationConditionGroup["conditions"],
  }
}

function parseRecord(value: unknown): Record<string, unknown> | null {
  if (!value || typeof value !== "object" || Array.isArray(value)) return null
  return value as Record<string, unknown>
}

function parseAction(value: unknown): AutomationAction | null {
  const record = parseRecord(value)
  if (!record) return null
  const type = typeof record.type === "string" ? record.type.trim().toUpperCase() : ""

  if (type === "SET_PRIORITY") {
    const priority = typeof record.priority === "string" ? record.priority.trim().toUpperCase() : ""
    if (!priority) return null
    return { type: "SET_PRIORITY", priority }
  }

  if (type === "MOVE_QUEUE") {
    const queueId = typeof record.queueId === "string" ? record.queueId : ""
    if (!queueId) return null
    return { type: "MOVE_QUEUE", queueId: queueId as Id<"queues"> }
  }

  if (type === "ASSIGN_TO") {
    const assigneeId = typeof record.assigneeId === "string" ? record.assigneeId : ""
    if (!assigneeId) return null
    return { type: "ASSIGN_TO", assigneeId: assigneeId as Id<"users"> }
  }

  if (type === "SET_FORM_TEMPLATE") {
    const formTemplateRaw = record.formTemplate
    if (formTemplateRaw === null || formTemplateRaw === undefined) {
      return { type: "SET_FORM_TEMPLATE", formTemplate: null }
    }
    if (typeof formTemplateRaw !== "string") return null
    const formTemplate = formTemplateRaw.trim()
    return { type: "SET_FORM_TEMPLATE", formTemplate: formTemplate.length > 0 ? formTemplate : null }
  }

  if (type === "SET_CHAT_ENABLED") {
    if (typeof record.enabled !== "boolean") return null
    return { type: "SET_CHAT_ENABLED", enabled: record.enabled }
  }

  if (type === "ADD_INTERNAL_COMMENT") {
    const body = typeof record.body === "string" ? record.body : ""
    if (!body.trim()) return null
    return { type: "ADD_INTERNAL_COMMENT", body }
  }

  if (type === "APPLY_CHECKLIST_TEMPLATE") {
    const templateId = typeof record.templateId === "string" ? record.templateId : ""
    if (!templateId) return null
    return { type: "APPLY_CHECKLIST_TEMPLATE", templateId: templateId as Id<"ticketChecklistTemplates"> }
  }

  if (type === "SEND_EMAIL") {
    const subject = typeof record.subject === "string" ? record.subject.trim() : ""
    const message = typeof record.message === "string" ? record.message : ""
    const recipientsRaw = record.recipients

    if (!subject) return null
    if (!message.trim()) return null
    if (!Array.isArray(recipientsRaw) || recipientsRaw.length === 0) return null

    const recipients: AutomationEmailRecipient[] = []
    for (const entry of recipientsRaw) {
      const rec = parseRecord(entry)
      if (!rec) continue
      const recType = typeof rec.type === "string" ? rec.type.trim().toUpperCase() : ""
      if (recType === "REQUESTER") {
        recipients.push({ type: "REQUESTER" })
        continue
      }
      if (recType === "ASSIGNEE") {
        recipients.push({ type: "ASSIGNEE" })
        continue
      }
      if (recType === "USER") {
        const userId = typeof rec.userId === "string" ? rec.userId : ""
        if (!userId) continue
        recipients.push({ type: "USER", userId: userId as Id<"users"> })
        continue
      }
      if (recType === "EMAIL") {
        const email = typeof rec.email === "string" ? rec.email.trim() : ""
        if (!email) continue
        recipients.push({ type: "EMAIL", email })
      }
    }

    if (recipients.length === 0) return null

    const ctaTargetRaw = typeof record.ctaTarget === "string" ? record.ctaTarget.trim().toUpperCase() : ""
    const ctaTarget: AutomationEmailTarget =
      ctaTargetRaw === "PORTAL" || ctaTargetRaw === "STAFF" ? (ctaTargetRaw as AutomationEmailTarget) : "AUTO"

    const ctaLabel = typeof record.ctaLabel === "string" ? record.ctaLabel.trim() : ""

    return {
      type: "SEND_EMAIL",
      recipients,
      subject,
      message,
      ctaTarget,
      ctaLabel: ctaLabel.length > 0 ? ctaLabel : undefined,
    }
  }

  return null
}

function parseActions(raw: unknown): AutomationAction[] {
  if (!Array.isArray(raw)) {
    throw new ConvexError("Ações inválidas")
  }
  if (raw.length === 0) {
    throw new ConvexError("Adicione pelo menos uma ação")
  }
  const parsed = raw.map(parseAction).filter((item): item is AutomationAction => Boolean(item))
  if (parsed.length === 0) {
    throw new ConvexError("Ações inválidas")
  }
  return parsed
}

function mapAutomation(doc: Doc<"ticketAutomations">) {
  return {
    id: doc._id,
    name: doc.name,
    enabled: doc.enabled,
    trigger: doc.trigger,
    timing: doc.timing,
    delayMs: doc.delayMs ?? null,
    conditions: (doc.conditions ?? null) as AutomationConditionGroup | null,
    actions: doc.actions as AutomationAction[],
    runCount: doc.runCount ?? 0,
    lastRunAt: doc.lastRunAt ?? null,
    createdAt: doc.createdAt,
    updatedAt: doc.updatedAt,
  }
}

function normalizeRunStatus(value: unknown): AutomationRunStatus {
  const status = typeof value === "string" ? value.toUpperCase() : ""
  if (status === "SUCCESS" || status === "SKIPPED" || status === "ERROR") return status
  return "ERROR"
}

function normalizeAppliedActions(value: unknown): AppliedAction[] | null {
  if (!Array.isArray(value)) return null
  const parsed: AppliedAction[] = []
  for (const entry of value) {
    const record = parseRecord(entry)
    if (!record) continue
    const type = typeof record.type === "string" ? record.type : null
    if (!type) continue
    const detailsRaw = record.details
    const details =
      detailsRaw && typeof detailsRaw === "object" && !Array.isArray(detailsRaw)
        ? (detailsRaw as Record<string, unknown>)
        : undefined
    parsed.push({ type, details })
  }
  return parsed.length > 0 ? parsed : []
}

export const list = query({
  args: { tenantId: v.string(), viewerId: v.id("users") },
  handler: async (ctx, { tenantId, viewerId }) => {
    await requireInternal(ctx, viewerId, tenantId)
    const docs = await ctx.db
      .query("ticketAutomations")
      .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
      .take(200)
    return docs.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).map(mapAutomation)
  },
})

export const listRunsPaginated = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    automationId: v.optional(v.id("ticketAutomations")),
    status: v.optional(v.string()),
    paginationOpts: paginationOptsValidator,
  },
  handler: async (ctx, args) => {
    await requireInternal(ctx, args.viewerId, args.tenantId)

    const statusFilter = args.status ? args.status.trim().toUpperCase() : null
    const automationId = args.automationId ?? null

    let baseQuery
    if (automationId) {
      const automation = await ctx.db.get(automationId)
      if (!automation || automation.tenantId !== args.tenantId) {
        throw new ConvexError("Automação não encontrada")
      }
      baseQuery = ctx.db
        .query("ticketAutomationRuns")
        .withIndex("by_automation_created", (q) => q.eq("automationId", automationId))
    } else {
      baseQuery = ctx.db
        .query("ticketAutomationRuns")
        .withIndex("by_tenant_created", (q) => q.eq("tenantId", args.tenantId))
    }

    const filteredQuery =
      statusFilter && statusFilter.length > 0
        ? baseQuery.filter((q) => q.eq(q.field("status"), statusFilter))
        : baseQuery

    const paginationResult = await filteredQuery.order("desc").paginate(args.paginationOpts)

    const ticketIds = new Set<string>()
    const automationIds = new Set<string>()
    for (const run of paginationResult.page) {
      // Hardening: em caso de drift, garante que só expomos o tenant correto.
      if (run.tenantId !== args.tenantId) continue
      ticketIds.add(String(run.ticketId))
      automationIds.add(String(run.automationId))
    }

    const ticketDocs = new Map<string, Doc<"tickets"> | null>()
    const automationDocs = new Map<string, Doc<"ticketAutomations"> | null>()

    await Promise.all(
      Array.from(ticketIds).map(async (id) => {
        const doc = (await ctx.db.get(id as Id<"tickets">)) as Doc<"tickets"> | null
        ticketDocs.set(id, doc)
      })
    )
    await Promise.all(
      Array.from(automationIds).map(async (id) => {
        const doc = (await ctx.db.get(id as Id<"ticketAutomations">)) as Doc<"ticketAutomations"> | null
        automationDocs.set(id, doc)
      })
    )

    const page = paginationResult.page
      .filter((run) => run.tenantId === args.tenantId)
      .map((run) => {
        const ticket = ticketDocs.get(String(run.ticketId)) ?? null
        const automation = automationDocs.get(String(run.automationId)) ?? null
        return {
          id: run._id,
          createdAt: run.createdAt,
          status: normalizeRunStatus(run.status),
          matched: Boolean(run.matched),
          eventType: run.eventType,
          error: run.error ?? null,
          actionsApplied: normalizeAppliedActions(run.actionsApplied ?? null),
          ticket: ticket
            ? {
                id: ticket._id,
                reference: ticket.reference ?? 0,
                subject: ticket.subject ?? "",
              }
            : null,
          automation: automation
            ? {
                id: automation._id,
                name: automation.name,
              }
            : null,
        }
      })

    return {
      page,
      isDone: paginationResult.isDone,
      continueCursor: paginationResult.continueCursor,
    }
  },
})

export const getById = query({
  args: { tenantId: v.string(), viewerId: v.id("users"), automationId: v.id("ticketAutomations") },
  handler: async (ctx, { tenantId, viewerId, automationId }) => {
    await requireInternal(ctx, viewerId, tenantId)
    const doc = await ctx.db.get(automationId)
    if (!doc || doc.tenantId !== tenantId) return null
    return mapAutomation(doc)
  },
})

export const create = mutation({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    name: v.string(),
    enabled: v.boolean(),
    trigger: v.string(),
    timing: v.string(),
    delayMs: v.optional(v.number()),
    conditions: v.optional(v.any()),
    actions: v.any(),
  },
  handler: async (ctx, args) => {
    await requireInternal(ctx, args.viewerId, args.tenantId)

    const name = args.name.trim()
    if (!name) throw new ConvexError("Nome inválido")

    const trigger = normalizeTrigger(args.trigger)
    const timing = normalizeTiming(args.timing)
    const delayMs = timing === "DELAYED" ? Math.max(0, args.delayMs ?? 0) : 0
    if (timing === "DELAYED" && delayMs < 1_000) {
      throw new ConvexError("Agendamento precisa ser de pelo menos 1 segundo")
    }

    const conditions = parseConditions(args.conditions)
    const actions = parseActions(args.actions)

    const now = Date.now()
    const id = await ctx.db.insert("ticketAutomations", {
      tenantId: args.tenantId,
      name,
      enabled: Boolean(args.enabled),
      trigger,
      timing,
      delayMs: timing === "DELAYED" ? delayMs : undefined,
      conditions: conditions ?? undefined,
      actions,
      createdBy: args.viewerId,
      updatedBy: undefined,
      createdAt: now,
      updatedAt: now,
      runCount: 0,
      lastRunAt: undefined,
    })
    return { id }
  },
})

export const update = mutation({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    automationId: v.id("ticketAutomations"),
    name: v.string(),
    enabled: v.boolean(),
    trigger: v.string(),
    timing: v.string(),
    delayMs: v.optional(v.number()),
    conditions: v.optional(v.any()),
    actions: v.any(),
  },
  handler: async (ctx, args) => {
    await requireInternal(ctx, args.viewerId, args.tenantId)
    const existing = await ctx.db.get(args.automationId)
    if (!existing || existing.tenantId !== args.tenantId) {
      throw new ConvexError("Automação não encontrada")
    }

    const name = args.name.trim()
    if (!name) throw new ConvexError("Nome inválido")

    const trigger = normalizeTrigger(args.trigger)
    const timing = normalizeTiming(args.timing)
    const delayMs = timing === "DELAYED" ? Math.max(0, args.delayMs ?? 0) : 0
    if (timing === "DELAYED" && delayMs < 1_000) {
      throw new ConvexError("Agendamento precisa ser de pelo menos 1 segundo")
    }

    const conditions = parseConditions(args.conditions)
    const actions = parseActions(args.actions)

    const now = Date.now()
    await ctx.db.patch(args.automationId, {
      name,
      enabled: Boolean(args.enabled),
      trigger,
      timing,
      delayMs: timing === "DELAYED" ? delayMs : undefined,
      conditions: conditions ?? undefined,
      actions,
      updatedBy: args.viewerId,
      updatedAt: now,
    })
    return { ok: true }
  },
})

export const remove = mutation({
  args: { tenantId: v.string(), viewerId: v.id("users"), automationId: v.id("ticketAutomations") },
  handler: async (ctx, { tenantId, viewerId, automationId }) => {
    await requireInternal(ctx, viewerId, tenantId)
    const existing = await ctx.db.get(automationId)
    if (!existing || existing.tenantId !== tenantId) {
      throw new ConvexError("Automação não encontrada")
    }
    await ctx.db.delete(automationId)
    return { ok: true }
  },
})

export const toggleEnabled = mutation({
  args: { tenantId: v.string(), viewerId: v.id("users"), automationId: v.id("ticketAutomations"), enabled: v.boolean() },
  handler: async (ctx, { tenantId, viewerId, automationId, enabled }) => {
    await requireInternal(ctx, viewerId, tenantId)
    const existing = await ctx.db.get(automationId)
    if (!existing || existing.tenantId !== tenantId) {
      throw new ConvexError("Automação não encontrada")
    }
    await ctx.db.patch(automationId, { enabled: Boolean(enabled), updatedBy: viewerId, updatedAt: Date.now() })
    return { ok: true }
  },
})

export async function runTicketAutomationsForEvent(
  ctx: MutationCtx,
  params: { tenantId: string; ticketId: Id<"tickets">; eventType: AutomationTrigger }
) {
  try {
    const { tenantId, ticketId, eventType } = params

    const ticket = await ctx.db.get(ticketId)
    if (!ticket || ticket.tenantId !== tenantId) return

    const automations = await ctx.db
      .query("ticketAutomations")
      .withIndex("by_tenant_trigger", (q) => q.eq("tenantId", tenantId).eq("trigger", eventType))
      .filter((q) => q.eq(q.field("enabled"), true))
      .take(200)

    if (automations.length === 0) return

    for (const automation of automations) {
      const conditions = (automation.conditions ?? null) as AutomationConditionGroup | null
      const matches = evaluateAutomationConditions(ticket as unknown as TicketForAutomation, conditions)
      if (!matches) continue

      const timing = (automation.timing ?? "IMMEDIATE").toUpperCase()
      const delayMs = timing === "DELAYED" ? Math.max(0, automation.delayMs ?? 0) : 0

      const schedulerRunAfter = ctx.scheduler?.runAfter
      if (timing === "DELAYED" && delayMs > 0 && typeof schedulerRunAfter === "function") {
        await schedulerRunAfter(delayMs, api.automations.execute, {
          automationId: automation._id,
          ticketId,
          eventType,
        })
        continue
      }

      await executeAutomationInternal(ctx, automation, ticket, eventType)
    }
  } catch (error) {
    if (process.env.NODE_ENV !== "test") {
      console.error("[automations] Falha ao processar automações", error)
    }
  }
}

export const execute = mutation({
  args: {
    automationId: v.id("ticketAutomations"),
    ticketId: v.id("tickets"),
    eventType: v.string(),
  },
  handler: async (ctx, { automationId, ticketId, eventType }) => {
    const automation = await ctx.db.get(automationId)
    if (!automation || automation.enabled !== true) return { ok: true, skipped: true }

    const ticket = await ctx.db.get(ticketId)
    if (!ticket || ticket.tenantId !== automation.tenantId) return { ok: true, skipped: true }

    await executeAutomationInternal(ctx, automation, ticket, eventType)
    return { ok: true }
  },
})

async function executeAutomationInternal(
  ctx: MutationCtx,
  automation: Doc<"ticketAutomations">,
  ticket: Doc<"tickets">,
  eventType: string
) {
  const now = Date.now()

  const conditions = (automation.conditions ?? null) as AutomationConditionGroup | null
  const matched = evaluateAutomationConditions(ticket as unknown as TicketForAutomation, conditions)
  if (!matched) {
    await ctx.db.insert("ticketAutomationRuns", {
      tenantId: automation.tenantId,
      automationId: automation._id,
      ticketId: ticket._id,
      eventType,
      status: "SKIPPED",
      matched: false,
      createdAt: now,
    })
    await enforceAutomationRunsRetention(ctx, automation._id, now)
    return
  }

  try {
    const applied = await applyActions(ctx, automation, ticket, now)
    await ctx.db.patch(automation._id, {
      runCount: (automation.runCount ?? 0) + 1,
      lastRunAt: now,
      updatedAt: now,
    })
    await ctx.db.insert("ticketAutomationRuns", {
      tenantId: automation.tenantId,
      automationId: automation._id,
      ticketId: ticket._id,
      eventType,
      status: "SUCCESS",
      matched: true,
      actionsApplied: applied,
      createdAt: now,
    })
    await enforceAutomationRunsRetention(ctx, automation._id, now)
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error)
    await ctx.db.insert("ticketAutomationRuns", {
      tenantId: automation.tenantId,
      automationId: automation._id,
      ticketId: ticket._id,
      eventType,
      status: "ERROR",
      matched: true,
      error: message,
      createdAt: now,
    })
    await enforceAutomationRunsRetention(ctx, automation._id, now)
  }
}

const RUNS_RETENTION_MAX_PER_AUTOMATION = 2000
const RUNS_RETENTION_MAX_AGE_DAYS = 90
const RUNS_RETENTION_DELETE_BATCH = 250

async function enforceAutomationRunsRetention(ctx: MutationCtx, automationId: Id<"ticketAutomations">, now: number) {
  const cutoff = now - RUNS_RETENTION_MAX_AGE_DAYS * 24 * 60 * 60 * 1000

  const runs = await ctx.db
    .query("ticketAutomationRuns")
    .withIndex("by_automation_created", (q) => q.eq("automationId", automationId))
    .order("desc")
    .take(RUNS_RETENTION_MAX_PER_AUTOMATION + RUNS_RETENTION_DELETE_BATCH)

  if (runs.length <= RUNS_RETENTION_MAX_PER_AUTOMATION) {
    // Mesmo com baixo volume, ainda remove registros antigos.
    const expired = runs.filter((run) => (run.createdAt ?? 0) < cutoff).slice(0, RUNS_RETENTION_DELETE_BATCH)
    for (const run of expired) {
      await ctx.db.delete(run._id)
    }
    return
  }

  const candidates = runs
    .slice(RUNS_RETENTION_MAX_PER_AUTOMATION)
    .concat(runs.filter((run) => (run.createdAt ?? 0) < cutoff))

  const unique = new Map<string, Id<"ticketAutomationRuns">>()
  for (const run of candidates) {
    unique.set(String(run._id), run._id)
    if (unique.size >= RUNS_RETENTION_DELETE_BATCH) break
  }

  for (const id of unique.values()) {
    await ctx.db.delete(id)
  }
}

async function applyActions(
  ctx: MutationCtx,
  automation: Doc<"ticketAutomations">,
  ticket: Doc<"tickets">,
  now: number
) {
  const actions = parseActions(automation.actions)

  const patch: Partial<Doc<"tickets">> = {}
  const applied: Array<{ type: string; details?: Record<string, unknown> }> = []
  const pendingEmails: Array<{
    recipients: AutomationEmailRecipient[]
    subject: string
    message: string
    ctaTarget: AutomationEmailTarget
    ctaLabel: string
  }> = []

  let checklist = (Array.isArray((ticket as unknown as { checklist?: unknown }).checklist)
    ? ((ticket as unknown as { checklist?: unknown }).checklist as TicketChecklistItem[])
    : []) as TicketChecklistItem[]

  for (const action of actions) {
    if (action.type === "SET_PRIORITY") {
      const next = action.priority.trim().toUpperCase()
      if (!next) continue
      if (ticket.priority !== next) {
        patch.priority = next
        applied.push({ type: action.type, details: { priority: next } })
        const pt: Record<string, string> = { LOW: "Baixa", MEDIUM: "Média", HIGH: "Alta", URGENT: "Urgente" }
        await ctx.db.insert("ticketEvents", {
          ticketId: ticket._id,
          type: "PRIORITY_CHANGED",
          payload: { to: next, toLabel: pt[next] ?? next },
          createdAt: now,
        })
      }
      continue
    }

    if (action.type === "MOVE_QUEUE") {
      const queue = (await ctx.db.get(action.queueId)) as Doc<"queues"> | null
      if (!queue || queue.tenantId !== ticket.tenantId) {
        throw new ConvexError("Fila inválida na automação")
      }
      if (ticket.queueId !== action.queueId) {
        patch.queueId = action.queueId
        const queueName = queue.name ?? ""
        const normalizedQueueLabel = queueName.toLowerCase()
        const isVisitQueueTarget = VISIT_QUEUE_KEYWORDS.some((keyword) => normalizedQueueLabel.includes(keyword))
        if (!isVisitQueueTarget) {
          patch.dueAt = ticket.slaSolutionDueAt ?? undefined
        }
        applied.push({ type: action.type, details: { queueId: String(action.queueId), queueName } })
        await ctx.db.insert("ticketEvents", {
          ticketId: ticket._id,
          type: "QUEUE_CHANGED",
          payload: { queueId: action.queueId, queueName },
          createdAt: now,
        })
      }
      continue
    }

    if (action.type === "ASSIGN_TO") {
      const assignee = (await ctx.db.get(action.assigneeId)) as Doc<"users"> | null
      if (!assignee || assignee.tenantId !== ticket.tenantId) {
        throw new ConvexError("Responsável inválido na automação")
      }
      if (ticket.assigneeId !== action.assigneeId) {
        patch.assigneeId = action.assigneeId
        patch.assigneeSnapshot = {
          name: assignee.name,
          email: assignee.email,
          avatarUrl: assignee.avatarUrl ?? undefined,
          teams: assignee.teams ?? undefined,
        }
        applied.push({ type: action.type, details: { assigneeId: String(action.assigneeId), assigneeName: assignee.name } })

        const previousAssigneeName =
          ((ticket.assigneeSnapshot as { name?: string } | null)?.name as string | undefined) ?? "Não atribuído"
        await ctx.db.insert("ticketEvents", {
          ticketId: ticket._id,
          type: "ASSIGNEE_CHANGED",
          payload: {
            assigneeId: action.assigneeId,
            assigneeName: assignee.name,
            previousAssigneeId: ticket.assigneeId ?? null,
            previousAssigneeName,
            viaAutomation: true,
            automationId: automation._id,
            automationName: automation.name,
          },
          createdAt: now,
        })
      }
      continue
    }

    if (action.type === "SET_FORM_TEMPLATE") {
      const normalizedKey = normalizeFormTemplateKey(action.formTemplate ?? null)
      let label: string | null = null
      if (normalizedKey) {
        const templateDoc = await getTemplateByKey(ctx, ticket.tenantId, normalizedKey)
        if (templateDoc && templateDoc.isArchived !== true) {
          label = templateDoc.label
        } else {
          const fallback = TICKET_FORM_CONFIG.find((tpl) => tpl.key === normalizedKey) ?? null
          if (fallback) label = fallback.label
        }
      }
      if ((ticket.formTemplate ?? null) !== normalizedKey || (ticket.formTemplateLabel ?? null) !== label) {
        patch.formTemplate = normalizedKey ?? undefined
        patch.formTemplateLabel = label ?? undefined
        applied.push({ type: action.type, details: { formTemplate: normalizedKey, formTemplateLabel: label } })
      }
      continue
    }

    if (action.type === "SET_CHAT_ENABLED") {
      const next = Boolean(action.enabled)
      if (Boolean(ticket.chatEnabled) !== next) {
        patch.chatEnabled = next
        applied.push({ type: action.type, details: { enabled: next } })
      }
      continue
    }

    if (action.type === "ADD_INTERNAL_COMMENT") {
      const body = action.body.replace(/\r\n/g, "\n").trim()
      if (!body) continue
      if (body.length > 4000) {
        throw new ConvexError("Comentário interno muito longo (máx. 4000 caracteres)")
      }
      const author = (await ctx.db.get(automation.createdBy)) as Doc<"users"> | null
      if (!author || author.tenantId !== ticket.tenantId) {
        throw new ConvexError("Autor da automação inválido")
      }
      const authorSnapshot = {
        name: author.name,
        email: author.email,
        avatarUrl: author.avatarUrl ?? undefined,
        teams: author.teams ?? undefined,
      }
      await ctx.db.insert("ticketComments", {
        ticketId: ticket._id,
        authorId: automation.createdBy,
        visibility: "INTERNAL",
        body,
        authorSnapshot,
        attachments: [],
        createdAt: now,
        updatedAt: now,
      })
      await ctx.db.insert("ticketEvents", {
        ticketId: ticket._id,
        type: "COMMENT_ADDED",
        payload: { authorId: automation.createdBy, authorName: author.name, viaAutomation: true, automationId: automation._id },
        createdAt: now,
      })
      applied.push({ type: action.type })
      continue
    }

    if (action.type === "APPLY_CHECKLIST_TEMPLATE") {
      const template = (await ctx.db.get(action.templateId)) as Doc<"ticketChecklistTemplates"> | null
      if (!template || template.tenantId !== ticket.tenantId || template.isArchived === true) {
        throw new ConvexError("Template de checklist inválido na automação")
      }
      if (template.companyId && (!ticket.companyId || String(template.companyId) !== String(ticket.companyId))) {
        throw new ConvexError("Template de checklist não pertence à empresa do ticket")
      }

      const result = applyChecklistTemplateToItems(checklist, template, {
        now,
        actorId: automation.createdBy ?? undefined,
      })

      if (result.added > 0) {
        checklist = result.checklist
        patch.checklist = checklist.length > 0 ? checklist : undefined
      }

      applied.push({
        type: action.type,
        details: { templateId: String(template._id), templateName: template.name, added: result.added },
      })
      continue
    }

    if (action.type === "SEND_EMAIL") {
      const subject = action.subject.trim()
      const message = action.message.replace(/\r\n/g, "\n").trim()
      if (!subject || !message) continue

      const ctaTarget = action.ctaTarget ?? "AUTO"
      const ctaLabel = (action.ctaLabel ?? "Abrir chamado").trim() || "Abrir chamado"

      pendingEmails.push({ recipients: action.recipients, subject, message, ctaTarget, ctaLabel })
      continue
    }
  }

  if (Object.keys(patch).length > 0) {
    patch.updatedAt = now
    await ctx.db.patch(ticket._id, patch)
  }

  if (pendingEmails.length > 0) {
    const schedulerRunAfter = ctx.scheduler?.runAfter
    if (typeof schedulerRunAfter !== "function") {
      throw new ConvexError("Scheduler indisponível para envio de e-mail")
    }

    const nextTicket = { ...ticket, ...patch } as Doc<"tickets">

    const baseUrl = buildBaseUrl()
    const portalUrl = `${baseUrl}/portal/tickets/${nextTicket._id}`
    const staffUrl = `${baseUrl}/tickets/${nextTicket._id}`

    const tokens: Record<string, string> = {
      "automation.name": automation.name,
      "ticket.id": String(nextTicket._id),
      "ticket.reference": String(nextTicket.reference ?? ""),
      "ticket.subject": nextTicket.subject ?? "",
      "ticket.status": nextTicket.status ?? "",
      "ticket.priority": nextTicket.priority ?? "",
      "ticket.url.portal": portalUrl,
      "ticket.url.staff": staffUrl,
      "company.name": (nextTicket.companySnapshot as { name?: string } | undefined)?.name ?? "",
      "requester.name": (nextTicket.requesterSnapshot as { name?: string } | undefined)?.name ?? "",
      "assignee.name": ((nextTicket.assigneeSnapshot as { name?: string } | undefined)?.name as string | undefined) ?? "",
    }

    const interpolate = (input: string) =>
      input.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (_m, key) => tokens[key] ?? "")

    const normalizeEmail = (email: string) => email.trim().toLowerCase()
    const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)

    for (const emailAction of pendingEmails) {
      const subject = interpolate(emailAction.subject).trim()
      const message = interpolate(emailAction.message).trim()
      const ctaLabel = interpolate(emailAction.ctaLabel).trim() || "Abrir chamado"

      const effectiveTarget: AutomationEmailTarget =
        emailAction.ctaTarget === "AUTO"
          ? emailAction.recipients.some((r) => r.type === "ASSIGNEE" || r.type === "USER")
            ? "STAFF"
            : "PORTAL"
          : emailAction.ctaTarget

      const ctaUrl = effectiveTarget === "PORTAL" ? portalUrl : staffUrl

      const recipientEmails = new Set<string>()

      for (const recipient of emailAction.recipients) {
        if (recipient.type === "REQUESTER") {
          const snapshotEmail =
            ((nextTicket.requesterSnapshot as { email?: string } | undefined)?.email as string | undefined) ?? null
          const email = normalizeEmail(snapshotEmail ?? "")
          if (email && isValidEmail(email)) {
            recipientEmails.add(email)
            continue
          }
          const requester = (await ctx.db.get(nextTicket.requesterId)) as Doc<"users"> | null
          if (requester && requester.tenantId === nextTicket.tenantId) {
            const fallback = normalizeEmail(requester.email ?? "")
            if (fallback && isValidEmail(fallback)) recipientEmails.add(fallback)
          }
          continue
        }

        if (recipient.type === "ASSIGNEE") {
          if (!nextTicket.assigneeId) continue
          const snapshotEmail =
            ((nextTicket.assigneeSnapshot as { email?: string } | undefined)?.email as string | undefined) ?? null
          const email = normalizeEmail(snapshotEmail ?? "")
          if (email && isValidEmail(email)) {
            recipientEmails.add(email)
            continue
          }
          const assignee = (await ctx.db.get(nextTicket.assigneeId)) as Doc<"users"> | null
          if (assignee && assignee.tenantId === nextTicket.tenantId) {
            const fallback = normalizeEmail(assignee.email ?? "")
            if (fallback && isValidEmail(fallback)) recipientEmails.add(fallback)
          }
          continue
        }

        if (recipient.type === "USER") {
          const user = (await ctx.db.get(recipient.userId)) as Doc<"users"> | null
          if (user && user.tenantId === nextTicket.tenantId) {
            const email = normalizeEmail(user.email ?? "")
            if (email && isValidEmail(email)) recipientEmails.add(email)
          }
          continue
        }

        if (recipient.type === "EMAIL") {
          const email = normalizeEmail(recipient.email)
          if (email && isValidEmail(email)) recipientEmails.add(email)
          continue
        }
      }

      const to = Array.from(recipientEmails).slice(0, 50)
      if (to.length === 0) continue

      const ticketSummary: AutomationEmailProps["ticket"] = {
        reference: nextTicket.reference ?? 0,
        subject: nextTicket.subject ?? "",
        status: nextTicket.status ?? null,
        priority: nextTicket.priority ?? null,
        companyName: (nextTicket.companySnapshot as { name?: string } | undefined)?.name ?? null,
        requesterName: (nextTicket.requesterSnapshot as { name?: string } | undefined)?.name ?? null,
        assigneeName: ((nextTicket.assigneeSnapshot as { name?: string } | undefined)?.name as string | undefined) ?? null,
      }

      const emailProps: AutomationEmailProps = {
        title: subject,
        message,
        ticket: ticketSummary,
        ctaLabel,
        ctaUrl,
      }

      await schedulerRunAfter(1, api.ticketNotifications.sendAutomationEmail, {
        to,
        subject,
        emailProps: {
          title: emailProps.title,
          message: emailProps.message,
          ticket: {
            reference: emailProps.ticket.reference,
            subject: emailProps.ticket.subject,
            status: emailProps.ticket.status ?? null,
            priority: emailProps.ticket.priority ?? null,
            companyName: emailProps.ticket.companyName ?? null,
            requesterName: emailProps.ticket.requesterName ?? null,
            assigneeName: emailProps.ticket.assigneeName ?? null,
          },
          ctaLabel: emailProps.ctaLabel,
          ctaUrl: emailProps.ctaUrl,
        },
      })

      applied.push({
        type: "SEND_EMAIL",
        details: {
          recipients: to,
          toCount: to.length,
          subject,
          messagePreview: message.length > 100 ? `${message.slice(0, 100)}...` : message,
          ctaTarget: effectiveTarget,
          ctaLabel,
          ctaUrl,
          scheduledAt: Date.now(),
        },
      })
    }
  }

  return applied
}
