// ci: trigger convex functions deploy (no-op)
import { mutation, query } from "./_generated/server"
import { internal, api } from "./_generated/api"
import { paginationOptsValidator } from "convex/server"
import { ConvexError, v, Infer } from "convex/values"
import { sha256 } from "@noble/hashes/sha2.js"
import { randomBytes } from "@noble/hashes/utils.js"
import type { Doc, Id } from "./_generated/dataModel"
import type { MutationCtx, QueryCtx } from "./_generated/server"
import { normalizeStatus } from "./tickets"
import { requireAdmin } from "./rbac"
import { ensureMobileDeviceFields } from "./deviceFieldDefaults"

const DEFAULT_TENANT_ID = "tenant-atlas"
const DEFAULT_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30 // 30 dias
const ALLOWED_MACHINE_PERSONAS = new Set(["collaborator", "manager"])
const DEFAULT_OFFLINE_THRESHOLD_MS = 10 * 60 * 1000
const OPEN_TICKET_STATUSES = new Set(["PENDING", "AWAITING_ATTENDANCE", "PAUSED"])
const MACHINE_TICKETS_STATS_PAGE_SIZE = 200

type NormalizedIdentifiers = {
  macs: string[]
  serials: string[]
}

function getTokenTtlMs(): number {
  const raw = process.env["MACHINE_TOKEN_TTL_MS"]
  if (!raw) return DEFAULT_TOKEN_TTL_MS
  const parsed = Number(raw)
  if (!Number.isFinite(parsed) || parsed < 60_000) {
    return DEFAULT_TOKEN_TTL_MS
  }
  return parsed
}

export function getOfflineThresholdMs(): number {
  const raw = process.env["MACHINE_OFFLINE_THRESHOLD_MS"]
  if (!raw) return DEFAULT_OFFLINE_THRESHOLD_MS
  const parsed = Number(raw)
  if (!Number.isFinite(parsed) || parsed <= 0) {
    return DEFAULT_OFFLINE_THRESHOLD_MS
  }
  return parsed
}

export function getStaleThresholdMs(offlineMs: number): number {
  const raw = process.env["MACHINE_STALE_THRESHOLD_MS"]
  if (!raw) return offlineMs * 12
  const parsed = Number(raw)
  if (!Number.isFinite(parsed) || parsed <= offlineMs) {
    return offlineMs * 12
  }
  return parsed
}

function normalizeIdentifiers(macAddresses: string[], serialNumbers: string[]): NormalizedIdentifiers {
  const normalizeMac = (value: string) => value.replace(/[^a-f0-9]/gi, "").toLowerCase()
  const normalizeSerial = (value: string) => value.trim().toLowerCase()

  const macs = Array.from(new Set(macAddresses.map(normalizeMac).filter(Boolean))).sort()
  const serials = Array.from(new Set(serialNumbers.map(normalizeSerial).filter(Boolean))).sort()

  if (macs.length === 0 && serials.length === 0) {
    throw new ConvexError("Informe ao menos um identificador (MAC ou serial)")
  }

  return { macs, serials }
}

function normalizeOptionalIdentifiers(macAddresses?: string[] | null, serialNumbers?: string[] | null): NormalizedIdentifiers {
  const normalizeMac = (value: string) => value.replace(/[^a-f0-9]/gi, "").toLowerCase()
  const normalizeSerial = (value: string) => value.trim().toLowerCase()
  const macs = Array.from(new Set((macAddresses ?? []).map(normalizeMac).filter(Boolean))).sort()
  const serials = Array.from(new Set((serialNumbers ?? []).map(normalizeSerial).filter(Boolean))).sort()
  return { macs, serials }
}

async function findActiveMachineToken(ctx: QueryCtx, machineId: Id<"machines">, now: number) {
  const tokens = await ctx.db
    .query("machineTokens")
    .withIndex("by_machine_revoked_expires", (q) =>
      q.eq("machineId", machineId).eq("revoked", false).gt("expiresAt", now),
    )
    .take(100)

  return tokens.length > 0 ? tokens[0]! : null
}

function toHex(input: Uint8Array) {
  return Array.from(input)
    .map((byte) => byte.toString(16).padStart(2, "0"))
    .join("")
}

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

function computeFingerprint(tenantId: string, companySlug: string | undefined, hostname: string, ids: NormalizedIdentifiers) {
  const payload = JSON.stringify({
    tenantId,
    companySlug: companySlug ?? null,
    hostname: hostname.trim().toLowerCase(),
    macs: ids.macs,
    serials: ids.serials,
  })
  return toHex(sha256(utf8(payload)))
}

function generateManualFingerprint(tenantId: string, displayName: string) {
  const payload = JSON.stringify({
    tenantId,
    displayName: displayName.trim().toLowerCase(),
    nonce: toHex(randomBytes(16)),
    createdAt: Date.now(),
  })
  return toHex(sha256(utf8(payload)))
}

function formatDeviceCustomFieldDisplay(
  type: string,
  value: unknown,
  options?: Array<{ value: string; label: string }>
): string | null {
  if (value === null || value === undefined) return null
  switch (type) {
    case "text":
      return String(value).trim()
    case "number": {
      const num = typeof value === "number" ? value : Number(value)
      if (!Number.isFinite(num)) return null
      return String(num)
    }
    case "boolean":
      return value ? "Sim" : "Não"
    case "date": {
      const date = value instanceof Date ? value : new Date(String(value))
      if (Number.isNaN(date.getTime())) return null
      return date.toISOString().slice(0, 10)
    }
    case "select": {
      const raw = String(value)
      const option = options?.find((opt) => opt.value === raw || opt.label === raw)
      return option?.label ?? raw
    }
    case "multiselect": {
      const arr = Array.isArray(value)
        ? value
        : typeof value === "string"
          ? value.split(",").map((s) => s.trim()).filter(Boolean)
          : []
      if (arr.length === 0) return null
      const labels = arr.map((raw) => {
        const opt = options?.find((o) => o.value === raw || o.label === raw)
        return opt?.label ?? String(raw)
      })
      return labels.join(", ")
    }
    default:
      try {
        return JSON.stringify(value)
      } catch {
        return String(value)
      }
  }
}

function extractCollaboratorEmail(metadata: unknown): string | null {
  if (!metadata || typeof metadata !== "object") return null
  const record = metadata as Record<string, unknown>
  const collaborator = record["collaborator"]
  if (!collaborator || typeof collaborator !== "object") return null
  const email = (collaborator as { email?: unknown }).email
  if (typeof email !== "string") return null
  const trimmed = email.trim().toLowerCase()
  return trimmed || null
}

function matchesExistingHardware(existing: Doc<"machines">, identifiers: NormalizedIdentifiers, hostname: string): boolean {
  const intersectsMac = existing.macAddresses.some((mac) => identifiers.macs.includes(mac))
  const intersectsSerial = existing.serialNumbers.some((serial) => identifiers.serials.includes(serial))
  const sameHostname = existing.hostname.trim().toLowerCase() === hostname.trim().toLowerCase()
  return intersectsMac || intersectsSerial || sameHostname
}

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

function getRemoteAccessTokenGraceMs() {
  const fallback = 15 * 60 * 1000
  const raw = process.env["REMOTE_ACCESS_TOKEN_GRACE_MS"]
  if (!raw) return fallback
  const parsed = Number(raw)
  if (!Number.isFinite(parsed) || parsed <= 0) return fallback
  return parsed
}

const REMOTE_ACCESS_TOKEN_GRACE_MS = getRemoteAccessTokenGraceMs()

async function getTokenRecord(
  ctx: MutationCtx | QueryCtx,
  tokenValue: string
): Promise<{ token: Doc<"machineTokens">; machine: Doc<"machines"> }> {
  const tokenHash = hashToken(tokenValue)
  const token = await ctx.db
    .query("machineTokens")
    .withIndex("by_token_hash", (q) => q.eq("tokenHash", tokenHash))
    .unique()

  if (!token) {
    throw new ConvexError("Token de dispositivo inválido")
  }

  const machine = await ctx.db.get(token.machineId)
  if (!machine) {
    throw new ConvexError("Dispositivo não encontrada para o token fornecido")
  }

  return { token, machine }
}

async function getTokenWithGrace(
  ctx: MutationCtx | QueryCtx,
  tokenValue: string,
  options?: { allowGraceMs?: number }
): Promise<{ token: Doc<"machineTokens">; machine: Doc<"machines">; mode: "active" | "grace" }> {
  const { token, machine } = await getTokenRecord(ctx, tokenValue)
  const now = Date.now()
  if (token.revoked) {
    const graceMs = options?.allowGraceMs ?? 0
    const revokedAt = token.revokedAt ?? token.lastUsedAt ?? token.createdAt
    if (!graceMs || now - revokedAt > graceMs) {
      throw new ConvexError("Token de dispositivo revogado")
    }
    return { token, machine, mode: "grace" }
  }
  if (token.expiresAt < now) {
    throw new ConvexError("Token de dispositivo expirado")
  }
  return { token, machine, mode: "active" }
}

async function getActiveToken(
  ctx: MutationCtx | QueryCtx,
  tokenValue: string
): Promise<{ token: Doc<"machineTokens">; machine: Doc<"machines"> }> {
  const { token, machine } = await getTokenWithGrace(ctx, tokenValue)
  return { token, machine }
}

function isObject(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value)
}

type JsonPrimitive = string | number | boolean | null
type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }
type JsonRecord = Record<string, JsonValue>

const MAX_JSON_DEPTH = 6
const MAX_ARRAY_LENGTH = 200

function sanitizeJsonValue(value: unknown, depth = 0): JsonValue | undefined {
  if (value === null) return null
  if (typeof value === "string") return value
  if (typeof value === "number") return Number.isFinite(value) ? value : undefined
  if (typeof value === "boolean") return value
  if (depth >= MAX_JSON_DEPTH) return undefined
  if (Array.isArray(value)) {
    const items: JsonValue[] = []
    for (const entry of value.slice(0, MAX_ARRAY_LENGTH)) {
      const sanitized = sanitizeJsonValue(entry, depth + 1)
      if (sanitized !== undefined) {
        items.push(sanitized)
      }
    }
    return items
  }
  if (isObject(value)) {
    const result: JsonRecord = {}
    for (const [key, entry] of Object.entries(value)) {
      const sanitized = sanitizeJsonValue(entry, depth + 1)
      if (sanitized !== undefined) {
        result[key] = sanitized
      }
    }
    return result
  }
  return undefined
}

function sanitizeRecord(value: unknown): JsonRecord | null {
  const sanitized = sanitizeJsonValue(value)
  if (!sanitized || Array.isArray(sanitized)) return null
  return sanitized as JsonRecord
}

function stableSerialize(value: JsonValue): string {
  if (value === null) return "null"
  if (typeof value !== "object") return JSON.stringify(value)
  if (Array.isArray(value)) {
    return `[${value.map((item) => stableSerialize(item)).join(",")}]`
  }
  const entries = Object.keys(value)
    .sort()
    .map((key) => `${JSON.stringify(key)}:${stableSerialize((value as JsonRecord)[key]!)}`)
  return `{${entries.join(",")}}`
}

function hashJson(value: JsonRecord | null): string | null {
  if (!value) return null
  const serialized = stableSerialize(value)
  return toHex(sha256(utf8(serialized)))
}

function areJsonValuesEqual(a: JsonValue | undefined, b: JsonValue | undefined): boolean {
  if (a === b) return true
  if (a === undefined || b === undefined) return false
  return stableSerialize(a) === stableSerialize(b)
}

// Busca o lastHeartbeatAt da tabela machineHeartbeats (fonte de verdade)
// Fallback para machine.lastHeartbeatAt para retrocompatibilidade durante migracao
async function getMachineLastHeartbeat(
  ctx: QueryCtx | MutationCtx,
  machineId: Id<"machines">,
  fallback?: number | null
): Promise<number | null> {
  const hb = await ctx.db
    .query("machineHeartbeats")
    .withIndex("by_machine", (q) => q.eq("machineId", machineId))
    .first()
  return hb?.lastHeartbeatAt ?? fallback ?? null
}

// Campo software é muito grande e é tratado separadamente via machineSoftware

// Extrai campos importantes do extended antes de bloqueá-lo
function extractFromExtended(extended: unknown): JsonRecord {
  const result: JsonRecord = {}
  const sanitizedExtended = sanitizeRecord(extended)
  if (!sanitizedExtended) return result

  // Extrair dados do Windows
  const windows = sanitizeRecord(sanitizedExtended["windows"])
  if (windows) {
    const windowsFields: JsonRecord = {}
    // bootInfo - informacoes de reinicio
    if (windows["bootInfo"]) {
      windowsFields["bootInfo"] = windows["bootInfo"] as JsonValue
    }
    // osInfo - informacoes do sistema operacional
    if (windows["osInfo"]) {
      windowsFields["osInfo"] = windows["osInfo"] as JsonValue
    }
    // cpu, baseboard, bios, memoryModules, videoControllers, disks
    for (const key of ["cpu", "baseboard", "bios", "memoryModules", "videoControllers", "disks", "bitLocker", "tpm", "secureBoot", "deviceGuard", "firewallProfiles", "windowsUpdate", "computerSystem", "azureAdStatus", "battery", "thermal", "networkAdapters", "monitors", "chassis", "defender", "hotfix"]) {
      if (windows[key]) {
        windowsFields[key] = windows[key] as JsonValue
      }
    }
    if (Object.keys(windowsFields).length > 0) {
      result["windows"] = windowsFields
    }
  }

  // Extrair dados do Linux
  const linux = sanitizeRecord(sanitizedExtended["linux"])
  if (linux) {
    const linuxFields: JsonRecord = {}
    for (const key of ["lsblk", "smart", "lspci", "lsusb", "dmidecode"]) {
      if (linux[key]) {
        linuxFields[key] = linux[key] as JsonValue
      }
    }
    if (Object.keys(linuxFields).length > 0) {
      result["linux"] = linuxFields
    }
  }

  // Extrair dados do macOS
  const macos = sanitizeRecord(sanitizedExtended["macos"])
  if (macos) {
    result["macos"] = macos as JsonValue
  }

  return result
}

function mergeInventory(current: JsonRecord | null | undefined, patch: Record<string, unknown>): JsonRecord {
  const sanitizedPatch = sanitizeRecord(patch)
  if (!sanitizedPatch) {
    return current ? { ...current } : {}
  }
  const base: JsonRecord = current ? { ...current } : {}

  for (const [key, value] of Object.entries(sanitizedPatch)) {
    // Filtrar software (extended já foi processado em sanitizeInventoryPayload)
    if (key === "software") continue
    if (value === undefined) continue
    if (isObject(value) && isObject(base[key])) {
      base[key] = mergeInventory(base[key] as JsonRecord, value as Record<string, unknown>)
    } else {
      base[key] = value as JsonValue
    }
  }
  return base
}

function mergeMetadata(current: unknown, patch: Record<string, unknown>): JsonRecord {
  const base: JsonRecord = sanitizeRecord(current) ?? {}
  const sanitizedPatch = sanitizeRecord(patch) ?? {}
  for (const [key, value] of Object.entries(sanitizedPatch)) {
    if (value === undefined) continue
    if (key === "inventory" && isObject(value)) {
      base[key] = mergeInventory(sanitizeRecord(base[key]), value as Record<string, unknown>)
    } else if (isObject(value) && isObject(base[key])) {
      base[key] = mergeInventory(sanitizeRecord(base[key]), value as Record<string, unknown>)
    } else {
      base[key] = value as JsonValue
    }
  }
  return base
}

function ensureRecord(value: unknown): JsonRecord | null {
  return sanitizeRecord(value)
}

function ensureRecordArray(value: unknown): JsonRecord[] {
  if (!Array.isArray(value)) return []
  return value
    .map((entry) => sanitizeRecord(entry))
    .filter((entry): entry is JsonRecord => Boolean(entry))
}

function ensureFiniteNumber(value: unknown): number | null {
  const num = typeof value === "number" ? value : Number(value)
  return Number.isFinite(num) ? num : null
}

function ensureString(value: unknown): string | null {
  return typeof value === "string" ? value : null
}

function sanitizeInventoryPayload(value: unknown): JsonRecord | null {
  const record = sanitizeRecord(value)
  if (!record) return null

  // Extrair campos importantes do extended antes de deletá-lo
  if (record["extended"]) {
    const extractedExtended = extractFromExtended(record["extended"])
    if (Object.keys(extractedExtended).length > 0) {
      record["extended"] = extractedExtended
    } else {
      delete record["extended"]
    }
  }

  // Deletar apenas software (extended já foi processado acima)
  delete record["software"]

  return record
}

function sanitizeMetricsPayload(value: unknown): JsonRecord | null {
  return sanitizeRecord(value)
}

function getNestedRecord(root: JsonRecord | null, ...keys: string[]): JsonRecord | null {
  let current: JsonRecord | null = root
  for (const key of keys) {
    if (!current) return null
    current = ensureRecord(current[key])
  }
  return current
}

function getNestedRecordArray(root: JsonRecord | null, ...keys: string[]): JsonRecord[] {
  if (keys.length === 0) return []
  const parent = getNestedRecord(root, ...keys.slice(0, -1))
  if (!parent) return []
  return ensureRecordArray(parent[keys[keys.length - 1]])
}

type PostureFinding = {
  kind: "CPU_HIGH" | "SERVICE_DOWN" | "SMART_FAIL"
  message: string
  severity: "warning" | "critical"
}

async function createTicketForAlert(
  ctx: MutationCtx,
  tenantId: string,
  companyId: Id<"companies"> | undefined,
  subject: string,
  summary: string
) {
  const actorEmail = process.env["MACHINE_ALERTS_TICKET_REQUESTER_EMAIL"] ?? "admin@sistema.dev"
  const actor = await ctx.db
    .query("users")
    .withIndex("by_tenant_email", (q) => q.eq("tenantId", tenantId).eq("email", actorEmail))
    .unique()
  if (!actor) return null

  // pick first category/subcategory if not configured
  const category = await ctx.db.query("ticketCategories").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).first()
  if (!category) return null
  const subcategory = await ctx.db
    .query("ticketSubcategories")
    .withIndex("by_category_order", (q) => q.eq("categoryId", category._id))
    .first()
  if (!subcategory) return null

  try {
    const id = await ctx.runMutation(api.tickets.create, {
      actorId: actor._id,
      tenantId,
      subject,
      summary,
      priority: "Alta",
      channel: "Automação",
      queueId: undefined,
      requesterId: actor._id,
      assigneeId: undefined,
      categoryId: category._id,
      subcategoryId: subcategory._id,
      customFields: undefined,
    })
    return id
  } catch (error) {
    console.error("[machines.alerts] Falha ao criar ticket:", error)
    return null
  }
}

async function evaluatePostureAndMaybeRaise(
  ctx: MutationCtx,
  machine: Doc<"machines">,
  args: {
    metrics?: JsonRecord | null
    inventory?: JsonRecord | null
    metadata?: JsonRecord | null
  }
) {
  const findings: PostureFinding[] = []

  // Janela temporal de CPU (5 minutos)
  const now = Date.now()
  const metadataPatch = ensureRecord(args.metadata)
  const metrics = ensureRecord(args.metrics) ?? ensureRecord(metadataPatch?.["metrics"])
  const metaObj: JsonRecord = ensureRecord(machine.metadata) ?? {}
  const prevWindowRecords = ensureRecordArray(metaObj["cpuWindow"])
  const prevWindow: Array<{ ts: number; usage: number }> = prevWindowRecords
    .map((entry) => {
      const ts = ensureFiniteNumber(entry["ts"])
      const usage = ensureFiniteNumber(entry["usage"])
      if (ts === null || usage === null) return null
      return { ts, usage }
    })
    .filter((entry): entry is { ts: number; usage: number } => entry !== null)
  const window = prevWindow.filter((p) => now - p.ts <= 5 * 60 * 1000)
  const usage =
    ensureFiniteNumber(metrics?.["cpuUsagePercent"]) ?? ensureFiniteNumber(metrics?.["cpu_usage_percent"])
  if (usage !== null) {
    window.push({ ts: now, usage })
  }
  if (window.length > 0) {
    const avg = window.reduce((acc, p) => acc + p.usage, 0) / window.length
    if (avg >= 90) {
      findings.push({ kind: "CPU_HIGH", message: `CPU média ${avg.toFixed(0)}% em 5 min`, severity: "warning" })
    }
  }

  const inventory = ensureRecord(args.inventory) ?? ensureRecord(metadataPatch?.["inventory"])
  if (inventory) {
    const services = ensureRecordArray(inventory["services"])
    if (services.length > 0) {
      const criticalList = (process.env["MACHINE_CRITICAL_SERVICES"] ?? "")
        .split(/[\s,]+/)
        .map((s) => s.trim().toLowerCase())
        .filter(Boolean)
      const criticalSet = new Set(criticalList)
      const firstDown = services.find((service) => {
        const status = ensureString(service["status"]) ?? ensureString(service["Status"]) ?? ""
        const name = ensureString(service["name"]) ?? ensureString(service["Name"]) ?? ""
        return Boolean(name) && status.toLowerCase() !== "running"
      })
      if (firstDown) {
        const name = ensureString(firstDown["name"]) ?? ensureString(firstDown["Name"]) ?? "serviço"
        const sev: "warning" | "critical" = criticalSet.has(name.toLowerCase()) ? "critical" : "warning"
        findings.push({ kind: "SERVICE_DOWN", message: `Serviço em falha: ${name}`, severity: sev })
      }
    }
    const smartEntries = getNestedRecordArray(inventory, "extended", "linux", "smart")
    if (smartEntries.length > 0) {
      const firstFail = smartEntries.find((disk) => {
        const status = ensureString(disk["smart_status"]) ?? ensureString(disk["status"]) ?? ""
        return status.toLowerCase() !== "ok"
      })
      if (firstFail) {
        const model =
          ensureString(firstFail["model_name"]) ??
          ensureString(firstFail["model_family"]) ??
          ensureString(firstFail["model"]) ??
          "Disco"
        const deviceRecord = getNestedRecord(firstFail, "device")
        const serial =
          ensureString(firstFail["serial_number"]) ??
          ensureString(deviceRecord?.["name"]) ??
          "—"
        const temperatureRecord = getNestedRecord(firstFail, "temperature")
        const temp =
          ensureFiniteNumber(temperatureRecord?.["current"]) ??
          ensureFiniteNumber(temperatureRecord?.["value"])
        const details = temp !== null ? `${model} (${serial}) · ${temp}ºC` : `${model} (${serial})`
        findings.push({ kind: "SMART_FAIL", message: `SMART em falha: ${details}`, severity: "critical" })
      }
    }
  }

  // Persistir janela de CPU (limite de 120 amostras)
  const cpuWindowCapped = window.slice(-120)
  await ctx.db.patch(machine._id, { metadata: mergeMetadata(machine.metadata, { cpuWindow: cpuWindowCapped }) })

  if (!findings.length) return

  const record = {
    postureAlerts: findings,
    lastPostureAt: now,
  }
  const prevMeta = ensureRecord(machine.metadata)
  const lastAtPrev = ensureFiniteNumber(prevMeta?.["lastPostureAt"]) ?? 0
  await ctx.db.patch(machine._id, { metadata: mergeMetadata(machine.metadata, record), updatedAt: now })

  await Promise.all(
    findings.map((finding) =>
      ctx.db.insert("machineAlerts", {
        tenantId: machine.tenantId,
        machineId: machine._id,
        companyId: machine.companyId ?? undefined,
        kind: finding.kind,
        message: finding.message,
        severity: finding.severity,
        createdAt: now,
      })
    )
  )

  if ((process.env["MACHINE_ALERTS_CREATE_TICKETS"] ?? "false").toLowerCase() !== "true") return
  if (lastAtPrev && now - lastAtPrev < 30 * 60 * 1000) return

  const subject = `Alerta de dispositivo: ${machine.hostname}`
  const summary = findings.map((f) => `${f.severity.toUpperCase()}: ${f.message}`).join(" | ")
  await createTicketForAlert(ctx, machine.tenantId, machine.companyId, subject, summary)
}

export const register = mutation({
  args: {
    provisioningCode: v.string(),
    hostname: v.string(),
    os: v.object({
      name: v.string(),
      version: v.optional(v.string()),
      architecture: v.optional(v.string()),
    }),
    macAddresses: v.array(v.string()),
    serialNumbers: v.array(v.string()),
    metadata: v.optional(v.any()),
    registeredBy: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const normalizedCode = args.provisioningCode.trim().toLowerCase()
    const companyRecord = await ctx.db
      .query("companies")
      .withIndex("by_provisioning_code", (q) => q.eq("provisioningCode", normalizedCode))
      .unique()

    if (!companyRecord) {
      throw new ConvexError("Código de provisionamento inválido")
    }

    const tenantId = companyRecord.tenantId ?? DEFAULT_TENANT_ID
    const companyId = companyRecord._id
    const companySlug = companyRecord.slug
    const identifiers = normalizeIdentifiers(args.macAddresses, args.serialNumbers)
    const fingerprint = computeFingerprint(tenantId, companySlug, args.hostname, identifiers)
    const now = Date.now()
    const metadataPatch = args.metadata && typeof args.metadata === "object" ? (args.metadata as Record<string, unknown>) : undefined

    // Busca 1: fingerprint exato (tenant + slug + hostname + MACs hash)
    let existing = await ctx.db
      .query("machines")
      .withIndex("by_tenant_fingerprint", (q) => q.eq("tenantId", tenantId).eq("fingerprint", fingerprint))
      .first()

    // Busca 2: por email + validacao de hardware (fallback se fingerprint mudou mas email igual)
    if (!existing) {
      const collaboratorEmail = extractCollaboratorEmail(metadataPatch ?? args.metadata)
      if (collaboratorEmail) {
        const candidate = await ctx.db
          .query("machines")
          .withIndex("by_tenant_assigned_email", (q) => q.eq("tenantId", tenantId).eq("assignedUserEmail", collaboratorEmail))
          .first()
        if (candidate && matchesExistingHardware(candidate, identifiers, args.hostname)) {
          existing = candidate
        }
      }
    }

    // Busca 3: por hostname + validacao de hardware (fallback se o usuario mudou mas e a mesma maquina fisica)
    // Isso garante que o historico de tickets da maquina seja preservado independente do usuario
    if (!existing) {
      const hostnameLower = args.hostname.trim().toLowerCase()
      const candidates = await ctx.db
        .query("machines")
        .withIndex("by_tenant_hostname", (q) => q.eq("tenantId", tenantId).eq("hostname", args.hostname))
        .take(200)
      // Procura uma maquina com hostname igual E hardware compativel (MAC ou serial)
      for (const candidate of candidates) {
        if (matchesExistingHardware(candidate, identifiers, args.hostname)) {
          existing = candidate
          break
        }
      }
      // Se nao encontrou por hostname exato, tenta busca mais ampla por hardware
      if (!existing) {
        // Busca maquinas do mesmo tenant (limitado a 500 para evitar OOM)
        // e verifica se alguma tem MAC/serial compativel
        const allMachines = await ctx.db
          .query("machines")
          .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
          .take(500)
        for (const candidate of allMachines) {
          // Verifica se compartilha MAC ou serial (hardware fisico)
          const sharedMac = candidate.macAddresses.some((mac) => identifiers.macs.includes(mac))
          const sharedSerial = candidate.serialNumbers.some((serial) => identifiers.serials.includes(serial))
          if (sharedMac || sharedSerial) {
            existing = candidate
            break
          }
        }
      }
    }

    let machineId: Id<"machines">

    if (existing) {
      await ctx.db.patch(existing._id, {
        tenantId,
        companyId: companyId ?? existing.companyId,
        companySlug: companySlug ?? existing.companySlug,
        hostname: args.hostname,
        displayName: existing.displayName ?? args.hostname,
        osName: args.os.name,
        osVersion: args.os.version,
        architecture: args.os.architecture,
        macAddresses: identifiers.macs,
        serialNumbers: identifiers.serials,
        fingerprint,
        metadata: metadataPatch ? mergeMetadata(existing.metadata, metadataPatch) : existing.metadata,
        lastHeartbeatAt: now,
        updatedAt: now,
        status: "online",
        isActive: true,
        registeredBy: args.registeredBy ?? existing.registeredBy,
        deviceType: existing.deviceType ?? "desktop",
        devicePlatform: args.os.name ?? existing.devicePlatform,
        managementMode: existing.managementMode ?? "agent",
        persona: existing.persona,
        assignedUserId: existing.assignedUserId,
        assignedUserEmail: existing.assignedUserEmail,
        assignedUserName: existing.assignedUserName,
        assignedUserRole: existing.assignedUserRole,
      })
      machineId = existing._id
    } else {
      machineId = await ctx.db.insert("machines", {
        tenantId,
        companyId,
        companySlug,
        hostname: args.hostname,
        displayName: args.hostname,
        osName: args.os.name,
        osVersion: args.os.version,
        architecture: args.os.architecture,
        macAddresses: identifiers.macs,
        serialNumbers: identifiers.serials,
        fingerprint,
        metadata: metadataPatch ? mergeMetadata(undefined, metadataPatch) : undefined,
        lastHeartbeatAt: now,
        status: "online",
        isActive: true,
        createdAt: now,
        updatedAt: now,
        registeredBy: args.registeredBy,
        deviceType: "desktop",
        devicePlatform: args.os.name,
        managementMode: "agent",
        persona: undefined,
        assignedUserId: undefined,
        assignedUserEmail: undefined,
        assignedUserName: undefined,
        assignedUserRole: undefined,
      })
    }

    const previousTokens = await ctx.db
      .query("machineTokens")
      .withIndex("by_machine", (q) => q.eq("machineId", machineId))
      .take(100)

    for (const token of previousTokens) {
      if (!token.revoked) {
        await ctx.db.patch(token._id, {
          revoked: true,
          revokedAt: now,
          lastUsedAt: now,
          expiresAt: now,
        })
      }
    }

    const tokenPlain = toHex(randomBytes(32))
    const tokenHash = hashToken(tokenPlain)
    const expiresAt = now + getTokenTtlMs()

    await ctx.db.insert("machineTokens", {
      tenantId,
      machineId,
      tokenHash,
      expiresAt,
      revoked: false,
      createdAt: now,
      usageCount: 0,
      type: "machine",
    })

    return {
      machineId,
      tenantId,
      companyId,
      companySlug,
      machineToken: tokenPlain,
      expiresAt,
    }
  },
})

export const upsertInventory = mutation({
  args: {
    provisioningCode: v.string(),
    hostname: v.string(),
    os: v.object({
      name: v.string(),
      version: v.optional(v.string()),
      architecture: v.optional(v.string()),
    }),
    macAddresses: v.array(v.string()),
    serialNumbers: v.array(v.string()),
    inventory: v.optional(v.any()),
    metrics: v.optional(v.any()),
    registeredBy: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const normalizedCode = args.provisioningCode.trim().toLowerCase()
    const companyRecord = await ctx.db
      .query("companies")
      .withIndex("by_provisioning_code", (q) => q.eq("provisioningCode", normalizedCode))
      .unique()

    if (!companyRecord) {
      throw new ConvexError("Código de provisionamento inválido")
    }

    const tenantId = companyRecord.tenantId ?? DEFAULT_TENANT_ID
    const companyId = companyRecord._id
    const companySlug = companyRecord.slug
    const identifiers = normalizeIdentifiers(args.macAddresses, args.serialNumbers)
    const fingerprint = computeFingerprint(tenantId, companySlug, args.hostname, identifiers)
    const now = Date.now()

    const metadataPatch: Record<string, unknown> = {}
    if (args.inventory && typeof args.inventory === "object") {
      metadataPatch.inventory = args.inventory as Record<string, unknown>
    }
    if (args.metrics && typeof args.metrics === "object") {
      metadataPatch.metrics = args.metrics as Record<string, unknown>
    }

    const existing = await ctx.db
      .query("machines")
      .withIndex("by_tenant_fingerprint", (q) => q.eq("tenantId", tenantId).eq("fingerprint", fingerprint))
      .first()

    let machineId: Id<"machines">

    if (existing) {
      await ctx.db.patch(existing._id, {
        tenantId,
        companyId: companyId ?? existing.companyId,
        companySlug: companySlug ?? existing.companySlug,
        hostname: args.hostname,
        displayName: existing.displayName ?? args.hostname,
        osName: args.os.name,
        osVersion: args.os.version,
        architecture: args.os.architecture,
        macAddresses: identifiers.macs,
        serialNumbers: identifiers.serials,
        metadata: Object.keys(metadataPatch).length ? mergeMetadata(existing.metadata, metadataPatch) : existing.metadata,
        lastHeartbeatAt: now,
        updatedAt: now,
        status: args.metrics ? "online" : existing.status ?? "unknown",
        registeredBy: args.registeredBy ?? existing.registeredBy,
        deviceType: existing.deviceType ?? "desktop",
        devicePlatform: args.os.name ?? existing.devicePlatform,
        managementMode: existing.managementMode ?? "agent",
        persona: existing.persona,
        assignedUserId: existing.assignedUserId,
        assignedUserEmail: existing.assignedUserEmail,
        assignedUserName: existing.assignedUserName,
        assignedUserRole: existing.assignedUserRole,
      })
      machineId = existing._id
    } else {
      machineId = await ctx.db.insert("machines", {
        tenantId,
        companyId,
        companySlug,
        hostname: args.hostname,
        displayName: args.hostname,
        osName: args.os.name,
        osVersion: args.os.version,
        architecture: args.os.architecture,
        macAddresses: identifiers.macs,
        serialNumbers: identifiers.serials,
        fingerprint,
        metadata: Object.keys(metadataPatch).length ? mergeMetadata(undefined, metadataPatch) : undefined,
        lastHeartbeatAt: now,
        status: args.metrics ? "online" : "unknown",
        createdAt: now,
        updatedAt: now,
        registeredBy: args.registeredBy,
        deviceType: "desktop",
        devicePlatform: args.os.name,
        managementMode: "agent",
        persona: undefined,
        assignedUserId: undefined,
        assignedUserEmail: undefined,
        assignedUserName: undefined,
        assignedUserRole: undefined,
      })
    }

    // Evaluate posture/alerts based on provided metrics/inventory
    const machine = (await ctx.db.get(machineId)) as Doc<"machines">
    await evaluatePostureAndMaybeRaise(ctx, machine, { metrics: args.metrics, inventory: args.inventory })

    return {
      machineId,
      tenantId,
      companyId,
      companySlug,
      status: args.metrics ? "online" : "unknown",
    }
  },
})

export const heartbeat = mutation({
  args: {
    machineToken: v.string(),
    status: v.optional(v.string()),
    hostname: v.optional(v.string()),
    os: v.optional(
      v.object({
        name: v.string(),
        version: v.optional(v.string()),
        architecture: v.optional(v.string()),
      })
    ),
    metrics: v.optional(v.record(v.string(), v.any())),
    inventory: v.optional(v.record(v.string(), v.any())),
    metadata: v.optional(v.record(v.string(), v.any())),
  },
  handler: async (ctx, args) => {
    const { machine, token } = await getActiveToken(ctx, args.machineToken)
    const now = Date.now()

    // 1. SEMPRE atualizar machineHeartbeats (documento pequeno, upsert)
    // Isso evita criar versoes do documento machines a cada heartbeat
    const existingHeartbeat = await ctx.db
      .query("machineHeartbeats")
      .withIndex("by_machine", (q) => q.eq("machineId", machine._id))
      .first()

    if (existingHeartbeat) {
      await ctx.db.patch(existingHeartbeat._id, { lastHeartbeatAt: now })
    } else {
      await ctx.db.insert("machineHeartbeats", { machineId: machine._id, lastHeartbeatAt: now })
    }

    // 2. Preparar patch de metadata (se houver mudancas REAIS)
    // IMPORTANTE: So incluimos no patch se os dados realmente mudaram
    // Isso evita criar versoes desnecessarias do documento machines
    const metadataPatch: JsonRecord = {}
    const currentMetadata = ensureRecord(machine.metadata) ?? {}
    const incomingMeta = ensureRecord(args.metadata)
    const remoteAccessSnapshot = incomingMeta ? ensureRecord(incomingMeta["remoteAccessSnapshot"]) : null

    if (incomingMeta) {
      // Filtrar apenas campos que realmente mudaram
      for (const [key, value] of Object.entries(incomingMeta)) {
        if (key === "inventory" || key === "metrics" || key === "remoteAccessSnapshot" || key === "inventoryHash" || key === "metricsHash") {
          continue
        }
        const currentValue = currentMetadata[key] as JsonValue | undefined
        if (!areJsonValuesEqual(value as JsonValue, currentValue)) {
          metadataPatch[key] = value as JsonValue
        }
      }
    }

    // Extrair inventory de args.inventory ou de args.metadata.inventory (agente envia em metadata)
    const rawInventory = args.inventory ?? (incomingMeta?.["inventory"] as Record<string, unknown> | undefined)
    const sanitizedInventory = sanitizeInventoryPayload(rawInventory)
    const currentInventory = ensureRecord(currentMetadata.inventory)
    const incomingInventoryHash = hashJson(sanitizedInventory)
    const currentInventoryHash = typeof currentMetadata["inventoryHash"] === "string" ? currentMetadata["inventoryHash"] : null

    if (sanitizedInventory && incomingInventoryHash && incomingInventoryHash !== currentInventoryHash) {
      metadataPatch.inventory = mergeInventory(currentInventory, sanitizedInventory)
      metadataPatch.inventoryHash = incomingInventoryHash
    }

    const sanitizedMetrics = sanitizeMetricsPayload(args.metrics)
    const currentMetrics = ensureRecord(currentMetadata.metrics)
    const incomingMetricsHash = hashJson(sanitizedMetrics)
    const currentMetricsHash = typeof currentMetadata["metricsHash"] === "string" ? currentMetadata["metricsHash"] : null
    if (sanitizedMetrics && incomingMetricsHash && incomingMetricsHash !== currentMetricsHash) {
      metadataPatch.metrics = sanitizedMetrics
      metadataPatch.metricsHash = incomingMetricsHash
    }

    // 3. Verificar se ha mudancas reais nos dados que justifiquem atualizar o documento machines
    const hasMetadataChanges = Object.keys(metadataPatch).length > 0
    const hasHostnameChange = args.hostname && args.hostname !== machine.hostname
    const hasOsChange = args.os && (
      args.os.name !== machine.osName ||
      args.os.version !== machine.osVersion ||
      args.os.architecture !== machine.architecture
    )
    const hasStatusChange = typeof args.status === "string" && args.status !== machine.status
    const needsMachineUpdate = hasMetadataChanges || hasHostnameChange || hasOsChange || hasStatusChange

    // 4. So atualizar machines se houver mudancas reais (evita criar versoes desnecessarias)
    // NOTA: lastHeartbeatAt agora vive na tabela machineHeartbeats, nao atualizamos mais aqui
    if (needsMachineUpdate) {
      const mergedMetadata = hasMetadataChanges ? mergeMetadata(machine.metadata, metadataPatch) : machine.metadata
      const nextStatus = args.status ?? machine.status ?? (sanitizedMetrics ? "online" : "unknown")

      await ctx.db.patch(machine._id, {
        hostname: args.hostname ?? machine.hostname,
        displayName: machine.displayName ?? args.hostname ?? machine.hostname,
        osName: args.os?.name ?? machine.osName,
        osVersion: args.os?.version ?? machine.osVersion,
        architecture: args.os?.architecture ?? machine.architecture,
        devicePlatform: args.os?.name ?? machine.devicePlatform,
        deviceType: machine.deviceType ?? "desktop",
        managementMode: machine.managementMode ?? "agent",
        updatedAt: now,
        status: nextStatus,
        metadata: mergedMetadata,
      })
    }

    if (remoteAccessSnapshot) {
      await upsertRemoteAccessSnapshotFromHeartbeat(ctx, machine, remoteAccessSnapshot, now)
    }

    // Processar softwares instalados (armazenados em tabela separada)
    // Os dados de software sao extraidos ANTES de sanitizar o inventory
    // Usa rawInventory ja extraido anteriormente (linha ~1022)
    if (rawInventory && typeof rawInventory === "object") {
      const softwareArray = (rawInventory as Record<string, unknown>)["software"]
      if (Array.isArray(softwareArray) && softwareArray.length > 0) {
        const validSoftware = softwareArray
          .filter((item): item is Record<string, unknown> => item !== null && typeof item === "object")
          .map((item) => ({
            name: typeof item.name === "string" ? item.name : "",
            version: typeof item.version === "string" ? item.version : undefined,
            publisher: typeof item.publisher === "string" || typeof item.source === "string"
              ? (item.publisher as string) || (item.source as string)
              : undefined,
            source: typeof item.source === "string" ? item.source : undefined,
          }))
          .filter((item) => item.name.length > 0)

        if (validSoftware.length > 0) {
          await ctx.runMutation(internal.machineSoftware.syncFromHeartbeat, {
            tenantId: machine.tenantId,
            machineId: machine._id,
            software: validSoftware,
          })
        }
      }
    }

    await ctx.db.patch(token._id, {
      lastUsedAt: now,
      usageCount: (token.usageCount ?? 0) + 1,
      expiresAt: now + getTokenTtlMs(),
    })

    // Evaluate posture/alerts & optionally create ticket
    const fresh = needsMachineUpdate ? (await ctx.db.get(machine._id)) as Doc<"machines"> : machine
    await evaluatePostureAndMaybeRaise(ctx, fresh, {
      metrics: sanitizedMetrics ?? null,
      inventory: sanitizedInventory ?? null,
      metadata: incomingMeta ?? null,
    })

    return {
      ok: true,
      machineId: machine._id,
      expiresAt: now + getTokenTtlMs(),
    }
  },
})

export const resolveToken = mutation({
  args: {
    machineToken: v.string(),
  },
  handler: async (ctx, args) => {
    const { machine, token } = await getActiveToken(ctx, args.machineToken)
    const now = Date.now()

    await ctx.db.patch(token._id, {
      lastUsedAt: now,
      usageCount: (token.usageCount ?? 0) + 1,
    })

    return {
      machine: {
        _id: machine._id,
        tenantId: machine.tenantId,
        companyId: machine.companyId,
        companySlug: machine.companySlug,
        hostname: machine.hostname,
        osName: machine.osName,
        osVersion: machine.osVersion,
        architecture: machine.architecture,
        authUserId: machine.authUserId,
        authEmail: machine.authEmail,
        persona: machine.persona ?? null,
        assignedUserId: machine.assignedUserId ?? null,
        assignedUserEmail: machine.assignedUserEmail ?? null,
        assignedUserName: machine.assignedUserName ?? null,
        assignedUserRole: machine.assignedUserRole ?? null,
        linkedUserIds: machine.linkedUserIds ?? [],
        status: machine.status,
        lastHeartbeatAt: machine.lastHeartbeatAt,
        metadata: machine.metadata,
        isActive: machine.isActive ?? true,
      },
      token: {
        expiresAt: token.expiresAt,
        lastUsedAt: token.lastUsedAt ?? null,
        usageCount: token.usageCount ?? 0,
      },
    }
  },
})

export const listByTenant = query({
  args: {
    tenantId: v.optional(v.string()),
    includeMetadata: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    const tenantId = args.tenantId ?? DEFAULT_TENANT_ID
    const includeMetadata = Boolean(args.includeMetadata)
    const now = Date.now()

    const tenantCompanies = await ctx.db
      .query("companies")
      .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
      .take(200)

    const companyById = new Map<string, typeof tenantCompanies[number]>()
    const companyBySlug = new Map<string, typeof tenantCompanies[number]>()
    for (const company of tenantCompanies) {
      companyById.set(company._id, company)
      if (company.slug) {
        companyBySlug.set(company.slug, company)
      }
    }

    // Limita a 500 maquinas para evitar OOM
    const machines = await ctx.db
      .query("machines")
      .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
      .take(500)

    return Promise.all(
      machines.map(async (machine) => {
        const activeToken = await findActiveMachineToken(ctx, machine._id, now)
        // Busca heartbeat da tabela separada (fonte de verdade), fallback para legado
        const lastHeartbeatAt = await getMachineLastHeartbeat(ctx, machine._id, machine.lastHeartbeatAt)
        const offlineThresholdMs = getOfflineThresholdMs()
        const staleThresholdMs = getStaleThresholdMs(offlineThresholdMs)
        const manualStatus = (machine.status ?? "").toLowerCase()
        let derivedStatus: string
        if (machine.isActive === false) {
          derivedStatus = "deactivated"
        } else if (["maintenance", "blocked"].includes(manualStatus)) {
          derivedStatus = manualStatus
        } else if (lastHeartbeatAt) {
          const age = now - lastHeartbeatAt
          if (age <= offlineThresholdMs) {
            derivedStatus = "online"
          } else if (age <= staleThresholdMs) {
            derivedStatus = "offline"
          } else {
            derivedStatus = "stale"
          }
        } else {
          derivedStatus = machine.status ?? "unknown"
        }

        const metadata = includeMetadata ? (machine.metadata ?? null) : null

        let metrics: Record<string, unknown> | null = null
        let inventory: Record<string, unknown> | null = null
        let postureAlerts: Array<Record<string, unknown>> | null = null
        let lastPostureAt: number | null = null

        if (metadata && typeof metadata === "object") {
          const metaRecord = metadata as Record<string, unknown>
          if (metaRecord.metrics && typeof metaRecord.metrics === "object") {
            metrics = metaRecord.metrics as Record<string, unknown>
          }
          if (metaRecord.inventory && typeof metaRecord.inventory === "object") {
            inventory = metaRecord.inventory as Record<string, unknown>
          }
          if (Array.isArray(metaRecord.postureAlerts)) {
            postureAlerts = metaRecord.postureAlerts as Array<Record<string, unknown>>
          }
          if (typeof metaRecord.lastPostureAt === "number") {
            lastPostureAt = metaRecord.lastPostureAt as number
          }
        }

        // linked users summary
        const linkedUserIds = machine.linkedUserIds ?? []
        const linkedUsers = await Promise.all(
          linkedUserIds.map(async (id) => {
            const u = await ctx.db.get(id)
            if (!u) return null
            return { id: u._id, email: u.email, name: u.name }
          })
        ).then((arr) => arr.filter(Boolean) as Array<{ id: string; email: string; name: string }>)

        // ticket count (limitado a 100 para performance)
        const ticketCount = await ctx.db
          .query("tickets")
          .withIndex("by_tenant_machine", (q) => q.eq("tenantId", tenantId).eq("machineId", machine._id))
          .take(100)
          .then((tickets) => tickets.length)

        const companyFromId = machine.companyId ? companyById.get(machine.companyId) ?? null : null
        const companyFromSlug = machine.companySlug ? companyBySlug.get(machine.companySlug) ?? null : null
        const resolvedCompany = companyFromId ?? companyFromSlug

        return {
          id: machine._id,
          tenantId: machine.tenantId,
          hostname: machine.hostname,
          displayName: machine.displayName ?? null,
          deviceType: machine.deviceType ?? "desktop",
          devicePlatform: machine.devicePlatform ?? null,
          deviceProfile: machine.deviceProfile ?? null,
          managementMode: machine.managementMode ?? "agent",
          companyId: machine.companyId ?? null,
          companySlug: machine.companySlug ?? companyFromId?.slug ?? companyFromSlug?.slug ?? null,
          companyName: resolvedCompany?.name ?? null,
          osName: machine.osName,
          osVersion: machine.osVersion ?? null,
          architecture: machine.architecture ?? null,
          macAddresses: machine.macAddresses,
          serialNumbers: machine.serialNumbers,
          authUserId: machine.authUserId ?? null,
          authEmail: machine.authEmail ?? null,
          persona: machine.persona ?? null,
          assignedUserId: machine.assignedUserId ?? null,
          assignedUserEmail: machine.assignedUserEmail ?? null,
          assignedUserName: machine.assignedUserName ?? null,
          assignedUserRole: machine.assignedUserRole ?? null,
          linkedUsers,
          status: derivedStatus,
          isActive: machine.isActive ?? true,
          lastHeartbeatAt: lastHeartbeatAt,
          heartbeatAgeMs: lastHeartbeatAt ? now - lastHeartbeatAt : null,
          registeredBy: machine.registeredBy ?? null,
          createdAt: machine.createdAt,
          updatedAt: machine.updatedAt,
          token: activeToken
            ? {
                expiresAt: activeToken.expiresAt,
                lastUsedAt: activeToken.lastUsedAt ?? null,
                usageCount: activeToken.usageCount ?? 0,
              }
            : null,
          metrics,
          inventory,
          postureAlerts,
          lastPostureAt,
          remoteAccess: machine.remoteAccess ?? null,
          customFields: machine.customFields ?? [],
          usbPolicy: machine.usbPolicy ?? null,
          usbPolicyStatus: machine.usbPolicyStatus ?? null,
          usbPolicyError: machine.usbPolicyError ?? null,
          ticketCount,
        }
      })
    )
  },
})

export async function getByIdHandler(
  ctx: QueryCtx,
  args: { id: Id<"machines">; includeMetadata?: boolean }
) {
  const includeMetadata = Boolean(args.includeMetadata)
  const now = Date.now()

  const machine = await ctx.db.get(args.id)
  if (!machine) return null

  const companyFromId = machine.companyId ? await ctx.db.get(machine.companyId) : null
  const machineSlug = machine.companySlug ?? null
  let companyFromSlug: typeof companyFromId | null = null
  if (!companyFromId && machineSlug) {
    companyFromSlug = await ctx.db
      .query("companies")
      .withIndex("by_tenant_slug", (q) => q.eq("tenantId", machine.tenantId).eq("slug", machineSlug))
      .unique()
  }
  const resolvedCompany = companyFromId ?? companyFromSlug

  const activeToken = await findActiveMachineToken(ctx, machine._id, now)
  // Busca heartbeat da tabela separada (fonte de verdade), fallback para legado
  const lastHeartbeatAt = await getMachineLastHeartbeat(ctx, machine._id, machine.lastHeartbeatAt)

  const offlineThresholdMs = getOfflineThresholdMs()
  const staleThresholdMs = getStaleThresholdMs(offlineThresholdMs)
  const manualStatus = (machine.status ?? "").toLowerCase()
  let derivedStatus: string
  if (machine.isActive === false) {
    derivedStatus = "deactivated"
  } else if (["maintenance", "blocked"].includes(manualStatus)) {
    derivedStatus = manualStatus
  } else if (lastHeartbeatAt) {
    const age = now - lastHeartbeatAt
    if (age <= offlineThresholdMs) {
      derivedStatus = "online"
    } else if (age <= staleThresholdMs) {
      derivedStatus = "offline"
    } else {
      derivedStatus = "stale"
    }
  } else {
    derivedStatus = machine.status ?? "unknown"
  }

  const meta = includeMetadata ? (machine.metadata ?? null) : null
  let metrics: Record<string, unknown> | null = null
  let inventory: Record<string, unknown> | null = null
  let postureAlerts: Array<Record<string, unknown>> | null = null
  let lastPostureAt: number | null = null
  if (meta && typeof meta === "object") {
    const metaRecord = meta as Record<string, unknown>
    if (metaRecord.metrics && typeof metaRecord.metrics === "object") {
      metrics = metaRecord.metrics as Record<string, unknown>
    }
    if (metaRecord.inventory && typeof metaRecord.inventory === "object") {
      inventory = metaRecord.inventory as Record<string, unknown>
    }
    if (Array.isArray(metaRecord.postureAlerts)) {
      postureAlerts = metaRecord.postureAlerts as Array<Record<string, unknown>>
    }
    if (typeof metaRecord.lastPostureAt === "number") {
      lastPostureAt = metaRecord.lastPostureAt as number
    }
  }

  const linkedUserIds = machine.linkedUserIds ?? []
  const linkedUsers = await Promise.all(
    linkedUserIds.map(async (id) => {
      const u = await ctx.db.get(id)
      if (!u) return null
      return { id: u._id, email: u.email, name: u.name }
    })
  ).then((arr) => arr.filter(Boolean) as Array<{ id: string; email: string; name: string }>)

  return {
    id: machine._id,
    tenantId: machine.tenantId,
    hostname: machine.hostname,
    displayName: machine.displayName ?? null,
    deviceType: machine.deviceType ?? "desktop",
    devicePlatform: machine.devicePlatform ?? null,
    deviceProfile: machine.deviceProfile ?? null,
    managementMode: machine.managementMode ?? "agent",
    companyId: machine.companyId ?? null,
    companySlug: machine.companySlug ?? resolvedCompany?.slug ?? null,
    companyName: resolvedCompany?.name ?? null,
    osName: machine.osName,
    osVersion: machine.osVersion ?? null,
    architecture: machine.architecture ?? null,
    macAddresses: machine.macAddresses,
    serialNumbers: machine.serialNumbers,
    authUserId: machine.authUserId ?? null,
    authEmail: machine.authEmail ?? null,
    persona: machine.persona ?? null,
    assignedUserId: machine.assignedUserId ?? null,
    assignedUserEmail: machine.assignedUserEmail ?? null,
    assignedUserName: machine.assignedUserName ?? null,
    assignedUserRole: machine.assignedUserRole ?? null,
    linkedUsers,
    status: derivedStatus,
    isActive: machine.isActive ?? true,
    lastHeartbeatAt: lastHeartbeatAt,
    heartbeatAgeMs: lastHeartbeatAt ? now - lastHeartbeatAt : null,
    registeredBy: machine.registeredBy ?? null,
    createdAt: machine.createdAt,
    updatedAt: machine.updatedAt,
    token: activeToken
      ? {
          expiresAt: activeToken.expiresAt,
          lastUsedAt: activeToken.lastUsedAt ?? null,
          usageCount: activeToken.usageCount ?? 0,
        }
      : null,
    metrics,
    inventory,
    postureAlerts,
    lastPostureAt,
    remoteAccess: machine.remoteAccess ?? null,
    customFields: machine.customFields ?? [],
    usbPolicy: machine.usbPolicy ?? null,
    usbPolicyStatus: machine.usbPolicyStatus ?? null,
    usbPolicyError: machine.usbPolicyError ?? null,
  }
}

export const getById = query({
  args: {
    id: v.id("machines"),
    includeMetadata: v.optional(v.boolean()),
  },
  handler: getByIdHandler,
})

export const listAlerts = query({
  args: {
    machineId: v.optional(v.id("machines")),
    deviceId: v.optional(v.id("machines")),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const machineId = args.machineId ?? args.deviceId
    if (!machineId) {
      throw new ConvexError("Identificador do dispositivo não informado")
    }
    const limit = Math.max(1, Math.min(args.limit ?? 50, 200))
    const alerts = await ctx.db
      .query("machineAlerts")
      .withIndex("by_machine_created", (q) => q.eq("machineId", machineId))
      .order("desc")
      .take(limit)

    return alerts.map((alert) => ({
      id: alert._id,
      machineId: alert.machineId,
      tenantId: alert.tenantId,
      companyId: alert.companyId ?? null,
      kind: alert.kind,
      message: alert.message,
      severity: alert.severity,
      createdAt: alert.createdAt,
    }))
  },
})

export const listOpenTickets = query({
  args: {
    machineId: v.optional(v.id("machines")),
    deviceId: v.optional(v.id("machines")),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, { machineId: providedMachineId, deviceId, limit }) => {
    const machineId = providedMachineId ?? deviceId
    if (!machineId) {
      throw new ConvexError("Identificador do dispositivo não informado")
    }
    const machine = await ctx.db.get(machineId)
    if (!machine) {
      return { totalOpen: 0, hasMore: false, tickets: [] }
    }
    const takeLimit = Math.max(1, Math.min(limit ?? 10, 50))
    const candidates = await ctx.db
      .query("tickets")
      .withIndex("by_tenant_machine", (q) => q.eq("tenantId", machine.tenantId).eq("machineId", machineId))
      .order("desc")
      .take(200)

    const openTickets = candidates
      .filter((ticket) => normalizeStatus(ticket.status) !== "RESOLVED")
      .sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))
    const totalOpen = openTickets.length
    const limited = openTickets.slice(0, takeLimit)

    return {
      totalOpen,
      hasMore: totalOpen > takeLimit,
      tickets: limited.map((ticket) => ({
        id: ticket._id,
        reference: ticket.reference,
        subject: ticket.subject,
        status: normalizeStatus(ticket.status),
        priority: ticket.priority ?? "MEDIUM",
        updatedAt: ticket.updatedAt,
        createdAt: ticket.createdAt,
        assignee: ticket.assigneeSnapshot
          ? {
              name: (ticket.assigneeSnapshot as { name?: string })?.name ?? null,
              email: (ticket.assigneeSnapshot as { email?: string })?.email ?? null,
            }
          : null,
        machine: {
          id: String(ticket.machineId ?? machineId),
          hostname:
            ((ticket.machineSnapshot as { hostname?: string } | undefined)?.hostname ?? machine.hostname ?? null),
        },
      })),
    }
  },
})

type MachineTicketsHistoryFilter = {
  statusFilter: "all" | "open" | "resolved"
  priorityFilter: string | null
  requesterEmail: string | null
  from: number | null
  to: number | null
}

type ListTicketsHistoryArgs = {
  machineId: Id<"machines">
  status?: "all" | "open" | "resolved"
  priority?: string
  requesterEmail?: string
  search?: string
  from?: number
  to?: number
  paginationOpts: Infer<typeof paginationOptsValidator>
}

type GetTicketsHistoryStatsArgs = {
  machineId: Id<"machines">
  status?: "all" | "open" | "resolved"
  priority?: string
  requesterEmail?: string
  search?: string
  from?: number
  to?: number
}

function createMachineTicketsQuery(
  ctx: QueryCtx,
  machine: Doc<"machines">,
  machineId: Id<"machines">,
  filters: MachineTicketsHistoryFilter
) {
  let working = ctx.db
    .query("tickets")
    .withIndex("by_tenant_machine", (q) => q.eq("tenantId", machine.tenantId).eq("machineId", machineId))
    .order("desc")

  if (filters.statusFilter === "open") {
    working = working.filter((q) =>
      q.or(
        q.eq(q.field("status"), "PENDING"),
        q.eq(q.field("status"), "AWAITING_ATTENDANCE"),
        q.eq(q.field("status"), "PAUSED")
      )
    )
  } else if (filters.statusFilter === "resolved") {
    working = working.filter((q) => q.eq(q.field("status"), "RESOLVED"))
  }

  if (filters.priorityFilter) {
    working = working.filter((q) => q.eq(q.field("priority"), filters.priorityFilter))
  }

  if (typeof filters.from === "number") {
    working = working.filter((q) => q.gte(q.field("updatedAt"), filters.from!))
  }

  if (typeof filters.to === "number") {
    working = working.filter((q) => q.lte(q.field("updatedAt"), filters.to!))
  }

  return working
}

function matchesRequesterEmail(ticket: Doc<"tickets">, requesterEmail: string | null): boolean {
  if (!requesterEmail) return true
  const requesterSnapshot = ticket.requesterSnapshot as { name?: string; email?: string } | undefined
  if (!requesterSnapshot?.email) return false
  return requesterSnapshot.email.toLowerCase() === requesterEmail.toLowerCase()
}

function matchesTicketSearch(ticket: Doc<"tickets">, searchTerm: string): boolean {
  const normalized = searchTerm.trim().toLowerCase()
  if (!normalized) return true

  const subject = ticket.subject.toLowerCase()
  if (subject.includes(normalized)) return true

  const summary = typeof ticket.summary === "string" ? ticket.summary.toLowerCase() : ""
  if (summary.includes(normalized)) return true

  const reference = `#${ticket.reference}`.toLowerCase()
  if (reference.includes(normalized)) return true

  const requesterSnapshot = ticket.requesterSnapshot as { name?: string; email?: string } | undefined
  if (requesterSnapshot) {
    if (requesterSnapshot.name?.toLowerCase().includes(normalized)) return true
    if (requesterSnapshot.email?.toLowerCase().includes(normalized)) return true
  }

  const assigneeSnapshot = ticket.assigneeSnapshot as { name?: string; email?: string } | undefined
  if (assigneeSnapshot) {
    if (assigneeSnapshot.name?.toLowerCase().includes(normalized)) return true
    if (assigneeSnapshot.email?.toLowerCase().includes(normalized)) return true
  }

  return false
}

export async function listTicketsHistoryHandler(ctx: QueryCtx, args: ListTicketsHistoryArgs) {
  const machine = await ctx.db.get(args.machineId)
  if (!machine) {
    return {
      page: [],
      isDone: true,
      continueCursor: args.paginationOpts.cursor ?? "",
    }
  }

  const normalizedStatusFilter = args.status ?? "all"
  const normalizedPriorityFilter = args.priority ? args.priority.toUpperCase() : null
  const normalizedRequesterEmail = args.requesterEmail?.trim().toLowerCase() ?? null
  const searchTerm = args.search?.trim().toLowerCase() ?? null
  const from = typeof args.from === "number" ? args.from : null
  const to = typeof args.to === "number" ? args.to : null
  const filters: MachineTicketsHistoryFilter = {
    statusFilter: normalizedStatusFilter,
    priorityFilter: normalizedPriorityFilter,
    requesterEmail: normalizedRequesterEmail,
    from,
    to,
  }

  const pageResult = await createMachineTicketsQuery(ctx, machine, args.machineId, filters).paginate(args.paginationOpts)

  let page = pageResult.page
  if (normalizedRequesterEmail) {
    page = page.filter((ticket) => matchesRequesterEmail(ticket, normalizedRequesterEmail))
  }
  if (searchTerm) {
    page = page.filter((ticket) => matchesTicketSearch(ticket, searchTerm))
  }
  const queueCache = new Map<string, Doc<"queues"> | null>()
  const items = await Promise.all(
    page.map(async (ticket) => {
      let queueName: string | null = null
      if (ticket.queueId) {
        const key = String(ticket.queueId)
        if (!queueCache.has(key)) {
          queueCache.set(key, (await ctx.db.get(ticket.queueId)) as Doc<"queues"> | null)
        }
        queueName = queueCache.get(key)?.name ?? null
      }
      const requesterSnapshot = ticket.requesterSnapshot as { name?: string; email?: string } | undefined
      const assigneeSnapshot = ticket.assigneeSnapshot as { name?: string; email?: string } | undefined
      return {
        id: ticket._id,
        reference: ticket.reference,
        subject: ticket.subject,
        status: normalizeStatus(ticket.status),
        priority: (ticket.priority ?? "MEDIUM").toString().toUpperCase(),
        updatedAt: ticket.updatedAt ?? ticket.createdAt ?? 0,
        createdAt: ticket.createdAt ?? 0,
        queue: queueName,
        requester: requesterSnapshot
          ? {
              name: requesterSnapshot.name ?? null,
              email: requesterSnapshot.email ?? null,
            }
          : null,
        assignee: assigneeSnapshot
          ? {
              name: assigneeSnapshot.name ?? null,
              email: assigneeSnapshot.email ?? null,
            }
          : null,
      }
    })
  )

  return {
    page: items,
    isDone: pageResult.isDone,
    continueCursor: pageResult.continueCursor,
    splitCursor: pageResult.splitCursor ?? undefined,
    pageStatus: pageResult.pageStatus ?? undefined,
  }
}

export const listTicketsHistory = query({
  args: {
    machineId: v.id("machines"),
    status: v.optional(v.union(v.literal("all"), v.literal("open"), v.literal("resolved"))),
    priority: v.optional(v.string()),
    requesterEmail: v.optional(v.string()),
    search: v.optional(v.string()),
    from: v.optional(v.number()),
    to: v.optional(v.number()),
    paginationOpts: paginationOptsValidator,
  },
  handler: listTicketsHistoryHandler,
})

export async function getTicketsHistoryStatsHandler(
  ctx: QueryCtx,
  args: GetTicketsHistoryStatsArgs
) {
  const machine = await ctx.db.get(args.machineId)
  if (!machine) {
    return { total: 0, openCount: 0, resolvedCount: 0 }
  }

  const normalizedStatusFilter = args.status ?? "all"
  const normalizedPriorityFilter = args.priority ? args.priority.toUpperCase() : null
  const normalizedRequesterEmail = args.requesterEmail?.trim().toLowerCase() ?? null
  const searchTerm = args.search?.trim().toLowerCase() ?? ""
  const from = typeof args.from === "number" ? args.from : null
  const to = typeof args.to === "number" ? args.to : null
  const filters: MachineTicketsHistoryFilter = {
    statusFilter: normalizedStatusFilter,
    priorityFilter: normalizedPriorityFilter,
    requesterEmail: normalizedRequesterEmail,
    from,
    to,
  }

  let cursor: string | null = null
  let total = 0
  let openCount = 0
  let done = false

  while (!done) {
    const pageResult = await createMachineTicketsQuery(ctx, machine, args.machineId, filters).paginate({
      numItems: MACHINE_TICKETS_STATS_PAGE_SIZE,
      cursor,
    })
    let page = pageResult.page
    if (normalizedRequesterEmail) {
      page = page.filter((ticket) => matchesRequesterEmail(ticket, normalizedRequesterEmail))
    }
    if (searchTerm) {
      page = page.filter((ticket) => matchesTicketSearch(ticket, searchTerm))
    }
    total += page.length
    for (const ticket of page) {
      if (OPEN_TICKET_STATUSES.has(normalizeStatus(ticket.status))) {
        openCount += 1
      }
    }
    done = pageResult.isDone
    cursor = pageResult.continueCursor ?? null
    if (!cursor) {
      done = true
    }
  }

  return {
    total,
    openCount,
    resolvedCount: total - openCount,
  }
}

export const getTicketsHistoryStats = query({
  args: {
    machineId: v.id("machines"),
    status: v.optional(v.union(v.literal("all"), v.literal("open"), v.literal("resolved"))),
    priority: v.optional(v.string()),
    requesterEmail: v.optional(v.string()),
    search: v.optional(v.string()),
    from: v.optional(v.number()),
    to: v.optional(v.number()),
  },
  handler: getTicketsHistoryStatsHandler,
})

// Lista os solicitantes unicos que abriram tickets nesta maquina
export const listMachineRequesters = query({
  args: {
    machineId: v.id("machines"),
  },
  handler: async (ctx, args) => {
    const machine = await ctx.db.get(args.machineId)
    if (!machine) {
      return []
    }

    const tickets = await ctx.db
      .query("tickets")
      .withIndex("by_tenant_machine", (q) => q.eq("tenantId", machine.tenantId).eq("machineId", args.machineId))
      .take(200)

    const requestersMap = new Map<string, { email: string; name: string | null }>()
    for (const ticket of tickets) {
      const snapshot = ticket.requesterSnapshot as { name?: string; email?: string } | undefined
      if (snapshot?.email) {
        const emailLower = snapshot.email.toLowerCase()
        if (!requestersMap.has(emailLower)) {
          requestersMap.set(emailLower, {
            email: snapshot.email,
            name: snapshot.name ?? null,
          })
        }
      }
    }

    return Array.from(requestersMap.values()).sort((a, b) => {
      const nameA = a.name ?? a.email
      const nameB = b.name ?? b.email
      return nameA.localeCompare(nameB)
    })
  },
})

export async function updatePersonaHandler(
  ctx: MutationCtx,
  args: {
    machineId: Id<"machines">
    persona?: string | null
    assignedUserId?: Id<"users">
    assignedUserEmail?: string | null
    assignedUserName?: string | null
    assignedUserRole?: string | null
  }
) {
  const machine = await ctx.db.get(args.machineId)
  if (!machine) {
    throw new ConvexError("Dispositivo não encontrada")
  }

  let nextPersona = machine.persona ?? undefined
  const personaProvided = args.persona !== undefined
  if (args.persona !== undefined) {
    const trimmed = (args.persona ?? "").trim().toLowerCase()
    if (!trimmed) {
      nextPersona = undefined
    } else if (!ALLOWED_MACHINE_PERSONAS.has(trimmed)) {
      throw new ConvexError("Perfil inválido para a dispositivo")
    } else {
      nextPersona = trimmed
    }
  }

  let nextAssignedUserId = machine.assignedUserId ?? undefined
  if (args.assignedUserId !== undefined) {
    nextAssignedUserId = args.assignedUserId
  }

  let nextAssignedEmail = machine.assignedUserEmail ?? undefined
  if (args.assignedUserEmail !== undefined) {
    const trimmedEmail = (args.assignedUserEmail ?? "").trim().toLowerCase()
    nextAssignedEmail = trimmedEmail || undefined
  }

  let nextAssignedName = machine.assignedUserName ?? undefined
  if (args.assignedUserName !== undefined) {
    const trimmedName = (args.assignedUserName ?? "").trim()
    nextAssignedName = trimmedName || undefined
  }

  let nextAssignedRole = machine.assignedUserRole ?? undefined
  if (args.assignedUserRole !== undefined) {
    const trimmedRole = (args.assignedUserRole ?? "").trim().toUpperCase()
    nextAssignedRole = trimmedRole || undefined
  }

  if (personaProvided && !nextPersona) {
    nextAssignedUserId = undefined
    nextAssignedEmail = undefined
    nextAssignedName = undefined
    nextAssignedRole = undefined
  }

  if (nextPersona && !nextAssignedUserId) {
    throw new ConvexError("Associe um usuário ao definir a persona da dispositivo")
  }

  if (nextPersona && nextAssignedUserId) {
    const assignedUser = await ctx.db.get(nextAssignedUserId)
    if (!assignedUser) {
      throw new ConvexError("Usuário vinculado não encontrado")
    }
    if (assignedUser.tenantId !== machine.tenantId) {
      throw new ConvexError("Usuário vinculado pertence a outro tenant")
    }
  }

  let nextMetadata = machine.metadata
  if (nextPersona) {
    const collaboratorMeta = {
      email: nextAssignedEmail ?? null,
      name: nextAssignedName ?? null,
      role: nextPersona,
    }
    nextMetadata = mergeMetadata(machine.metadata, { collaborator: collaboratorMeta })
  }

  const patch: Record<string, unknown> = {
    persona: nextPersona,
    assignedUserId: nextPersona ? nextAssignedUserId : undefined,
    assignedUserEmail: nextPersona ? nextAssignedEmail : undefined,
    assignedUserName: nextPersona ? nextAssignedName : undefined,
    assignedUserRole: nextPersona ? nextAssignedRole : undefined,
    updatedAt: Date.now(),
  }
  if (nextMetadata !== machine.metadata) {
    patch.metadata = nextMetadata
  }

  if (personaProvided) {
    patch.persona = nextPersona
  }

  if (nextPersona) {
    patch.assignedUserId = nextAssignedUserId
    patch.assignedUserEmail = nextAssignedEmail
    patch.assignedUserName = nextAssignedName
    patch.assignedUserRole = nextAssignedRole
  } else if (personaProvided) {
    patch.assignedUserId = undefined
    patch.assignedUserEmail = undefined
    patch.assignedUserName = undefined
    patch.assignedUserRole = undefined
  }

  await ctx.db.patch(machine._id, patch)
  return { ok: true, persona: nextPersona ?? null }
}

export const saveDeviceCustomFields = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    machineId: v.id("machines"),
    fields: v.array(
      v.object({
        fieldId: v.id("deviceFields"),
        value: v.any(),
      })
    ),
  },
  handler: async (ctx, args) => {
    await requireAdmin(ctx, args.actorId, args.tenantId)
    const machine = await ctx.db.get(args.machineId)
    if (!machine || machine.tenantId !== args.tenantId) {
      throw new ConvexError("Dispositivo não encontrado")
    }

    const companyId = machine.companyId ?? null
    const deviceType = (machine.deviceType ?? "desktop").toLowerCase()

    const entries = await Promise.all(
      args.fields.map(async ({ fieldId, value }) => {
        const definition = await ctx.db.get(fieldId)
        if (!definition || definition.tenantId !== args.tenantId) {
          return null
        }
        if (companyId && definition.companyId && definition.companyId !== companyId) {
          return null
        }
        if (!companyId && definition.companyId) {
          return null
        }
        const scope = (definition.scope ?? "all").toLowerCase()
        if (scope !== "all" && scope !== deviceType) {
          return null
        }
        const displayValue =
          value === null || value === undefined
            ? null
            : formatDeviceCustomFieldDisplay(definition.type, value, definition.options ?? undefined)
        return {
          fieldId: definition._id,
          fieldKey: definition.key,
          label: definition.label,
          type: definition.type,
          value: value ?? null,
          displayValue: displayValue ?? undefined,
        }
      })
    )

    const customFields = entries.filter(Boolean) as Array<{
      fieldId: Id<"deviceFields">
      fieldKey: string
      label: string
      type: string
      value: unknown
      displayValue?: string
    }>

    await ctx.db.patch(args.machineId, {
      customFields,
      updatedAt: Date.now(),
    })
  },
})

export const saveDeviceProfile = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    machineId: v.optional(v.id("machines")),
    companyId: v.optional(v.id("companies")),
    companySlug: v.optional(v.string()),
    displayName: v.string(),
    hostname: v.optional(v.string()),
    deviceType: v.string(),
    devicePlatform: v.optional(v.string()),
    osName: v.optional(v.string()),
    osVersion: v.optional(v.string()),
    macAddresses: v.optional(v.array(v.string())),
    serialNumbers: v.optional(v.array(v.string())),
    profile: v.optional(v.any()),
    status: v.optional(v.string()),
    isActive: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    await requireAdmin(ctx, args.actorId, args.tenantId)
    await ensureMobileDeviceFields(ctx, args.tenantId)
    const displayName = args.displayName.trim()
    if (!displayName) {
      throw new ConvexError("Informe o nome do dispositivo")
    }
    const hostname = (args.hostname ?? displayName).trim()
    if (!hostname) {
      throw new ConvexError("Informe o identificador do dispositivo")
    }

    const normalizedType = (() => {
      const candidate = args.deviceType.trim().toLowerCase()
      if (["desktop", "mobile", "tablet"].includes(candidate)) return candidate
      return "desktop"
    })()
    const normalizedPlatform = args.devicePlatform?.trim() || args.osName?.trim() || null
    const normalizedStatus = (args.status ?? "unknown").trim() || "unknown"
    const normalizedSlug = args.companySlug?.trim() || undefined
    const osNameInput = args.osName === undefined ? undefined : args.osName.trim()
    const osVersionInput = args.osVersion === undefined ? undefined : args.osVersion.trim()
    const now = Date.now()

    if (args.machineId) {
      const machine = await ctx.db.get(args.machineId)
      if (!machine || machine.tenantId !== args.tenantId) {
        throw new ConvexError("Dispositivo não encontrado para atualização")
      }
      if (machine.managementMode && machine.managementMode !== "manual") {
        throw new ConvexError("Somente dispositivos manuais podem ser editados por esta ação")
      }

      const normalizedIdentifiers = normalizeOptionalIdentifiers(args.macAddresses, args.serialNumbers)
      const macAddresses =
        args.macAddresses === undefined ? machine.macAddresses : normalizedIdentifiers.macs
      const serialNumbers =
        args.serialNumbers === undefined ? machine.serialNumbers : normalizedIdentifiers.serials
      const deviceProfilePatch = args.profile === undefined ? undefined : args.profile ?? null

      const osNameValue = osNameInput === undefined ? machine.osName : osNameInput || machine.osName
      const osVersionValue =
        osVersionInput === undefined ? machine.osVersion ?? undefined : osVersionInput || undefined

      await ctx.db.patch(args.machineId, {
        companyId: args.companyId ?? machine.companyId ?? undefined,
        companySlug: normalizedSlug ?? machine.companySlug ?? undefined,
        hostname,
        displayName,
        osName: osNameValue,
        osVersion: osVersionValue,
        macAddresses,
        serialNumbers,
        deviceType: normalizedType,
        devicePlatform: normalizedPlatform ?? machine.devicePlatform ?? undefined,
        deviceProfile: deviceProfilePatch,
        managementMode: "manual",
        status: normalizedStatus,
        isActive: args.isActive ?? machine.isActive ?? true,
        updatedAt: now,
      })

      return { machineId: args.machineId }
    }

    const normalizedIdentifiers = normalizeOptionalIdentifiers(args.macAddresses, args.serialNumbers)
    const fingerprint = generateManualFingerprint(args.tenantId, displayName)
    const deviceProfile = args.profile ?? undefined
    const osNameValue = osNameInput || normalizedPlatform || "Desconhecido"
    const osVersionValue = osVersionInput || undefined

    const machineId = await ctx.db.insert("machines", {
      tenantId: args.tenantId,
      companyId: args.companyId ?? undefined,
      companySlug: normalizedSlug ?? undefined,
      hostname,
      displayName,
      osName: osNameValue,
      osVersion: osVersionValue,
      macAddresses: normalizedIdentifiers.macs,
      serialNumbers: normalizedIdentifiers.serials,
      fingerprint,
      metadata: undefined,
      deviceType: normalizedType,
      devicePlatform: normalizedPlatform ?? undefined,
      deviceProfile,
      managementMode: "manual",
      status: normalizedStatus,
      isActive: args.isActive ?? true,
      createdAt: now,
      updatedAt: now,
      registeredBy: "manual",
      persona: undefined,
      assignedUserId: undefined,
      assignedUserEmail: undefined,
      assignedUserName: undefined,
      assignedUserRole: undefined,
    })

    return { machineId }
  },
})

export const updatePersona = mutation({
  args: {
    machineId: v.id("machines"),
    persona: v.optional(v.string()),
    assignedUserId: v.optional(v.id("users")),
    assignedUserEmail: v.optional(v.string()),
    assignedUserName: v.optional(v.string()),
    assignedUserRole: v.optional(v.string()),
  },
  handler: updatePersonaHandler,
})

export const getContext = query({
  args: {
    machineId: v.id("machines"),
  },
  handler: async (ctx, args) => {
    const machine = await ctx.db.get(args.machineId)
    if (!machine) {
      throw new ConvexError("Dispositivo não encontrada")
    }

    const linkedUserIds = machine.linkedUserIds ?? []
    const linkedUsers = await Promise.all(
      linkedUserIds.map(async (id) => {
        const u = await ctx.db.get(id)
        if (!u) return null
        return { id: u._id, email: u.email, name: u.name }
      })
    ).then((arr) => arr.filter(Boolean) as Array<{ id: string; email: string; name: string }>)

    return {
      id: machine._id,
      tenantId: machine.tenantId,
      companyId: machine.companyId ?? null,
      companySlug: machine.companySlug ?? null,
      persona: machine.persona ?? null,
      assignedUserId: machine.assignedUserId ?? null,
      assignedUserEmail: machine.assignedUserEmail ?? null,
      assignedUserName: machine.assignedUserName ?? null,
      assignedUserRole: machine.assignedUserRole ?? null,
      metadata: machine.metadata ?? null,
      authEmail: machine.authEmail ?? null,
      isActive: machine.isActive ?? true,
      linkedUsers,
    }
  },
})

export const findByAuthEmail = query({
  args: {
    authEmail: v.string(),
  },
  handler: async (ctx, args) => {
    const normalizedEmail = args.authEmail.trim().toLowerCase()

    const machine = await ctx.db
      .query("machines")
      .withIndex("by_auth_email", (q) => q.eq("authEmail", normalizedEmail))
      .first()

    if (!machine) {
      return null
    }

    return {
      id: machine._id,
    }
  },
})

export const linkAuthAccount = mutation({
  args: {
    machineId: v.id("machines"),
    authUserId: v.string(),
    authEmail: v.string(),
  },
  handler: async (ctx, args) => {
    const machine = await ctx.db.get(args.machineId)
    if (!machine) {
      throw new ConvexError("Dispositivo não encontrada")
    }

    await ctx.db.patch(machine._id, {
      authUserId: args.authUserId,
      authEmail: args.authEmail,
      updatedAt: Date.now(),
    })

    return { ok: true }
  },
})

export const linkUser = mutation({
  args: {
    machineId: v.id("machines"),
    email: v.string(),
  },
  handler: async (ctx, { machineId, email }) => {
    const machine = await ctx.db.get(machineId)
    if (!machine) throw new ConvexError("Dispositivo não encontrada")
    const tenantId = machine.tenantId
    const normalized = email.trim().toLowerCase()

    const user = await ctx.db
      .query("users")
      .withIndex("by_tenant_email", (q) => q.eq("tenantId", tenantId).eq("email", normalized))
      .first()
    if (!user) throw new ConvexError("Usuário não encontrado")
    const role = (user.role ?? "").toUpperCase()
    if (role === 'ADMIN' || role === 'AGENT') {
      throw new ConvexError('Usuários administrativos não podem ser vinculados a dispositivos')
    }

    const current = new Set<Id<"users">>(machine.linkedUserIds ?? [])
    current.add(user._id)
    await ctx.db.patch(machine._id, { linkedUserIds: Array.from(current), updatedAt: Date.now() })
    return { ok: true }
  },
})

export const unlinkUser = mutation({
  args: {
    machineId: v.id("machines"),
    userId: v.id("users"),
  },
  handler: async (ctx, { machineId, userId }) => {
    const machine = await ctx.db.get(machineId)
    if (!machine) throw new ConvexError("Dispositivo não encontrada")
    const next = (machine.linkedUserIds ?? []).filter((id) => id !== userId)
    await ctx.db.patch(machine._id, { linkedUserIds: next, updatedAt: Date.now() })
    return { ok: true }
  },
})

export const rename = mutation({
  args: {
    machineId: v.id("machines"),
    actorId: v.id("users"),
    hostname: v.string(),
  },
  handler: async (ctx, { machineId, actorId, hostname }) => {
    // Reutiliza requireStaff através de tickets.ts helpers
    const machine = await ctx.db.get(machineId)
    if (!machine) {
      throw new ConvexError("Dispositivo não encontrada")
    }
    // Verifica permissão no tenant da dispositivo
    const viewer = await ctx.db.get(actorId)
    if (!viewer || viewer.tenantId !== machine.tenantId) {
      throw new ConvexError("Acesso negado ao tenant da dispositivo")
    }
    const normalizedRole = (viewer.role ?? "AGENT").toUpperCase()
    const STAFF = new Set(["ADMIN", "MANAGER", "AGENT"])
    if (!STAFF.has(normalizedRole)) {
      throw new ConvexError("Apenas equipe interna pode renomear dispositivos")
    }

    const nextName = hostname.trim()
    if (nextName.length < 2) {
      throw new ConvexError("Informe um nome válido para a dispositivo")
    }

    await ctx.db.patch(machineId, {
      hostname: nextName,
      displayName: nextName,
      updatedAt: Date.now(),
    })
    return { ok: true }
  },
})

export const toggleActive = mutation({
  args: {
    machineId: v.id("machines"),
    actorId: v.id("users"),
    active: v.boolean(),
  },
  handler: async (ctx, { machineId, actorId, active }) => {
    const machine = await ctx.db.get(machineId)
    if (!machine) {
      throw new ConvexError("Dispositivo não encontrada")
    }

    const actor = await ctx.db.get(actorId)
    if (!actor || actor.tenantId !== machine.tenantId) {
      throw new ConvexError("Acesso negado ao tenant da dispositivo")
    }
    const normalizedRole = (actor.role ?? "AGENT").toUpperCase()
    const STAFF = new Set(["ADMIN", "MANAGER", "AGENT"])
    if (!STAFF.has(normalizedRole)) {
      throw new ConvexError("Apenas equipe interna pode atualizar o status da dispositivo")
    }

    await ctx.db.patch(machineId, {
      isActive: active,
      updatedAt: Date.now(),
    })

    return { ok: true }
  },
})

export const resetAgent = mutation({
  args: {
    machineId: v.id("machines"),
    actorId: v.id("users"),
  },
  handler: async (ctx, { machineId, actorId }) => {
    const machine = await ctx.db.get(machineId)
    if (!machine) {
      throw new ConvexError("Dispositivo não encontrada")
    }

    const actor = await ctx.db.get(actorId)
    if (!actor || actor.tenantId !== machine.tenantId) {
      throw new ConvexError("Acesso negado ao tenant da dispositivo")
    }
    const normalizedRole = (actor.role ?? "AGENT").toUpperCase()
    const STAFF = new Set(["ADMIN", "MANAGER", "AGENT"])
    if (!STAFF.has(normalizedRole)) {
      throw new ConvexError("Apenas equipe interna pode resetar o agente da dispositivo")
    }

    const tokens = await ctx.db
      .query("machineTokens")
      .withIndex("by_machine", (q) => q.eq("machineId", machineId))
      .take(100)

    const now = Date.now()
    let revokedCount = 0
    for (const token of tokens) {
      if (!token.revoked) {
        await ctx.db.patch(token._id, {
          revoked: true,
          revokedAt: now,
          expiresAt: now,
        })
        revokedCount += 1
      }
    }

    await ctx.db.patch(machineId, {
      status: "unknown",
      updatedAt: now,
    })

    return { machineId, revoked: revokedCount }
  },
})

/**
 * Query para o desktop monitorar o estado da máquina em tempo real.
 * O desktop faz subscribe nessa query e reage imediatamente quando:
 * - isActive muda para false (desativação)
 * - hasValidToken muda para false (reset/revogação de tokens)
 */
export const getMachineState = query({
  args: {
    machineId: v.id("machines"),
  },
  handler: async (ctx, { machineId }) => {
    const machine = await ctx.db.get(machineId)
    if (!machine) {
      return { found: false, isActive: false, hasValidToken: false, status: "unknown" as const }
    }

    // Verifica se existe algum token válido (não revogado e não expirado)
    const now = Date.now()
    const tokens = await ctx.db
      .query("machineTokens")
      .withIndex("by_machine", (q) => q.eq("machineId", machineId))
      .take(10)

    const hasValidToken = tokens.some((token) => {
      if (token.revoked) return false
      if (token.expiresAt && token.expiresAt < now) return false
      return true
    })

    return {
      found: true,
      isActive: machine.isActive ?? true,
      hasValidToken,
      status: machine.status ?? "unknown",
    }
  },
})

type RemoteAccessEntry = {
  id: string
  provider: string
  identifier: string
  url: string | null
  username: string | null
  password: string | null
  notes: string | null
  lastVerifiedAt: number | null
  metadata: Record<string, unknown> | null
}

function createRemoteAccessId() {
  return `ra_${Math.random().toString(36).slice(2, 8)}${Date.now().toString(36)}`
}

function coerceString(value: unknown): string | null {
  if (typeof value === "string") {
    const trimmed = value.trim()
    return trimmed.length > 0 ? trimmed : null
  }
  return null
}

function coerceNumber(value: unknown): number | null {
  if (typeof value === "number" && Number.isFinite(value)) {
    return value
  }
  if (typeof value === "string") {
    const trimmed = value.trim()
    if (!trimmed) return null
    const parsed = Number(trimmed)
    return Number.isFinite(parsed) ? parsed : null
  }
  return null
}

function normalizeRemoteAccessEntry(raw: unknown): RemoteAccessEntry | null {
  if (!raw) return null
  if (typeof raw === "string") {
    const trimmed = raw.trim()
    if (!trimmed) return null
    const isUrl = /^https?:\/\//i.test(trimmed)
    return {
      id: createRemoteAccessId(),
      provider: "Remoto",
      identifier: isUrl ? trimmed : trimmed,
      url: isUrl ? trimmed : null,
      username: null,
      password: null,
      notes: null,
      lastVerifiedAt: null,
      metadata: null,
    }
  }
  if (typeof raw !== "object") return null
  const record = raw as Record<string, unknown>
  const provider =
    coerceString(record.provider) ??
    coerceString(record.tool) ??
    coerceString(record.vendor) ??
    coerceString(record.name) ??
    "Remoto"
  const identifier =
    coerceString(record.identifier) ??
    coerceString(record.code) ??
    coerceString(record.id) ??
    coerceString(record.accessId)
  const url =
    coerceString(record.url) ??
    coerceString(record.link) ??
    coerceString(record.remoteUrl) ??
    coerceString(record.console) ??
    coerceString(record.viewer) ??
    null
  const resolvedIdentifier = identifier ?? url ?? "Acesso remoto"
  const notes = coerceString(record.notes) ?? coerceString(record.note) ?? coerceString(record.description) ?? coerceString(record.obs) ?? null
  const username =
    coerceString((record as Record<string, unknown>).username) ??
    coerceString((record as Record<string, unknown>).user) ??
    coerceString((record as Record<string, unknown>).login) ??
    coerceString((record as Record<string, unknown>).email) ??
    coerceString((record as Record<string, unknown>).account) ??
    null
  const password =
    coerceString((record as Record<string, unknown>).password) ??
    coerceString((record as Record<string, unknown>).pass) ??
    coerceString((record as Record<string, unknown>).secret) ??
    coerceString((record as Record<string, unknown>).pin) ??
    null
  const timestamp =
    coerceNumber(record.lastVerifiedAt) ??
    coerceNumber(record.verifiedAt) ??
    coerceNumber(record.checkedAt) ??
    coerceNumber(record.updatedAt) ??
    null
  const id = coerceString(record.id) ?? createRemoteAccessId()
  const metadata =
    record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata)
      ? (record.metadata as Record<string, unknown>)
      : null
  return {
    id,
    provider,
    identifier: resolvedIdentifier,
    url,
    username,
    password,
    notes,
    lastVerifiedAt: timestamp,
    metadata,
  }
}

function normalizeRemoteAccessList(raw: unknown): RemoteAccessEntry[] {
  const source = Array.isArray(raw) ? raw : raw ? [raw] : []
  const seen = new Set<string>()
  const entries: RemoteAccessEntry[] = []
  for (const item of source) {
    const entry = normalizeRemoteAccessEntry(item)
    if (!entry) continue
    let nextId = entry.id
    while (seen.has(nextId)) {
      nextId = createRemoteAccessId()
    }
    seen.add(nextId)
    entries.push(nextId === entry.id ? entry : { ...entry, id: nextId })
  }
  return entries
}

async function removeDuplicateRemoteAccessEntries(
  ctx: MutationCtx,
  tenantId: string,
  currentMachineId: Id<"machines">,
  provider: string,
  identifier: string,
  now: number
) {
  // Limita a 500 maquinas para evitar OOM
  const machines = await ctx.db
    .query("machines")
    .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
    .take(500)

  const providerLc = provider.toLowerCase()
  const identifierLc = identifier.toLowerCase()

  for (const device of machines) {
    if (device._id === currentMachineId) continue
    const entries = normalizeRemoteAccessList(device.remoteAccess)
    const filtered = entries.filter(
      (entry) =>
        entry.provider.toLowerCase() !== providerLc || entry.identifier.toLowerCase() !== identifierLc
    )
    if (filtered.length === entries.length) continue
    await ctx.db.patch(device._id, {
      remoteAccess: filtered.length > 0 ? filtered : null,
      updatedAt: now,
    })
  }
}

async function upsertRemoteAccessSnapshotFromHeartbeat(
  ctx: MutationCtx,
  machine: Doc<"machines">,
  snapshot: unknown,
  timestamp: number
) {
  const normalized = normalizeRemoteAccessEntry(snapshot)
  if (!normalized) return
  const provider = (normalized.provider ?? "Remote").trim()
  const identifier = (normalized.identifier ?? "").trim()
  if (!identifier) return

  const existingEntries = normalizeRemoteAccessList(machine.remoteAccess)

  // Busca primeiro por provider + identifier (atualização exata)
  let idx = existingEntries.findIndex(
    (entry) => entry.provider.toLowerCase() === provider.toLowerCase() && entry.identifier.toLowerCase() === identifier.toLowerCase()
  )

  // Se não encontrou, busca apenas por provider (substituição - ex: RustDesk ID mudou)
  if (idx < 0) {
    idx = existingEntries.findIndex(
      (entry) => entry.provider.toLowerCase() === provider.toLowerCase()
    )
  }

  const entryId = idx >= 0 ? existingEntries[idx].id : createRemoteAccessId()
  const metadata = {
    ...(normalized.metadata ?? {}),
    snapshotSource: "heartbeat",
    provider,
    identifier,
    machineId: machine._id,
    hostname: machine.hostname,
    lastVerifiedAt: timestamp,
  }

  const updatedEntry: RemoteAccessEntry = {
    id: entryId,
    provider,
    identifier,
    url: normalized.url ?? null,
    username: normalized.username ?? null,
    password: normalized.password ?? null,
    notes: normalized.notes ?? null,
    lastVerifiedAt: timestamp,
    metadata,
  }

  const nextEntries =
    idx >= 0
      ? existingEntries.map((entry, index) => (index === idx ? updatedEntry : entry))
      : [...existingEntries, updatedEntry]

  await ctx.db.patch(machine._id, {
    remoteAccess: nextEntries,
    updatedAt: timestamp,
  })
}

export const updateRemoteAccess = mutation({
  args: {
    machineId: v.id("machines"),
    actorId: v.id("users"),
    provider: v.optional(v.string()),
    identifier: v.optional(v.string()),
    url: v.optional(v.string()),
    username: v.optional(v.string()),
    password: v.optional(v.string()),
    notes: v.optional(v.string()),
    action: v.optional(v.string()),
    entryId: v.optional(v.string()),
    clear: v.optional(v.boolean()),
  },
  handler: async (ctx, { machineId, actorId, provider, identifier, url, username, password, notes, action, entryId, clear }) => {
    const machine = await ctx.db.get(machineId)
    if (!machine) {
      throw new ConvexError("Dispositivo não encontrada")
    }

    const actor = await ctx.db.get(actorId)
    if (!actor || actor.tenantId !== machine.tenantId) {
      throw new ConvexError("Acesso negado ao tenant da dispositivo")
    }

    const normalizedRole = (actor.role ?? "AGENT").toUpperCase()
    if (normalizedRole !== "ADMIN" && normalizedRole !== "AGENT") {
      throw new ConvexError("Somente administradores e agentes podem ajustar o acesso remoto.")
    }

    const actionMode = (() => {
      if (clear) return "clear" as const
      const normalized = (action ?? "").toLowerCase()
      if (normalized === "clear") return "clear" as const
      if (normalized === "delete" || normalized === "remove") return "delete" as const
      return "upsert" as const
    })()

    const existingEntries = normalizeRemoteAccessList(machine.remoteAccess)

    if (actionMode === "clear") {
      await ctx.db.patch(machineId, { remoteAccess: null, updatedAt: Date.now() })
      return { remoteAccess: null }
    }

    if (actionMode === "delete") {
      const trimmedEntryId = coerceString(entryId)
      const trimmedProvider = coerceString(provider)
      const trimmedIdentifier = coerceString(identifier)
      let target: RemoteAccessEntry | undefined
      if (trimmedEntryId) {
        target = existingEntries.find((entry) => entry.id === trimmedEntryId)
      }
      if (!target && trimmedProvider && trimmedIdentifier) {
        target = existingEntries.find(
          (entry) => entry.provider === trimmedProvider && entry.identifier === trimmedIdentifier
        )
      }
      if (!target && trimmedIdentifier) {
        target = existingEntries.find((entry) => entry.identifier === trimmedIdentifier)
      }
      if (!target && trimmedProvider) {
        target = existingEntries.find((entry) => entry.provider === trimmedProvider)
      }
      if (!target) {
        throw new ConvexError("Entrada de acesso remoto não encontrada.")
      }
      const nextEntries = existingEntries.filter((entry) => entry.id !== target!.id)
      const nextValue = nextEntries.length > 0 ? nextEntries : null
      await ctx.db.patch(machineId, { remoteAccess: nextValue, updatedAt: Date.now() })
      return { remoteAccess: nextValue }
    }

    const trimmedProvider = (provider ?? "").trim()
    const trimmedIdentifier = (identifier ?? "").trim()
    if (!trimmedProvider || !trimmedIdentifier) {
      throw new ConvexError("Informe provedor e identificador do acesso remoto.")
    }

    let normalizedUrl: string | null = null
    if (url) {
      const trimmedUrl = url.trim()
      if (trimmedUrl) {
        if (!/^https?:\/\//i.test(trimmedUrl)) {
          throw new ConvexError("Informe uma URL válida iniciando com http:// ou https://.")
        }
        try {
          new URL(trimmedUrl)
        } catch {
          throw new ConvexError("Informe uma URL válida para o acesso remoto.")
        }
        normalizedUrl = trimmedUrl
      }
    }

    const cleanedNotes = notes?.trim() ? notes.trim() : null
    const cleanedUsername = username?.trim() ? username.trim() : null
    const cleanedPassword = password?.trim() ? password.trim() : null
    const lastVerifiedAt = Date.now()
    const targetEntryId =
      coerceString(entryId) ??
      existingEntries.find(
        (entry) => entry.provider === trimmedProvider && entry.identifier === trimmedIdentifier
      )?.id ??
      createRemoteAccessId()

    const updatedEntry: RemoteAccessEntry = {
      id: targetEntryId,
      provider: trimmedProvider,
      identifier: trimmedIdentifier,
      url: normalizedUrl,
      username: cleanedUsername,
      password: cleanedPassword,
      notes: cleanedNotes,
      lastVerifiedAt,
      metadata: {
        provider: trimmedProvider,
        identifier: trimmedIdentifier,
        url: normalizedUrl,
        username: cleanedUsername,
        password: cleanedPassword,
        notes: cleanedNotes,
        lastVerifiedAt,
      },
    }

    const existingIndex = existingEntries.findIndex((entry) => entry.id === targetEntryId)
    let nextEntries: RemoteAccessEntry[]
    if (existingIndex >= 0) {
      nextEntries = [...existingEntries]
      nextEntries[existingIndex] = updatedEntry
    } else {
      nextEntries = [...existingEntries, updatedEntry]
    }

    await ctx.db.patch(machineId, {
      remoteAccess: nextEntries,
      updatedAt: Date.now(),
    })

    return { remoteAccess: nextEntries }
  },
})

export const upsertRemoteAccessViaToken = mutation({
  args: {
    machineToken: v.string(),
    provider: v.string(),
    identifier: v.string(),
    url: v.optional(v.string()),
    username: v.optional(v.string()),
    password: v.optional(v.string()),
    notes: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const { machine, mode } = await getTokenWithGrace(ctx, args.machineToken, {
      allowGraceMs: REMOTE_ACCESS_TOKEN_GRACE_MS,
    })
    if (mode === "grace") {
      console.warn("[remote-access] Token revogado aceito dentro da janela de graça", {
        machineId: machine._id,
      })
    }
    const trimmedProvider = args.provider.trim()
    const trimmedIdentifier = args.identifier.trim()
    if (!trimmedProvider || !trimmedIdentifier) {
      throw new ConvexError("Informe provedor e identificador do acesso remoto.")
    }

    let normalizedUrl: string | null = null
    if (args.url) {
      const trimmedUrl = args.url.trim()
      if (trimmedUrl) {
        const isValidScheme = /^https?:\/\//i.test(trimmedUrl) || /^rustdesk:\/\//i.test(trimmedUrl)
        if (!isValidScheme) {
          throw new ConvexError("Informe uma URL iniciando com http://, https:// ou rustdesk://.")
        }
        try {
          new URL(trimmedUrl.replace(/^rustdesk:\/\//i, "https://"))
        } catch {
          throw new ConvexError("Informe uma URL válida para o acesso remoto.")
        }
        normalizedUrl = trimmedUrl
      }
    }

    const cleanedUsername = args.username?.trim() ? args.username.trim() : null
    const cleanedPassword = args.password?.trim() ? args.password.trim() : null
    const cleanedNotes = args.notes?.trim() ? args.notes.trim() : null
    const timestamp = Date.now()
    const existingEntries = normalizeRemoteAccessList(machine.remoteAccess)
    let existingIndex = existingEntries.findIndex(
      (entry) =>
        entry.provider.toLowerCase() === trimmedProvider.toLowerCase() &&
        entry.identifier.toLowerCase() === trimmedIdentifier.toLowerCase()
    )

    // Se o identificador mudar (ex.: RustDesk gerando novo ID), reaproveitamos a entrada do mesmo provider
    if (existingIndex < 0) {
      existingIndex = existingEntries.findIndex(
        (entry) => entry.provider.toLowerCase() === trimmedProvider.toLowerCase()
      )
    }

    const entryId = existingIndex >= 0 ? existingEntries[existingIndex].id : createRemoteAccessId()
    const updatedEntry: RemoteAccessEntry = {
      id: entryId,
      provider: trimmedProvider,
      identifier: trimmedIdentifier,
      url: normalizedUrl,
      username: cleanedUsername,
      password: cleanedPassword,
      notes: cleanedNotes,
      lastVerifiedAt: timestamp,
      metadata: {
        source: "machine-token",
        provider: trimmedProvider,
        identifier: trimmedIdentifier,
        url: normalizedUrl,
        username: cleanedUsername,
        password: cleanedPassword,
        notes: cleanedNotes,
        lastVerifiedAt: timestamp,
        machineId: machine._id,
        hostname: machine.hostname,
        tenantId: machine.tenantId,
      },
    }

    const nextEntries =
      existingIndex >= 0
        ? existingEntries.map((entry, index) => (index === existingIndex ? updatedEntry : entry))
        : [...existingEntries, updatedEntry]

    await removeDuplicateRemoteAccessEntries(ctx, machine.tenantId, machine._id, trimmedProvider, trimmedIdentifier, timestamp)

    await ctx.db.patch(machine._id, {
      remoteAccess: nextEntries,
      updatedAt: timestamp,
    })

    return { remoteAccess: nextEntries }
  },
})

export const remove = mutation({
  args: {
    machineId: v.id("machines"),
    actorId: v.id("users"),
  },
  handler: async (ctx, { machineId, actorId }) => {
    const machine = await ctx.db.get(machineId)
    if (!machine) {
      throw new ConvexError("Dispositivo não encontrada")
    }

    const actor = await ctx.db.get(actorId)
    if (!actor || actor.tenantId !== machine.tenantId) {
      throw new ConvexError("Acesso negado ao tenant da dispositivo")
    }
    const role = (actor.role ?? "AGENT").toUpperCase()
    const STAFF = new Set(["ADMIN", "MANAGER", "AGENT"])
    if (!STAFF.has(role)) {
      throw new ConvexError("Apenas equipe interna pode excluir dispositivos")
    }

    const tokens = await ctx.db
      .query("machineTokens")
      .withIndex("by_machine", (q) => q.eq("machineId", machineId))
      .take(100)

    await Promise.all(tokens.map((token) => ctx.db.delete(token._id)))
    await ctx.db.delete(machineId)
    return { ok: true }
  },
})
