import { ConvexError, v } from "convex/values"

import type { Doc, Id } from "./_generated/dataModel"
import { mutation, query } from "./_generated/server"
import { requireStaff } from "./rbac"

const WIDGET_TYPES = [
  "kpi",
  "bar",
  "line",
  "area",
  "pie",
  "radar",
  "gauge",
  "table",
  "queue-summary",
  "text",
] as const

type WidgetType = (typeof WIDGET_TYPES)[number]

const gridItemValidator = v.object({
  i: v.string(),
  x: v.number(),
  y: v.number(),
  w: v.number(),
  h: v.number(),
  minW: v.optional(v.number()),
  minH: v.optional(v.number()),
  static: v.optional(v.boolean()),
})

const widgetLayoutValidator = v.object({
  x: v.number(),
  y: v.number(),
  w: v.number(),
  h: v.number(),
  minW: v.optional(v.number()),
  minH: v.optional(v.number()),
  static: v.optional(v.boolean()),
})

function assertWidgetType(type: string): asserts type is WidgetType {
  if (!WIDGET_TYPES.includes(type as WidgetType)) {
    throw new ConvexError(`Tipo de widget inválido: ${type}`)
  }
}

function sanitizeTitle(input?: string | null): string | undefined {
  if (!input) return undefined
  const trimmed = input.trim()
  return trimmed.length > 0 ? trimmed : undefined
}

function generateWidgetKey(dashboardId: Id<"dashboards">) {
  const rand = Math.random().toString(36).slice(2, 8)
  return `${dashboardId.toString().slice(-6)}-${rand}`
}

function normalizeQueueSummaryConfig(config: unknown) {
  return {
    type: "queue-summary",
    title: "Resumo das filas",
    dataSource: { metricKey: "queues.summary_cards" },
    ...(typeof config === "object" && config ? (config as Record<string, unknown>) : {}),
  }
}

function queueSummaryLayout(widgetKey: string) {
  return {
    i: widgetKey,
    x: 0,
    y: 0,
    w: 12,
    h: 8,
    minW: 8,
    minH: 7,
    static: false,
  }
}

function normalizeWidgetConfig(type: WidgetType, config: unknown) {
  if (config && typeof config === "object") {
    return config
  }
  switch (type) {
    case "kpi":
      return {
        type: "kpi",
        title: "Novo KPI",
        dataSource: { metricKey: "tickets.waiting_action_now" },
        options: { trend: "tickets.waiting_action_last_7d" },
      }
    case "bar":
      return {
        type: "bar",
        title: "Abertos x Resolvidos",
        dataSource: { metricKey: "tickets.opened_resolved_by_day", params: { range: "30d" } },
        encoding: {
          x: "date",
          y: [
            { field: "opened", label: "Abertos" },
            { field: "resolved", label: "Resolvidos" },
          ],
        },
        options: { legend: true },
      }
    case "line":
      return {
        type: "line",
        title: "Resoluções por dia",
        dataSource: { metricKey: "tickets.opened_resolved_by_day", params: { range: "30d" } },
        encoding: {
          x: "date",
          y: [{ field: "resolved", label: "Resolvidos" }],
        },
        options: { legend: false },
      }
    case "area":
      return {
        type: "area",
        title: "Volume acumulado",
        dataSource: { metricKey: "tickets.opened_resolved_by_day", params: { range: "30d" } },
        encoding: {
          x: "date",
          y: [
            { field: "opened", label: "Abertos" },
            { field: "resolved", label: "Resolvidos" },
          ],
          stacked: true,
        },
        options: { legend: true },
      }
    case "pie":
      return {
        type: "pie",
        title: "Backlog por prioridade",
        dataSource: { metricKey: "tickets.open_by_priority", params: { range: "30d" } },
        encoding: { category: "priority", value: "total" },
        options: { legend: true },
      }
    case "radar":
      return {
        type: "radar",
        title: "SLA por fila",
        dataSource: { metricKey: "tickets.sla_compliance_by_queue", params: { range: "30d" } },
        encoding: { angle: "queue", radius: "compliance" },
        options: {},
      }
    case "gauge":
      return {
        type: "gauge",
        title: "Cumprimento de SLA",
        dataSource: { metricKey: "tickets.sla_rate", params: { range: "7d" } },
        options: { min: 0, max: 1, thresholds: [0.5, 0.8] },
      }
    case "table":
      return {
        type: "table",
        title: "Tickets recentes",
        dataSource: { metricKey: "tickets.awaiting_table", params: { limit: 20 } },
        columns: [
          { field: "reference", label: "Referência" },
          { field: "subject", label: "Assunto" },
          { field: "status", label: "Status" },
          { field: "updatedAt", label: "Atualizado em" },
        ],
        options: { downloadCSV: true },
      }
    case "queue-summary":
      return {
        type: "queue-summary",
        title: "Resumo por fila",
        dataSource: { metricKey: "queues.summary_cards" },
      }
    case "text":
    default:
      return {
        type: "text",
        title: "Notas",
        content: "Use este espaço para destacar insights ou orientações.",
      }
  }
}

function sanitizeDashboard(dashboard: Doc<"dashboards">) {
  return {
    id: dashboard._id,
    tenantId: dashboard.tenantId,
    name: dashboard.name,
    description: dashboard.description ?? null,
    aspectRatio: dashboard.aspectRatio ?? "16:9",
    theme: dashboard.theme ?? "system",
    filters: dashboard.filters ?? {},
    layout: dashboard.layout ?? [],
    sections: dashboard.sections ?? [],
    tvIntervalSeconds: dashboard.tvIntervalSeconds ?? 30,
    readySelector: dashboard.readySelector ?? null,
    createdBy: dashboard.createdBy,
    updatedBy: dashboard.updatedBy ?? null,
    createdAt: dashboard.createdAt,
    updatedAt: dashboard.updatedAt,
    isArchived: dashboard.isArchived ?? false,
  }
}

function normalizeOrder(index: number) {
  return index >= 0 ? index : 0
}

export const list = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    includeArchived: v.optional(v.boolean()),
  },
  handler: async (ctx, { tenantId, viewerId, includeArchived }) => {
    await requireStaff(ctx, viewerId, tenantId)
    const dashboards = await ctx.db
      .query("dashboards")
      .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
      .take(100)

    const filtered = (includeArchived ? dashboards : dashboards.filter((d) => !(d.isArchived ?? false))).sort(
      (a, b) => b.updatedAt - a.updatedAt,
    )

    return Promise.all(
      filtered.map(async (dashboard) => {
        const widgets = await ctx.db
          .query("dashboardWidgets")
          .withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboard._id))
          .take(100)
        return {
          ...sanitizeDashboard(dashboard),
          widgetsCount: widgets.length,
        }
      }),
    )
  },
})

export const get = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    dashboardId: v.id("dashboards"),
  },
  handler: async (ctx, { tenantId, viewerId, dashboardId }) => {
    await requireStaff(ctx, viewerId, tenantId)
    const dashboard = await ctx.db.get(dashboardId)
    if (!dashboard || dashboard.tenantId !== tenantId) {
      throw new ConvexError("Dashboard não encontrado")
    }

    const widgets = await ctx.db
      .query("dashboardWidgets")
      .withIndex("by_dashboard_order", (q) => q.eq("dashboardId", dashboardId))
      .take(100)

    widgets.sort((a, b) => a.order - b.order || a.createdAt - b.createdAt)

    const shares = await ctx.db
      .query("dashboardShares")
      .withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId))
      .take(50)

    return {
      dashboard: sanitizeDashboard(dashboard),
      widgets: widgets.map((widget) => ({
        id: widget._id,
        dashboardId: widget.dashboardId,
        widgetKey: widget.widgetKey,
        title: widget.title ?? null,
        type: widget.type,
        config: widget.config,
        layout: widget.layout ?? null,
        order: widget.order,
        isHidden: widget.isHidden ?? false,
        createdAt: widget.createdAt,
        updatedAt: widget.updatedAt,
      })),
    shares: shares.map((share) => ({
        id: share._id,
        audience: share.audience,
        token: share.token ?? null,
        expiresAt: share.expiresAt ?? null,
        canEdit: share.canEdit,
        createdBy: share.createdBy,
        createdAt: share.createdAt,
        lastAccessAt: share.lastAccessAt ?? null,
      })),
    }
  },
})

export const create = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    name: v.string(),
    description: v.optional(v.string()),
    aspectRatio: v.optional(v.string()),
    theme: v.optional(v.string()),
  },
  handler: async (ctx, { tenantId, actorId, name, description, aspectRatio, theme }) => {
    await requireStaff(ctx, actorId, tenantId)
    const trimmedName = name.trim()
    if (trimmedName.length === 0) {
      throw new ConvexError("Nome do dashboard inválido")
    }

    const now = Date.now()
    const dashboardId = await ctx.db.insert("dashboards", {
      tenantId,
      name: trimmedName,
      description: sanitizeTitle(description),
      aspectRatio: aspectRatio?.trim() || "16:9",
      theme: theme?.trim() || "system",
      filters: {},
      layout: [],
      sections: [],
      tvIntervalSeconds: 30,
      readySelector: "[data-dashboard-ready=true]",
      createdBy: actorId,
      updatedBy: actorId,
      createdAt: now,
      updatedAt: now,
      isArchived: false,
    })

    return { id: dashboardId }
  },
})

export const updateMetadata = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    dashboardId: v.id("dashboards"),
    name: v.optional(v.string()),
    description: v.optional(v.string()),
    aspectRatio: v.optional(v.string()),
    theme: v.optional(v.string()),
    readySelector: v.optional(v.string()),
    tvIntervalSeconds: v.optional(v.number()),
  },
  handler: async (ctx, { tenantId, actorId, dashboardId, name, description, aspectRatio, theme, readySelector, tvIntervalSeconds }) => {
    await requireStaff(ctx, actorId, tenantId)
    const dashboard = await ctx.db.get(dashboardId)
    if (!dashboard || dashboard.tenantId !== tenantId) {
      throw new ConvexError("Dashboard não encontrado")
    }
    const patch: Partial<Doc<"dashboards">> = {}
    if (typeof name === "string") {
      const trimmed = name.trim()
      if (!trimmed) throw new ConvexError("Nome do dashboard inválido")
      patch.name = trimmed
    }
    if (typeof description !== "undefined") {
      patch.description = sanitizeTitle(description)
    }
    if (typeof aspectRatio === "string") {
      patch.aspectRatio = aspectRatio.trim() || "16:9"
    }
    if (typeof theme === "string") {
      patch.theme = theme.trim() || "system"
    }
    if (typeof readySelector === "string") {
      patch.readySelector = readySelector.trim() || "[data-dashboard-ready=true]"
    }
    if (typeof tvIntervalSeconds === "number" && Number.isFinite(tvIntervalSeconds) && tvIntervalSeconds > 0) {
      patch.tvIntervalSeconds = Math.max(5, Math.round(tvIntervalSeconds))
    }
    patch.updatedAt = Date.now()
    patch.updatedBy = actorId
    await ctx.db.patch(dashboardId, patch)
    return { ok: true }
  },
})

export const updateFilters = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    dashboardId: v.id("dashboards"),
    filters: v.any(),
  },
  handler: async (ctx, { tenantId, actorId, dashboardId, filters }) => {
    await requireStaff(ctx, actorId, tenantId)
    const dashboard = await ctx.db.get(dashboardId)
    if (!dashboard || dashboard.tenantId !== tenantId) {
      throw new ConvexError("Dashboard não encontrado")
    }
    await ctx.db.patch(dashboardId, {
      filters,
      updatedAt: Date.now(),
      updatedBy: actorId,
    })
    return { ok: true }
  },
})

export const updateSections = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    dashboardId: v.id("dashboards"),
    sections: v.array(
      v.object({
        id: v.string(),
        title: v.optional(v.string()),
        description: v.optional(v.string()),
        widgetKeys: v.array(v.string()),
        durationSeconds: v.optional(v.number()),
      }),
    ),
  },
  handler: async (ctx, { tenantId, actorId, dashboardId, sections }) => {
    await requireStaff(ctx, actorId, tenantId)
    const dashboard = await ctx.db.get(dashboardId)
    if (!dashboard || dashboard.tenantId !== tenantId) {
      throw new ConvexError("Dashboard não encontrado")
    }
    const normalized = sections.map((section) => ({
      ...section,
      title: sanitizeTitle(section.title),
      description: sanitizeTitle(section.description),
      durationSeconds:
        typeof section.durationSeconds === "number" && Number.isFinite(section.durationSeconds)
          ? Math.max(5, Math.round(section.durationSeconds))
          : undefined,
    }))
    await ctx.db.patch(dashboardId, {
      sections: normalized,
      updatedAt: Date.now(),
      updatedBy: actorId,
    })
    return { ok: true }
  },
})

export const updateLayout = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    dashboardId: v.id("dashboards"),
    layout: v.array(gridItemValidator),
  },
  handler: async (ctx, { tenantId, actorId, dashboardId, layout }) => {
    await requireStaff(ctx, actorId, tenantId)
    const dashboard = await ctx.db.get(dashboardId)
    if (!dashboard || dashboard.tenantId !== tenantId) {
      throw new ConvexError("Dashboard não encontrado")
    }

    const widgets = await ctx.db
      .query("dashboardWidgets")
      .withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId))
      .take(100)

    const byKey = new Map<string, Doc<"dashboardWidgets">>()
    widgets.forEach((widget) => byKey.set(widget.widgetKey, widget))

    const now = Date.now()
    await ctx.db.patch(dashboardId, {
      layout,
      updatedAt: now,
      updatedBy: actorId,
    })

    for (let index = 0; index < layout.length; index++) {
      const item = layout[index]
      const widget = byKey.get(item.i)
      if (!widget) {
        throw new ConvexError(`Widget ${item.i} não encontrado neste dashboard`)
      }
      await ctx.db.patch(widget._id, {
        layout: {
          x: item.x,
          y: item.y,
          w: item.w,
          h: item.h,
          minW: item.minW,
          minH: item.minH,
          static: item.static,
        },
        order: normalizeOrder(index),
        updatedAt: now,
        updatedBy: actorId,
      })
    }

    return { ok: true }
  },
})

export const addWidget = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    dashboardId: v.id("dashboards"),
    type: v.string(),
    title: v.optional(v.string()),
    config: v.optional(v.any()),
    layout: v.optional(widgetLayoutValidator),
  },
  handler: async (ctx, { tenantId, actorId, dashboardId, type, title, config, layout }) => {
    await requireStaff(ctx, actorId, tenantId)
    const dashboard = await ctx.db.get(dashboardId)
    if (!dashboard || dashboard.tenantId !== tenantId) {
      throw new ConvexError("Dashboard não encontrado")
    }

    assertWidgetType(type)
    const widgetKey = generateWidgetKey(dashboardId)
    const now = Date.now()
    const existingWidgets = await ctx.db
      .query("dashboardWidgets")
      .withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId))
      .take(100)

    const widgetId = await ctx.db.insert("dashboardWidgets", {
      tenantId,
      dashboardId,
      widgetKey,
      title: sanitizeTitle(title),
      type,
      config: normalizeWidgetConfig(type, config),
      layout: layout ?? undefined,
      order: existingWidgets.length,
      createdBy: actorId,
      updatedBy: actorId,
      createdAt: now,
      updatedAt: now,
      isHidden: false,
    })

    const nextLayout = [...(dashboard.layout ?? [])]
    if (layout) {
      nextLayout.push({ i: widgetKey, ...layout })
    } else {
      const baseY = Math.max(0, nextLayout.length * 4)
      nextLayout.push({ i: widgetKey, x: 0, y: baseY, w: 6, h: 6 })
    }

    await ctx.db.patch(dashboardId, {
      layout: nextLayout,
      updatedAt: now,
      updatedBy: actorId,
    })

    return { id: widgetId, widgetKey }
  },
})

export const updateWidget = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    widgetId: v.id("dashboardWidgets"),
    title: v.optional(v.string()),
    type: v.optional(v.string()),
    config: v.optional(v.any()),
    layout: v.optional(widgetLayoutValidator),
    hidden: v.optional(v.boolean()),
    order: v.optional(v.number()),
  },
  handler: async (ctx, { tenantId, actorId, widgetId, title, type, config, layout, hidden, order }) => {
    await requireStaff(ctx, actorId, tenantId)
    const widget = await ctx.db.get(widgetId)
    if (!widget || widget.tenantId !== tenantId) {
      throw new ConvexError("Widget não encontrado")
    }

    const patch: Partial<Doc<"dashboardWidgets">> = {}
    if (typeof title !== "undefined") {
      patch.title = sanitizeTitle(title)
    }
    if (typeof type === "string") {
      assertWidgetType(type)
      patch.type = type
      patch.config = normalizeWidgetConfig(type, config ?? widget.config)
    } else if (typeof config !== "undefined") {
      patch.config = config
    }
    if (typeof layout !== "undefined") {
      patch.layout = layout
    }
    if (typeof hidden === "boolean") {
      patch.isHidden = hidden
    }
    if (typeof order === "number" && Number.isFinite(order)) {
      patch.order = Math.max(0, Math.round(order))
    }
    patch.updatedAt = Date.now()
    patch.updatedBy = actorId

    await ctx.db.patch(widgetId, patch)
    return { ok: true }
  },
})

export const ensureQueueSummaryWidget = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    dashboardId: v.id("dashboards"),
  },
  handler: async (ctx, { tenantId, actorId, dashboardId }) => {
    await requireStaff(ctx, actorId, tenantId)
    const dashboard = await ctx.db.get(dashboardId)
    if (!dashboard || dashboard.tenantId !== tenantId) {
      throw new ConvexError("Dashboard não encontrado")
    }

    const widgets = await ctx.db
      .query("dashboardWidgets")
      .withIndex("by_dashboard_order", (q) => q.eq("dashboardId", dashboardId))
      .take(100)

    widgets.sort((a, b) => a.order - b.order || a.createdAt - b.createdAt)

    const now = Date.now()
    let queueWidget = widgets.find((widget) => widget.type === "queue-summary")
    let changed = false

    if (!queueWidget) {
      // Shift existing widgets to make room at the top.
      await Promise.all(
        widgets.map((widget) =>
          ctx.db.patch(widget._id, {
            order: widget.order + 1,
            updatedAt: now,
            updatedBy: actorId,
          }),
        ),
      )
      const widgetKey = generateWidgetKey(dashboardId)
      const config = normalizeQueueSummaryConfig(undefined)
      const layoutWithKey = queueSummaryLayout(widgetKey)
      const widgetLayout = { ...layoutWithKey }
      delete (widgetLayout as { i?: string }).i
      const widgetId = await ctx.db.insert("dashboardWidgets", {
        tenantId,
        dashboardId,
        widgetKey,
        title: config.title,
        type: "queue-summary",
        config,
        layout: widgetLayout,
        order: 0,
        createdBy: actorId,
        updatedBy: actorId,
        createdAt: now,
        updatedAt: now,
        isHidden: false,
      })
      const createdWidget = await ctx.db.get(widgetId)
      if (!createdWidget) {
        throw new ConvexError("Falha ao criar widget de resumo por fila.")
      }
      queueWidget = createdWidget
      widgets.unshift(queueWidget)
      changed = true
    } else {
      // Ensure the existing widget is first and has the expected config.
      const desiredConfig = normalizeQueueSummaryConfig(queueWidget.config)
      if (JSON.stringify(queueWidget.config) !== JSON.stringify(desiredConfig)) {
        await ctx.db.patch(queueWidget._id, { config: desiredConfig, updatedAt: now, updatedBy: actorId })
        queueWidget = { ...queueWidget, config: desiredConfig }
        changed = true
      }
      if (queueWidget.order !== 0) {
        let nextOrder = 1
        for (const widget of widgets) {
          if (widget._id === queueWidget._id) continue
          if (widget.order !== nextOrder) {
            await ctx.db.patch(widget._id, { order: nextOrder, updatedAt: now, updatedBy: actorId })
          }
          nextOrder += 1
        }
        await ctx.db.patch(queueWidget._id, { order: 0, updatedAt: now, updatedBy: actorId })
        changed = true
      }
    }

    if (!queueWidget) {
      throw new ConvexError("Não foi possível garantir o widget de resumo por fila.")
    }

    const widgetKey = queueWidget.widgetKey

    // Normalize dashboard layout (queue summary first).
    const currentLayout = Array.isArray(dashboard.layout) ? dashboard.layout : []
    const filteredLayout = currentLayout.filter((item) => item.i !== widgetKey)
    const normalizedLayout = [queueSummaryLayout(widgetKey), ...filteredLayout]

    let dashboardPatch: Partial<Doc<"dashboards">> = {}
    if (JSON.stringify(currentLayout) !== JSON.stringify(normalizedLayout)) {
      dashboardPatch.layout = normalizedLayout
      changed = true
    }

    // Ensure sections used in TV playlists include the widget on the first slide.
    if (Array.isArray(dashboard.sections) && dashboard.sections.length > 0) {
      const updatedSections = dashboard.sections.map((section, index) => {
        const keys = Array.isArray(section.widgetKeys) ? section.widgetKeys : []
        const without = keys.filter((key) => key !== widgetKey)
        if (index === 0) {
          const nextKeys = [widgetKey, ...without]
          if (JSON.stringify(nextKeys) !== JSON.stringify(keys)) {
            changed = true
            return { ...section, widgetKeys: nextKeys }
          }
          return section
        }
        if (without.length !== keys.length) {
          changed = true
          return { ...section, widgetKeys: without }
        }
        return section
      })
      if (changed && JSON.stringify(updatedSections) !== JSON.stringify(dashboard.sections)) {
        dashboardPatch.sections = updatedSections
      }
    }

    if (Object.keys(dashboardPatch).length > 0) {
      dashboardPatch.updatedAt = now
      dashboardPatch.updatedBy = actorId
      await ctx.db.patch(dashboardId, dashboardPatch)
    } else if (changed) {
      await ctx.db.patch(dashboardId, { updatedAt: now, updatedBy: actorId })
    }

    if (!changed) {
      // Nothing changed; still return success.
      return { ensured: false, widgetKey }
    }

    return { ensured: true, widgetKey }
  },
})

export const duplicateWidget = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    widgetId: v.id("dashboardWidgets"),
  },
  handler: async (ctx, { tenantId, actorId, widgetId }) => {
    await requireStaff(ctx, actorId, tenantId)
    const widget = await ctx.db.get(widgetId)
    if (!widget || widget.tenantId !== tenantId) {
      throw new ConvexError("Widget não encontrado")
    }
    const dashboard = await ctx.db.get(widget.dashboardId)
    if (!dashboard || dashboard.tenantId !== tenantId) {
      throw new ConvexError("Dashboard não encontrado")
    }

    assertWidgetType(widget.type)
    const now = Date.now()
    const widgetKey = generateWidgetKey(widget.dashboardId)
    const newWidgetId = await ctx.db.insert("dashboardWidgets", {
      tenantId,
      dashboardId: widget.dashboardId,
      widgetKey,
      title: sanitizeTitle(widget.title),
      type: widget.type,
      config: widget.config,
      layout: widget.layout ?? undefined,
      order: widget.order + 1,
      createdBy: actorId,
      updatedBy: actorId,
      createdAt: now,
      updatedAt: now,
      isHidden: widget.isHidden ?? false,
    })

    const duplicateLayout = widget.layout
      ? {
          x: widget.layout.x,
          y: (widget.layout.y ?? 0) + (widget.layout.h ?? 6) + 1,
          w: widget.layout.w,
          h: widget.layout.h ?? 6,
          minW: widget.layout.minW,
          minH: widget.layout.minH,
          static: widget.layout.static,
        }
      : { x: 0, y: Math.max(0, (dashboard.layout?.length ?? 0) * 4), w: 6, h: 6 }
    const nextLayout = [...(dashboard.layout ?? []), { i: widgetKey, ...duplicateLayout }]
    await ctx.db.patch(dashboard._id, {
      layout: nextLayout,
      updatedAt: now,
      updatedBy: actorId,
    })
    return { id: newWidgetId, widgetKey }
  },
})

export const removeWidget = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    widgetId: v.id("dashboardWidgets"),
  },
  handler: async (ctx, { tenantId, actorId, widgetId }) => {
    await requireStaff(ctx, actorId, tenantId)
    const widget = await ctx.db.get(widgetId)
    if (!widget || widget.tenantId !== tenantId) {
      throw new ConvexError("Widget não encontrado")
    }
    const dashboard = await ctx.db.get(widget.dashboardId)
    if (!dashboard || dashboard.tenantId !== tenantId) {
      throw new ConvexError("Dashboard não encontrado")
    }

    await ctx.db.delete(widgetId)
    const filteredLayout = (dashboard.layout ?? []).filter((item) => item.i !== widget.widgetKey)
    await ctx.db.patch(dashboard._id, {
      layout: filteredLayout,
      updatedAt: Date.now(),
      updatedBy: actorId,
    })

    return { ok: true }
  },
})

export const archive = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    dashboardId: v.id("dashboards"),
    archived: v.optional(v.boolean()),
  },
  handler: async (ctx, { tenantId, actorId, dashboardId, archived }) => {
    await requireStaff(ctx, actorId, tenantId)
    const dashboard = await ctx.db.get(dashboardId)
    if (!dashboard || dashboard.tenantId !== tenantId) {
      throw new ConvexError("Dashboard não encontrado")
    }
    await ctx.db.patch(dashboardId, {
      isArchived: archived ?? !(dashboard.isArchived ?? false),
      updatedAt: Date.now(),
      updatedBy: actorId,
    })
    return { ok: true }
  },
})

export const upsertShare = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    dashboardId: v.id("dashboards"),
    audience: v.union(v.literal("private"), v.literal("tenant"), v.literal("public-link")),
    canEdit: v.boolean(),
    expiresAt: v.optional(v.union(v.number(), v.null())),
    token: v.optional(v.union(v.string(), v.null())),
  },
  handler: async (ctx, { tenantId, actorId, dashboardId, audience, canEdit, expiresAt, token }) => {
    await requireStaff(ctx, actorId, tenantId)
    const dashboard = await ctx.db.get(dashboardId)
    if (!dashboard || dashboard.tenantId !== tenantId) {
      throw new ConvexError("Dashboard não encontrado")
    }

    const existingShares = await ctx.db
      .query("dashboardShares")
      .withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId))
      .take(50)

    const now = Date.now()
    let shareDoc = existingShares.find((share) => share.audience === audience)
    const normalizedExpiresAt =
      typeof expiresAt === "number" && Number.isFinite(expiresAt) ? Math.max(now, Math.round(expiresAt)) : undefined
    const normalizedToken = typeof token === "string" && token.trim().length > 0 ? token.trim() : undefined

    if (!shareDoc) {
      const generatedToken = audience === "public-link" ? normalizedToken ?? cryptoToken() : undefined
      await ctx.db.insert("dashboardShares", {
        tenantId,
        dashboardId,
        audience,
        token: generatedToken,
        canEdit,
        expiresAt: normalizedExpiresAt,
        createdBy: actorId,
        createdAt: now,
        lastAccessAt: undefined,
      })
    } else {
      await ctx.db.patch(shareDoc._id, {
        canEdit,
        token: audience === "public-link" ? normalizedToken ?? shareDoc.token ?? cryptoToken() : undefined,
        expiresAt: normalizedExpiresAt,
        lastAccessAt: shareDoc.lastAccessAt,
      })
    }

    await ctx.db.patch(dashboardId, { updatedAt: now, updatedBy: actorId })
    return { ok: true }
  },
})

export const revokeShareToken = mutation({
  args: {
    tenantId: v.string(),
    actorId: v.id("users"),
    dashboardId: v.id("dashboards"),
  },
  handler: async (ctx, { tenantId, actorId, dashboardId }) => {
    await requireStaff(ctx, actorId, tenantId)
    const shares = await ctx.db
      .query("dashboardShares")
      .withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId))
      .take(50)

    for (const share of shares) {
      if (share.audience === "public-link") {
        await ctx.db.patch(share._id, {
          token: cryptoToken(),
          lastAccessAt: undefined,
        })
      }
    }
    await ctx.db.patch(dashboardId, { updatedAt: Date.now(), updatedBy: actorId })
    return { ok: true }
  },
})

function cryptoToken() {
  return Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 6)
}
