import { mutation, query } from "./_generated/server";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { ConvexError, v } from "convex/values";
import { Id, type Doc } from "./_generated/dataModel";

import { requireAdmin, requireStaff, requireUser } from "./rbac";

const STAFF_ROLES = new Set(["ADMIN", "MANAGER", "AGENT"]);
const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT"]);
const PAUSE_REASON_LABELS: Record<string, string> = {
  NO_CONTACT: "Falta de contato",
  WAITING_THIRD_PARTY: "Aguardando terceiro",
  IN_PROCEDURE: "Em procedimento",
};

type TicketStatusNormalized = "PENDING" | "AWAITING_ATTENDANCE" | "PAUSED" | "RESOLVED";

const STATUS_LABELS: Record<TicketStatusNormalized, string> = {
  PENDING: "Pendente",
  AWAITING_ATTENDANCE: "Aguardando atendimento",
  PAUSED: "Pausado",
  RESOLVED: "Resolvido",
};

const LEGACY_STATUS_MAP: Record<string, TicketStatusNormalized> = {
  NEW: "PENDING",
  PENDING: "PENDING",
  OPEN: "AWAITING_ATTENDANCE",
  AWAITING_ATTENDANCE: "AWAITING_ATTENDANCE",
  ON_HOLD: "PAUSED",
  PAUSED: "PAUSED",
  RESOLVED: "RESOLVED",
  CLOSED: "RESOLVED",
};

const missingRequesterLogCache = new Set<string>();
const missingCommentAuthorLogCache = new Set<string>();

function normalizeStatus(status: string | null | undefined): TicketStatusNormalized {
  if (!status) return "PENDING";
  const normalized = LEGACY_STATUS_MAP[status.toUpperCase()];
  return normalized ?? "PENDING";
}

async function ensureManagerTicketAccess(
  ctx: MutationCtx | QueryCtx,
  manager: Doc<"users">,
  ticket: Doc<"tickets">,
): Promise<Doc<"users"> | null> {
  if (!manager.companyId) {
    throw new ConvexError("Gestor não possui empresa vinculada")
  }
  if (ticket.companyId && ticket.companyId === manager.companyId) {
    return null
  }
  const requester = await ctx.db.get(ticket.requesterId)
  if (!requester || requester.companyId !== manager.companyId) {
    throw new ConvexError("Acesso restrito à empresa")
  }
  return requester as Doc<"users">
}

async function requireTicketStaff(
  ctx: MutationCtx | QueryCtx,
  actorId: Id<"users">,
  ticket: Doc<"tickets">
) {
  const viewer = await requireStaff(ctx, actorId, ticket.tenantId)
  if (viewer.role === "MANAGER") {
    await ensureManagerTicketAccess(ctx, viewer.user, ticket)
  }
  return viewer
}

const QUEUE_RENAME_LOOKUP: Record<string, string> = {
  "Suporte N1": "Chamados",
  "suporte-n1": "Chamados",
  chamados: "Chamados",
  "Suporte N2": "Laboratório",
  "suporte-n2": "Laboratório",
  laboratorio: "Laboratório",
  Laboratorio: "Laboratório",
  visitas: "Visitas",
};

function renameQueueString(value?: string | null): string | null {
  if (!value) return value ?? null;
  const direct = QUEUE_RENAME_LOOKUP[value];
  if (direct) return direct;
  const normalizedKey = value.replace(/\s+/g, "-").toLowerCase();
  return QUEUE_RENAME_LOOKUP[normalizedKey] ?? value;
}

function normalizeQueueName(queue?: Doc<"queues"> | null): string | null {
  if (!queue) return null;
  const normalized = renameQueueString(queue.name);
  if (normalized) {
    return normalized;
  }
  if (queue.slug) {
    const fromSlug = renameQueueString(queue.slug);
    if (fromSlug) return fromSlug;
  }
  return queue.name;
}

function normalizeTeams(teams?: string[] | null): string[] {
  if (!teams) return [];
  return teams.map((team) => renameQueueString(team) ?? team);
}

type RequesterFallbackContext = {
  ticketId?: Id<"tickets">;
  fallbackName?: string | null;
  fallbackEmail?: string | null;
};

function buildRequesterSummary(
  requester: Doc<"users"> | null,
  requesterId: Id<"users">,
  context?: RequesterFallbackContext,
) {
  if (requester) {
    return {
      id: requester._id,
      name: requester.name,
      email: requester.email,
      avatarUrl: requester.avatarUrl,
      teams: normalizeTeams(requester.teams),
    };
  }

  const idString = String(requesterId);
  const fallbackName =
    typeof context?.fallbackName === "string" && context.fallbackName.trim().length > 0
      ? context.fallbackName.trim()
      : "Solicitante não encontrado";
  const fallbackEmailCandidate =
    typeof context?.fallbackEmail === "string" && context.fallbackEmail.includes("@")
      ? context.fallbackEmail
      : null;
  const fallbackEmail = fallbackEmailCandidate ?? `requester-${idString}@example.invalid`;

  if (process.env.NODE_ENV !== "test") {
    const ticketInfo = context?.ticketId ? ` (ticket ${String(context.ticketId)})` : "";
    const cacheKey = `${idString}:${context?.ticketId ? String(context.ticketId) : "unknown"}`;
    if (!missingRequesterLogCache.has(cacheKey)) {
      missingRequesterLogCache.add(cacheKey);
      console.warn(
        `[tickets] requester ${idString} ausente ao hidratar resposta${ticketInfo}; usando placeholders.`,
      );
    }
  }

  return {
    id: requesterId,
    name: fallbackName,
    email: fallbackEmail,
    teams: [],
  };
}

type CommentAuthorFallbackContext = {
  ticketId?: Id<"tickets">;
  commentId?: Id<"ticketComments">;
};

type CommentAuthorSnapshot = {
  name: string;
  email?: string;
  avatarUrl?: string;
  teams?: string[];
};

export function buildCommentAuthorSummary(
  comment: Doc<"ticketComments">,
  author: Doc<"users"> | null,
  context?: CommentAuthorFallbackContext,
) {
  if (author) {
    return {
      id: author._id,
      name: author.name,
      email: author.email,
      avatarUrl: author.avatarUrl,
      teams: normalizeTeams(author.teams),
    };
  }

  if (process.env.NODE_ENV !== "test") {
    const ticketInfo = context?.ticketId ? ` (ticket ${String(context.ticketId)})` : "";
    const commentInfo = context?.commentId ? ` (comentário ${String(context.commentId)})` : "";
    const cacheKeyParts = [String(comment.authorId), context?.ticketId ? String(context.ticketId) : "unknown"];
    if (context?.commentId) cacheKeyParts.push(String(context.commentId));
    const cacheKey = cacheKeyParts.join(":");
    if (!missingCommentAuthorLogCache.has(cacheKey)) {
      missingCommentAuthorLogCache.add(cacheKey);
      console.warn(
        `[tickets] autor ${String(comment.authorId)} ausente ao hidratar comentário${ticketInfo}${commentInfo}; usando placeholders.`,
      );
    }
  }

  const idString = String(comment.authorId);
  const fallbackName = "Usuário removido";
  const fallbackEmail = `author-${idString}@example.invalid`;
  const snapshot = comment.authorSnapshot as CommentAuthorSnapshot | undefined;
  if (snapshot) {
    const name =
      typeof snapshot.name === "string" && snapshot.name.trim().length > 0
        ? snapshot.name.trim()
        : fallbackName;
    const emailCandidate =
      typeof snapshot.email === "string" && snapshot.email.includes("@") ? snapshot.email : null;
    const email = emailCandidate ?? fallbackEmail;
    return {
      id: comment.authorId,
      name,
      email,
      avatarUrl: snapshot.avatarUrl ?? undefined,
      teams: normalizeTeams(snapshot.teams ?? []),
    };
  }

  return {
    id: comment.authorId,
    name: fallbackName,
    email: fallbackEmail,
    teams: [],
  };
}

type CustomFieldInput = {
  fieldId: Id<"ticketFields">;
  value: unknown;
};

type NormalizedCustomField = {
  fieldId: Id<"ticketFields">;
  fieldKey: string;
  label: string;
  type: string;
  value: unknown;
  displayValue?: string;
};

function coerceCustomFieldValue(field: Doc<"ticketFields">, raw: unknown): { value: unknown; displayValue?: string } {
  switch (field.type) {
    case "text":
      return { value: String(raw).trim() };
    case "number": {
      const value = typeof raw === "number" ? raw : Number(String(raw).replace(",", "."));
      if (!Number.isFinite(value)) {
        throw new ConvexError(`Valor numérico inválido para o campo ${field.label}`);
      }
      return { value };
    }
    case "date": {
      if (typeof raw === "number") {
        if (!Number.isFinite(raw)) {
          throw new ConvexError(`Data inválida para o campo ${field.label}`);
        }
        return { value: raw };
      }
      const parsed = Date.parse(String(raw));
      if (!Number.isFinite(parsed)) {
        throw new ConvexError(`Data inválida para o campo ${field.label}`);
      }
      return { value: parsed };
    }
    case "boolean": {
      if (typeof raw === "boolean") {
        return { value: raw };
      }
      if (typeof raw === "string") {
        const normalized = raw.toLowerCase();
        if (normalized === "true" || normalized === "1") return { value: true };
        if (normalized === "false" || normalized === "0") return { value: false };
      }
      throw new ConvexError(`Valor inválido para o campo ${field.label}`);
    }
    case "select": {
      if (!field.options || field.options.length === 0) {
        throw new ConvexError(`Campo ${field.label} sem opções configuradas`);
      }
      const value = String(raw);
      const option = field.options.find((opt) => opt.value === value);
      if (!option) {
        throw new ConvexError(`Seleção inválida para o campo ${field.label}`);
      }
      return { value: option.value, displayValue: option.label ?? option.value };
    }
    default:
      return { value: raw };
  }
}

async function normalizeCustomFieldValues(
  ctx: Pick<MutationCtx, "db">,
  tenantId: string,
  inputs: CustomFieldInput[] | undefined
): Promise<NormalizedCustomField[]> {
  const definitions = await ctx.db
    .query("ticketFields")
    .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
    .collect();

  if (!definitions.length) {
    if (inputs && inputs.length > 0) {
      throw new ConvexError("Nenhum campo personalizado configurado para este tenant");
    }
    return [];
  }

  const provided = new Map<Id<"ticketFields">, unknown>();
  for (const entry of inputs ?? []) {
    provided.set(entry.fieldId, entry.value);
  }

  const normalized: NormalizedCustomField[] = [];

  for (const definition of definitions.sort((a, b) => a.order - b.order)) {
    const raw = provided.has(definition._id) ? provided.get(definition._id) : undefined;
    const isMissing =
      raw === undefined ||
      raw === null ||
      (typeof raw === "string" && raw.trim().length === 0);

    if (isMissing) {
      if (definition.required) {
        throw new ConvexError(`Preencha o campo obrigatório: ${definition.label}`);
      }
      continue;
    }

    const { value, displayValue } = coerceCustomFieldValue(definition, raw);
    normalized.push({
      fieldId: definition._id,
      fieldKey: definition.key,
      label: definition.label,
      type: definition.type,
      value,
      displayValue,
    });
  }

  return normalized;
}

function mapCustomFieldsToRecord(entries: NormalizedCustomField[] | undefined) {
  if (!entries || entries.length === 0) return {};
  return entries.reduce<Record<string, { label: string; type: string; value: unknown; displayValue?: string }>>((acc, entry) => {
    acc[entry.fieldKey] = {
      label: entry.label,
      type: entry.type,
      value: entry.value,
      displayValue: entry.displayValue,
    };
    return acc;
  }, {});
}

export const list = query({
  args: {
    viewerId: v.optional(v.id("users")),
    tenantId: v.string(),
    status: v.optional(v.string()),
    priority: v.optional(v.string()),
    channel: v.optional(v.string()),
    queueId: v.optional(v.id("queues")),
    search: v.optional(v.string()),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    if (!args.viewerId) {
      return []
    }
    const { user, role } = await requireUser(ctx, args.viewerId, args.tenantId)

    // Choose best index based on provided args for efficiency
    let base: Doc<"tickets">[] = [];
    if (role === "MANAGER") {
      if (!user.companyId) {
        throw new ConvexError("Gestor não possui empresa vinculada")
      }
      base = await ctx.db
        .query("tickets")
        .withIndex("by_tenant_company", (q) => q.eq("tenantId", args.tenantId).eq("companyId", user.companyId!))
        .collect();
    } else if (args.queueId) {
      base = await ctx.db
        .query("tickets")
        .withIndex("by_tenant_queue", (q) => q.eq("tenantId", args.tenantId).eq("queueId", args.queueId!))
        .collect();
    } else if (role === "COLLABORATOR") {
      // Colaborador: exibir apenas tickets onde ele é o solicitante
      const all = await ctx.db
        .query("tickets")
        .withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId))
        .collect()
      base = all.filter((t) => t.requesterId === args.viewerId)
    } else {
      base = await ctx.db
        .query("tickets")
        .withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId))
        .collect();
    }
    let filtered = base;

    if (role === "MANAGER") {
      if (!user.companyId) {
        throw new ConvexError("Gestor não possui empresa vinculada")
      }
      filtered = filtered.filter((t) => t.companyId === user.companyId)
    }
    const normalizedStatusFilter = args.status ? normalizeStatus(args.status) : null;

    if (args.priority) filtered = filtered.filter((t) => t.priority === args.priority);
    if (args.channel) filtered = filtered.filter((t) => t.channel === args.channel);
    if (normalizedStatusFilter) {
      filtered = filtered.filter((t) => normalizeStatus(t.status) === normalizedStatusFilter);
    }
    if (args.search) {
      const term = args.search.toLowerCase();
      filtered = filtered.filter(
        (t) =>
          t.subject.toLowerCase().includes(term) ||
          t.summary?.toLowerCase().includes(term) ||
          `#${t.reference}`.toLowerCase().includes(term)
      );
    }
    const limited = args.limit ? filtered.slice(0, args.limit) : filtered;
    const categoryCache = new Map<string, Doc<"ticketCategories"> | null>();
    const subcategoryCache = new Map<string, Doc<"ticketSubcategories"> | null>();
    // hydrate requester and assignee
    const result = await Promise.all(
      limited.map(async (t) => {
        const requester = (await ctx.db.get(t.requesterId)) as Doc<"users"> | null;
        const assignee = t.assigneeId ? ((await ctx.db.get(t.assigneeId)) as Doc<"users"> | null) : null;
        const queue = t.queueId ? ((await ctx.db.get(t.queueId)) as Doc<"queues"> | null) : null;
        const company = t.companyId ? ((await ctx.db.get(t.companyId)) as Doc<"companies"> | null) : null;
        const queueName = normalizeQueueName(queue);
        const activeSession = t.activeSessionId ? await ctx.db.get(t.activeSessionId) : null;
        let categorySummary: { id: Id<"ticketCategories">; name: string } | null = null;
        let subcategorySummary: { id: Id<"ticketSubcategories">; name: string } | null = null;
        if (t.categoryId) {
          if (!categoryCache.has(t.categoryId)) {
            categoryCache.set(t.categoryId, await ctx.db.get(t.categoryId));
          }
          const category = categoryCache.get(t.categoryId);
          if (category) {
            categorySummary = { id: category._id, name: category.name };
          }
        }
        if (t.subcategoryId) {
          if (!subcategoryCache.has(t.subcategoryId)) {
            subcategoryCache.set(t.subcategoryId, await ctx.db.get(t.subcategoryId));
          }
          const subcategory = subcategoryCache.get(t.subcategoryId);
          if (subcategory) {
            subcategorySummary = { id: subcategory._id, name: subcategory.name };
          }
        }
        return {
          id: t._id,
          reference: t.reference,
          tenantId: t.tenantId,
          subject: t.subject,
          summary: t.summary,
          status: normalizeStatus(t.status),
          priority: t.priority,
          channel: t.channel,
          queue: queueName,
          company: company ? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false } : null,
          requester: buildRequesterSummary(requester, t.requesterId, { ticketId: t._id }),
          assignee: assignee
            ? {
                id: assignee._id,
                name: assignee.name,
                email: assignee.email,
                avatarUrl: assignee.avatarUrl,
                teams: normalizeTeams(assignee.teams),
              }
            : null,
          slaPolicy: null,
          dueAt: t.dueAt ?? null,
          firstResponseAt: t.firstResponseAt ?? null,
          resolvedAt: t.resolvedAt ?? null,
          updatedAt: t.updatedAt,
          createdAt: t.createdAt,
          tags: t.tags ?? [],
          lastTimelineEntry: null,
          metrics: null,
          category: categorySummary,
          subcategory: subcategorySummary,
          workSummary: {
            totalWorkedMs: t.totalWorkedMs ?? 0,
            internalWorkedMs: t.internalWorkedMs ?? 0,
            externalWorkedMs: t.externalWorkedMs ?? 0,
            activeSession: activeSession
              ? {
                  id: activeSession._id,
                  agentId: activeSession.agentId,
                  startedAt: activeSession.startedAt,
                  workType: activeSession.workType ?? "INTERNAL",
                }
              : null,
          },
        };
      })
    );
    // sort by updatedAt desc
    return result.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
  },
});

export const getById = query({
  args: { tenantId: v.string(), id: v.id("tickets"), viewerId: v.id("users") },
  handler: async (ctx, { tenantId, id, viewerId }) => {
    const { user, role } = await requireUser(ctx, viewerId, tenantId)
    const t = await ctx.db.get(id);
    if (!t || t.tenantId !== tenantId) return null;
    // no customer role; managers are constrained to company via ensureManagerTicketAccess
    let requester: Doc<"users"> | null = null
    if (role === "MANAGER") {
      requester = (await ensureManagerTicketAccess(ctx, user, t)) ?? null
    }
    if (!requester) {
      requester = (await ctx.db.get(t.requesterId)) as Doc<"users"> | null
    }
    const assignee = t.assigneeId ? ((await ctx.db.get(t.assigneeId)) as Doc<"users"> | null) : null;
    const queue = t.queueId ? ((await ctx.db.get(t.queueId)) as Doc<"queues"> | null) : null;
    const company = t.companyId ? ((await ctx.db.get(t.companyId)) as Doc<"companies"> | null) : null;
    const queueName = normalizeQueueName(queue);
    const category = t.categoryId ? await ctx.db.get(t.categoryId) : null;
    const subcategory = t.subcategoryId ? await ctx.db.get(t.subcategoryId) : null;
    const comments = await ctx.db
      .query("ticketComments")
      .withIndex("by_ticket", (q) => q.eq("ticketId", id))
      .collect();
    const canViewInternalComments = role === "ADMIN" || role === "AGENT";
    const visibleComments = canViewInternalComments
      ? comments
      : comments.filter((comment) => comment.visibility !== "INTERNAL");
    const visibleCommentKeys = new Set(
      visibleComments.map((comment) => `${comment.createdAt}:${comment.authorId}`)
    )
    const visibleCommentTimestamps = new Set(visibleComments.map((comment) => comment.createdAt))

    let timelineRecords = await ctx.db
      .query("ticketEvents")
      .withIndex("by_ticket", (q) => q.eq("ticketId", id))
      .collect();

    if (!(role === "ADMIN" || role === "AGENT")) {
      timelineRecords = timelineRecords.filter((event) => {
        const payload = (event.payload ?? {}) as Record<string, unknown>
        switch (event.type) {
          case "CREATED":
            return true
          case "QUEUE_CHANGED":
            return true
          case "ASSIGNEE_CHANGED":
            return true
          case "CATEGORY_CHANGED":
            return true
          case "COMMENT_ADDED": {
            const authorIdRaw = (payload as { authorId?: string }).authorId
            if (typeof authorIdRaw === "string" && authorIdRaw.trim().length > 0) {
              const key = `${event.createdAt}:${authorIdRaw}`
              if (visibleCommentKeys.has(key)) {
                return true
              }
            }
            return visibleCommentTimestamps.has(event.createdAt)
          }
          case "STATUS_CHANGED": {
            const toLabelRaw = (payload as { toLabel?: string }).toLabel
            const toRaw = (payload as { to?: string }).to
            const normalized = (typeof toLabelRaw === "string" && toLabelRaw.trim().length > 0
              ? toLabelRaw.trim()
              : typeof toRaw === "string"
                ? toRaw.trim()
                : "").toUpperCase()
            if (!normalized) return false
            return (
              normalized === "RESOLVED" ||
              normalized === "RESOLVIDO" ||
              normalized === "CLOSED" ||
              normalized === "FINALIZADO" ||
              normalized === "FINALIZED"
            )
          }
          default:
            return false
        }
      })
    }

    const customFieldsRecord = mapCustomFieldsToRecord(
      (t.customFields as NormalizedCustomField[] | undefined) ?? undefined
    );

    const commentsHydrated = await Promise.all(
      visibleComments.map(async (c) => {
        const author = (await ctx.db.get(c.authorId)) as Doc<"users"> | null;
        const attachments = await Promise.all(
          (c.attachments ?? []).map(async (att) => ({
            id: att.storageId,
            name: att.name,
            size: att.size,
            type: att.type,
            url: await ctx.storage.getUrl(att.storageId),
          }))
        );
        const authorSummary = buildCommentAuthorSummary(c, author, {
          ticketId: t._id,
          commentId: c._id,
        });
        return {
          id: c._id,
          author: authorSummary,
          visibility: c.visibility,
          body: c.body,
          attachments,
          createdAt: c.createdAt,
          updatedAt: c.updatedAt,
        };
      })
    );

    const activeSession = t.activeSessionId ? await ctx.db.get(t.activeSessionId) : null;

    return {
      id: t._id,
      reference: t.reference,
      tenantId: t.tenantId,
      subject: t.subject,
      summary: t.summary,
      status: normalizeStatus(t.status),
      priority: t.priority,
      channel: t.channel,
      queue: queueName,
      company: company ? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false } : null,
      requester: buildRequesterSummary(requester, t.requesterId, { ticketId: t._id }),
      assignee: assignee
        ? {
            id: assignee._id,
            name: assignee.name,
            email: assignee.email,
            avatarUrl: assignee.avatarUrl,
            teams: normalizeTeams(assignee.teams),
          }
        : null,
      slaPolicy: null,
      dueAt: t.dueAt ?? null,
      firstResponseAt: t.firstResponseAt ?? null,
      resolvedAt: t.resolvedAt ?? null,
      updatedAt: t.updatedAt,
      createdAt: t.createdAt,
      tags: t.tags ?? [],
      lastTimelineEntry: null,
      metrics: null,
      category: category
        ? {
            id: category._id,
            name: category.name,
          }
        : null,
      subcategory: subcategory
        ? {
            id: subcategory._id,
            name: subcategory.name,
            categoryId: subcategory.categoryId,
          }
        : null,
      workSummary: {
        totalWorkedMs: t.totalWorkedMs ?? 0,
        internalWorkedMs: t.internalWorkedMs ?? 0,
        externalWorkedMs: t.externalWorkedMs ?? 0,
        activeSession: activeSession
          ? {
              id: activeSession._id,
              agentId: activeSession.agentId,
              startedAt: activeSession.startedAt,
              workType: activeSession.workType ?? "INTERNAL",
            }
          : null,
      },
      description: undefined,
      customFields: customFieldsRecord,
      timeline: timelineRecords.map((ev) => {
        let payload = ev.payload;
        if (ev.type === "QUEUE_CHANGED" && payload && typeof payload === "object" && "queueName" in payload) {
          const normalized = renameQueueString((payload as { queueName?: string }).queueName ?? null);
          if (normalized && normalized !== (payload as { queueName?: string }).queueName) {
            payload = { ...payload, queueName: normalized };
          }
        }
        return {
          id: ev._id,
          type: ev.type,
          payload,
          createdAt: ev.createdAt,
        };
      }),
      comments: commentsHydrated,
    };
  },
});

export const create = mutation({
  args: {
    actorId: v.id("users"),
    tenantId: v.string(),
    subject: v.string(),
    summary: v.optional(v.string()),
    priority: v.string(),
    channel: v.string(),
    queueId: v.optional(v.id("queues")),
    requesterId: v.id("users"),
    assigneeId: v.optional(v.id("users")),
    categoryId: v.id("ticketCategories"),
    subcategoryId: v.id("ticketSubcategories"),
    customFields: v.optional(
      v.array(
        v.object({
          fieldId: v.id("ticketFields"),
          value: v.any(),
        })
      )
    ),
  },
  handler: async (ctx, args) => {
    const { user: actorUser, role } = await requireUser(ctx, args.actorId, args.tenantId)
    // no customer role; managers validated below

    if (args.assigneeId && (!role || !INTERNAL_STAFF_ROLES.has(role))) {
      throw new ConvexError("Somente a equipe interna pode definir o responsável")
    }

    let initialAssigneeId: Id<"users"> | undefined
    let initialAssignee: Doc<"users"> | null = null

    if (args.assigneeId) {
      const assignee = (await ctx.db.get(args.assigneeId)) as Doc<"users"> | null
      if (!assignee || assignee.tenantId !== args.tenantId) {
        throw new ConvexError("Responsável inválido")
      }
      const normalizedAssigneeRole = (assignee.role ?? "AGENT").toUpperCase()
      if (!STAFF_ROLES.has(normalizedAssigneeRole)) {
        throw new ConvexError("Responsável inválido")
      }
      initialAssigneeId = assignee._id
      initialAssignee = assignee
    } else if (role && INTERNAL_STAFF_ROLES.has(role)) {
      initialAssigneeId = actorUser._id
      initialAssignee = actorUser
    }

    const subject = args.subject.trim();
    if (subject.length < 3) {
      throw new ConvexError("Informe um assunto com pelo menos 3 caracteres");
    }
    const category = await ctx.db.get(args.categoryId);
    if (!category || category.tenantId !== args.tenantId) {
      throw new ConvexError("Categoria inválida");
    }
    const subcategory = await ctx.db.get(args.subcategoryId);
    if (!subcategory || subcategory.categoryId !== args.categoryId || subcategory.tenantId !== args.tenantId) {
      throw new ConvexError("Subcategoria inválida");
    }

    const requester = (await ctx.db.get(args.requesterId)) as Doc<"users"> | null
    if (!requester || requester.tenantId !== args.tenantId) {
      throw new ConvexError("Solicitante inválido")
    }
    if (role === "MANAGER") {
      if (!actorUser.companyId) {
        throw new ConvexError("Gestor não possui empresa vinculada")
      }
      if (requester.companyId !== actorUser.companyId) {
        throw new ConvexError("Gestores só podem abrir chamados para sua própria empresa")
      }
    }

    const normalizedCustomFields = await normalizeCustomFieldValues(ctx, args.tenantId, args.customFields ?? undefined);
    // compute next reference (simple monotonic counter per tenant)
    const existing = await ctx.db
      .query("tickets")
      .withIndex("by_tenant_reference", (q) => q.eq("tenantId", args.tenantId))
      .order("desc")
      .take(1);
    const nextRef = existing[0]?.reference ? existing[0].reference + 1 : 41000;
    const now = Date.now();
    const initialStatus: TicketStatusNormalized = initialAssigneeId ? "AWAITING_ATTENDANCE" : "PENDING";
    const id = await ctx.db.insert("tickets", {
      tenantId: args.tenantId,
      reference: nextRef,
      subject,
      summary: args.summary?.trim() || undefined,
      status: initialStatus,
      priority: args.priority,
      channel: args.channel,
      queueId: args.queueId,
      categoryId: args.categoryId,
      subcategoryId: args.subcategoryId,
      requesterId: args.requesterId,
      assigneeId: initialAssigneeId,
      companyId: requester.companyId ?? undefined,
      working: false,
      activeSessionId: undefined,
      totalWorkedMs: 0,
      createdAt: now,
      updatedAt: now,
      firstResponseAt: undefined,
      resolvedAt: undefined,
      closedAt: undefined,
      tags: [],
      slaPolicyId: undefined,
      dueAt: undefined,
      customFields: normalizedCustomFields.length ? normalizedCustomFields : undefined,
    });
    await ctx.db.insert("ticketEvents", {
      ticketId: id,
      type: "CREATED",
      payload: { requesterId: args.requesterId, requesterName: requester?.name, requesterAvatar: requester?.avatarUrl },
      createdAt: now,
    });

    if (initialAssigneeId && initialAssignee) {
      await ctx.db.insert("ticketEvents", {
        ticketId: id,
        type: "ASSIGNEE_CHANGED",
        payload: { assigneeId: initialAssigneeId, assigneeName: initialAssignee.name, actorId: args.actorId },
        createdAt: now,
      })
    }

    return id;
  },
});

export const addComment = mutation({
  args: {
    ticketId: v.id("tickets"),
    authorId: v.id("users"),
    visibility: v.string(),
    body: v.string(),
    attachments: v.optional(
      v.array(
        v.object({
          storageId: v.id("_storage"),
          name: v.string(),
          size: v.optional(v.number()),
          type: v.optional(v.string()),
        })
      )
    ),
  },
  handler: async (ctx, args) => {
    const ticket = await ctx.db.get(args.ticketId);
    if (!ticket) {
      throw new ConvexError("Ticket não encontrado")
    }
    const ticketDoc = ticket as Doc<"tickets">

    const author = (await ctx.db.get(args.authorId)) as Doc<"users"> | null
    if (!author || author.tenantId !== ticketDoc.tenantId) {
      throw new ConvexError("Autor do comentário inválido")
    }

    const normalizedRole = (author.role ?? "AGENT").toUpperCase()

    const requestedVisibility = (args.visibility ?? "").toUpperCase()
    if (requestedVisibility !== "PUBLIC" && requestedVisibility !== "INTERNAL") {
      throw new ConvexError("Visibilidade inválida")
    }

    if (normalizedRole === "MANAGER") {
      await ensureManagerTicketAccess(ctx, author, ticketDoc)
      if (requestedVisibility !== "PUBLIC") {
        throw new ConvexError("Gestores só podem registrar comentários públicos")
      }
    }
    const canUseInternalComments = normalizedRole === "ADMIN" || normalizedRole === "AGENT"
    if (requestedVisibility === "INTERNAL" && !canUseInternalComments) {
      throw new ConvexError("Apenas administradores e agentes podem registrar comentários internos")
    }

    // Regra: a equipe (ADMIN/AGENT/MANAGER) só pode comentar se o ticket tiver responsável.
    // O solicitante (colaborador) pode comentar sempre.
    const isRequester = String(ticketDoc.requesterId) === String(author._id)
    const isAdminOrAgent = normalizedRole === "ADMIN" || normalizedRole === "AGENT"
    const hasAssignee = Boolean(ticketDoc.assigneeId)
    // Gestores podem comentar mesmo sem responsável; admin/agent só com responsável
    if (!isRequester && isAdminOrAgent && !hasAssignee) {
      throw new ConvexError("Somente é possível comentar quando o chamado possui um responsável.")
    }

    if (ticketDoc.requesterId === args.authorId) {
      // O próprio solicitante pode comentar seu ticket.
      // Comentários internos já são bloqueados acima para quem não é STAFF.
      // Portanto, nada a fazer aqui.
    } else {
      await requireTicketStaff(ctx, args.authorId, ticketDoc)
    }

    const attachments = args.attachments ?? []
    if (attachments.length > 5) {
      throw new ConvexError("É permitido anexar no máximo 5 arquivos por comentário")
    }
    const maxAttachmentSize = 5 * 1024 * 1024
    for (const attachment of attachments) {
      if (typeof attachment.size === "number" && attachment.size > maxAttachmentSize) {
        throw new ConvexError("Cada anexo pode ter até 5MB")
      }
    }

    const authorSnapshot: CommentAuthorSnapshot = {
      name: author.name,
      email: author.email,
      avatarUrl: author.avatarUrl ?? undefined,
      teams: author.teams ?? undefined,
    };

    const now = Date.now();
    const id = await ctx.db.insert("ticketComments", {
      ticketId: args.ticketId,
      authorId: args.authorId,
      visibility: requestedVisibility,
      body: args.body,
      authorSnapshot,
      attachments,
      createdAt: now,
      updatedAt: now,
    });
    await ctx.db.insert("ticketEvents", {
      ticketId: args.ticketId,
      type: "COMMENT_ADDED",
      payload: { authorId: args.authorId, authorName: author.name, authorAvatar: author.avatarUrl },
      createdAt: now,
    });
    // bump ticket updatedAt
    await ctx.db.patch(args.ticketId, { updatedAt: now });
    return id;
  },
});

export const updateComment = mutation({
  args: {
    ticketId: v.id("tickets"),
    commentId: v.id("ticketComments"),
    actorId: v.id("users"),
    body: v.string(),
  },
  handler: async (ctx, { ticketId, commentId, actorId, body }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      throw new ConvexError("Ticket não encontrado")
    }
    const ticketDoc = ticket as Doc<"tickets">
    const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null
    if (!actor || actor.tenantId !== ticketDoc.tenantId) {
      throw new ConvexError("Autor do comentário inválido")
    }
    const comment = await ctx.db.get(commentId);
    if (!comment || comment.ticketId !== ticketId) {
      throw new ConvexError("Comentário não encontrado");
    }
    if (comment.authorId !== actorId) {
      throw new ConvexError("Você não tem permissão para editar este comentário");
    }
    const normalizedRole = (actor.role ?? "AGENT").toUpperCase()
    if (ticketDoc.requesterId === actorId) {
      if (STAFF_ROLES.has(normalizedRole)) {
        await requireTicketStaff(ctx, actorId, ticketDoc)
      } else {
        throw new ConvexError("Autor não possui permissão para editar")
      }
    } else {
      await requireTicketStaff(ctx, actorId, ticketDoc)
    }

    const now = Date.now();
    await ctx.db.patch(commentId, {
      body,
      updatedAt: now,
    });

    await ctx.db.insert("ticketEvents", {
      ticketId,
      type: "COMMENT_EDITED",
      payload: {
        commentId,
        actorId,
        actorName: actor?.name,
        actorAvatar: actor?.avatarUrl,
      },
      createdAt: now,
    });

    await ctx.db.patch(ticketId, { updatedAt: now });
  },
});

export const removeCommentAttachment = mutation({
  args: {
    ticketId: v.id("tickets"),
    commentId: v.id("ticketComments"),
    attachmentId: v.id("_storage"),
    actorId: v.id("users"),
  },
  handler: async (ctx, { ticketId, commentId, attachmentId, actorId }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      throw new ConvexError("Ticket não encontrado")
    }
    const ticketDoc = ticket as Doc<"tickets">
    const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null
    if (!actor || actor.tenantId !== ticketDoc.tenantId) {
      throw new ConvexError("Autor do comentário inválido")
    }
    const comment = await ctx.db.get(commentId);
    if (!comment || comment.ticketId !== ticketId) {
      throw new ConvexError("Comentário não encontrado");
    }
    if (comment.authorId !== actorId) {
      throw new ConvexError("Você não pode alterar anexos de outro usuário")
    }

    const normalizedRole = (actor.role ?? "AGENT").toUpperCase()
    if (ticketDoc.requesterId === actorId) {
      if (STAFF_ROLES.has(normalizedRole)) {
        await requireTicketStaff(ctx, actorId, ticketDoc)
      } else {
        throw new ConvexError("Autor não possui permissão para alterar anexos")
      }
    } else {
      await requireTicketStaff(ctx, actorId, ticketDoc)
    }

    const attachments = comment.attachments ?? [];
    const target = attachments.find((att) => att.storageId === attachmentId);
    if (!target) {
      throw new ConvexError("Anexo não encontrado");
    }

    await ctx.storage.delete(attachmentId);

    const now = Date.now();
    await ctx.db.patch(commentId, {
      attachments: attachments.filter((att) => att.storageId !== attachmentId),
      updatedAt: now,
    });

    await ctx.db.insert("ticketEvents", {
      ticketId,
      type: "ATTACHMENT_REMOVED",
      payload: {
        attachmentId,
        attachmentName: target.name,
        actorId,
        actorName: actor?.name,
        actorAvatar: actor?.avatarUrl,
      },
      createdAt: now,
    });

    await ctx.db.patch(ticketId, { updatedAt: now });
  },
});

export const updateStatus = mutation({
  args: { ticketId: v.id("tickets"), status: v.string(), actorId: v.id("users") },
  handler: async (ctx, { ticketId, status, actorId }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      throw new ConvexError("Ticket não encontrado")
    }
    const ticketDoc = ticket as Doc<"tickets">
    await requireTicketStaff(ctx, actorId, ticketDoc)
    const normalizedStatus = normalizeStatus(status)
    const now = Date.now();
    await ctx.db.patch(ticketId, { status: normalizedStatus, updatedAt: now });
    await ctx.db.insert("ticketEvents", {
      ticketId,
      type: "STATUS_CHANGED",
      payload: { to: normalizedStatus, toLabel: STATUS_LABELS[normalizedStatus], actorId },
      createdAt: now,
    });
  },
});

export const changeAssignee = mutation({
  args: { ticketId: v.id("tickets"), assigneeId: v.id("users"), actorId: v.id("users") },
  handler: async (ctx, { ticketId, assigneeId, actorId }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      throw new ConvexError("Ticket não encontrado")
    }
    const ticketDoc = ticket as Doc<"tickets">
    const viewer = await requireTicketStaff(ctx, actorId, ticketDoc)
    const isAdmin = viewer.role === "ADMIN"
    const assignee = (await ctx.db.get(assigneeId)) as Doc<"users"> | null
    if (!assignee || assignee.tenantId !== ticketDoc.tenantId) {
      throw new ConvexError("Responsável inválido")
    }
    if (viewer.role === "MANAGER") {
      throw new ConvexError("Gestores não podem reatribuir chamados")
    }
    const currentAssigneeId = ticketDoc.assigneeId ?? null
    if (currentAssigneeId && currentAssigneeId !== actorId && !isAdmin) {
      throw new ConvexError("Somente o responsável atual pode reatribuir este chamado")
    }

    const now = Date.now();
    await ctx.db.patch(ticketId, { assigneeId, updatedAt: now });
    await ctx.db.insert("ticketEvents", {
      ticketId,
      type: "ASSIGNEE_CHANGED",
      payload: { assigneeId, assigneeName: assignee.name, actorId },
      createdAt: now,
    });
  },
});

export const changeQueue = mutation({
  args: { ticketId: v.id("tickets"), queueId: v.id("queues"), actorId: v.id("users") },
  handler: async (ctx, { ticketId, queueId, actorId }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      throw new ConvexError("Ticket não encontrado")
    }
    const ticketDoc = ticket as Doc<"tickets">
    const viewer = await requireTicketStaff(ctx, actorId, ticketDoc)
    if (viewer.role === "MANAGER") {
      throw new ConvexError("Gestores não podem alterar a fila do chamado")
    }
    const queue = (await ctx.db.get(queueId)) as Doc<"queues"> | null
    if (!queue || queue.tenantId !== ticketDoc.tenantId) {
      throw new ConvexError("Fila inválida")
    }
    const now = Date.now();
    await ctx.db.patch(ticketId, { queueId, updatedAt: now });
    const queueName = normalizeQueueName(queue);
    await ctx.db.insert("ticketEvents", {
      ticketId,
      type: "QUEUE_CHANGED",
      payload: { queueId, queueName, actorId },
      createdAt: now,
    });
  },
});

export const updateCategories = mutation({
  args: {
    ticketId: v.id("tickets"),
    categoryId: v.union(v.id("ticketCategories"), v.null()),
    subcategoryId: v.union(v.id("ticketSubcategories"), v.null()),
    actorId: v.id("users"),
  },
  handler: async (ctx, { ticketId, categoryId, subcategoryId, actorId }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      throw new ConvexError("Ticket não encontrado")
    }
    const ticketDoc = ticket as Doc<"tickets">
    const viewer = await requireTicketStaff(ctx, actorId, ticketDoc)
    if (viewer.role === "MANAGER") {
      throw new ConvexError("Gestores não podem alterar a categorização do chamado")
    }

    if (categoryId === null) {
      if (subcategoryId !== null) {
        throw new ConvexError("Subcategoria inválida")
      }
      if (!ticketDoc.categoryId && !ticketDoc.subcategoryId) {
        return { status: "unchanged" }
      }
      const now = Date.now()
      await ctx.db.patch(ticketId, {
        categoryId: undefined,
        subcategoryId: undefined,
        updatedAt: now,
      })
      const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null
      await ctx.db.insert("ticketEvents", {
        ticketId,
        type: "CATEGORY_CHANGED",
        payload: {
          categoryId: null,
          categoryName: null,
          subcategoryId: null,
          subcategoryName: null,
          actorId,
          actorName: actor?.name,
          actorAvatar: actor?.avatarUrl,
        },
        createdAt: now,
      })
      return { status: "cleared" }
    }

    const category = await ctx.db.get(categoryId)
    if (!category || category.tenantId !== ticketDoc.tenantId) {
      throw new ConvexError("Categoria inválida")
    }

    let subcategoryName: string | null = null
    if (subcategoryId !== null) {
      const subcategory = await ctx.db.get(subcategoryId)
      if (!subcategory || subcategory.categoryId !== categoryId || subcategory.tenantId !== ticketDoc.tenantId) {
        throw new ConvexError("Subcategoria inválida")
      }
      subcategoryName = subcategory.name
    }

    if (ticketDoc.categoryId === categoryId && (ticketDoc.subcategoryId ?? null) === subcategoryId) {
      return { status: "unchanged" }
    }

    const now = Date.now()
    await ctx.db.patch(ticketId, {
      categoryId,
      subcategoryId: subcategoryId ?? undefined,
      updatedAt: now,
    })

    const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null
    await ctx.db.insert("ticketEvents", {
      ticketId,
      type: "CATEGORY_CHANGED",
      payload: {
        categoryId,
        categoryName: category.name,
        subcategoryId,
        subcategoryName,
        actorId,
        actorName: actor?.name,
        actorAvatar: actor?.avatarUrl,
      },
      createdAt: now,
    })

    return { status: "updated" }
  },
})

export const workSummary = query({
  args: { ticketId: v.id("tickets"), viewerId: v.id("users") },
  handler: async (ctx, { ticketId, viewerId }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) return null
    await requireStaff(ctx, viewerId, ticket.tenantId)

    const activeSession = ticket.activeSessionId ? await ctx.db.get(ticket.activeSessionId) : null
    return {
      ticketId,
      totalWorkedMs: ticket.totalWorkedMs ?? 0,
      internalWorkedMs: ticket.internalWorkedMs ?? 0,
      externalWorkedMs: ticket.externalWorkedMs ?? 0,
      activeSession: activeSession
        ? {
            id: activeSession._id,
            agentId: activeSession.agentId,
            startedAt: activeSession.startedAt,
            workType: activeSession.workType ?? "INTERNAL",
          }
        : null,
    }
  },
})

export const updatePriority = mutation({
  args: { ticketId: v.id("tickets"), priority: v.string(), actorId: v.id("users") },
  handler: async (ctx, { ticketId, priority, actorId }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      throw new ConvexError("Ticket não encontrado")
    }
    await requireStaff(ctx, actorId, ticket.tenantId)
    const now = Date.now();
    await ctx.db.patch(ticketId, { priority, updatedAt: now });
    const pt: Record<string, string> = { LOW: "Baixa", MEDIUM: "Média", HIGH: "Alta", URGENT: "Urgente" };
    await ctx.db.insert("ticketEvents", {
      ticketId,
      type: "PRIORITY_CHANGED",
      payload: { to: priority, toLabel: pt[priority] ?? priority, actorId },
      createdAt: now,
    });
  },
});

export const startWork = mutation({
  args: { ticketId: v.id("tickets"), actorId: v.id("users"), workType: v.optional(v.string()) },
  handler: async (ctx, { ticketId, actorId, workType }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      throw new ConvexError("Ticket não encontrado")
    }
    const ticketDoc = ticket as Doc<"tickets">
    const viewer = await requireTicketStaff(ctx, actorId, ticketDoc)
    const isAdmin = viewer.role === "ADMIN"
    const currentAssigneeId = ticketDoc.assigneeId ?? null

    if (currentAssigneeId && currentAssigneeId !== actorId && !isAdmin) {
      throw new ConvexError("Somente o responsável atual pode iniciar este chamado")
    }

    if (ticketDoc.activeSessionId) {
      return { status: "already_started", sessionId: ticketDoc.activeSessionId }
    }

    const now = Date.now()
    let assigneePatched = false

    if (!currentAssigneeId) {
      await ctx.db.patch(ticketId, { assigneeId: actorId, updatedAt: now })
      ticketDoc.assigneeId = actorId
      assigneePatched = true
    }

    const sessionId = await ctx.db.insert("ticketWorkSessions", {
      ticketId,
      agentId: actorId,
      workType: (workType ?? "INTERNAL").toUpperCase(),
      startedAt: now,
    })

    await ctx.db.patch(ticketId, {
      working: true,
      activeSessionId: sessionId,
      updatedAt: now,
    })

    if (assigneePatched) {
      await ctx.db.insert("ticketEvents", {
        ticketId,
        type: "ASSIGNEE_CHANGED",
        payload: { assigneeId: actorId, assigneeName: viewer.user.name, actorId },
        createdAt: now,
      })
    }

    await ctx.db.insert("ticketEvents", {
      ticketId,
      type: "WORK_STARTED",
      payload: {
        actorId,
        actorName: viewer.user.name,
        actorAvatar: viewer.user.avatarUrl,
        sessionId,
        workType: (workType ?? "INTERNAL").toUpperCase(),
      },
      createdAt: now,
    })

    return { status: "started", sessionId, startedAt: now }
  },
})

export const pauseWork = mutation({
  args: {
    ticketId: v.id("tickets"),
    actorId: v.id("users"),
    reason: v.string(),
    note: v.optional(v.string()),
  },
  handler: async (ctx, { ticketId, actorId, reason, note }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      throw new ConvexError("Ticket não encontrado")
    }
    const ticketDoc = ticket as Doc<"tickets">
    const viewer = await requireTicketStaff(ctx, actorId, ticketDoc)
    const isAdmin = viewer.role === "ADMIN"
    if (ticketDoc.assigneeId && ticketDoc.assigneeId !== actorId && !isAdmin) {
      throw new ConvexError("Somente o responsável atual pode pausar este chamado")
    }

    if (!ticketDoc.activeSessionId) {
      return { status: "already_paused" }
    }

    if (!PAUSE_REASON_LABELS[reason]) {
      throw new ConvexError("Motivo de pausa inválido")
    }

    const session = await ctx.db.get(ticketDoc.activeSessionId)
    if (!session) {
      await ctx.db.patch(ticketId, { activeSessionId: undefined, working: false })
      return { status: "session_missing" }
    }

    const now = Date.now()
    const durationMs = now - session.startedAt

    await ctx.db.patch(ticketDoc.activeSessionId, {
      stoppedAt: now,
      durationMs,
      pauseReason: reason,
      pauseNote: note ?? "",
    })

  const sessionType = (session.workType ?? "INTERNAL").toUpperCase()
    const deltaInternal = sessionType === "INTERNAL" ? durationMs : 0
    const deltaExternal = sessionType === "EXTERNAL" ? durationMs : 0

    await ctx.db.patch(ticketId, {
      working: false,
      activeSessionId: undefined,
      totalWorkedMs: (ticket.totalWorkedMs ?? 0) + durationMs,
      internalWorkedMs: (ticket.internalWorkedMs ?? 0) + deltaInternal,
      externalWorkedMs: (ticket.externalWorkedMs ?? 0) + deltaExternal,
      updatedAt: now,
    })

    const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null
    await ctx.db.insert("ticketEvents", {
      ticketId,
      type: "WORK_PAUSED",
      payload: {
        actorId,
        actorName: actor?.name,
        actorAvatar: actor?.avatarUrl,
        sessionId: session._id,
        sessionDurationMs: durationMs,
        workType: sessionType,
        pauseReason: reason,
        pauseReasonLabel: PAUSE_REASON_LABELS[reason],
        pauseNote: note ?? "",
      },
      createdAt: now,
    })

    return {
      status: "paused",
      durationMs,
      pauseReason: reason,
      pauseNote: note ?? "",
    }
  },
})

export const updateSubject = mutation({
  args: { ticketId: v.id("tickets"), subject: v.string(), actorId: v.id("users") },
  handler: async (ctx, { ticketId, subject, actorId }) => {
    const now = Date.now();
    const t = await ctx.db.get(ticketId);
    if (!t) {
      throw new ConvexError("Ticket não encontrado")
    }
    await requireStaff(ctx, actorId, t.tenantId)
    const trimmed = subject.trim();
    if (trimmed.length < 3) {
      throw new ConvexError("Informe um assunto com pelo menos 3 caracteres");
    }
    await ctx.db.patch(ticketId, { subject: trimmed, updatedAt: now });
    const actor = await ctx.db.get(actorId);
    await ctx.db.insert("ticketEvents", {
      ticketId,
      type: "SUBJECT_CHANGED",
      payload: { from: t.subject, to: trimmed, actorId, actorName: (actor as Doc<"users"> | null)?.name, actorAvatar: (actor as Doc<"users"> | null)?.avatarUrl },
      createdAt: now,
    });
  },
});

export const updateSummary = mutation({
  args: { ticketId: v.id("tickets"), summary: v.optional(v.string()), actorId: v.id("users") },
  handler: async (ctx, { ticketId, summary, actorId }) => {
    const now = Date.now();
    const t = await ctx.db.get(ticketId);
    if (!t) {
      throw new ConvexError("Ticket não encontrado")
    }
    await requireStaff(ctx, actorId, t.tenantId)
    await ctx.db.patch(ticketId, { summary, updatedAt: now });
    const actor = await ctx.db.get(actorId);
    await ctx.db.insert("ticketEvents", {
      ticketId,
      type: "SUMMARY_CHANGED",
      payload: { actorId, actorName: (actor as Doc<"users"> | null)?.name, actorAvatar: (actor as Doc<"users"> | null)?.avatarUrl },
      createdAt: now,
    });
  },
});

export const playNext = mutation({
  args: {
    tenantId: v.string(),
    queueId: v.optional(v.id("queues")),
    agentId: v.id("users"),
  },
  handler: async (ctx, { tenantId, queueId, agentId }) => {
    const { user: agent } = await requireStaff(ctx, agentId, tenantId)
    // Find eligible tickets: not resolved/closed and not assigned
    let candidates: Doc<"tickets">[] = []
    if (queueId) {
      candidates = await ctx.db
        .query("tickets")
        .withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", queueId))
        .collect()
    } else {
      candidates = await ctx.db
        .query("tickets")
        .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
        .collect()
    }

    candidates = candidates.filter(
      (t) => t.status !== "RESOLVED" && !t.assigneeId
    );

    if (candidates.length === 0) return null;

    // prioritize by priority then createdAt
    const rank: Record<string, number> = { URGENT: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
    candidates.sort((a, b) => {
      const pa = rank[a.priority] ?? 999
      const pb = rank[b.priority] ?? 999
      if (pa !== pb) return pa - pb
      return a.createdAt - b.createdAt
    })

    const chosen = candidates[0];
    const now = Date.now();
    const currentStatus = normalizeStatus(chosen.status);
    const nextStatus: TicketStatusNormalized =
      currentStatus === "PENDING" ? "AWAITING_ATTENDANCE" : currentStatus;
    await ctx.db.patch(chosen._id, { assigneeId: agentId, status: nextStatus, updatedAt: now });
    await ctx.db.insert("ticketEvents", {
      ticketId: chosen._id,
      type: "ASSIGNEE_CHANGED",
      payload: { assigneeId: agentId, assigneeName: agent.name },
      createdAt: now,
    });

    // hydrate minimal public ticket like in list
    const requester = (await ctx.db.get(chosen.requesterId)) as Doc<"users"> | null
    const assignee = chosen.assigneeId ? ((await ctx.db.get(chosen.assigneeId)) as Doc<"users"> | null) : null
    const queue = chosen.queueId ? ((await ctx.db.get(chosen.queueId)) as Doc<"queues"> | null) : null
    const queueName = normalizeQueueName(queue)
    return {
      id: chosen._id,
      reference: chosen.reference,
      tenantId: chosen.tenantId,
      subject: chosen.subject,
      summary: chosen.summary,
      status: nextStatus,
      priority: chosen.priority,
      channel: chosen.channel,
      queue: queueName,
      requester: buildRequesterSummary(requester, chosen.requesterId, { ticketId: chosen._id }),
      assignee: assignee
        ? {
            id: assignee._id,
            name: assignee.name,
            email: assignee.email,
            avatarUrl: assignee.avatarUrl,
            teams: normalizeTeams(assignee.teams),
          }
        : null,
      slaPolicy: null,
      dueAt: chosen.dueAt ?? null,
      firstResponseAt: chosen.firstResponseAt ?? null,
      resolvedAt: chosen.resolvedAt ?? null,
      updatedAt: chosen.updatedAt,
      createdAt: chosen.createdAt,
      tags: chosen.tags ?? [],
      lastTimelineEntry: null,
      metrics: null,
    }
  },
});

export const remove = mutation({
  args: { ticketId: v.id("tickets"), actorId: v.id("users") },
  handler: async (ctx, { ticketId, actorId }) => {
    const ticket = await ctx.db.get(ticketId)
    if (!ticket) {
      throw new ConvexError("Ticket não encontrado")
    }
    await requireAdmin(ctx, actorId, ticket.tenantId)
    // delete comments (and attachments)
    const comments = await ctx.db
      .query("ticketComments")
      .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
      .collect();
    for (const c of comments) {
      for (const att of c.attachments ?? []) {
        try { await ctx.storage.delete(att.storageId); } catch {}
      }
      await ctx.db.delete(c._id);
    }
    // delete events
    const events = await ctx.db
      .query("ticketEvents")
      .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
      .collect();
    for (const ev of events) await ctx.db.delete(ev._id);
    // delete ticket
    await ctx.db.delete(ticketId);
    // (optional) event is moot after deletion
    return true;
  },
});
