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

import { requireStaff } from "./rbac";

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

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",
};

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;
}

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

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 isNotNull<T>(value: T | null): value is T {
  return value !== null;
}

async function fetchTickets(ctx: QueryCtx, tenantId: string) {
  return ctx.db
    .query("tickets")
    .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
    .collect();
}

async function fetchScopedTickets(
  ctx: QueryCtx,
  tenantId: string,
  viewer: Awaited<ReturnType<typeof requireStaff>>,
) {
  if (viewer.role === "MANAGER") {
    if (!viewer.user.companyId) {
      throw new ConvexError("Gestor não possui empresa vinculada");
    }
    return ctx.db
      .query("tickets")
      .withIndex("by_tenant_company", (q) =>
        q.eq("tenantId", tenantId).eq("companyId", viewer.user.companyId!)
      )
      .collect();
  }
  return fetchTickets(ctx, tenantId);
}

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

type CsatSurvey = {
  ticketId: Id<"tickets">;
  reference: number;
  score: number;
  receivedAt: number;
};

async function collectCsatSurveys(ctx: QueryCtx, tickets: Doc<"tickets">[]): Promise<CsatSurvey[]> {
  const perTicket = await Promise.all(
    tickets.map(async (ticket) => {
      const events = await ctx.db
        .query("ticketEvents")
        .withIndex("by_ticket", (q) => q.eq("ticketId", ticket._id))
        .collect();
      return events
        .filter((event) => event.type === "CSAT_RECEIVED" || event.type === "CSAT_RATED")
        .map((event) => {
          const score = extractScore(event.payload);
          if (score === null) return null;
          return {
            ticketId: ticket._id,
            reference: ticket.reference,
            score,
            receivedAt: event.createdAt,
          } as CsatSurvey;
        })
        .filter(isNotNull);
    })
  );
  return perTicket.flat();
}

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 const slaOverview = query({
  args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) },
  handler: async (ctx, { tenantId, viewerId, range, companyId }) => {
    const viewer = await requireStaff(ctx, viewerId, tenantId);
    let tickets = await fetchScopedTickets(ctx, tenantId, viewer);
    if (companyId) tickets = tickets.filter((t) => t.companyId === companyId)
    // Optional range filter (createdAt) for reporting purposes, similar ao backlog/csat
    const days = 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 - days * ONE_DAY_MS;
    const inRange = tickets.filter((t) => t.createdAt >= startMs && t.createdAt < endMs);
    const queues = await fetchQueues(ctx, tenantId);

    const now = Date.now();
    const openTickets = inRange.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status)));
    const resolvedTickets = inRange.filter((ticket) => {
      const status = normalizeStatus(ticket.status);
      return status === "RESOLVED";
    });
    const overdueTickets = openTickets.filter((ticket) => ticket.dueAt && ticket.dueAt < now);

    const firstResponseTimes = inRange
      .filter((ticket) => ticket.firstResponseAt)
      .map((ticket) => (ticket.firstResponseAt! - ticket.createdAt) / 60000);
    const resolutionTimes = resolvedTickets
      .filter((ticket) => ticket.resolvedAt)
      .map((ticket) => (ticket.resolvedAt! - ticket.createdAt) / 60000);

    const queueBreakdown = queues.map((queue) => {
      const count = openTickets.filter((ticket) => ticket.queueId === queue._id).length;
      return {
        id: queue._id,
        name: queue.name,
        open: count,
      };
    });

    return {
      totals: {
        total: inRange.length,
        open: openTickets.length,
        resolved: resolvedTickets.length,
        overdue: overdueTickets.length,
      },
      response: {
        averageFirstResponseMinutes: average(firstResponseTimes),
        responsesRegistered: firstResponseTimes.length,
      },
      resolution: {
        averageResolutionMinutes: average(resolutionTimes),
        resolvedCount: resolutionTimes.length,
      },
      queueBreakdown,
      rangeDays: days,
    };
  },
});

export const csatOverview = query({
  args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) },
  handler: async (ctx, { tenantId, viewerId, range, companyId }) => {
    const viewer = await requireStaff(ctx, viewerId, tenantId);
    let tickets = await fetchScopedTickets(ctx, tenantId, viewer);
    if (companyId) tickets = tickets.filter((t) => t.companyId === companyId)
    const surveysAll = await collectCsatSurveys(ctx, tickets);
    const days = 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 - days * ONE_DAY_MS;
    const surveys = surveysAll.filter((s) => s.receivedAt >= startMs && s.receivedAt < endMs);

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

    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,
          receivedAt: item.receivedAt,
        })),
      rangeDays: days,
    };
  },
});

export const openedResolvedByDay = query({
  args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) },
  handler: async (ctx, { tenantId, viewerId, range, companyId }) => {
    const viewer = await requireStaff(ctx, viewerId, tenantId);
    let tickets = await fetchScopedTickets(ctx, tenantId, viewer);
    if (companyId) tickets = tickets.filter((t) => t.companyId === companyId)

    const days = 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 - days * ONE_DAY_MS;

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

    // pre-fill buckets
    for (let i = days - 1; i >= 0; i--) {
      const d = new Date(endMs - (i + 1) * ONE_DAY_MS)
      const key = formatDateKey(d.getTime())
      opened[key] = 0
      resolved[key] = 0
    }

    for (const t of tickets) {
      if (t.createdAt >= startMs && t.createdAt < endMs) {
        const key = formatDateKey(t.createdAt)
        opened[key] = (opened[key] ?? 0) + 1
      }
      if (t.resolvedAt && t.resolvedAt >= startMs && t.resolvedAt < endMs) {
        const key = formatDateKey(t.resolvedAt)
        resolved[key] = (resolved[key] ?? 0) + 1
      }
    }

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

    return { rangeDays: days, series }
  },
})

export const backlogOverview = query({
  args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) },
  handler: async (ctx, { tenantId, viewerId, range, companyId }) => {
    const viewer = await requireStaff(ctx, viewerId, tenantId);
    let tickets = await fetchScopedTickets(ctx, tenantId, viewer);
    if (companyId) tickets = tickets.filter((t) => t.companyId === companyId)

    // Optional range filter (createdAt) for reporting purposes
    const days = 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 - days * ONE_DAY_MS;
    const inRange = tickets.filter((t) => t.createdAt >= startMs && t.createdAt < endMs);

    const statusCounts = inRange.reduce<Record<TicketStatusNormalized, number>>((acc, ticket) => {
      const status = normalizeStatus(ticket.status);
      acc[status] = (acc[status] ?? 0) + 1;
      return acc;
    }, {} as Record<TicketStatusNormalized, number>);

    const priorityCounts = inRange.reduce<Record<string, number>>((acc, ticket) => {
      acc[ticket.priority] = (acc[ticket.priority] ?? 0) + 1;
      return acc;
    }, {});

    const openTickets = inRange.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status)));

    const queueMap = new Map<string, { name: string; count: number }>();
    for (const ticket of openTickets) {
      const queueId = ticket.queueId ? ticket.queueId : "sem-fila";
      const current = queueMap.get(queueId) ?? { name: queueId === "sem-fila" ? "Sem fila" : "", count: 0 };
      current.count += 1;
      queueMap.set(queueId, 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: days,
      statusCounts,
      priorityCounts,
      queueCounts: Array.from(queueMap.entries()).map(([id, data]) => ({
        id,
        name: data.name,
        total: data.count,
      })),
      totalOpen: openTickets.length,
    };
  },
});

export const dashboardOverview = query({
  args: { tenantId: v.string(), viewerId: v.id("users") },
  handler: async (ctx, { tenantId, viewerId }) => {
    const viewer = await requireStaff(ctx, viewerId, tenantId);
    const tickets = await fetchScopedTickets(ctx, tenantId, viewer);
    const now = Date.now();

    const lastDayStart = now - ONE_DAY_MS;
    const previousDayStart = now - 2 * ONE_DAY_MS;

    const newTickets = tickets.filter((ticket) => ticket.createdAt >= lastDayStart);
    const previousTickets = tickets.filter(
      (ticket) => ticket.createdAt >= previousDayStart && ticket.createdAt < lastDayStart
    );

    const trend = percentageChange(newTickets.length, previousTickets.length);

    const inProgressCurrent = tickets.filter((ticket) => {
      if (!ticket.firstResponseAt) return false;
      const status = normalizeStatus(ticket.status);
      if (status === "RESOLVED") return false;
      return !ticket.resolvedAt;
    });

    const inProgressPrevious = tickets.filter((ticket) => {
      if (!ticket.firstResponseAt || ticket.firstResponseAt >= lastDayStart) return false;
      if (ticket.resolvedAt && ticket.resolvedAt < lastDayStart) return false;
      const status = normalizeStatus(ticket.status);
      return status !== "RESOLVED" || !ticket.resolvedAt;
    });

    const inProgressTrend = percentageChange(inProgressCurrent.length, inProgressPrevious.length);

    const lastWindowStart = now - 7 * ONE_DAY_MS;
    const previousWindowStart = now - 14 * ONE_DAY_MS;

    const firstResponseWindow = tickets
      .filter(
        (ticket) =>
          ticket.createdAt >= lastWindowStart &&
          ticket.createdAt < now &&
          ticket.firstResponseAt
      )
      .map((ticket) => (ticket.firstResponseAt! - ticket.createdAt) / 60000);
    const firstResponsePrevious = tickets
      .filter(
        (ticket) =>
          ticket.createdAt >= previousWindowStart &&
          ticket.createdAt < lastWindowStart &&
          ticket.firstResponseAt
      )
      .map((ticket) => (ticket.firstResponseAt! - ticket.createdAt) / 60000);

    const averageWindow = average(firstResponseWindow);
    const averagePrevious = average(firstResponsePrevious);
    const deltaMinutes =
      averageWindow !== null && averagePrevious !== null ? averageWindow - averagePrevious : null;

    const awaitingTickets = tickets.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status)));
    const atRiskTickets = awaitingTickets.filter((ticket) => ticket.dueAt && ticket.dueAt < now);

    const resolvedLastWindow = tickets.filter(
      (ticket) => ticket.resolvedAt && ticket.resolvedAt >= lastWindowStart && ticket.resolvedAt < now
    );
    const resolvedPreviousWindow = tickets.filter(
      (ticket) =>
        ticket.resolvedAt &&
        ticket.resolvedAt >= previousWindowStart &&
        ticket.resolvedAt < lastWindowStart
    );
    const resolutionRate = tickets.length > 0 ? (resolvedLastWindow.length / tickets.length) * 100 : null;
    const resolutionDelta =
      resolvedPreviousWindow.length > 0
        ? ((resolvedLastWindow.length - resolvedPreviousWindow.length) / resolvedPreviousWindow.length) * 100
        : null;

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

export const ticketsByChannel = query({
  args: {
    tenantId: v.string(),
    viewerId: v.id("users"),
    range: v.optional(v.string()),
    companyId: v.optional(v.id("companies")),
  },
  handler: async (ctx, { tenantId, viewerId, range, companyId }) => {
    const viewer = await requireStaff(ctx, viewerId, tenantId);
    let tickets = await fetchScopedTickets(ctx, tenantId, viewer);
    if (companyId) tickets = tickets.filter((t) => t.companyId === companyId)
    const days = 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 - days * ONE_DAY_MS;

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

    const channels = new Set<string>();

    for (const ticket of tickets) {
      if (ticket.createdAt < startMs || ticket.createdAt >= endMs) continue;
      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: days,
      channels: sortedChannels,
      points,
    };
  },
});

export const hoursByClient = query({
  args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()) },
  handler: async (ctx, { tenantId, viewerId, range }) => {
    const viewer = await requireStaff(ctx, viewerId, tenantId)
    const tickets = await fetchScopedTickets(ctx, tenantId, viewer)

    const days = 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 - days * ONE_DAY_MS

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

    for (const t of tickets) {
      // only consider tickets updated in range as a proxy for recent work
      if (t.updatedAt < startMs || t.updatedAt >= endMs) continue
      const companyId = t.companyId ?? null
      if (!companyId) continue

      let acc = map.get(companyId)
      if (!acc) {
        const company = await ctx.db.get(companyId)
        acc = {
          companyId,
          name: company?.name ?? "Sem empresa",
          isAvulso: Boolean(company?.isAvulso ?? false),
          internalMs: 0,
          externalMs: 0,
          totalMs: 0,
          contractedHoursPerMonth: company?.contractedHoursPerMonth ?? null,
        }
        map.set(companyId, acc)
      }
      const internal = t.internalWorkedMs ?? 0
      const external = t.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: days,
      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,
      })),
    }
  },
})

// Internal variant used by scheduled jobs: skips viewer scoping and aggregates for the whole tenant
export const hoursByClientInternal = query({
  args: { tenantId: v.string(), range: v.optional(v.string()) },
  handler: async (ctx, { tenantId, range }) => {
    const tickets = await fetchTickets(ctx, tenantId)

    const days = 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 - days * ONE_DAY_MS

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

    for (const t of tickets) {
      if (t.updatedAt < startMs || t.updatedAt >= endMs) continue
      const companyId = t.companyId ?? null
      if (!companyId) continue

      let acc = map.get(companyId)
      if (!acc) {
        const company = await ctx.db.get(companyId)
        acc = {
          companyId,
          name: company?.name ?? "Sem empresa",
          isAvulso: Boolean(company?.isAvulso ?? false),
          internalMs: 0,
          externalMs: 0,
          totalMs: 0,
          contractedHoursPerMonth: company?.contractedHoursPerMonth ?? null,
        }
        map.set(companyId, acc)
      }
      const internal = t.internalWorkedMs ?? 0
      const external = t.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: days,
      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,
      })),
    }
  },
})
