// ci: trigger convex functions deploy
import { mutation, query } from "./_generated/server"
import { api } from "./_generated/api"
import { ConvexError, v } from "convex/values"
import { sha256 } from "@noble/hashes/sha256"
import { randomBytes } from "@noble/hashes/utils"
import type { Doc, Id } from "./_generated/dataModel"
import type { MutationCtx } from "./_generated/server"

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

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
}

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
}

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 toHex(input: Uint8Array) {
  return Array.from(input)
    .map((byte) => byte.toString(16).padStart(2, "0"))
    .join("")
}

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(payload))
}

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(token))
}

async function getActiveToken(
  ctx: MutationCtx,
  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 máquina inválido")
  }
  if (token.revoked) {
    throw new ConvexError("Token de máquina revogado")
  }
  if (token.expiresAt < Date.now()) {
    throw new ConvexError("Token de máquina expirado")
  }

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

  return { token, machine }
}

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

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

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

type JsonRecord = Record<string, unknown>

function ensureRecord(value: unknown): JsonRecord | null {
  return isObject(value) ? (value as JsonRecord) : null
}

function ensureRecordArray(value: unknown): JsonRecord[] {
  if (!Array.isArray(value)) return []
  return value.filter(isObject) as JsonRecord[]
}

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 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 máquina: ${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

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

    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
        }
      }
    }

    let machineId: Id<"machines">

    if (existing) {
      await ctx.db.patch(existing._id, {
        tenantId,
        companyId: companyId ?? existing.companyId,
        companySlug: companySlug ?? existing.companySlug,
        hostname: 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,
        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,
        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,
        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))
      .collect()

    for (const token of previousTokens) {
      if (!token.revoked) {
        await ctx.db.patch(token._id, { revoked: true, lastUsedAt: 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,
        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,
        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,
        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,
        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.any()),
    inventory: v.optional(v.any()),
    metadata: v.optional(v.any()),
  },
  handler: async (ctx, args) => {
    const { machine, token } = await getActiveToken(ctx, args.machineToken)
    const now = Date.now()

    const metadataPatch: Record<string, unknown> = {}
    if (args.metadata && typeof args.metadata === "object") {
      Object.assign(metadataPatch, args.metadata as Record<string, unknown>)
    }
    if (args.inventory && typeof args.inventory === "object") {
      metadataPatch.inventory = mergeInventory(metadataPatch.inventory, args.inventory as Record<string, unknown>)
    }
    if (args.metrics && typeof args.metrics === "object") {
      metadataPatch.metrics = args.metrics as Record<string, unknown>
    }
    const mergedMetadata = Object.keys(metadataPatch).length ? mergeMetadata(machine.metadata, metadataPatch) : machine.metadata

    await ctx.db.patch(machine._id, {
      hostname: args.hostname ?? machine.hostname,
      osName: args.os?.name ?? machine.osName,
      osVersion: args.os?.version ?? machine.osVersion,
      architecture: args.os?.architecture ?? machine.architecture,
      lastHeartbeatAt: now,
      updatedAt: now,
      status: args.status ?? "online",
      metadata: mergedMetadata,
    })

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

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

    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,
        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 machines = await ctx.db
      .query("machines")
      .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
      .collect()

    return Promise.all(
      machines.map(async (machine) => {
        const tokens = await ctx.db
          .query("machineTokens")
          .withIndex("by_machine", (q) => q.eq("machineId", machine._id))
          .collect()

        const activeToken = tokens.find((token) => !token.revoked && token.expiresAt > now) ?? null
        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 (machine.lastHeartbeatAt) {
          const age = now - machine.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
          }
        }

        return {
          id: machine._id,
          tenantId: machine.tenantId,
          hostname: machine.hostname,
          companyId: machine.companyId ?? null,
          companySlug: machine.companySlug ?? 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,
          status: derivedStatus,
          isActive: machine.isActive ?? true,
          lastHeartbeatAt: machine.lastHeartbeatAt ?? null,
          heartbeatAgeMs: machine.lastHeartbeatAt ? now - machine.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,
        }
      })
    )
  },
})

export const listAlerts = query({
  args: {
    machineId: v.id("machines"),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    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", args.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 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: async (ctx, args) => {
    const machine = await ctx.db.get(args.machineId)
    if (!machine) {
      throw new ConvexError("Máquina 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 máquina")
      } 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 máquina")
    }

    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 getContext = query({
  args: {
    machineId: v.id("machines"),
  },
  handler: async (ctx, args) => {
    const machine = await ctx.db.get(args.machineId)
    if (!machine) {
      throw new ConvexError("Máquina não encontrada")
    }

    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,
    }
  },
})

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("Máquina não encontrada")
    }

    await ctx.db.patch(machine._id, {
      authUserId: args.authUserId,
      authEmail: args.authEmail,
      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("Máquina não encontrada")
    }
    // Verifica permissão no tenant da máquina
    const viewer = await ctx.db.get(actorId)
    if (!viewer || viewer.tenantId !== machine.tenantId) {
      throw new ConvexError("Acesso negado ao tenant da máquina")
    }
    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 máquinas")
    }

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

    await ctx.db.patch(machineId, { hostname: 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("Máquina não encontrada")
    }

    const actor = await ctx.db.get(actorId)
    if (!actor || actor.tenantId !== machine.tenantId) {
      throw new ConvexError("Acesso negado ao tenant da máquina")
    }
    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 máquina")
    }

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

    return { ok: true }
  },
})

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("Máquina não encontrada")
    }

    const actor = await ctx.db.get(actorId)
    if (!actor || actor.tenantId !== machine.tenantId) {
      throw new ConvexError("Acesso negado ao tenant da máquina")
    }
    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 máquinas")
    }

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

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