// ci: trigger convex functions deploy
import { mutation, query } from "./_generated/server"
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

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

function getProvisioningSecret() {
  const secret = process.env["MACHINE_PROVISIONING_SECRET"]
  if (!secret) {
    throw new ConvexError("Provisionamento de máquinas não configurado")
  }
  return secret
}

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 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 hashToken(token: string) {
  return toHex(sha256(token))
}

async function ensureCompany(
  ctx: MutationCtx,
  tenantId: string,
  companySlug?: string
): Promise<{ companyId?: Id<"companies">; companySlug?: string }> {
  if (!companySlug) return {}
  const company = await ctx.db
    .query("companies")
    .withIndex("by_tenant_slug", (q: any) => q.eq("tenantId", tenantId).eq("slug", companySlug))
    .unique()
  if (!company) {
    throw new ConvexError("Empresa não encontrada para o tenant informado")
  }
  return { companyId: company._id, companySlug: company.slug }
}

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: any) => 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 mergeMetadata(current: unknown, patch: Record<string, unknown>) {
  if (!current || typeof current !== "object") return patch
  return { ...(current as Record<string, unknown>), ...patch }
}

export const register = mutation({
  args: {
    provisioningSecret: v.string(),
    tenantId: v.optional(v.string()),
    companySlug: v.optional(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 secret = getProvisioningSecret()
    if (args.provisioningSecret !== secret) {
      throw new ConvexError("Código de provisionamento inválido")
    }

    const tenantId = args.tenantId ?? DEFAULT_TENANT_ID
    const identifiers = normalizeIdentifiers(args.macAddresses, args.serialNumbers)
    const fingerprint = computeFingerprint(tenantId, args.companySlug, args.hostname, identifiers)
    const { companyId, companySlug } = await ensureCompany(ctx, tenantId, args.companySlug)
    const now = Date.now()

    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: args.metadata ? mergeMetadata(existing.metadata, { inventory: args.metadata }) : existing.metadata,
        lastHeartbeatAt: now,
        updatedAt: now,
        status: "online",
        registeredBy: args.registeredBy ?? existing.registeredBy,
      })
      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: args.metadata ? { inventory: args.metadata } : undefined,
        lastHeartbeatAt: now,
        status: "online",
        createdAt: now,
        updatedAt: now,
        registeredBy: args.registeredBy,
      })
    }

    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: {
    provisioningSecret: v.string(),
    tenantId: v.optional(v.string()),
    companySlug: v.optional(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 secret = getProvisioningSecret()
    if (args.provisioningSecret !== secret) {
      throw new ConvexError("Código de provisionamento inválido")
    }

    const tenantId = args.tenantId ?? DEFAULT_TENANT_ID
    const identifiers = normalizeIdentifiers(args.macAddresses, args.serialNumbers)
    const fingerprint = computeFingerprint(tenantId, args.companySlug, args.hostname, identifiers)
    const { companyId, companySlug } = await ensureCompany(ctx, tenantId, args.companySlug)
    const now = Date.now()

    const metadataPatch = mergeMetadata({}, {
      ...(args.inventory ? { inventory: args.inventory } : {}),
      ...(args.metrics ? { metrics: args.metrics } : {}),
    })

    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: mergeMetadata(existing.metadata, metadataPatch),
        lastHeartbeatAt: now,
        updatedAt: now,
        status: args.metrics ? "online" : existing.status ?? "unknown",
        registeredBy: args.registeredBy ?? existing.registeredBy,
      })
      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,
        lastHeartbeatAt: now,
        status: args.metrics ? "online" : "unknown",
        createdAt: now,
        updatedAt: now,
        registeredBy: args.registeredBy,
      })
    }

    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 mergedMetadata = mergeMetadata(machine.metadata, {
      ...(args.metadata ?? {}),
      ...(args.metrics ? { metrics: args.metrics } : {}),
      ...(args.inventory ? { inventory: args.inventory } : {}),
    })

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

    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,
        status: machine.status,
        lastHeartbeatAt: machine.lastHeartbeatAt,
        metadata: machine.metadata,
      },
      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 derivedStatus =
          machine.status ??
          (machine.lastHeartbeatAt && now - machine.lastHeartbeatAt <= 5 * 60 * 1000 ? "online" : machine.lastHeartbeatAt ? "offline" : "unknown")

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

        let metrics: Record<string, unknown> | null = null
        let inventory: Record<string, unknown> | 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>
          }
        }

        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,
          status: derivedStatus,
          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,
        }
      })
    )
  },
})

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