import { v } from "convex/values"
import { mutation, query } from "./_generated/server"
import type { Id, Doc } from "./_generated/dataModel"
import { sha256 } from "@noble/hashes/sha2.js"

const DEFAULT_TENANT_ID = "default"

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

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

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

export const USB_POLICY_VALUES = ["ALLOW", "BLOCK_ALL", "READONLY"] as const
export type UsbPolicyValue = (typeof USB_POLICY_VALUES)[number]

export const USB_POLICY_STATUS = ["PENDING", "APPLYING", "APPLIED", "FAILED"] as const
export type UsbPolicyStatus = (typeof USB_POLICY_STATUS)[number]

export const setUsbPolicy = mutation({
  args: {
    machineId: v.id("machines"),
    policy: v.string(),
    actorId: v.optional(v.id("users")),
    actorEmail: v.optional(v.string()),
    actorName: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const machine = await ctx.db.get(args.machineId)
    if (!machine) {
      throw new Error("Dispositivo nao encontrado")
    }

    if (!USB_POLICY_VALUES.includes(args.policy as UsbPolicyValue)) {
      throw new Error(`Politica USB invalida: ${args.policy}. Valores validos: ${USB_POLICY_VALUES.join(", ")}`)
    }

    const now = Date.now()
    const oldPolicy = machine.usbPolicy ?? "ALLOW"

    await ctx.db.patch(args.machineId, {
      usbPolicy: args.policy,
      usbPolicyStatus: "PENDING",
      usbPolicyError: undefined,
      usbPolicyAppliedAt: now,
      updatedAt: now,
    })

    await ctx.db.insert("usbPolicyEvents", {
      tenantId: machine.tenantId,
      machineId: args.machineId,
      actorId: args.actorId,
      actorEmail: args.actorEmail,
      actorName: args.actorName,
      oldPolicy,
      newPolicy: args.policy,
      status: "PENDING",
      createdAt: now,
    })

    return { ok: true, policy: args.policy, status: "PENDING" }
  },
})

export const reportUsbPolicyStatus = mutation({
  args: {
    machineToken: v.string(),
    status: v.string(),
    // Rust envia null para Option<String>::None, entao precisamos aceitar null tambem
    error: v.optional(v.union(v.string(), v.null())),
    currentPolicy: v.optional(v.union(v.string(), v.null())),
  },
  handler: async (ctx, args) => {
    // Converte null para undefined para uso interno
    const errorValue = args.error ?? undefined
    const currentPolicyValue = args.currentPolicy ?? undefined
    const tokenHash = hashToken(args.machineToken)

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

    if (!tokenRecord || tokenRecord.revoked) {
      throw new Error("Token de maquina invalido ou revogado")
    }

    if (tokenRecord.expiresAt < Date.now()) {
      throw new Error("Token de maquina expirado")
    }

    const machine = await ctx.db.get(tokenRecord.machineId)
    if (!machine) {
      throw new Error("Dispositivo nao encontrado")
    }

    if (!USB_POLICY_STATUS.includes(args.status as UsbPolicyStatus)) {
      throw new Error(`Status de politica USB invalido: ${args.status}`)
    }

    const now = Date.now()

    await ctx.db.patch(machine._id, {
      usbPolicyStatus: args.status,
      usbPolicyError: errorValue,
      usbPolicyReportedAt: now,
      updatedAt: now,
    })

    const latestEvent = await ctx.db
      .query("usbPolicyEvents")
      .withIndex("by_machine_created", (q) => q.eq("machineId", machine._id))
      .order("desc")
      .first()

    // Atualiza o evento se ainda nao foi finalizado (PENDING ou APPLYING)
    // Isso permite a transicao: PENDING -> APPLYING -> APPLIED/FAILED
    if (latestEvent && (latestEvent.status === "PENDING" || latestEvent.status === "APPLYING")) {
      await ctx.db.patch(latestEvent._id, {
        status: args.status,
        error: errorValue,
        appliedAt: args.status === "APPLIED" ? now : undefined,
      })
    }

    return { ok: true }
  },
})

export const getUsbPolicy = query({
  args: {
    machineId: v.id("machines"),
  },
  handler: async (ctx, args) => {
    const machine = await ctx.db.get(args.machineId)
    if (!machine) {
      return null
    }

    return {
      policy: machine.usbPolicy ?? "ALLOW",
      status: machine.usbPolicyStatus ?? null,
      error: machine.usbPolicyError ?? null,
      appliedAt: machine.usbPolicyAppliedAt ?? null,
      reportedAt: machine.usbPolicyReportedAt ?? null,
    }
  },
})

export const getPendingUsbPolicy = query({
  args: {
    machineToken: v.string(),
  },
  handler: async (ctx, args) => {
    const tokenHash = hashToken(args.machineToken)

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

    if (!tokenRecord || tokenRecord.revoked || tokenRecord.expiresAt < Date.now()) {
      return null
    }

    const machine = await ctx.db.get(tokenRecord.machineId)
    if (!machine) {
      return null
    }

    if (machine.usbPolicyStatus === "PENDING") {
      return {
        policy: machine.usbPolicy ?? "ALLOW",
        appliedAt: machine.usbPolicyAppliedAt,
      }
    }

    return null
  },
})

export const listUsbPolicyEvents = query({
  args: {
    machineId: v.id("machines"),
    limit: v.optional(v.number()),
    cursor: v.optional(v.number()),
    status: v.optional(v.string()),
    dateFrom: v.optional(v.number()),
    dateTo: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const limit = args.limit ?? 10
    const maxFetch = 1000 // Limite maximo de eventos a buscar

    let events = await ctx.db
      .query("usbPolicyEvents")
      .withIndex("by_machine_created", (q) => q.eq("machineId", args.machineId))
      .order("desc")
      .take(maxFetch)

    // Aplica filtro de cursor (paginacao)
    if (args.cursor !== undefined) {
      events = events.filter((e) => e.createdAt < args.cursor!)
    }

    // Aplica filtro de status
    if (args.status) {
      events = events.filter((e) => e.status === args.status)
    }

    // Aplica filtro de data
    if (args.dateFrom !== undefined) {
      events = events.filter((e) => e.createdAt >= args.dateFrom!)
    }
    if (args.dateTo !== undefined) {
      events = events.filter((e) => e.createdAt <= args.dateTo!)
    }

    const hasMore = events.length > limit
    const results = events.slice(0, limit)
    const nextCursor = results.length > 0 ? results[results.length - 1].createdAt : undefined

    return {
      events: results.map((event) => ({
        id: event._id,
        oldPolicy: event.oldPolicy,
        newPolicy: event.newPolicy,
        status: event.status,
        error: event.error,
        actorEmail: event.actorEmail,
        actorName: event.actorName,
        createdAt: event.createdAt,
        appliedAt: event.appliedAt,
      })),
      hasMore,
      nextCursor,
    }
  },
})

export const bulkSetUsbPolicy = mutation({
  args: {
    machineIds: v.array(v.id("machines")),
    policy: v.string(),
    actorId: v.optional(v.id("users")),
    actorEmail: v.optional(v.string()),
    actorName: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    if (!USB_POLICY_VALUES.includes(args.policy as UsbPolicyValue)) {
      throw new Error(`Politica USB invalida: ${args.policy}`)
    }

    const now = Date.now()
    const results: Array<{ machineId: Id<"machines">; success: boolean; error?: string }> = []

    for (const machineId of args.machineIds) {
      try {
        const machine = await ctx.db.get(machineId)
        if (!machine) {
          results.push({ machineId, success: false, error: "Dispositivo nao encontrado" })
          continue
        }

        const oldPolicy = machine.usbPolicy ?? "ALLOW"

        await ctx.db.patch(machineId, {
          usbPolicy: args.policy,
          usbPolicyStatus: "PENDING",
          usbPolicyError: undefined,
          usbPolicyAppliedAt: now,
          updatedAt: now,
        })

        await ctx.db.insert("usbPolicyEvents", {
          tenantId: machine.tenantId,
          machineId,
          actorId: args.actorId,
          actorEmail: args.actorEmail,
          actorName: args.actorName,
          oldPolicy,
          newPolicy: args.policy,
          status: "PENDING",
          createdAt: now,
        })

        results.push({ machineId, success: true })
      } catch (err) {
        results.push({ machineId, success: false, error: String(err) })
      }
    }

    return { results, total: args.machineIds.length, successful: results.filter((r) => r.success).length }
  },
})

/**
 * Cleanup de policies USB pendentes por mais de 1 hora.
 * Marca como FAILED com mensagem de timeout.
 */
export const cleanupStalePendingPolicies = mutation({
  args: {
    staleThresholdMs: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    // Log obrigatorio para evitar shape_inference errors com logLines vazios
    console.log("cron: cleanupStalePendingPolicies iniciado")
    const thresholdMs = args.staleThresholdMs ?? 3600000 // 1 hora por padrao
    const now = Date.now()
    const cutoff = now - thresholdMs

    // Buscar maquinas com status PENDING e appliedAt antigo
    const staleMachines = await ctx.db
      .query("machines")
      .withIndex("by_usbPolicyStatus", (q) => q.eq("usbPolicyStatus", "PENDING"))
      .filter((q) => q.lt(q.field("usbPolicyAppliedAt"), cutoff))
      .take(1000)

    let cleaned = 0
    for (const machine of staleMachines) {
      await ctx.db.patch(machine._id, {
        usbPolicyStatus: "FAILED",
        usbPolicyError: "Timeout: Agent nao reportou status apos 1 hora. Verifique se o agent esta ativo.",
        updatedAt: now,
      })

      // Atualizar evento correspondente
      const latestEvent = await ctx.db
        .query("usbPolicyEvents")
        .withIndex("by_machine_created", (q) => q.eq("machineId", machine._id))
        .order("desc")
        .first()

      if (latestEvent && latestEvent.status === "PENDING") {
        await ctx.db.patch(latestEvent._id, {
          status: "FAILED",
          error: "Timeout automatico",
        })
      }

      cleaned++
    }

    return { cleaned, checked: staleMachines.length }
  },
})
