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

import { requireStaff } from "./rbac";
import { getOfflineThresholdMs, getStaleThresholdMs } from "./machines";

export type TicketStatusNormalized = "PENDING" | "AWAITING_ATTENDANCE" | "PAUSED" | "RESOLVED";
type QueryFilterBuilder = { lt: (field: unknown, value: number) => unknown; field: (name: string) => unknown };
export const STATUS_NORMALIZE_MAP: Record<string, TicketStatusNormalized> = {
  NEW: "PENDING",
  PENDING: "PENDING",
  OPEN: "AWAITING_ATTENDANCE",
  AWAITING_ATTENDANCE: "AWAITING_ATTENDANCE",
  ON_HOLD: "PAUSED",
  PAUSED: "PAUSED",
  RESOLVED: "RESOLVED",
  CLOSED: "RESOLVED",
};

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

function average(values: number[]) {
  if (values.length === 0) return null;
  return values.reduce((sum, value) => sum + value, 0) / values.length;
}

function resolveCategoryName(
  categoryId: string | null,
  snapshot: { categoryName?: string } | null,
  categories: Map<string, Doc<"ticketCategories">>
) {
  if (categoryId) {
    const category = categories.get(categoryId)
    if (category?.name) {
      return category.name
    }
  }
  if (snapshot?.categoryName && snapshot.categoryName.trim().length > 0) {
    return snapshot.categoryName.trim()
  }
  return "Sem categoria"
}

export const OPEN_STATUSES = new Set<TicketStatusNormalized>(["PENDING", "AWAITING_ATTENDANCE", "PAUSED"]);
export const ONE_DAY_MS = 24 * 60 * 60 * 1000;

const REPORTS_PAGE_SIZE = 200;
const RESOLVED_CHUNK_MS = 6 * 60 * 60 * 1000;
const MAX_RANGE_DAYS = 90;
const MAX_RANGE_MS = MAX_RANGE_DAYS * ONE_DAY_MS;
const CACHE_TTL_MS = 60_000;
const LOCK_TTL_MS = 45_000;
const DASHBOARD_LOG_INTERVAL = 400;

function clampRangeWindow(startMs: number, endMs: number) {
  if (endMs <= startMs) {
    throw new ConvexError("Intervalo inválido");
  }
  if (endMs - startMs > MAX_RANGE_MS) {
    return {
      startMs,
      endMs: startMs + MAX_RANGE_MS,
      wasClamped: true,
    };
  }
  return { startMs, endMs, wasClamped: false };
}

function deriveRangeDays(startMs: number, endMs: number) {
  return Math.max(1, Math.ceil((endMs - startMs) / ONE_DAY_MS));
}

function buildDashboardCacheKey({
  tenantId,
  companyId,
  range,
  dateFrom,
  dateTo,
  allowWide,
}: {
  tenantId: string;
  companyId?: Id<"companies">;
  range?: string;
  dateFrom?: string;
  dateTo?: string;
  allowWide?: boolean;
}) {
  return JSON.stringify({
    tenantId,
    companyId: companyId ? String(companyId) : "all",
    range: range ?? null,
    dateFrom: dateFrom ?? null,
    dateTo: dateTo ?? null,
    allowWide: allowWide === true,
  });
}

async function readDashboardCache(ctx: MutationCtx, tenantId: string, cacheKey: string) {
  const cached = await ctx.db
    .query("analyticsCache")
    .withIndex("by_key", (q) => q.eq("tenantId", tenantId).eq("cacheKey", cacheKey))
    .first();
  if (!cached) return null;
  if (cached.expiresAt <= Date.now()) {
    await ctx.db.delete(cached._id);
    return null;
  }
  return cached;
}

async function writeDashboardCache(
  ctx: MutationCtx,
  tenantId: string,
  cacheKey: string,
  payload: unknown,
) {
  const expiresAt = Date.now() + CACHE_TTL_MS;
  await ctx.db.insert("analyticsCache", {
    tenantId,
    cacheKey,
    payload,
    expiresAt,
    _ttl: expiresAt,
  });
}

async function acquireDashboardLock(ctx: MutationCtx, tenantId: string, cacheKey: string) {
  const now = Date.now();
  const existing = await ctx.db
    .query("analyticsLocks")
    .withIndex("by_key", (q) => q.eq("tenantId", tenantId).eq("cacheKey", cacheKey))
    .first();
  if (existing) {
    if (existing.expiresAt > now) {
      return null;
    }
    await ctx.db.delete(existing._id);
  }
  const expiresAt = now + LOCK_TTL_MS;
  const lockId = await ctx.db.insert("analyticsLocks", {
    tenantId,
    cacheKey,
    expiresAt,
    _ttl: expiresAt,
  });
  return lockId;
}

async function releaseDashboardLock(ctx: MutationCtx, lockId: Id<"analyticsLocks"> | null) {
  if (!lockId) return;
  try {
    await ctx.db.delete(lockId);
  } catch {
    // ignore stale lock deletions
  }
}

function logDashboardProgress(_processed: number, _tenantId: string) {
  // Log de progresso removido para reduzir ruido no console
}

function mapToChronologicalSeries(map: Map<string, number>) {
  return Array.from(map.entries())
    .sort((a, b) => a[0].localeCompare(b[0]))
    .map(([key, count]) => ({ key, count }));
}

function mapToDescendingSeries(map: Map<string, number>) {
  return Array.from(map.entries())
    .sort((a, b) => {
      const diff = b[1] - a[1];
      return diff !== 0 ? diff : a[0].localeCompare(b[0]);
    })
    .map(([key, count]) => ({ key, count }));
}

function flattenMachineCategoryMap(map: Map<string, Map<string, number>>) {
  const results: Array<{ machineId: string; categoryId: string; count: number }> = [];
  for (const [machineId, categories] of map.entries()) {
    for (const [categoryId, count] of categories.entries()) {
      results.push({ machineId, categoryId, count });
    }
  }
  return results;
}

type PaginatedResult<T> = {
  page: T[];
  continueCursor?: string | null;
  done?: boolean;
  isDone?: boolean;
};

type QueryBuilder<T> = {
  paginate?: (options: { cursor: string | null; numItems: number }) => Promise<PaginatedResult<T>>;
  collect?: () => Promise<T[]>;
};

async function paginateTickets<T>(
  buildQuery: () => QueryBuilder<T>,
  handler: (doc: T) => void | Promise<void>,
  pageSize = REPORTS_PAGE_SIZE,
) {
  let cursor: string | null = null;

  while (true) {
    const query = buildQuery();
    const paginateFn:
      | ((options: { cursor: string | null; numItems: number }) => Promise<PaginatedResult<T>>)
      | undefined = query.paginate;

    if (typeof paginateFn !== "function") {
      if (cursor !== null) {
        throw new ConvexError("Query does not support pagination but cursor was provided");
      }
      const collectFn = query.collect;
      if (typeof collectFn !== "function") {
        throw new ConvexError("Query does not support paginate or collect");
      }
      const docs = await collectFn.call(query);
      for (const doc of docs) {
        await handler(doc);
      }
      return;
    }

    let page: PaginatedResult<T>;
    try {
      page = await paginateFn.call(query, { cursor, numItems: pageSize });
    } catch (error) {
      // Em cenários raros o Convex pode lançar InvalidCursor quando
      // o fingerprint da query muda entre páginas. Nesse caso,
      // fazemos fallback para uma leitura simples via collect()
      // para evitar quebrar o dashboard.
      const message = (error as Error | null | undefined)?.message ?? "";
      const isInvalidCursor =
        typeof message === "string" &&
        message.includes("InvalidCursor");
      // Alguns erros de paginação vêm embrulhados em ConvexError com metadados.
      type PaginationErrorMetadata = {
        paginationError?: string;
      };
      type PaginationError = {
        data?: PaginationErrorMetadata;
      };
      const data = (error as PaginationError | null | undefined)?.data;
      const isPaginationInvalidCursor =
        data && typeof data === "object" && data.paginationError === "InvalidCursor";

      if (isInvalidCursor || isPaginationInvalidCursor) {
        const fallbackQuery = buildQuery();
        const collectFn = fallbackQuery.collect;
        if (typeof collectFn !== "function") {
          throw error;
        }
        const docs = await collectFn.call(fallbackQuery);
        for (const doc of docs) {
          await handler(doc);
        }
        return;
      }

      throw error;
    }

    for (const doc of page.page) {
      await handler(doc);
    }

    const continueCursor = page.continueCursor ?? null;
    const done = page.done ?? page.isDone ?? !continueCursor;
    if (done) {
      break;
    }
    cursor = continueCursor;
  }
}

function withLowerBound<T>(range: T, field: string, value: number): T {
  const candidate = range as { gte?: (field: string, value: number) => unknown };
  if (candidate && typeof candidate.gte === "function") {
    return candidate.gte(field, value) as T;
  }
  return range;
}

function withUpperBound<T>(range: T, field: string, value: number): T {
  const candidate = range as { lt?: (field: string, value: number) => unknown };
  if (candidate && typeof candidate.lt === "function") {
    return candidate.lt(field, value) as T;
  }
  return range;
}

function parseIsoDateToMs(value?: string | null): number | null {
  if (!value) return null;
  const [yearStr, monthStr, dayStr] = value.split("-");
  const year = Number(yearStr);
  const month = Number(monthStr);
  const day = Number(dayStr);
  if (!year || !month || !day) return null;
  const date = new Date(Date.UTC(year, month - 1, day, 0, 0, 0, 0));
  return date.getTime();
}

function resolveRangeWindow(
  range: string | undefined,
  dateFrom: string | undefined,
  dateTo: string | undefined,
  defaultDays: number,
): { startMs: number; endMs: number; days: number } {
  const fromMs = parseIsoDateToMs(dateFrom);
  const toMs = parseIsoDateToMs(dateTo);

  if (fromMs !== null || toMs !== null) {
    const startMs = fromMs ?? toMs ?? Date.now();
    const endMsBase = toMs ?? fromMs ?? startMs;
    const endMs = endMsBase + ONE_DAY_MS;
    const days = Math.max(1, Math.round((endMs - startMs) / ONE_DAY_MS));
    return { startMs, endMs, days };
  }

  const normalizedRange = range ?? "90d";
  let days = defaultDays;
  if (normalizedRange === "7d") days = 7;
  else if (normalizedRange === "30d") days = 30;
  else if (normalizedRange === "365d" || normalizedRange === "12m") days = 365;
  else if (normalizedRange === "all") {
    const now = new Date();
    now.setUTCHours(0, 0, 0, 0);
    const endMs = now.getTime() + ONE_DAY_MS;
    return { startMs: 0, endMs, days: 0 };
  }

  const end = new Date();
  end.setUTCHours(0, 0, 0, 0);
  const endMs = end.getTime() + ONE_DAY_MS;
  const startMs = endMs - days * ONE_DAY_MS;
  return { startMs, endMs, days };
}

function resolveScopedCompanyId(
  viewer: Awaited<ReturnType<typeof requireStaff>>,
  companyId?: Id<"companies">,
): Id<"companies"> | undefined {
  if (viewer.role === "MANAGER") {
    if (!viewer.user.companyId) {
      throw new ConvexError("Gestor não possui empresa vinculada");
    }
    return viewer.user.companyId;
  }
  return companyId;
}

type TicketProcessor = (ticket: Doc<"tickets">) => void | Promise<void>;

async function forEachScopedTicket(
  ctx: QueryCtx,
  tenantId: string,
  viewer: Awaited<ReturnType<typeof requireStaff>>,
  processor: TicketProcessor,
) {
  const scopedCompanyId = resolveScopedCompanyId(viewer);
  await paginateTickets(
    () =>
      scopedCompanyId
        ? ctx.db
            .query("tickets")
            .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", scopedCompanyId))
            .order("desc")
        : ctx.db.query("tickets").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).order("desc"),
    async (ticket) => {
      await processor(ticket);
    },
  );
}

async function forEachTenantTicket(ctx: QueryCtx, tenantId: string, processor: TicketProcessor) {
  await paginateTickets(
    () => ctx.db.query("tickets").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).order("desc"),
    async (ticket) => {
      await processor(ticket);
    },
  );
}

async function forEachScopedTicketByCreatedRange(
  ctx: QueryCtx,
  tenantId: string,
  viewer: Awaited<ReturnType<typeof requireStaff>>,
  startMs: number,
  endMs: number,
  companyId: Id<"companies"> | undefined,
  processor: TicketProcessor,
) {
  const scopedCompanyId = resolveScopedCompanyId(viewer, companyId);
  const makeQuery = () =>
    scopedCompanyId
      ? ctx.db
          .query("tickets")
          .withIndex("by_tenant_company_created", (q) => {
            let range = q.eq("tenantId", tenantId).eq("companyId", scopedCompanyId);
            range = withLowerBound(range, "createdAt", startMs);
            range = withUpperBound(range, "createdAt", endMs);
            return range;
          })
          .order("desc")
      : ctx.db
          .query("tickets")
          .withIndex("by_tenant_created", (q) => {
            let range = q.eq("tenantId", tenantId);
            range = withLowerBound(range, "createdAt", startMs);
            range = withUpperBound(range, "createdAt", endMs);
            return range;
          })
          .order("desc");

  await paginateTickets(
    () => makeQuery(),
    async (ticket) => {
      const createdAt = typeof ticket.createdAt === "number" ? ticket.createdAt : null;
      if (createdAt === null) return;
      if (createdAt < startMs || createdAt >= endMs) return;
      await processor(ticket);
    },
  );
}

async function forEachScopedTicketByResolvedRange(
  ctx: QueryCtx,
  tenantId: string,
  viewer: Awaited<ReturnType<typeof requireStaff>>,
  startMs: number,
  endMs: number,
  companyId: Id<"companies"> | undefined,
  processor: TicketProcessor,
) {
  const scopedCompanyId = resolveScopedCompanyId(viewer, companyId);
  const query = scopedCompanyId
    ? ctx.db
        .query("tickets")
        .withIndex("by_tenant_company_resolved", (q) => {
          let range = q.eq("tenantId", tenantId).eq("companyId", scopedCompanyId);
          range = withLowerBound(range, "resolvedAt", startMs);
          range = withUpperBound(range, "resolvedAt", endMs);
          return range;
        })
        .order("desc")
    : ctx.db
        .query("tickets")
        .withIndex("by_tenant_resolved", (q) => {
          let range = q.eq("tenantId", tenantId);
          range = withLowerBound(range, "resolvedAt", startMs);
          range = withUpperBound(range, "resolvedAt", endMs);
          return range;
        })
        .order("desc");

  await paginateTickets(
    () => query,
    async (ticket) => {
      const resolvedAt = typeof ticket.resolvedAt === "number" ? ticket.resolvedAt : null;
      if (resolvedAt === null) return;
      if (resolvedAt < startMs || resolvedAt >= endMs) return;
      await processor(ticket);
    },
  );
}

async function forEachScopedTicketByResolvedRangeChunked(
  ctx: QueryCtx,
  tenantId: string,
  viewer: Awaited<ReturnType<typeof requireStaff>>,
  startMs: number,
  endMs: number,
  companyId: Id<"companies"> | undefined,
  processor: TicketProcessor,
) {
  const scopedCompanyId = resolveScopedCompanyId(viewer, companyId);
  for (let chunkStart = startMs; chunkStart < endMs; chunkStart += RESOLVED_CHUNK_MS) {
    const chunkEnd = Math.min(chunkStart + RESOLVED_CHUNK_MS, endMs);
    const query = scopedCompanyId
      ? ctx.db
          .query("tickets")
          .withIndex("by_tenant_company_resolved", (q) => {
            let range = q.eq("tenantId", tenantId).eq("companyId", scopedCompanyId);
            range = withLowerBound(range, "resolvedAt", chunkStart);
            range = withUpperBound(range, "resolvedAt", chunkEnd);
            return range;
          })
          .order("desc")
      : ctx.db
          .query("tickets")
          .withIndex("by_tenant_resolved", (q) => {
            let range = q.eq("tenantId", tenantId);
            range = withLowerBound(range, "resolvedAt", chunkStart);
            range = withUpperBound(range, "resolvedAt", chunkEnd);
            return range;
          })
          .order("desc");

    // Coleta tickets do chunk (o chunk ja e limitado por periodo)
    const snapshot = await query.take(1000);
    // Limita processamento a 1000 tickets por chunk para evitar timeout
    const limitedSnapshot = snapshot;
    for (const ticket of limitedSnapshot) {
      const resolvedAt = typeof ticket.resolvedAt === "number" ? ticket.resolvedAt : null;
      if (resolvedAt === null) continue;
      if (resolvedAt < chunkStart || resolvedAt >= chunkEnd) continue;
      await processor(ticket);
    }
  }
}

export async function fetchOpenScopedTickets(
  ctx: QueryCtx,
  tenantId: string,
  viewer: Awaited<ReturnType<typeof requireStaff>>,
  companyId?: Id<"companies">,
): Promise<Doc<"tickets">[]> {
  const scopedCompanyId = resolveScopedCompanyId(viewer, companyId);
  const statuses: TicketStatusNormalized[] = ["PENDING", "AWAITING_ATTENDANCE", "PAUSED"];
  const results: Doc<"tickets">[] = [];
  const seen = new Set<string>();

  // Limita a 500 tickets por status para evitar OOM
  const MAX_PER_STATUS = 500;
  for (const status of statuses) {
    const snapshot = await ctx.db
      .query("tickets")
      .withIndex("by_tenant_status", (q) => q.eq("tenantId", tenantId).eq("status", status))
      .take(500);
    for (const ticket of snapshot) {
      if (!OPEN_STATUSES.has(normalizeStatus(ticket.status))) continue;
      if (scopedCompanyId && ticket.companyId !== scopedCompanyId) continue;
      const key = String(ticket._id);
      if (seen.has(key)) continue;
      seen.add(key);
      results.push(ticket);
    }
  }

  return results;
}

function percentageChange(current: number, previous: number) {
  if (previous === 0) {
    return current === 0 ? 0 : null;
  }
  return ((current - previous) / previous) * 100;
}

function extractScore(payload: unknown): number | null {
  if (typeof payload === "number") return payload;
  if (payload && typeof payload === "object" && "score" in payload) {
    const value = (payload as { score: unknown }).score;
    if (typeof value === "number") {
      return value;
    }
  }
  return null;
}

function extractMaxScore(payload: unknown): number | null {
  if (payload && typeof payload === "object" && "maxScore" in payload) {
    const value = (payload as { maxScore: unknown }).maxScore;
    if (typeof value === "number" && Number.isFinite(value) && value > 0) {
      return value;
    }
  }
  return null;
}

function extractComment(payload: unknown): string | null {
  if (payload && typeof payload === "object" && "comment" in payload) {
    const value = (payload as { comment: unknown }).comment;
    if (typeof value === "string") {
      const trimmed = value.trim();
      return trimmed.length > 0 ? trimmed : null;
    }
  }
  return null;
}

function extractAssignee(payload: unknown): { id: string | null; name: string | null } {
  if (!payload || typeof payload !== "object") {
    return { id: null, name: null }
  }
  const record = payload as Record<string, unknown>
  const rawId = record["assigneeId"]
  const rawName = record["assigneeName"]
  const id = typeof rawId === "string" && rawId.trim().length > 0 ? rawId.trim() : null
  const name = typeof rawName === "string" && rawName.trim().length > 0 ? rawName.trim() : null
  return { id, name }
}

function isNotNull<T>(value: T | null): value is T {
  return value !== null;
}

export async function fetchTickets(ctx: QueryCtx, tenantId: string) {
  const results: Doc<"tickets">[] = [];
  await forEachTenantTicket(ctx, tenantId, (ticket) => {
    results.push(ticket);
  });
  return results;
}

async function fetchCategoryMap(ctx: QueryCtx, tenantId: string) {
  const categories = await ctx.db
    .query("ticketCategories")
    .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
    .take(500);
  const map = new Map<string, Doc<"ticketCategories">>();
  for (const category of categories) {
    map.set(String(category._id), category);
  }
  return map;
}

export async function fetchScopedTickets(
  ctx: QueryCtx,
  tenantId: string,
  viewer: Awaited<ReturnType<typeof requireStaff>>,
) {
  const scopedCompanyId = resolveScopedCompanyId(viewer);
  const results: Doc<"tickets">[] = [];

  await forEachScopedTicket(ctx, tenantId, viewer, (ticket) => {
    results.push(ticket);
  });

  return results;
}

export async function fetchScopedTicketsByCreatedRange(
  ctx: QueryCtx,
  tenantId: string,
  viewer: Awaited<ReturnType<typeof requireStaff>>,
  startMs: number,
  endMs: number,
  companyId?: Id<"companies">,
) {
  const results: Doc<"tickets">[] = [];
  await forEachScopedTicketByCreatedRange(ctx, tenantId, viewer, startMs, endMs, companyId, (ticket) => {
    results.push(ticket);
  });

  return results;
}

export async function fetchScopedTicketsByResolvedRange(
  ctx: QueryCtx,
  tenantId: string,
  viewer: Awaited<ReturnType<typeof requireStaff>>,
  startMs: number,
  endMs: number,
  companyId?: Id<"companies">,
) {
  const results: Doc<"tickets">[] = [];
  await forEachScopedTicketByResolvedRange(ctx, tenantId, viewer, startMs, endMs, companyId, (ticket) => {
    results.push(ticket);
  });

  return results;
}

export async function fetchScopedTicketsByResolvedRangeSnapshot(
  ctx: QueryCtx,
  tenantId: string,
  viewer: Awaited<ReturnType<typeof requireStaff>>,
  startMs: number,
  endMs: number,
  companyId?: Id<"companies">,
) {
  const results: Doc<"tickets">[] = [];
  await forEachScopedTicketByResolvedRangeChunked(
    ctx,
    tenantId,
    viewer,
    startMs,
    endMs,
    companyId,
    (ticket) => {
      results.push(ticket);
    },
  );
  return results;
}

async function fetchQueues(ctx: QueryCtx, tenantId: string) {
  return ctx.db
    .query("queues")
    .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
    .take(1000);
}

type CompanySummary = {
  name: string;
  isAvulso: boolean;
  contractedHoursPerMonth: number | null;
};

async function getCompanySummary(
  ctx: QueryCtx,
  companyId: Id<"companies">,
  cache: Map<string, CompanySummary>,
): Promise<CompanySummary> {
  const key = String(companyId);
  const cached = cache.get(key);
  if (cached) return cached;
  const company = await ctx.db.get(companyId);
  const summary: CompanySummary = {
    name: company?.name ?? "Sem empresa",
    isAvulso: Boolean(company?.isAvulso ?? false),
    contractedHoursPerMonth: company?.contractedHoursPerMonth ?? null,
  };
  cache.set(key, summary);
  return summary;
}

function deriveMachineStatus(machine: Doc<"machines">, now: number) {
  if (machine.isActive === false) {
    return "deactivated";
  }
  const manualStatus = (machine.status ?? "").toLowerCase();
  if (manualStatus === "maintenance" || manualStatus === "blocked") {
    return manualStatus;
  }
  const offlineMs = getOfflineThresholdMs();
  const staleMs = getStaleThresholdMs(offlineMs);
  if (machine.lastHeartbeatAt) {
    const age = now - machine.lastHeartbeatAt;
    if (age <= offlineMs) return "online";
    if (age <= staleMs) return "offline";
    return "stale";
  }
  return (machine.status ?? "unknown") || "unknown";
}

function formatOsLabel(osName?: string | null, osVersion?: string | null) {
  const name = osName?.trim();
  if (!name) return "Desconhecido";
  const version = osVersion?.trim();
  if (!version) return name;
  const conciseVersion = version.split(" ")[0];
  if (!conciseVersion) return name;
  return `${name} ${conciseVersion}`.trim();
}

type CsatSurvey = {
  ticketId: Id<"tickets">;
  reference: number;
  score: number;
  maxScore: number;
  comment: string | null;
  receivedAt: number;
  assigneeId: string | null;
  assigneeName: string | null;
};

function formatDateKey(timestamp: number) {
  const date = new Date(timestamp);
  const year = date.getUTCFullYear();
  const month = `${date.getUTCMonth() + 1}`.padStart(2, "0");
  const day = `${date.getUTCDate()}`.padStart(2, "0");
  return `${year}-${month}-${day}`;
}

export async function slaOverviewHandler(
  ctx: QueryCtx,
  {
    tenantId,
    viewerId,
    range,
    companyId,
    dateFrom,
    dateTo,
  }: {
    tenantId: string
    viewerId: Id<"users">
    range?: string
    companyId?: Id<"companies">
    dateFrom?: string
    dateTo?: string
  }
) {
  const viewer = await requireStaff(ctx, viewerId, tenantId);
  const { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90);
  const { endMs: limitedEndMs } = clampRangeWindow(startMs, endMs);
  const rangeDays = Math.min(days, deriveRangeDays(startMs, limitedEndMs));
  const queues = await fetchQueues(ctx, tenantId);
  const categoriesMap = await fetchCategoryMap(ctx, tenantId);

  const now = Date.now();
  const totals = { total: 0, open: 0, resolved: 0, overdue: 0 };
  const queueOpenCounts = new Map<string, number>();
  let firstResponseSum = 0;
  let firstResponseCount = 0;
  let resolutionSum = 0;
  let resolutionCount = 0;

  const categoryStats = new Map<
    string,
    {
      categoryId: string | null
      categoryName: string
      priority: string
      total: number
      responseMet: number
      solutionMet: number
    }
  >()

  await forEachScopedTicketByCreatedRange(ctx, tenantId, viewer, startMs, limitedEndMs, companyId, (ticket) => {
    totals.total += 1;
    const status = normalizeStatus(ticket.status);
    if (OPEN_STATUSES.has(status)) {
      totals.open += 1;
      if (ticket.dueAt && ticket.dueAt < now) {
        totals.overdue += 1;
      }
      if (ticket.queueId) {
        const key = String(ticket.queueId);
        queueOpenCounts.set(key, (queueOpenCounts.get(key) ?? 0) + 1);
      }
    }
    if (status === "RESOLVED") {
      totals.resolved += 1;
      if (typeof ticket.resolvedAt === "number") {
        resolutionSum += (ticket.resolvedAt - ticket.createdAt) / 60000;
        resolutionCount += 1;
      }
    }
    if (typeof ticket.firstResponseAt === "number") {
      firstResponseSum += (ticket.firstResponseAt - ticket.createdAt) / 60000;
      firstResponseCount += 1;
    }

    const snapshot = (ticket.slaSnapshot ?? null) as { categoryId?: Id<"ticketCategories">; categoryName?: string; priority?: string } | null;
    const rawCategoryId = ticket.categoryId ? String(ticket.categoryId) : snapshot?.categoryId ? String(snapshot.categoryId) : null;
    const categoryName = resolveCategoryName(rawCategoryId, snapshot, categoriesMap);
    const priority = (snapshot?.priority ?? ticket.priority ?? "MEDIUM").toUpperCase();
    const key = `${rawCategoryId ?? "uncategorized"}::${priority}`;
    let stat = categoryStats.get(key);
    if (!stat) {
      stat = {
        categoryId: rawCategoryId,
        categoryName,
        priority,
        total: 0,
        responseMet: 0,
        solutionMet: 0,
      };
      categoryStats.set(key, stat);
    }
    stat.total += 1;
    if (ticket.slaResponseStatus === "met") {
      stat.responseMet += 1;
    }
    if (ticket.slaSolutionStatus === "met") {
      stat.solutionMet += 1;
    }
  });

  const queueBreakdown = queues.map((queue) => ({
    id: queue._id,
    name: queue.name,
    open: queueOpenCounts.get(String(queue._id)) ?? 0,
  }));

  const categoryBreakdown = Array.from(categoryStats.values())
    .map((entry) => ({
      ...entry,
      responseRate: entry.total > 0 ? entry.responseMet / entry.total : null,
      solutionRate: entry.total > 0 ? entry.solutionMet / entry.total : null,
    }))
    .sort((a, b) => b.total - a.total)

  return {
    totals: {
      total: totals.total,
      open: totals.open,
      resolved: totals.resolved,
      overdue: totals.overdue,
    },
    response: {
      averageFirstResponseMinutes:
        firstResponseCount > 0 ? firstResponseSum / firstResponseCount : null,
      responsesRegistered: firstResponseCount,
    },
    resolution: {
      averageResolutionMinutes: resolutionCount > 0 ? resolutionSum / resolutionCount : null,
      resolvedCount: resolutionCount,
    },
    queueBreakdown,
    categoryBreakdown,
    rangeDays,
  };
}

export const slaOverview = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    range: v.optional(v.string()),
    companyId: v.optional(v.id("companies")),
    dateFrom: v.optional(v.string()),
    dateTo: v.optional(v.string()),
  },
  handler: slaOverviewHandler,
});

export const triggerScheduledExports = action({
  args: {
    tenantId: v.optional(v.string()),
  },
  handler: async (_ctx, args) => {
    const secret = process.env.REPORTS_CRON_SECRET
    const baseUrl =
      process.env.REPORTS_CRON_BASE_URL ??
      process.env.NEXT_PUBLIC_APP_URL ??
      process.env.BETTER_AUTH_URL

    if (!secret || !baseUrl) {
      console.warn("[reports] cron skip: missing REPORTS_CRON_SECRET or base URL")
      return { skipped: true }
    }

    const endpoint = `${baseUrl.replace(/\/$/, "")}/api/reports/schedules/run`
    const response = await fetch(endpoint, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${secret}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ tenantId: args.tenantId }),
    })

    if (!response.ok) {
      const detail = await response.text().catch(() => response.statusText)
      throw new ConvexError(`Falha ao disparar agendamentos: ${response.status} ${detail}`)
    }

    return response.json()
  },
})

export async function csatOverviewHandler(
  ctx: QueryCtx,
  {
    tenantId,
    viewerId,
    range,
    companyId,
    dateFrom,
    dateTo,
  }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies">; dateFrom?: string; dateTo?: string }
) {
  const viewer = await requireStaff(ctx, viewerId, tenantId);
  const { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90);
  const { endMs: limitedEndMs } = clampRangeWindow(startMs, endMs);
  const rangeDays = Math.min(days, deriveRangeDays(startMs, limitedEndMs));
  const surveys: CsatSurvey[] = [];

  await forEachScopedTicketByCreatedRange(ctx, tenantId, viewer, startMs, limitedEndMs, companyId, async (ticket) => {
    if (typeof ticket.csatScore === "number") {
      const snapshot = (ticket.csatAssigneeSnapshot ?? null) as {
        name?: string | null;
        email?: string | null;
      } | null;
      const assigneeId =
        ticket.csatAssigneeId && typeof ticket.csatAssigneeId === "string"
          ? ticket.csatAssigneeId
          : ticket.csatAssigneeId
          ? String(ticket.csatAssigneeId)
          : ticket.assigneeId
          ? String(ticket.assigneeId)
          : null;
      const assigneeName = snapshot?.name?.trim?.() || snapshot?.email?.trim?.() || null;
      const maxScore =
        typeof ticket.csatMaxScore === "number" && Number.isFinite(ticket.csatMaxScore)
          ? (ticket.csatMaxScore as number)
          : 5;
      const receivedAtRaw =
        typeof ticket.csatRatedAt === "number"
          ? ticket.csatRatedAt
          : typeof ticket.resolvedAt === "number"
          ? ticket.resolvedAt
          : ticket.updatedAt ?? ticket.createdAt;
      if (typeof receivedAtRaw !== "number") {
        return;
      }
      if (receivedAtRaw < startMs || receivedAtRaw >= endMs) {
        return;
      }
      surveys.push({
        ticketId: ticket._id,
        reference: ticket.reference,
        score: ticket.csatScore,
        maxScore,
        comment:
          typeof ticket.csatComment === "string" && ticket.csatComment.trim().length > 0
            ? ticket.csatComment.trim()
            : null,
        receivedAt: receivedAtRaw,
        assigneeId,
        assigneeName,
      });
      return;
    }

    const events = await ctx.db
      .query("ticketEvents")
      .withIndex("by_ticket", (q) => q.eq("ticketId", ticket._id))
      .take(1000);

    for (const event of events) {
      if (event.type !== "CSAT_RECEIVED" && event.type !== "CSAT_RATED") continue;
      const score = extractScore(event.payload);
      if (score === null) continue;
      const assignee = extractAssignee(event.payload);
      const receivedAt = event.createdAt;
      if (receivedAt < startMs || receivedAt >= endMs) continue;
      surveys.push({
        ticketId: ticket._id,
        reference: ticket.reference,
        score,
        maxScore: extractMaxScore(event.payload) ?? 5,
        comment: extractComment(event.payload),
        receivedAt,
        assigneeId: assignee.id,
        assigneeName: assignee.name,
      });
    }
  });

  const normalizeToFive = (value: CsatSurvey) => {
    if (!value.maxScore || value.maxScore <= 0) return value.score;
    return Math.min(5, Math.max(1, (value.score / value.maxScore) * 5));
  };

  const averageScore = average(surveys.map((item) => normalizeToFive(item)));
  const distribution = [1, 2, 3, 4, 5].map((score) => ({
    score,
    total: surveys.filter((item) => Math.round(normalizeToFive(item)) === score).length,
  }));

  const positiveThreshold = 4;
  const positiveCount = surveys.filter((item) => normalizeToFive(item) >= positiveThreshold).length;
  const positiveRate = surveys.length > 0 ? positiveCount / surveys.length : null;

  const agentStats = new Map<
    string,
    { id: string; name: string; total: number; sum: number; positive: number }
  >();

  for (const survey of surveys) {
    const normalizedScore = normalizeToFive(survey);
    const key = survey.assigneeId ?? "unassigned";
    const existing = agentStats.get(key) ?? {
      id: key,
      name: survey.assigneeName ?? "Sem responsável",
      total: 0,
      sum: 0,
      positive: 0,
    };
    existing.total += 1;
    existing.sum += normalizedScore;
    if (normalizedScore >= positiveThreshold) {
      existing.positive += 1;
    }
    if (survey.assigneeName && survey.assigneeName.trim().length > 0) {
      existing.name = survey.assigneeName.trim();
    }
    agentStats.set(key, existing);
  }

  const byAgent = Array.from(agentStats.values())
    .map((entry) => ({
      agentId: entry.id === "unassigned" ? null : entry.id,
      agentName: entry.id === "unassigned" ? "Sem responsável" : entry.name,
      totalResponses: entry.total,
      averageScore: entry.total > 0 ? entry.sum / entry.total : null,
      positiveRate: entry.total > 0 ? entry.positive / entry.total : null,
    }))
    .sort((a, b) => {
      const diff = (b.averageScore ?? 0) - (a.averageScore ?? 0);
      if (Math.abs(diff) > 0.0001) return diff;
      return (b.totalResponses ?? 0) - (a.totalResponses ?? 0);
    });

  return {
    totalSurveys: surveys.length,
    averageScore,
    distribution,
    recent: surveys
      .slice()
      .sort((a, b) => b.receivedAt - a.receivedAt)
      .slice(0, 10)
      .map((item) => ({
        ticketId: item.ticketId,
        reference: item.reference,
        score: item.score,
        maxScore: item.maxScore,
        comment: item.comment,
        receivedAt: item.receivedAt,
        assigneeId: item.assigneeId,
        assigneeName: item.assigneeName,
      })),
    rangeDays,
    positiveRate,
    byAgent,
  };
}

export const csatOverview = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    range: v.optional(v.string()),
    companyId: v.optional(v.id("companies")),
    dateFrom: v.optional(v.string()),
    dateTo: v.optional(v.string()),
  },
  handler: csatOverviewHandler,
});

export async function openedResolvedByDayHandler(
  ctx: QueryCtx,
  {
    tenantId,
    viewerId,
    range,
    companyId,
    dateFrom,
    dateTo,
  }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies">; dateFrom?: string; dateTo?: string }
) {
  const viewer = await requireStaff(ctx, viewerId, tenantId);
  const { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90);
  const { endMs: limitedEndMs } = clampRangeWindow(startMs, endMs);
  const rangeDays = Math.min(days, deriveRangeDays(startMs, limitedEndMs));

  const opened: Record<string, number> = {}
  const resolved: Record<string, number> = {}

  for (let i = rangeDays - 1; i >= 0; i--) {
    const d = new Date(limitedEndMs - (i + 1) * ONE_DAY_MS)
    const key = formatDateKey(d.getTime())
    opened[key] = 0
    resolved[key] = 0
  }

  await forEachScopedTicketByCreatedRange(ctx, tenantId, viewer, startMs, limitedEndMs, companyId, (ticket) => {
    const createdAt = typeof ticket.createdAt === "number" ? ticket.createdAt : null
    if (createdAt === null || createdAt < startMs || createdAt >= endMs) {
      return
    }
    const key = formatDateKey(createdAt)
    opened[key] = (opened[key] ?? 0) + 1
  })

  await forEachScopedTicketByResolvedRangeChunked(ctx, tenantId, viewer, startMs, limitedEndMs, companyId, (ticket) => {
    const resolvedAt = typeof ticket.resolvedAt === "number" ? ticket.resolvedAt : null
    if (resolvedAt === null || resolvedAt < startMs || resolvedAt >= endMs) {
      return
    }
    const key = formatDateKey(resolvedAt)
    resolved[key] = (resolved[key] ?? 0) + 1
  })

  const series: Array<{ date: string; opened: number; resolved: number }> = []
  for (let i = rangeDays - 1; i >= 0; i--) {
    const d = new Date(limitedEndMs - (i + 1) * ONE_DAY_MS)
    const key = formatDateKey(d.getTime())
    series.push({ date: key, opened: opened[key] ?? 0, resolved: resolved[key] ?? 0 })
  }

  return { rangeDays, series }
}

export const openedResolvedByDay = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    range: v.optional(v.string()),
    companyId: v.optional(v.id("companies")),
    dateFrom: v.optional(v.string()),
    dateTo: v.optional(v.string()),
  },
  handler: openedResolvedByDayHandler,
})

export async function backlogOverviewHandler(
  ctx: QueryCtx,
  {
    tenantId,
    viewerId,
    range,
    companyId,
    dateFrom,
    dateTo,
  }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies">; dateFrom?: string; dateTo?: string }
) {
  const viewer = await requireStaff(ctx, viewerId, tenantId);
  const { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90);
  const { endMs: limitedEndMs } = clampRangeWindow(startMs, endMs);
  const rangeDays = Math.min(days, deriveRangeDays(startMs, limitedEndMs));
  const statusCounts = {} as Record<TicketStatusNormalized, number>;
  const priorityCounts: Record<string, number> = {};
  const queueMap = new Map<string, { name: string; count: number }>();
  let totalOpen = 0;

  await forEachScopedTicketByCreatedRange(ctx, tenantId, viewer, startMs, limitedEndMs, companyId, (ticket) => {
    const status = normalizeStatus(ticket.status);
    statusCounts[status] = (statusCounts[status] ?? 0) + 1;
    priorityCounts[ticket.priority] = (priorityCounts[ticket.priority] ?? 0) + 1;

    if (!OPEN_STATUSES.has(status)) {
      return;
    }

    totalOpen += 1;
    const queueKey = ticket.queueId ? String(ticket.queueId) : "sem-fila";
    const current = queueMap.get(queueKey) ?? { name: queueKey === "sem-fila" ? "Sem fila" : "", count: 0 };
    current.count += 1;
    queueMap.set(queueKey, current);
  });

  const queues = await fetchQueues(ctx, tenantId);

  for (const queue of queues) {
    const entry = queueMap.get(queue._id) ?? { name: queue.name, count: 0 };
    entry.name = queue.name;
    queueMap.set(queue._id, entry);
  }

  return {
    rangeDays,
    statusCounts,
    priorityCounts,
    queueCounts: Array.from(queueMap.entries()).map(([id, data]) => ({
      id,
      name: data.name,
      total: data.count,
    })),
    totalOpen,
  };
}

export const backlogOverview = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    range: v.optional(v.string()),
    companyId: v.optional(v.id("companies")),
    dateFrom: v.optional(v.string()),
    dateTo: v.optional(v.string()),
  },
  handler: backlogOverviewHandler,
});

type QueueTrendPoint = { date: string; opened: number; resolved: number }
type QueueTrendEntry = {
  id: string
  name: string
  openedTotal: number
  resolvedTotal: number
  series: Map<string, QueueTrendPoint>
}

export async function queueLoadTrendHandler(
  ctx: QueryCtx,
  {
    tenantId,
    viewerId,
    range,
    limit,
  }: { tenantId: string; viewerId: Id<"users">; range?: string; limit?: number }
) {
  const viewer = await requireStaff(ctx, viewerId, tenantId)
  const { startMs, endMs, days } = resolveRangeWindow(range, undefined, undefined, 90)
  const { endMs: limitedEndMs } = clampRangeWindow(startMs, endMs)
  const rangeDays = Math.min(days, deriveRangeDays(startMs, limitedEndMs))
  const queues = await fetchQueues(ctx, tenantId)

  const queueNames = new Map<string, string>()
  queues.forEach((queue) => queueNames.set(String(queue._id), queue.name))
  queueNames.set("unassigned", "Sem fila")

  const dayKeys: string[] = []
  for (let i = rangeDays - 1; i >= 0; i--) {
    const key = formatDateKey(limitedEndMs - (i + 1) * ONE_DAY_MS)
    dayKeys.push(key)
  }

  const stats = new Map<string, QueueTrendEntry>()
  const ensureEntry = (queueId: string) => {
    if (!stats.has(queueId)) {
      const series = new Map<string, QueueTrendPoint>()
      dayKeys.forEach((key) => {
        series.set(key, { date: key, opened: 0, resolved: 0 })
      })
      stats.set(queueId, {
        id: queueId,
        name: queueNames.get(queueId) ?? "Sem fila",
        openedTotal: 0,
        resolvedTotal: 0,
        series,
      })
    }
    return stats.get(queueId)!
  }

  await forEachScopedTicketByCreatedRange(ctx, tenantId, viewer, startMs, limitedEndMs, undefined, (ticket) => {
    const createdAt = typeof ticket.createdAt === "number" ? ticket.createdAt : null
    if (createdAt === null) return
    const queueId = ticket.queueId ? String(ticket.queueId) : "unassigned"
    const entry = ensureEntry(queueId)
    const bucket = entry.series.get(formatDateKey(createdAt))
    if (bucket) {
      bucket.opened += 1
    }
    entry.openedTotal += 1
  })

  await forEachScopedTicketByResolvedRangeChunked(ctx, tenantId, viewer, startMs, limitedEndMs, undefined, (ticket) => {
    const resolvedAt = typeof ticket.resolvedAt === "number" ? ticket.resolvedAt : null
    if (resolvedAt === null) return
    const queueId = ticket.queueId ? String(ticket.queueId) : "unassigned"
    const entry = ensureEntry(queueId)
    const bucket = entry.series.get(formatDateKey(resolvedAt))
    if (bucket) {
      bucket.resolved += 1
    }
    entry.resolvedTotal += 1
  })

  const maxEntries = Math.max(1, Math.min(limit ?? 3, 6))
  const queuesTrend = Array.from(stats.values())
    .sort((a, b) => b.openedTotal - a.openedTotal)
    .slice(0, maxEntries)
    .map((entry) => ({
      id: entry.id,
      name: entry.name,
      openedTotal: entry.openedTotal,
      resolvedTotal: entry.resolvedTotal,
      series: dayKeys.map((key) => entry.series.get(key)!),
    }))

  return { rangeDays, queues: queuesTrend }
}

export const queueLoadTrend = query({
  args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), limit: v.optional(v.number()) },
  handler: queueLoadTrendHandler,
})

// Touch to ensure CI convex_deploy runs and that agentProductivity is deployed
export async function agentProductivityHandler(
  ctx: QueryCtx,
  { tenantId, viewerId, range, companyId }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> }
) {
  const viewer = await requireStaff(ctx, viewerId, tenantId)

  const windowDays = range === "7d" ? 7 : range === "30d" ? 30 : 90
  const end = new Date()
  end.setUTCHours(0, 0, 0, 0)
  const endMs = end.getTime() + ONE_DAY_MS
  const startMs = endMs - windowDays * ONE_DAY_MS

  const inRange = await fetchScopedTicketsByCreatedRange(ctx, tenantId, viewer, startMs, endMs, companyId)
  type Acc = {
    agentId: Id<"users">
    name: string | null
    email: string | null
    open: number
    resolved: number
    avgFirstResponseMinValues: number[]
    avgResolutionMinValues: number[]
    workedMs: number
  }
  const map = new Map<string, Acc>()

  for (const t of inRange) {
    const assigneeId = t.assigneeId ?? null
    if (!assigneeId) continue
    let acc = map.get(assigneeId)
    if (!acc) {
      const user = await ctx.db.get(assigneeId)
      acc = {
        agentId: assigneeId,
        name: user?.name ?? null,
        email: user?.email ?? null,
        open: 0,
        resolved: 0,
        avgFirstResponseMinValues: [],
        avgResolutionMinValues: [],
        workedMs: 0,
      }
      map.set(assigneeId, acc)
    }
    const status = normalizeStatus(t.status)
    if (OPEN_STATUSES.has(status)) acc.open += 1
    if (status === "RESOLVED") acc.resolved += 1
    if (t.firstResponseAt) acc.avgFirstResponseMinValues.push((t.firstResponseAt - t.createdAt) / 60000)
    if (t.resolvedAt) acc.avgResolutionMinValues.push((t.resolvedAt - t.createdAt) / 60000)
  }

  for (const [agentId, acc] of map) {
    // Limita a 1000 sessoes por agente para evitar OOM
    const sessions = await ctx.db
      .query("ticketWorkSessions")
      .withIndex("by_agent", (q) => q.eq("agentId", agentId as Id<"users">))
      .take(1000)
    let total = 0
    for (const s of sessions) {
      const started = s.startedAt
      const ended = s.stoppedAt ?? s.startedAt
      if (ended < startMs || started >= endMs) continue
      total += s.durationMs ?? Math.max(0, (s.stoppedAt ?? Date.now()) - s.startedAt)
    }
    acc.workedMs = total
  }

  const items = Array.from(map.values()).map((acc) => ({
    agentId: acc.agentId,
    name: acc.name,
    email: acc.email,
    open: acc.open,
    resolved: acc.resolved,
    avgFirstResponseMinutes: average(acc.avgFirstResponseMinValues),
    avgResolutionMinutes: average(acc.avgResolutionMinValues),
    workedHours: Math.round((acc.workedMs / 3600000) * 100) / 100,
  }))
  items.sort((a, b) => b.resolved - a.resolved)
  return { rangeDays: windowDays, items }
}

export const agentProductivity = query({
  args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) },
  handler: agentProductivityHandler,
})

type CategoryAgentAccumulator = {
  id: Id<"ticketCategories"> | null
  name: string
  total: number
  resolved: number
  agents: Map<string, { agentId: Id<"users"> | null; name: string | null; total: number }>
}

export async function ticketCategoryInsightsHandler(
  ctx: QueryCtx,
  {
    tenantId,
    viewerId,
    range,
    companyId,
    dateFrom,
    dateTo,
  }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies">; dateFrom?: string; dateTo?: string }
) {
  const viewer = await requireStaff(ctx, viewerId, tenantId)
  const { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90)
  const { endMs: limitedEndMs } = clampRangeWindow(startMs, endMs)
  const rangeDays = Math.min(days, deriveRangeDays(startMs, limitedEndMs))

  const categories = await ctx.db
    .query("ticketCategories")
    .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
    .take(500)

  const categoriesById = new Map<Id<"ticketCategories">, Doc<"ticketCategories">>()
  for (const category of categories) {
    categoriesById.set(category._id, category)
  }

  const stats = new Map<string, CategoryAgentAccumulator>()
  let totalTickets = 0

  await forEachScopedTicketByCreatedRange(ctx, tenantId, viewer, startMs, limitedEndMs, companyId, (ticket) => {
    const categoryKey = ticket.categoryId ? String(ticket.categoryId) : "uncategorized"
    let stat = stats.get(categoryKey)
    if (!stat) {
      const categoryDoc = ticket.categoryId ? categoriesById.get(ticket.categoryId) : null
      stat = {
        id: ticket.categoryId ?? null,
        name: categoryDoc?.name ?? (ticket.categoryId ? "Categoria removida" : "Sem categoria"),
        total: 0,
        resolved: 0,
        agents: new Map(),
      }
      stats.set(categoryKey, stat)
    }
    stat.total += 1
    if (typeof ticket.resolvedAt === "number" && ticket.resolvedAt >= startMs && ticket.resolvedAt < limitedEndMs) {
      stat.resolved += 1
    }

    const agentKey = ticket.assigneeId ? String(ticket.assigneeId) : "unassigned"
    let agent = stat.agents.get(agentKey)
    if (!agent) {
      const snapshotName = ticket.assigneeSnapshot?.name ?? null
      const fallbackName = ticket.assigneeId ? null : "Sem responsável"
      agent = {
        agentId: ticket.assigneeId ?? null,
        name: snapshotName ?? fallbackName ?? "Agente",
        total: 0,
      }
      stat.agents.set(agentKey, agent)
    }
    agent.total += 1
    totalTickets += 1
  })

  const categoriesData = Array.from(stats.values())
    .map((stat) => {
      const agents = Array.from(stat.agents.values()).sort((a, b) => b.total - a.total)
      const topAgent = agents[0] ?? null
      return {
        id: stat.id ? String(stat.id) : null,
        name: stat.name,
        total: stat.total,
        resolved: stat.resolved,
        topAgent: topAgent
          ? {
              id: topAgent.agentId ? String(topAgent.agentId) : null,
              name: topAgent.name,
              total: topAgent.total,
            }
          : null,
        agents: agents.slice(0, 5).map((agent) => ({
          id: agent.agentId ? String(agent.agentId) : null,
          name: agent.name,
          total: agent.total,
        })),
      }
    })
    .sort((a, b) => b.total - a.total)

  const spotlight = categoriesData.reduce<
    | null
    | {
        categoryId: string | null
        categoryName: string
        agentId: string | null
        agentName: string | null
        tickets: number
      }
  >((best, current) => {
    if (!current.topAgent) return best
    if (!best || current.topAgent.total > best.tickets) {
      return {
        categoryId: current.id,
        categoryName: current.name,
        agentId: current.topAgent.id,
        agentName: current.topAgent.name,
        tickets: current.topAgent.total,
      }
    }
    return best
  }, null)

  return {
    rangeDays,
    totalTickets,
    categories: categoriesData,
    spotlight,
  }
}

export const categoryInsights = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    range: v.optional(v.string()),
    companyId: v.optional(v.id("companies")),
    dateFrom: v.optional(v.string()),
    dateTo: v.optional(v.string()),
  },
  handler: ticketCategoryInsightsHandler,
})

export async function dashboardOverviewHandler(
  ctx: QueryCtx,
  { tenantId, viewerId }: { tenantId: string; viewerId: Id<"users"> }
) {
  const viewer = await requireStaff(ctx, viewerId, tenantId);
  const now = Date.now();

  const lastDayStart = now - ONE_DAY_MS;
  const previousDayStart = now - 2 * ONE_DAY_MS;
  const lastWindowStart = now - 7 * ONE_DAY_MS;
  const previousWindowStart = now - 14 * ONE_DAY_MS;

  let totalRecentCreated = 0;
  let newTicketsCount = 0;
  let previousTicketsCount = 0;
  let firstResponseWindowSum = 0;
  let firstResponseWindowCount = 0;
  let firstResponsePreviousSum = 0;
  let firstResponsePreviousCount = 0;

  await forEachScopedTicketByCreatedRange(
    ctx,
    tenantId,
    viewer,
    previousWindowStart,
    now,
    undefined,
    (ticket) => {
      const createdAt = typeof ticket.createdAt === "number" ? ticket.createdAt : null;
      if (createdAt === null) return;
      totalRecentCreated += 1;
      if (createdAt >= lastDayStart) {
        newTicketsCount += 1;
      } else if (createdAt >= previousDayStart) {
        previousTicketsCount += 1;
      }

      const firstResponseAt = typeof ticket.firstResponseAt === "number" ? ticket.firstResponseAt : null;
      if (firstResponseAt === null) return;
      const durationMinutes = (firstResponseAt - createdAt) / 60000;
      if (createdAt >= lastWindowStart && createdAt < now) {
        firstResponseWindowSum += durationMinutes;
        firstResponseWindowCount += 1;
      } else if (createdAt >= previousWindowStart && createdAt < lastWindowStart) {
        firstResponsePreviousSum += durationMinutes;
        firstResponsePreviousCount += 1;
      }
    },
  );

  const openTickets = await fetchOpenScopedTickets(ctx, tenantId, viewer);
  const inProgressCurrent = openTickets.filter((ticket) => typeof ticket.firstResponseAt === "number");
  const inProgressPrevious = openTickets.filter(
    (ticket) => typeof ticket.firstResponseAt === "number" && ticket.firstResponseAt < lastDayStart,
  );
  const inProgressTrend = percentageChange(inProgressCurrent.length, inProgressPrevious.length);

  const awaitingTickets = openTickets;
  const atRiskTickets = awaitingTickets.filter((ticket) => ticket.dueAt && ticket.dueAt < now);

  let resolvedLastWindow = 0;
  let resolvedPreviousWindow = 0;
  await forEachScopedTicketByResolvedRangeChunked(
    ctx,
    tenantId,
    viewer,
    previousWindowStart,
    now,
    undefined,
    (ticket) => {
      const resolvedAt = typeof ticket.resolvedAt === "number" ? ticket.resolvedAt : null;
      if (resolvedAt === null) return;
      if (resolvedAt >= lastWindowStart && resolvedAt < now) {
        resolvedLastWindow += 1;
      } else if (resolvedAt >= previousWindowStart && resolvedAt < lastWindowStart) {
        resolvedPreviousWindow += 1;
      }
    },
  );

  const averageWindow =
    firstResponseWindowCount > 0 ? firstResponseWindowSum / firstResponseWindowCount : null;
  const averagePrevious =
    firstResponsePreviousCount > 0 ? firstResponsePreviousSum / firstResponsePreviousCount : null;
  const deltaMinutes =
    averageWindow !== null && averagePrevious !== null ? averageWindow - averagePrevious : null;

  const trend = percentageChange(newTicketsCount, previousTicketsCount);
  const resolutionBase = Math.max(totalRecentCreated, awaitingTickets.length);
  const resolutionRate = resolutionBase > 0 ? (resolvedLastWindow / resolutionBase) * 100 : null;
  const resolutionDelta = percentageChange(resolvedLastWindow, resolvedPreviousWindow);

  return {
    newTickets: {
      last24h: newTicketsCount,
      previous24h: previousTicketsCount,
      trendPercentage: trend,
    },
    inProgress: {
      current: inProgressCurrent.length,
      previousSnapshot: inProgressPrevious.length,
      trendPercentage: inProgressTrend,
    },
    firstResponse: {
      averageMinutes: averageWindow,
      previousAverageMinutes: averagePrevious,
      deltaMinutes,
      responsesCount: firstResponseWindowCount,
    },
    awaitingAction: {
      total: awaitingTickets.length,
      atRisk: atRiskTickets.length,
    },
    resolution: {
      resolvedLast7d: resolvedLastWindow,
      previousResolved: resolvedPreviousWindow,
      rate: resolutionRate,
      deltaPercentage: resolutionDelta,
    },
  };
}

export const dashboardOverview = query({
  args: { tenantId: v.string(), viewerId: v.id("users") },
  handler: dashboardOverviewHandler,
});

type DashboardAggregateResult = {
  range: {
    startMs: number
    endMs: number
    requestedEndMs: number
    days: number
    clamped: boolean
  }
  openedResolvedByDay: {
    opened: Array<{ key: string; count: number }>
    resolved: Array<{ key: string; count: number }>
  }
  categories: Array<{ key: string; count: number }>
  machineCategory: Array<{ machineId: string; categoryId: string; count: number }>
  channels: Array<{ key: string; count: number }>
  backlog: { open: number }
  csat: { average: number | null; count: number }
  processed: number
};

export const dashboardOverviewAggregate = mutation({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    range: v.optional(v.string()),
    companyId: v.optional(v.id("companies")),
    dateFrom: v.optional(v.string()),
    dateTo: v.optional(v.string()),
    allowWide: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    const viewer = await requireStaff(ctx, args.viewerId, args.tenantId);
    const { startMs, endMs, days } = resolveRangeWindow(
      args.range,
      args.dateFrom,
      args.dateTo,
      90,
    );

    let limitedEndMs = endMs;
    let wasClamped = false;
    if (args.allowWide !== true) {
      const clamped = clampRangeWindow(startMs, endMs);
      limitedEndMs = clamped.endMs;
      wasClamped = clamped.wasClamped;
    }
    const rangeDays =
      args.allowWide === true ? days : Math.min(days, deriveRangeDays(startMs, limitedEndMs));

    const cacheKey = buildDashboardCacheKey(args);
    const cached = await readDashboardCache(ctx, args.tenantId, cacheKey);
    if (cached) {
      return {
        ...cached.payload,
        cache: { hit: true, expiresAt: cached.expiresAt },
      };
    }

    const lockId = await acquireDashboardLock(ctx, args.tenantId, cacheKey);
    if (!lockId) {
      return { status: "locked" as const };
    }

    try {
      const payload = await aggregateDashboardMetrics(ctx, viewer, {
        tenantId: args.tenantId,
        companyId: args.companyId,
        startMs,
        endMs: limitedEndMs,
        requestedEndMs: endMs,
        days: rangeDays,
        clamped: args.allowWide === true ? false : wasClamped,
      });

      await writeDashboardCache(ctx, args.tenantId, cacheKey, payload);
      return {
        ...payload,
        cache: { hit: false, expiresAt: Date.now() + CACHE_TTL_MS },
      };
    } finally {
      await releaseDashboardLock(ctx, lockId);
    }
  },
});

async function aggregateDashboardMetrics(
  ctx: MutationCtx,
  viewer: Awaited<ReturnType<typeof requireStaff>>,
  {
    tenantId,
    companyId,
    startMs,
    endMs,
    requestedEndMs,
    days,
    clamped,
  }: {
    tenantId: string
    companyId?: Id<"companies">
    startMs: number
    endMs: number
    requestedEndMs: number
    days: number
    clamped: boolean
  },
): Promise<DashboardAggregateResult> {
  const openedByDay = new Map<string, number>();
  const resolvedByDay = new Map<string, number>();
  const categories = new Map<string, number>();
  const channels = new Map<string, number>();
  const machineCategory = new Map<string, Map<string, number>>();
  let backlogOpen = 0;
  let csatSum = 0;
  let csatCount = 0;
  let processed = 0;

  await forEachScopedTicketByCreatedRange(
    ctx as QueryCtx,
    tenantId,
    viewer,
    startMs,
    endMs,
    companyId,
    (ticket) => {
      processed += 1;
      if (processed % DASHBOARD_LOG_INTERVAL === 0) {
        logDashboardProgress(processed, tenantId);
      }

      const createdKey =
        typeof ticket.createdAt === "number" ? formatDateKey(ticket.createdAt) : null;
      if (createdKey) {
        openedByDay.set(createdKey, (openedByDay.get(createdKey) ?? 0) + 1);
      }

      const resolvedKey =
        typeof ticket.resolvedAt === "number" && ticket.resolvedAt < endMs
          ? formatDateKey(ticket.resolvedAt)
          : null;
      if (resolvedKey) {
        resolvedByDay.set(resolvedKey, (resolvedByDay.get(resolvedKey) ?? 0) + 1);
      }

      const categoryKey = ticket.categoryId ? String(ticket.categoryId) : "uncategorized";
      categories.set(categoryKey, (categories.get(categoryKey) ?? 0) + 1);

      const channelKey = ticket.channel ?? "unknown";
      channels.set(channelKey, (channels.get(channelKey) ?? 0) + 1);

      const machineKey = ticket.machineId ? String(ticket.machineId) : "no-machine";
      const machineRow = machineCategory.get(machineKey) ?? new Map<string, number>();
      machineRow.set(categoryKey, (machineRow.get(categoryKey) ?? 0) + 1);
      machineCategory.set(machineKey, machineRow);

      if (!ticket.resolvedAt || ticket.resolvedAt > endMs) {
        backlogOpen += 1;
      }

      if (typeof ticket.csatScore === "number") {
        csatSum += ticket.csatScore;
        csatCount += 1;
      }
    },
  );

  return {
    range: {
      startMs,
      endMs,
      requestedEndMs,
      days,
      clamped,
    },
    openedResolvedByDay: {
      opened: mapToChronologicalSeries(openedByDay),
      resolved: mapToChronologicalSeries(resolvedByDay),
    },
    categories: mapToDescendingSeries(categories),
    machineCategory: flattenMachineCategoryMap(machineCategory),
    channels: mapToDescendingSeries(channels),
    backlog: { open: backlogOpen },
    csat: { average: csatCount > 0 ? csatSum / csatCount : null, count: csatCount },
    processed,
  };
}

export async function ticketsByChannelHandler(
  ctx: QueryCtx,
  { tenantId, viewerId, range, companyId }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> }
) {
  const viewer = await requireStaff(ctx, viewerId, tenantId);
  const { startMs, endMs, days } = resolveRangeWindow(range, undefined, undefined, 90);
  const { endMs: limitedEndMs } = clampRangeWindow(startMs, endMs);
  const rangeDays = Math.min(days, deriveRangeDays(startMs, limitedEndMs));

  const timeline = new Map<string, Map<string, number>>();
  for (let ts = startMs; ts < limitedEndMs; ts += ONE_DAY_MS) {
    timeline.set(formatDateKey(ts), new Map());
  }

  const channels = new Set<string>();

  await forEachScopedTicketByCreatedRange(ctx, tenantId, viewer, startMs, limitedEndMs, companyId, (ticket) => {
    const dateKey = formatDateKey(ticket.createdAt);
    const channelKey = ticket.channel ?? "OUTRO";
    channels.add(channelKey);
    const dayMap = timeline.get(dateKey) ?? new Map<string, number>();
    dayMap.set(channelKey, (dayMap.get(channelKey) ?? 0) + 1);
    timeline.set(dateKey, dayMap);
  });

  const sortedChannels = Array.from(channels).sort();

  const points = Array.from(timeline.entries())
    .sort((a, b) => a[0].localeCompare(b[0]))
    .map(([date, map]) => {
      const values: Record<string, number> = {};
      for (const channel of sortedChannels) {
        values[channel] = map.get(channel) ?? 0;
      }
      return { date, values };
    });

  return {
    rangeDays,
    channels: sortedChannels,
    points,
  };
}

export const ticketsByChannel = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    range: v.optional(v.string()),
    companyId: v.optional(v.id("companies")),
  },
  handler: ticketsByChannelHandler,
});

type MachineCategoryDailyEntry = {
  date: string
  machineId: Id<"machines"> | null
  machineHostname: string | null
  companyId: Id<"companies"> | null
  companyName: string | null
  categoryId: Id<"ticketCategories"> | null
  categoryName: string
  total: number
}

export async function ticketsByMachineAndCategoryHandler(
  ctx: QueryCtx,
  {
    tenantId,
    viewerId,
    range,
    companyId,
    machineId,
    userId,
    dateFrom,
    dateTo,
  }: {
    tenantId: string
    viewerId: Id<"users">
    range?: string
    companyId?: Id<"companies">
    machineId?: Id<"machines">
    userId?: Id<"users">
    dateFrom?: string
    dateTo?: string
  }
) {
  const viewer = await requireStaff(ctx, viewerId, tenantId)
  const { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90)
  const { endMs: limitedEndMs } = clampRangeWindow(startMs, endMs)
  const rangeDays = Math.min(days, deriveRangeDays(startMs, limitedEndMs))
  const categoriesMap = await fetchCategoryMap(ctx, tenantId)

  const companiesById = new Map<string, Doc<"companies"> | null>()

  const aggregated = new Map<string, MachineCategoryDailyEntry>()

  const resolveCompany = async (id: Id<"companies"> | null) => {
    if (!id) return null
    const key = String(id)
    if (!companiesById.has(key)) {
      const doc = await ctx.db.get(id)
      companiesById.set(key, doc ?? null)
    }
    return companiesById.get(key) ?? null
  }

  await forEachScopedTicketByCreatedRange(ctx, tenantId, viewer, startMs, limitedEndMs, companyId, async (ticket) => {
    const createdAt = typeof ticket.createdAt === "number" ? ticket.createdAt : null
    if (createdAt === null || createdAt < startMs || createdAt >= limitedEndMs) return

    const hasMachine = Boolean(ticket.machineId) || Boolean(ticket.machineSnapshot)
    if (!hasMachine) return

    if (machineId && ticket.machineId !== machineId) return
    if (userId && ticket.requesterId !== userId) return

    const date = formatDateKey(createdAt)
    const machineIdValue = (ticket.machineId ?? null) as Id<"machines"> | null
    const machineSnapshot = (ticket.machineSnapshot ?? null) as
      | {
          hostname?: string | null
        }
      | null
    const rawHostname =
      typeof machineSnapshot?.hostname === "string" && machineSnapshot.hostname.trim().length > 0
        ? machineSnapshot.hostname.trim()
        : null
    const machineHostname = rawHostname ?? null

    const snapshot = (ticket.slaSnapshot ?? null) as { categoryId?: Id<"ticketCategories">; categoryName?: string } | null
    const rawCategoryId =
      ticket.categoryId && typeof ticket.categoryId === "string"
        ? String(ticket.categoryId)
        : snapshot?.categoryId
        ? String(snapshot.categoryId)
        : null
    const categoryName = resolveCategoryName(rawCategoryId, snapshot, categoriesMap)

    const companyIdValue = (ticket.companyId ?? null) as Id<"companies"> | null
    let companyName: string | null = null
    if (companyIdValue) {
      const company = await resolveCompany(companyIdValue)
      if (company?.name && company.name.trim().length > 0) {
        companyName = company.name.trim()
      }
    }
    if (!companyName) {
      const companySnapshot = (ticket.companySnapshot ?? null) as { name?: string | null } | null
      if (companySnapshot?.name && companySnapshot.name.trim().length > 0) {
        companyName = companySnapshot.name.trim()
      }
    }
    if (!companyName) {
      companyName = "Sem empresa"
    }

    const key = [
      date,
      machineIdValue ? String(machineIdValue) : "null",
      machineHostname ?? "",
      rawCategoryId ?? "uncategorized",
      companyIdValue ? String(companyIdValue) : "null",
    ].join("|")

      const existing = aggregated.get(key)
      if (existing) {
        existing.total += 1
      } else {
        aggregated.set(key, {
          date,
          machineId: machineIdValue,
        machineHostname,
        companyId: companyIdValue,
        companyName,
        categoryId: (rawCategoryId as Id<"ticketCategories"> | null) ?? null,
        categoryName,
        total: 1,
      })
    }
  })

  const items = Array.from(aggregated.values()).sort((a, b) => {
    if (a.date !== b.date) return a.date.localeCompare(b.date)
    const machineA = (a.machineHostname ?? "").toLowerCase()
    const machineB = (b.machineHostname ?? "").toLowerCase()
    if (machineA !== machineB) return machineA.localeCompare(machineB)
    return a.categoryName.localeCompare(b.categoryName, "pt-BR")
  })

  return {
    rangeDays,
    items,
  }
}

export const ticketsByMachineAndCategory = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    range: v.optional(v.string()),
    companyId: v.optional(v.id("companies")),
    machineId: v.optional(v.id("machines")),
    userId: v.optional(v.id("users")),
    dateFrom: v.optional(v.string()),
    dateTo: v.optional(v.string()),
  },
  handler: ticketsByMachineAndCategoryHandler,
})

type MachineHoursEntry = {
  machineId: Id<"machines">
  machineHostname: string | null
  companyId: Id<"companies"> | null
  companyName: string | null
  internalMs: number
  externalMs: number
  totalMs: number
}

export async function hoursByMachineHandler(
  ctx: QueryCtx,
  {
    tenantId,
    viewerId,
    range,
    companyId,
    machineId,
    userId,
    dateFrom,
    dateTo,
  }: {
    tenantId: string
    viewerId: Id<"users">
    range?: string
    companyId?: Id<"companies">
    machineId?: Id<"machines">
    userId?: Id<"users">
    dateFrom?: string
    dateTo?: string
  }
) {
  const viewer = await requireStaff(ctx, viewerId, tenantId)
  const { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90)
  const { endMs: limitedEndMs } = clampRangeWindow(startMs, endMs)
  const rangeDays = Math.min(days, deriveRangeDays(startMs, limitedEndMs))

  const machinesById = new Map<string, Doc<"machines"> | null>()
  const companiesById = new Map<string, Doc<"companies"> | null>()

  const map = new Map<string, MachineHoursEntry>()

  await forEachScopedTicket(ctx, tenantId, viewer, async (t) => {
    const updatedAt =
      typeof t.updatedAt === "number"
        ? t.updatedAt
        : typeof t.resolvedAt === "number"
        ? t.resolvedAt
        : typeof t.createdAt === "number"
        ? t.createdAt
        : null
    if (updatedAt === null || updatedAt < startMs || updatedAt >= limitedEndMs) return
    if (companyId && t.companyId && t.companyId !== companyId) return
    if (machineId && t.machineId !== machineId) return
    if (userId && t.requesterId !== userId) return

    const machineIdValue = (t.machineId ?? null) as Id<"machines"> | null
    if (!machineIdValue) return

    const key = String(machineIdValue)

    let acc = map.get(key)
    if (!acc) {
      let machineDoc = machinesById.get(key)
      if (machineDoc === undefined) {
        machineDoc = (await ctx.db.get(machineIdValue)) as Doc<"machines"> | null
        machinesById.set(key, machineDoc ?? null)
      }

      const snapshot = (t.machineSnapshot ?? null) as { hostname?: string | null } | null
      const machineHostname =
        typeof machineDoc?.hostname === "string" && machineDoc.hostname.trim().length > 0
          ? machineDoc.hostname.trim()
          : snapshot?.hostname && snapshot.hostname.trim().length > 0
          ? snapshot.hostname.trim()
          : null

      const companyIdValue = (t.companyId ??
        (machineDoc?.companyId as Id<"companies"> | undefined) ??
        null) as Id<"companies"> | null

      let companyName: string | null = null
      if (companyIdValue) {
        const companyKey = String(companyIdValue)
        let companyDoc = companiesById.get(companyKey)
        if (companyDoc === undefined) {
          companyDoc = (await ctx.db.get(companyIdValue)) as Doc<"companies"> | null
          companiesById.set(companyKey, companyDoc ?? null)
        }
        if (companyDoc?.name && companyDoc.name.trim().length > 0) {
          companyName = companyDoc.name.trim()
        }
      }

      acc = {
        machineId: machineIdValue,
        machineHostname,
        companyId: companyIdValue,
        companyName,
        internalMs: 0,
        externalMs: 0,
        totalMs: 0,
      }
      map.set(key, acc)
    }

    const internal = (t.internalWorkedMs ?? 0) as number
    const external = (t.externalWorkedMs ?? 0) as number
    acc.internalMs += internal
    acc.externalMs += external
    acc.totalMs += internal + external
  })

  const items = Array.from(map.values()).sort((a, b) => {
    if (b.totalMs !== a.totalMs) return b.totalMs - a.totalMs
    const hostA = (a.machineHostname ?? "").toLowerCase()
    const hostB = (b.machineHostname ?? "").toLowerCase()
    return hostA.localeCompare(hostB)
  })

  return {
    rangeDays,
    items,
  }
}

export const hoursByMachine = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    range: v.optional(v.string()),
    companyId: v.optional(v.id("companies")),
    machineId: v.optional(v.id("machines")),
    userId: v.optional(v.id("users")),
    dateFrom: v.optional(v.string()),
    dateTo: v.optional(v.string()),
  },
  handler: hoursByMachineHandler,
})

export async function hoursByClientHandler(
  ctx: QueryCtx,
  { tenantId, viewerId, range, dateFrom, dateTo }: { tenantId: string; viewerId: Id<"users">; range?: string; dateFrom?: string; dateTo?: string }
) {
  const viewer = await requireStaff(ctx, viewerId, tenantId)
  const { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90)
  const { endMs: limitedEndMs } = clampRangeWindow(startMs, endMs)
  const rangeDays = Math.min(days, deriveRangeDays(startMs, limitedEndMs))
  const companyCache = new Map<string, CompanySummary>()

  type Acc = {
    companyId: Id<"companies">
    name: string
    isAvulso: boolean
    internalMs: number
    externalMs: number
    totalMs: number
    contractedHoursPerMonth?: number | null
  }
  const map = new Map<string, Acc>()

  await forEachScopedTicket(ctx, tenantId, viewer, async (ticket) => {
    const updatedAt = typeof ticket.updatedAt === "number" ? ticket.updatedAt : ticket.createdAt
    if (updatedAt < startMs || updatedAt >= limitedEndMs) return
    const companyId = ticket.companyId ?? null
    if (!companyId) return
    const key = String(companyId)
    let acc = map.get(key)
    if (!acc) {
      const company = await getCompanySummary(ctx, companyId, companyCache)
      acc = {
        companyId,
        name: company.name,
        isAvulso: company.isAvulso,
        internalMs: 0,
        externalMs: 0,
        totalMs: 0,
        contractedHoursPerMonth: company.contractedHoursPerMonth,
      }
      map.set(key, acc)
    }
    const internal = ticket.internalWorkedMs ?? 0
    const external = ticket.externalWorkedMs ?? 0
    acc.internalMs += internal
    acc.externalMs += external
    acc.totalMs += internal + external
  })

  const items = Array.from(map.values()).sort((a, b) => b.totalMs - a.totalMs)
  return {
    rangeDays,
    items: items.map((i) => ({
      companyId: i.companyId,
      name: i.name,
      isAvulso: i.isAvulso,
      internalMs: i.internalMs,
      externalMs: i.externalMs,
      totalMs: i.totalMs,
      contractedHoursPerMonth: i.contractedHoursPerMonth ?? null,
    })),
  }
}

export const hoursByClient = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    range: v.optional(v.string()),
    dateFrom: v.optional(v.string()),
    dateTo: v.optional(v.string()),
  },
  handler: hoursByClientHandler,
})

// Internal variant used by scheduled jobs: skips viewer scoping and aggregates for the whole tenant
export async function hoursByClientInternalHandler(
  ctx: QueryCtx,
  { tenantId, range, dateFrom, dateTo }: { tenantId: string; range?: string; dateFrom?: string; dateTo?: string }
) {
  const { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90)
  const { endMs: limitedEndMs } = clampRangeWindow(startMs, endMs)
  const rangeDays = Math.min(days, deriveRangeDays(startMs, limitedEndMs))
  const companyCache = new Map<string, CompanySummary>()

  type Acc = {
    companyId: Id<"companies">
    name: string
    isAvulso: boolean
    internalMs: number
    externalMs: number
    totalMs: number
    contractedHoursPerMonth?: number | null
  }
  const map = new Map<string, Acc>()

  await forEachTenantTicket(ctx, tenantId, async (ticket) => {
    if (ticket.updatedAt < startMs || ticket.updatedAt >= limitedEndMs) return
    const companyId = ticket.companyId ?? null
    if (!companyId) return
    const key = String(companyId)
    let acc = map.get(key)
    if (!acc) {
      const company = await getCompanySummary(ctx, companyId, companyCache)
      acc = {
        companyId,
        name: company.name,
        isAvulso: company.isAvulso,
        internalMs: 0,
        externalMs: 0,
        totalMs: 0,
        contractedHoursPerMonth: company.contractedHoursPerMonth,
      }
      map.set(key, acc)
    }
    const internal = ticket.internalWorkedMs ?? 0
    const external = ticket.externalWorkedMs ?? 0
    acc.internalMs += internal
    acc.externalMs += external
    acc.totalMs += internal + external
  })

  const items = Array.from(map.values()).sort((a, b) => b.totalMs - a.totalMs)
  return {
    rangeDays,
    items: items.map((i) => ({
      companyId: i.companyId,
      name: i.name,
      isAvulso: i.isAvulso,
      internalMs: i.internalMs,
      externalMs: i.externalMs,
      totalMs: i.totalMs,
      contractedHoursPerMonth: i.contractedHoursPerMonth ?? null,
    })),
  }
}

export const hoursByClientInternal = query({
  args: {
    tenantId: v.string(),
    range: v.optional(v.string()),
    dateFrom: v.optional(v.string()),
    dateTo: v.optional(v.string()),
  },
  handler: hoursByClientInternalHandler,
})


export const companyOverview = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    companyId: v.optional(v.id("companies")),
    range: v.optional(v.string()),
  },
  handler: async (ctx, { tenantId, viewerId, companyId, range }) => {
    const viewer = await requireStaff(ctx, viewerId, tenantId);
    const scopedCompanyId = resolveScopedCompanyId(viewer, companyId);

    // Buscar dados da empresa selecionada (se houver)
    let company: Doc<"companies"> | null = null;
    if (scopedCompanyId) {
      company = await ctx.db.get(scopedCompanyId);
      if (!company || company.tenantId !== tenantId) {
        throw new ConvexError("Empresa não encontrada");
      }
    }

    const normalizedRange = (range ?? "30d").toLowerCase();
    const rangeDays = normalizedRange === "90d" ? 90 : normalizedRange === "7d" ? 7 : 30;
    const now = Date.now();
    const startMs = now - rangeDays * ONE_DAY_MS;

    // Limita consultas para evitar OOM em empresas muito grandes
    const tickets = scopedCompanyId
      ? await ctx.db
          .query("tickets")
          .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", scopedCompanyId))
          .take(2000)
      : await ctx.db
          .query("tickets")
          .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
          .take(2000);

    const machines = scopedCompanyId
      ? await ctx.db
          .query("machines")
          .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", scopedCompanyId))
          .take(1000)
      : await ctx.db
          .query("machines")
          .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
          .take(1000);

    const users = scopedCompanyId
      ? await ctx.db
          .query("users")
          .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", scopedCompanyId))
          .take(500)
      : await ctx.db
          .query("users")
          .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
          .take(500);

    const statusCounts = {} as Record<string, number>;
    const priorityCounts = {} as Record<string, number>;
    const channelCounts = {} as Record<string, number>;
    const trendMap = new Map<string, { opened: number; resolved: number }>();
    const openTickets: Doc<"tickets">[] = [];

    tickets.forEach((ticket) => {
      const normalizedStatus = normalizeStatus(ticket.status);
      statusCounts[normalizedStatus] = (statusCounts[normalizedStatus] ?? 0) + 1;

      const priorityKey = (ticket.priority ?? "MEDIUM").toUpperCase();
      priorityCounts[priorityKey] = (priorityCounts[priorityKey] ?? 0) + 1;

      const channelKey = (ticket.channel ?? "MANUAL").toUpperCase();
      channelCounts[channelKey] = (channelCounts[channelKey] ?? 0) + 1;

      if (normalizedStatus !== "RESOLVED") {
        openTickets.push(ticket);
      }

      if (ticket.createdAt >= startMs) {
        const key = formatDateKey(ticket.createdAt);
        if (!trendMap.has(key)) {
          trendMap.set(key, { opened: 0, resolved: 0 });
        }
        trendMap.get(key)!.opened += 1;
      }

      if (typeof ticket.resolvedAt === "number" && ticket.resolvedAt >= startMs) {
        const key = formatDateKey(ticket.resolvedAt);
        if (!trendMap.has(key)) {
          trendMap.set(key, { opened: 0, resolved: 0 });
        }
        trendMap.get(key)!.resolved += 1;
      }
    });

    const machineStatusCounts: Record<string, number> = {};
    const machineOsCounts: Record<string, number> = {};
    const machineLookup = new Map<string, Doc<"machines">>();

    machines.forEach((machine) => {
      machineLookup.set(String(machine._id), machine);
      const status = deriveMachineStatus(machine, now);
      machineStatusCounts[status] = (machineStatusCounts[status] ?? 0) + 1;
      const osLabel = formatOsLabel(machine.osName ?? null, machine.osVersion ?? null);
      machineOsCounts[osLabel] = (machineOsCounts[osLabel] ?? 0) + 1;
    });

    const roleCounts: Record<string, number> = {};
    users.forEach((user) => {
      const roleKey = (user.role ?? "COLLABORATOR").toUpperCase();
      roleCounts[roleKey] = (roleCounts[roleKey] ?? 0) + 1;
    });

    const trend = Array.from(trendMap.entries())
      .sort((a, b) => a[0].localeCompare(b[0]))
      .map(([date, value]) => ({ date, opened: value.opened, resolved: value.resolved }));

    const machineOsDistribution = Object.entries(machineOsCounts)
      .map(([label, value]) => ({ label, value }))
      .sort((a, b) => b.value - a.value);

    const openTicketSummaries = openTickets
      .sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))
      .slice(0, 20)
      .map((ticket) => {
        const machineSnapshot = ticket.machineSnapshot as { hostname?: string } | undefined;
        const machine = ticket.machineId ? machineLookup.get(String(ticket.machineId)) : null;
        return {
          id: ticket._id,
          reference: ticket.reference,
          subject: ticket.subject,
          status: normalizeStatus(ticket.status),
          priority: ticket.priority,
          updatedAt: ticket.updatedAt,
          createdAt: ticket.createdAt,
          machine: ticket.machineId
            ? {
                id: ticket.machineId,
                hostname: machine?.hostname ?? machineSnapshot?.hostname ?? null,
              }
            : null,
          assignee: ticket.assigneeSnapshot
            ? {
                name: (ticket.assigneeSnapshot as { name?: string })?.name ?? null,
                email: (ticket.assigneeSnapshot as { email?: string })?.email ?? null,
              }
            : null,
        };
      });

    return {
      company: company
        ? {
            id: company._id,
            name: company.name,
            isAvulso: company.isAvulso ?? false,
          }
        : null,
      rangeDays,
      generatedAt: now,
      tickets: {
        total: tickets.length,
        byStatus: statusCounts,
        byPriority: priorityCounts,
        byChannel: channelCounts,
        trend,
        open: openTicketSummaries,
      },
      machines: {
        total: machines.length,
        byStatus: machineStatusCounts,
        byOs: machineOsDistribution,
      },
      users: {
        total: users.length,
        byRole: roleCounts,
      },
    };
  },
});
