import { v } from "convex/values"
import { action, mutation, query, type MutationCtx, type QueryCtx } from "./_generated/server"
import { ConvexError } from "convex/values"
import { api } from "./_generated/api"
import type { Doc, Id } from "./_generated/dataModel"
import { sha256 } from "@noble/hashes/sha2.js"
import { bytesToHex as toHex } from "@noble/hashes/utils.js"

// ============================================
// HELPERS
// ============================================

const utf8 = (s: string) => new TextEncoder().encode(s)

function hashToken(token: string) {
  return toHex(sha256(utf8(token)))
}

async function getLastHeartbeatAt(ctx: MutationCtx | QueryCtx, machineId: Id<"machines">) {
  const heartbeat = await ctx.db
    .query("machineHeartbeats")
    .withIndex("by_machine", (q) => q.eq("machineId", machineId))
    .first()

  if (heartbeat?.lastHeartbeatAt) return heartbeat.lastHeartbeatAt

  const machine = await ctx.db.get(machineId)
  return machine?.lastHeartbeatAt ?? null
}

async function validateMachineToken(
  ctx: MutationCtx | QueryCtx,
  machineToken: string
): Promise<{ machine: Doc<"machines">; tenantId: string }> {
  const tokenHash = hashToken(machineToken)

  const tokenRecord = await ctx.db
    .query("machineTokens")
    .withIndex("by_token_hash", (q) => q.eq("tokenHash", tokenHash))
    .first()

  if (!tokenRecord) {
    throw new ConvexError("Token de máquina inválido")
  }

  if (tokenRecord.revoked) {
    throw new ConvexError("Token de máquina revogado")
  }

  if (tokenRecord.expiresAt < Date.now()) {
    throw new ConvexError("Token de máquina expirado")
  }

  const machine = await ctx.db.get(tokenRecord.machineId)
  if (!machine) {
    throw new ConvexError("Máquina não encontrada")
  }

  return { machine, tenantId: tokenRecord.tenantId }
}

// ============================================
// MUTATIONS (Agente)
// ============================================

// Agente inicia sessao de chat com cliente
export const startSession = mutation({
  args: {
    ticketId: v.id("tickets"),
    actorId: v.id("users"),
  },
  handler: async (ctx, { ticketId, actorId }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      throw new ConvexError("Ticket não encontrado")
    }

    if (!ticket.machineId) {
      throw new ConvexError("Este ticket não está vinculado a uma máquina")
    }

    // Verificar se agente tem permissao
    const agent = await ctx.db.get(actorId)
    if (!agent || agent.tenantId !== ticket.tenantId) {
      throw new ConvexError("Acesso negado")
    }

    const role = agent.role?.toUpperCase() ?? ""
    if (!["ADMIN", "MANAGER", "AGENT"].includes(role)) {
      throw new ConvexError("Apenas agentes podem iniciar chat ao vivo")
    }

    // Verificar se maquina esta online (heartbeat nos ultimos 5 minutos)
    const machine = await ctx.db.get(ticket.machineId)
    if (!machine) {
      throw new ConvexError("Máquina não encontrada")
    }

    const lastHeartbeatAt = await getLastHeartbeatAt(ctx, machine._id)
    const fiveMinutesAgo = Date.now() - 5 * 60 * 1000
    if (!lastHeartbeatAt || lastHeartbeatAt < fiveMinutesAgo) {
      throw new ConvexError("Máquina offline. A máquina precisa estar online para iniciar o chat.")
    }

    // Verificar se ja existe sessao ativa para este ticket
    const existingSession = await ctx.db
      .query("liveChatSessions")
      .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
      .filter((q) => q.eq(q.field("status"), "ACTIVE"))
      .first()

    if (existingSession) {
      // Retornar sessao existente
      return { sessionId: existingSession._id, isNew: false }
    }

    const now = Date.now()

    // Buscar ultima sessao encerrada usando ordem descendente (otimizado)
    // Nota: se houver muitas sessoes encerradas, pegamos apenas as 10 mais recentes
    const recentEndedSessions = await ctx.db
      .query("liveChatSessions")
      .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
      .filter((q) => q.eq(q.field("status"), "ENDED"))
      .take(10)

    const lastEndedSession = recentEndedSessions.reduce(
      (latest, current) =>
        !latest || (current.endedAt ?? 0) > (latest.endedAt ?? 0) ? current : latest,
      null as typeof recentEndedSessions[number] | null
    )

    // Criar nova sessao
    // unreadByMachine inicia em 0 - a janela de chat só abrirá quando o agente
    // efetivamente enviar uma mensagem nova (incrementado em tickets.postChatMessage)
    const sessionId = await ctx.db.insert("liveChatSessions", {
      tenantId: ticket.tenantId,
      ticketId,
      machineId: ticket.machineId,
      agentId: actorId,
      agentSnapshot: {
        name: agent.name,
        email: agent.email,
        avatarUrl: agent.avatarUrl,
      },
      status: "ACTIVE",
      startedAt: now,
      lastActivityAt: now,
      unreadByMachine: 0,
      unreadByAgent: 0,
    })

    // Habilitar chat no ticket se nao estiver
    if (!ticket.chatEnabled) {
      await ctx.db.patch(ticketId, { chatEnabled: true })
    }

    // Registrar evento na timeline
    await ctx.db.insert("ticketEvents", {
      ticketId,
      type: "LIVE_CHAT_STARTED",
      payload: {
        sessionId,
        agentId: actorId,
        agentName: agent.name,
        machineHostname: machine.hostname,
      },
      createdAt: now,
    })

    return { sessionId, isNew: true }
  },
})

// Agente encerra sessao de chat (somente ADMIN, MANAGER ou AGENT podem encerrar)
export const endSession = mutation({
  args: {
    sessionId: v.id("liveChatSessions"),
    actorId: v.id("users"),
  },
  handler: async (ctx, { sessionId, actorId }) => {
    const session = await ctx.db.get(sessionId)
    if (!session) {
      throw new ConvexError("Sessão não encontrada")
    }

    // Verificar permissao do usuario
    const actor = await ctx.db.get(actorId)
    if (!actor || actor.tenantId !== session.tenantId) {
      throw new ConvexError("Acesso negado")
    }

    // Somente ADMIN, MANAGER ou AGENT podem encerrar sessoes de chat
    const role = actor.role?.toUpperCase() ?? ""
    if (!["ADMIN", "MANAGER", "AGENT"].includes(role)) {
      throw new ConvexError("Apenas agentes de suporte podem encerrar sessões de chat")
    }

    if (session.status !== "ACTIVE") {
      throw new ConvexError("Sessão já encerrada")
    }

    const now = Date.now()

    await ctx.db.patch(sessionId, {
      status: "ENDED",
      endedAt: now,
    })

    // Calcular duracao da sessao
    const durationMs = now - session.startedAt

    // Registrar evento na timeline
    await ctx.db.insert("ticketEvents", {
      ticketId: session.ticketId,
      type: "LIVE_CHAT_ENDED",
      payload: {
        sessionId,
        agentId: actorId,
        agentName: actor.name,
        durationMs,
        startedAt: session.startedAt,
        endedAt: now,
      },
      createdAt: now,
    })

    return { ok: true }
  },
})

// ============================================
// MUTATIONS (Maquina/Cliente)
// ============================================

// Cliente envia mensagem via machineToken
export const postMachineMessage = mutation({
  args: {
    machineToken: v.string(),
    ticketId: v.id("tickets"),
    body: v.string(),
    attachments: v.optional(
      v.array(
        v.object({
          storageId: v.id("_storage"),
          name: v.string(),
          size: v.optional(v.number()),
          type: v.optional(v.string()),
        })
      )
    ),
  },
  handler: async (ctx, args) => {
    const { machine, tenantId } = await validateMachineToken(ctx, args.machineToken)

    const ticket = await ctx.db.get(args.ticketId)
    if (!ticket || ticket.tenantId !== tenantId) {
      throw new ConvexError("Ticket não encontrado")
    }

    if (ticket.machineId?.toString() !== machine._id.toString()) {
      throw new ConvexError("Esta máquina não está vinculada a este ticket")
    }

    // Verificar se existe sessao ativa
    const session = await ctx.db
      .query("liveChatSessions")
      .withIndex("by_ticket", (q) => q.eq("ticketId", args.ticketId))
      .filter((q) => q.eq(q.field("status"), "ACTIVE"))
      .first()

    if (!session) {
      throw new ConvexError("Nenhuma sessão de chat ativa para este ticket")
    }

    // Obter usuario vinculado a maquina (ou usar nome do hostname)
    let authorId: Id<"users"> | null = machine.assignedUserId ?? null
    let authorSnapshot = {
      name: machine.assignedUserName ?? machine.hostname,
      email: machine.assignedUserEmail ?? undefined,
      avatarUrl: undefined as string | undefined,
    }

    // Se nao tem usuario vinculado, buscar por linkedUserIds
    if (!authorId && machine.linkedUserIds?.length) {
      authorId = machine.linkedUserIds[0]
      const linkedUser = await ctx.db.get(authorId)
      if (linkedUser) {
        authorSnapshot = {
          name: linkedUser.name,
          email: linkedUser.email,
          avatarUrl: linkedUser.avatarUrl,
        }
      }
    }

    // Se ainda nao tem authorId, criar mensagem sem autor (usando ID do agente temporariamente)
    if (!authorId) {
      // Usar o primeiro user do tenant como fallback (requester do ticket)
      authorId = ticket.requesterId
    }

    const now = Date.now()

    // Limitar tamanho do body
    if (args.body.length > 4000) {
      throw new ConvexError("Mensagem muito longa (máximo 4000 caracteres)")
    }

    // Inserir mensagem
    const messageId = await ctx.db.insert("ticketChatMessages", {
      ticketId: args.ticketId,
      authorId,
      authorSnapshot,
      body: args.body.trim(),
      attachments: args.attachments,
      createdAt: now,
      updatedAt: now,
      tenantId,
      companyId: ticket.companyId,
      readBy: [{ userId: authorId, readAt: now }], // Autor ja leu
    })

    // Atualizar sessao
    await ctx.db.patch(session._id, {
      lastActivityAt: now,
      unreadByAgent: (session.unreadByAgent ?? 0) + 1,
    })

    return { messageId, createdAt: now }
  },
})

// Cliente marca mensagens como lidas (otimizado para evitar race conditions)
export const markMachineMessagesRead = mutation({
  args: {
    machineToken: v.string(),
    ticketId: v.id("tickets"),
    messageIds: v.array(v.id("ticketChatMessages")),
  },
  handler: async (ctx, args) => {
    const { machine, tenantId } = await validateMachineToken(ctx, args.machineToken)

    const ticket = await ctx.db.get(args.ticketId)
    if (!ticket || ticket.tenantId !== tenantId) {
      throw new ConvexError("Ticket não encontrado")
    }

    if (ticket.machineId?.toString() !== machine._id.toString()) {
      throw new ConvexError("Esta máquina não está vinculada a este ticket")
    }

    // Obter userId para marcar leitura
    const userId = machine.assignedUserId ?? machine.linkedUserIds?.[0] ?? ticket.requesterId

    // Limitar quantidade de mensagens por chamada para evitar timeout
    const maxMessages = 50
    const messageIdsToProcess = args.messageIds.slice(0, maxMessages)

    // Buscar todas as mensagens de uma vez
    const messages = await Promise.all(
      messageIdsToProcess.map((id) => ctx.db.get(id))
    )

    const now = Date.now()

    // Filtrar mensagens válidas que precisam ser atualizadas
    const messagesToUpdate = messages.filter((message): message is NonNullable<typeof message> => {
      if (!message || message.ticketId.toString() !== args.ticketId.toString()) {
        return false
      }
      const readBy = message.readBy ?? []
      return !readBy.some((r) => r.userId.toString() === userId.toString())
    })

    // Executar todas as atualizações
    await Promise.all(
      messagesToUpdate.map((message) =>
        ctx.db.patch(message._id, {
          readBy: [...(message.readBy ?? []), { userId, readAt: now }],
        })
      )
    )

    // Zerar contador de não lidas pela máquina
    const session = await ctx.db
      .query("liveChatSessions")
      .withIndex("by_ticket", (q) => q.eq("ticketId", args.ticketId))
      .filter((q) => q.eq(q.field("status"), "ACTIVE"))
      .first()

    if (session) {
      await ctx.db.patch(session._id, { unreadByMachine: 0 })
    }

    return { ok: true, processed: messagesToUpdate.length }
  },
})

// ============================================
// QUERIES (Maquina/Cliente)
// ============================================

// Listar sessoes ativas para uma maquina
export const listMachineSessions = query({
  args: {
    machineToken: v.string(),
  },
  handler: async (ctx, { machineToken }) => {
    const { machine, tenantId } = await validateMachineToken(ctx, machineToken)

    const sessions = await ctx.db
      .query("liveChatSessions")
      .withIndex("by_machine_status", (q) =>
        q.eq("machineId", machine._id).eq("status", "ACTIVE")
      )
      // Proteção: limita sessões ativas retornadas (evita scan completo em caso de leak)
      .take(50)

    // Filtrar apenas sessão problemática legada (ID hardcoded)
    // Nota: lastAgentMessageAt pode ser undefined em sessões novas onde o agente ainda não enviou mensagem
    const validSessions = sessions.filter(
      (s) => s._id !== "pd71bvfbxx7th3npdj519hcf3s7xbe2j"
    )

    const result = await Promise.all(
      validSessions.map(async (session) => {
        const ticket = await ctx.db.get(session.ticketId)
        return {
          sessionId: session._id,
          ticketId: session.ticketId,
          ticketRef: ticket?.reference ?? 0,
          ticketSubject: ticket?.subject ?? "",
          agentName: session.agentSnapshot?.name ?? "Agente",
          agentEmail: session.agentSnapshot?.email,
          agentAvatarUrl: session.agentSnapshot?.avatarUrl,
          unreadCount: session.unreadByMachine ?? 0,
          lastActivityAt: session.lastActivityAt,
          startedAt: session.startedAt,
        }
      })
    )

    return result
  },
})

// Listar mensagens de um chat para máquina (otimizado para não carregar tudo)
export const listMachineMessages = query({
  args: {
    machineToken: v.string(),
    ticketId: v.id("tickets"),
    since: v.optional(v.number()),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const { machine, tenantId } = await validateMachineToken(ctx, args.machineToken)

    const ticket = await ctx.db.get(args.ticketId)
    if (!ticket || ticket.tenantId !== tenantId) {
      throw new ConvexError("Ticket não encontrado")
    }

    if (ticket.machineId?.toString() !== machine._id.toString()) {
      throw new ConvexError("Esta máquina não está vinculada a este ticket")
    }

    // Buscar sessão ativa
    const session = await ctx.db
      .query("liveChatSessions")
      .withIndex("by_ticket", (q) => q.eq("ticketId", args.ticketId))
      .filter((q) => q.eq(q.field("status"), "ACTIVE"))
      .first()

    if (!session) {
      return { messages: [], hasSession: false, unreadCount: 0 }
    }

    // Aplicar limite (máximo 100 mensagens por chamada)
    const limit = Math.min(args.limit ?? 50, 200)

    // Buscar mensagens usando índice, ordenando no servidor e limitando antes de trazer
    let messagesQuery = ctx.db
      .query("ticketChatMessages")
      .withIndex("by_ticket_created", (q) => q.eq("ticketId", args.ticketId))
      .order("desc")

    if (args.since) {
      messagesQuery = messagesQuery.filter((q) => q.gt(q.field("createdAt"), args.since!))
    }

    // Traz do mais recente para o mais antigo e reverte para manter ordem cronológica
    const messages = (await messagesQuery.take(limit)).reverse()

    // Obter userId da máquina para verificar se é autor
    // Deve refletir o mesmo "userId" usado em postMachineMessage/markMachineMessagesRead
    const machineUserId = machine.assignedUserId ?? machine.linkedUserIds?.[0] ?? ticket.requesterId

    const result = messages.map((msg) => {
      const isFromMachine = machineUserId
        ? msg.authorId.toString() === machineUserId.toString()
        : false

      return {
        id: msg._id,
        body: msg.body,
        authorName: msg.authorSnapshot?.name ?? "Usuário",
        authorAvatarUrl: msg.authorSnapshot?.avatarUrl,
        isFromMachine,
        createdAt: msg.createdAt,
        attachments: msg.attachments ?? [],
      }
    })

    return { messages: result, hasSession: true, unreadCount: session.unreadByMachine ?? 0 }
  },
})

// Polling leve para verificar updates
export const checkMachineUpdates = query({
  args: {
    machineToken: v.string(),
    lastCheckedAt: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const { machine } = await validateMachineToken(ctx, args.machineToken)

    // Protecao: limita sessoes ativas retornadas (evita scan completo em caso de leak)
    const rawSessions = await ctx.db
      .query("liveChatSessions")
      .withIndex("by_machine_status", (q) =>
        q.eq("machineId", machine._id).eq("status", "ACTIVE")
      )
      .take(50)

    // Filtrar sessões problemáticas (sem campos obrigatórios)
    const sessions = rawSessions.filter(
      (s) => s._id !== "pd71bvfbxx7th3npdj519hcf3s7xbe2j" && s.lastAgentMessageAt !== undefined
    )

    if (sessions.length === 0) {
      return {
        hasActiveSessions: false,
        sessions: [],
        totalUnread: 0,
      }
    }

    // Buscar tickets para obter o reference
    const sessionSummaries = await Promise.all(
      sessions.map(async (s) => {
        const ticket = await ctx.db.get(s.ticketId)
        return {
          ticketId: s.ticketId,
          ticketRef: ticket?.reference ?? 0,
          unreadCount: s.unreadByMachine ?? 0,
          lastActivityAt: s.lastActivityAt,
        }
      })
    )

    const totalUnread = sessionSummaries.reduce((sum, s) => sum + s.unreadCount, 0)

    return {
      hasActiveSessions: true,
      sessions: sessionSummaries,
      totalUnread,
    }
  },
})

// ============================================
// QUERIES (Agente)
// ============================================

// Listar sessao ativa de um ticket e status da maquina
export const getTicketSession = query({
  args: {
    ticketId: v.id("tickets"),
    viewerId: v.id("users"),
  },
  handler: async (ctx, { ticketId, viewerId }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      return null
    }

    const viewer = await ctx.db.get(viewerId)
    if (!viewer || viewer.tenantId !== ticket.tenantId) {
      return null
    }

    // Verificar se maquina esta online (sempre, mesmo sem sessao)
    const machine = ticket.machineId ? await ctx.db.get(ticket.machineId) : null
    const lastHeartbeatAt = machine ? await getLastHeartbeatAt(ctx, machine._id) : null
    const fiveMinutesAgo = Date.now() - 5 * 60 * 1000
    const machineOnline = lastHeartbeatAt !== null && lastHeartbeatAt > fiveMinutesAgo

    const session = await ctx.db
      .query("liveChatSessions")
      .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
      .filter((q) => q.eq(q.field("status"), "ACTIVE"))
      .first()

    // Sempre retorna status da maquina, mesmo sem sessao ativa
    if (!session) {
      return {
        sessionId: null,
        agentId: null,
        agentName: null,
        startedAt: null,
        lastActivityAt: null,
        unreadByAgent: 0,
        machineOnline: Boolean(machineOnline),
      }
    }

    return {
      sessionId: session._id,
      agentId: session.agentId,
      agentName: session.agentSnapshot?.name,
      startedAt: session.startedAt,
      lastActivityAt: session.lastActivityAt,
      unreadByAgent: session.unreadByAgent ?? 0,
      machineOnline: Boolean(machineOnline),
    }
  },
})

// Listar sessoes ativas de um agente (para widget flutuante)
export const listAgentSessions = query({
  args: {
    agentId: v.id("users"),
  },
  handler: async (ctx, { agentId }) => {
    const agent = await ctx.db.get(agentId)
    if (!agent) {
      return []
    }

    const role = agent.role?.toUpperCase() ?? ""
    if (!["ADMIN", "MANAGER", "AGENT"].includes(role)) {
      // Nao expor sessoes de chat para usuarios nao-staff (ex.: portal/collaborator)
      return []
    }

    // Buscar sessoes ativas do tenant do agente (limitado para evitar OOM)
    const sessions = await ctx.db
      .query("liveChatSessions")
      .withIndex("by_tenant_status", (q) =>
        q.eq("tenantId", agent.tenantId).eq("status", "ACTIVE")
      )
      .take(100)

    // Buscar detalhes dos tickets
    const result = await Promise.all(
      sessions.map(async (session) => {
        const ticket = await ctx.db.get(session.ticketId)
        return {
          ticketId: session.ticketId,
          ticketRef: ticket?.reference ?? 0,
          ticketSubject: ticket?.subject ?? "",
          sessionId: session._id,
          agentId: session.agentId,
          agentName: session.agentSnapshot?.name ?? "Agente",
          unreadCount: session.unreadByAgent ?? 0,
          lastActivityAt: session.lastActivityAt,
          startedAt: session.startedAt,
        }
      })
    )

    // Ordenar por ultima atividade (mais recente primeiro)
    return result.sort((a, b) => b.lastActivityAt - a.lastActivityAt)
  },
})

// Historico de sessoes de chat de um ticket (para exibicao no painel)
export const getTicketChatHistory = query({
  args: {
    ticketId: v.id("tickets"),
    viewerId: v.id("users"),
  },
  handler: async (ctx, { ticketId, viewerId }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      return { sessions: [], totalMessages: 0 }
    }

    const viewer = await ctx.db.get(viewerId)
    if (!viewer || viewer.tenantId !== ticket.tenantId) {
      return { sessions: [], totalMessages: 0 }
    }

    // Buscar sessoes do ticket (limitado para evitar OOM em tickets muito antigos)
    const sessions = await ctx.db
      .query("liveChatSessions")
      .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
      .take(50)

    if (sessions.length === 0) {
      return { sessions: [], totalMessages: 0 }
    }

    // Buscar mensagens do ticket (limitado a 500 mais recentes para performance)
    const allMessages = await ctx.db
      .query("ticketChatMessages")
      .withIndex("by_ticket_created", (q) => q.eq("ticketId", ticketId))
      .order("desc")
      .take(500)
      .then((msgs) => msgs.reverse())

    // Agrupar mensagens por sessao (baseado no timestamp)
    // Mensagens entre startedAt e endedAt pertencem a sessao
    const sessionResults = await Promise.all(
      sessions
        .sort((a, b) => b.startedAt - a.startedAt) // Mais recente primeiro
        .map(async (session) => {
          const sessionMessages = allMessages.filter((msg) => {
            // Mensagem criada durante a sessao
            if (msg.createdAt < session.startedAt) return false
            if (session.endedAt && msg.createdAt > session.endedAt) return false
            return true
          })

          // Obter nome da maquina
          let machineName = "Cliente"
          if (ticket.machineId) {
            const machine = await ctx.db.get(ticket.machineId)
            if (machine?.hostname) {
              machineName = machine.hostname
            }
          }

          return {
            sessionId: session._id,
            agentName: session.agentSnapshot?.name ?? "Agente",
            agentEmail: session.agentSnapshot?.email ?? null,
            agentAvatarUrl: session.agentSnapshot?.avatarUrl ?? null,
            machineName,
            status: session.status,
            startedAt: session.startedAt,
            endedAt: session.endedAt ?? null,
            messageCount: sessionMessages.length,
            messages: sessionMessages
              .sort((a, b) => a.createdAt - b.createdAt)
              .map((msg) => ({
                id: msg._id,
                body: msg.body,
                authorName: msg.authorSnapshot?.name ?? "Usuario",
                authorId: String(msg.authorId),
                createdAt: msg.createdAt,
                attachments: (msg.attachments ?? []).map((att) => ({
                  storageId: att.storageId,
                  name: att.name,
                  type: att.type ?? null,
                })),
              })),
          }
        })
    )

    return {
      sessions: sessionResults,
      totalMessages: allMessages.length,
    }
  },
})

// ============================================
// ENCERRAMENTO AUTOMATICO POR INATIVIDADE
// ============================================

// Timeout de maquina offline: 5 minutos sem heartbeat
const MACHINE_OFFLINE_TIMEOUT_MS = 5 * 60 * 1000

// Timeout de inatividade do chat: 12 horas sem atividade
// Isso evita acumular sessoes abertas indefinidamente quando usuario esquece de encerrar
const CHAT_INACTIVITY_TIMEOUT_MS = 12 * 60 * 60 * 1000

// Mutation interna para encerrar sessões inativas (chamada pelo cron)
// Critérios de encerramento:
// 1. Máquina offline (5 min sem heartbeat)
// 2. Chat inativo (12 horas sem atividade) - mesmo se máquina online
// 3. Ticket órfão (sem máquina vinculada)
export const autoEndInactiveSessions = mutation({
  args: {},
  handler: async (ctx) => {
    console.log("cron: autoEndInactiveSessions iniciado")
    const now = Date.now()
    const offlineCutoff = now - MACHINE_OFFLINE_TIMEOUT_MS
    const inactivityCutoff = now - CHAT_INACTIVITY_TIMEOUT_MS

    // Limitar a 50 sessões por execução para evitar timeout do cron (30s)
    const maxSessionsPerRun = 50

    // Buscar todas as sessões ativas
    const rawActiveSessions = await ctx.db
      .query("liveChatSessions")
      .withIndex("by_status_lastActivity", (q) => q.eq("status", "ACTIVE"))
      .take(maxSessionsPerRun)

    // Filtrar sessões problemáticas (sem campos obrigatórios)
    const activeSessions = rawActiveSessions.filter(
      (s) => s._id !== "pd71bvfbxx7th3npdj519hcf3s7xbe2j" && s.lastAgentMessageAt !== undefined
    )

    let endedCount = 0
    let checkedCount = 0
    const reasons: Record<string, number> = {}

    for (const session of activeSessions) {
      checkedCount++

      // Buscar o ticket para obter a máquina
      const ticket = await ctx.db.get(session.ticketId)
      if (!ticket || !ticket.machineId) {
        // Ticket sem máquina - encerrar sessão órfã
        await ctx.db.patch(session._id, {
          status: "ENDED",
          endedAt: now,
        })
        await ctx.db.insert("ticketEvents", {
          ticketId: session.ticketId,
          type: "LIVE_CHAT_ENDED",
          payload: {
            sessionId: session._id,
            agentId: session.agentId,
            agentName: session.agentSnapshot?.name ?? "Sistema",
            durationMs: now - session.startedAt,
            startedAt: session.startedAt,
            endedAt: now,
            autoEnded: true,
            reason: "ticket_sem_maquina",
          },
          createdAt: now,
        })
        endedCount++
        reasons["ticket_sem_maquina"] = (reasons["ticket_sem_maquina"] ?? 0) + 1
        continue
      }

      // Verificar inatividade do chat (12 horas sem atividade)
      // Isso tem prioridade sobre o status da máquina
      const chatIsInactive = session.lastActivityAt < inactivityCutoff
      if (chatIsInactive) {
        await ctx.db.patch(session._id, {
          status: "ENDED",
          endedAt: now,
        })
        await ctx.db.insert("ticketEvents", {
          ticketId: session.ticketId,
          type: "LIVE_CHAT_ENDED",
          payload: {
            sessionId: session._id,
            agentId: session.agentId,
            agentName: session.agentSnapshot?.name ?? "Sistema",
            durationMs: now - session.startedAt,
            startedAt: session.startedAt,
            endedAt: now,
            autoEnded: true,
            reason: "inatividade_chat",
            inactiveForMs: now - session.lastActivityAt,
          },
          createdAt: now,
        })
        endedCount++
        reasons["inatividade_chat"] = (reasons["inatividade_chat"] ?? 0) + 1
        continue
      }

      // Verificar heartbeat da máquina
      const lastHeartbeatAt = await getLastHeartbeatAt(ctx, ticket.machineId)
      const machineIsOnline = lastHeartbeatAt !== null && lastHeartbeatAt > offlineCutoff

      // Se máquina está online e chat está ativo, manter sessão
      if (machineIsOnline) {
        continue
      }

      // Máquina está offline - encerrar sessão
      await ctx.db.patch(session._id, {
        status: "ENDED",
        endedAt: now,
      })

      const durationMs = now - session.startedAt

      await ctx.db.insert("ticketEvents", {
        ticketId: session.ticketId,
        type: "LIVE_CHAT_ENDED",
        payload: {
          sessionId: session._id,
          agentId: session.agentId,
          agentName: session.agentSnapshot?.name ?? "Sistema",
          durationMs,
          startedAt: session.startedAt,
          endedAt: now,
          autoEnded: true,
          reason: "maquina_offline",
        },
        createdAt: now,
      })

      endedCount++
      reasons["maquina_offline"] = (reasons["maquina_offline"] ?? 0) + 1
    }

    const reasonsSummary = Object.entries(reasons).map(([r, c]) => `${r}=${c}`).join(", ")
    console.log(`cron: verificadas ${checkedCount} sessoes, encerradas ${endedCount} (${reasonsSummary || "nenhuma"})`)
    return { endedCount, checkedCount, reasons, hasMore: activeSessions.length === maxSessionsPerRun }
  },
})

// Mutation para corrigir sessoes antigas sem campos obrigatorios
export const fixLegacySessions = mutation({
  args: {},
  handler: async (ctx) => {
    // IDs problematicos conhecidos - sessoes sem lastAgentMessageAt
    const knownProblematicIds = [
      "pd71bvfbxx7th3npdj519hcf3s7xbe2j",
    ]

    let deleted = 0
    const results: string[] = []

    for (const sessionId of knownProblematicIds) {
      try {
        // Deletar a sessao problematica diretamente (evita erro de shape ao ler)
        await ctx.db.delete(sessionId as Id<"liveChatSessions">)
        deleted++
        results.push(`${sessionId}: deleted`)
      } catch (error) {
        results.push(`${sessionId}: error - ${error}`)
      }
    }

    console.log(`fixLegacySessions: deleted=${deleted}, results=${results.join(", ")}`)
    return { deleted, results }
  },
})

// ============================================
// UPLOAD DE ARQUIVOS (Maquina/Cliente)
// ============================================

// Tipos de arquivo permitidos para upload
const ALLOWED_MIME_TYPES = [
  // Imagens
  "image/jpeg",
  "image/png",
  "image/gif",
  "image/webp",
  // Documentos
  "application/pdf",
  "text/plain",
  "application/msword",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  "application/vnd.ms-excel",
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
]

const ALLOWED_EXTENSIONS = [
  ".jpg", ".jpeg", ".png", ".gif", ".webp",
  ".pdf", ".txt", ".doc", ".docx", ".xls", ".xlsx",
]

// Tamanho maximo: 10MB
const MAX_FILE_SIZE = 10 * 1024 * 1024

// Mutation interna para validar token (usada pela action)
export const validateMachineTokenForUpload = query({
  args: { machineToken: v.string() },
  handler: async (ctx, args) => {
    const tokenHash = hashToken(args.machineToken)
    const tokenRecord = await ctx.db
      .query("machineTokens")
      .withIndex("by_token_hash", (q) => q.eq("tokenHash", tokenHash))
      .first()

    if (!tokenRecord) {
      return { valid: false }
    }

    const machine = await ctx.db.get(tokenRecord.machineId)
    if (!machine || machine.status === "REVOKED") {
      return { valid: false }
    }

    return { valid: true, tenantId: tokenRecord.tenantId }
  },
})

// Action para gerar URL de upload (validada por token de maquina)
export const generateMachineUploadUrl = action({
  args: {
    machineToken: v.string(),
    fileName: v.string(),
    fileType: v.string(),
    fileSize: v.number(),
  },
  handler: async (ctx, args) => {
    // Validar token
    const validation = await ctx.runQuery(api.liveChat.validateMachineTokenForUpload, {
      machineToken: args.machineToken,
    })

    if (!validation.valid) {
      throw new ConvexError("Token de máquina inválido")
    }

    // Validar tipo de arquivo
    const ext = args.fileName.toLowerCase().slice(args.fileName.lastIndexOf("."))
    if (!ALLOWED_EXTENSIONS.includes(ext)) {
      throw new ConvexError(`Tipo de arquivo não permitido. Permitidos: ${ALLOWED_EXTENSIONS.join(", ")}`)
    }

    if (!ALLOWED_MIME_TYPES.includes(args.fileType)) {
      throw new ConvexError("Tipo MIME não permitido")
    }

    // Validar tamanho
    if (args.fileSize > MAX_FILE_SIZE) {
      throw new ConvexError(`Arquivo muito grande. Máximo: ${MAX_FILE_SIZE / 1024 / 1024}MB`)
    }

    // Gerar URL de upload
    const uploadUrl = await ctx.storage.generateUploadUrl()

    return { uploadUrl }
  },
})
