import type {
  MetricsBreakdownRow,
  MetricsEventSeries,
  MetricsSnapshotView,
  MetricsTimeBucket,
  MetricsView,
} from "@batch-cooking/shared";
import { prisma } from "../../db/prisma.js";

/** UTC `YYYY-MM-DD` for a `Date` — the bucket key used by {@link bucketByDay}. */
function utcDayKey(date: Date): string {
  return date.toISOString().slice(0, 10);
}

/** Start-of-day (UTC) `Date` that is `daysAgo` days before `from`. */
function startOfUtcDay(from: Date, daysAgo: number): Date {
  return new Date(Date.UTC(from.getUTCFullYear(), from.getUTCMonth(), from.getUTCDate() - daysAgo));
}

/**
 * Buckets `dates` into `days` consecutive daily counts starting at `since`
 * (a start-of-UTC-day `Date`). Every day in the window is present, days
 * with no matching date carry `count: 0`. Pure/synchronous — factored out
 * so the bucketing is unit-testable without a database, same split as
 * `aggregateShoppingList`.
 */
export function bucketByDay(dates: Date[], since: Date, days: number): MetricsTimeBucket[] {
  const counts = new Map<string, number>();
  for (let i = 0; i < days; i++) {
    const day = new Date(since.getTime() + i * 86_400_000);
    counts.set(utcDayKey(day), 0);
  }
  for (const date of dates) {
    const key = utcDayKey(date);
    const current = counts.get(key);
    if (current !== undefined) counts.set(key, current + 1);
  }
  return [...counts.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([date, count]) => ({ date, count }));
}

/** Shapes a Prisma `groupBy ... _count` result into the frontend's `{ key, label, count }` rows, sorted by count desc. */
function toBreakdown(
  rows: { key: string | null; count: number }[],
  labelFor: (key: string) => string = (key) => key,
): MetricsBreakdownRow[] {
  return rows
    .map(({ key, count }) => {
      const resolved = key ?? "unknown";
      return { key: resolved, label: labelFor(resolved), count };
    })
    .sort((a, b) => b.count - a.count);
}

/** Every point-in-time `COUNT` for the KPI tiles — see {@link MetricsSnapshotView}. */
async function getSnapshot(): Promise<MetricsSnapshotView> {
  try {
    const [
      admins,
      users,
      households,
      activeHouseholdGroups,
      recipes,
      recipesManual,
      recipesImported,
      recipeBySourceGroups,
      sources,
      plannings,
      planningItems,
      steps,
      detectedTechniques,
      favorites,
      corrections,
      correctionsUnconsumed,
      correctionsRemoval,
      trainingSuggestions,
      suggestionStatusGroups,
      suggestionSourceTypeGroups,
    ] = await Promise.all([
      prisma.adminUser.count(),
      prisma.userProfile.count(),
      prisma.house.count(),
      prisma.planning.groupBy({ by: ["houseId"] }),
      prisma.recipe.count(),
      prisma.recipe.count({ where: { sourceId: null } }),
      prisma.recipe.count({ where: { sourceId: { not: null } } }),
      prisma.recipe.groupBy({
        by: ["sourceId"],
        where: { sourceId: { not: null } },
        _count: { _all: true },
      }),
      prisma.source.findMany({ select: { id: true, key: true, name: true } }),
      prisma.planning.count(),
      prisma.planningItem.count(),
      prisma.step.count(),
      prisma.stepTechStep.count(),
      prisma.recipeFavorite.count(),
      prisma.stepTechStepCorrection.count(),
      prisma.stepTechStepCorrection.count({ where: { consumedAt: null } }),
      prisma.stepTechStepCorrection.count({ where: { correctedTechStepId: null } }),
      prisma.techStepTrainingSuggestion.count(),
      prisma.techStepTrainingSuggestion.groupBy({ by: ["status"], _count: { _all: true } }),
      prisma.techStepTrainingSuggestion.groupBy({ by: ["sourceType"], _count: { _all: true } }),
    ]);

    const sourceById = new Map(sources.map((source) => [source.id, source]));

    return {
      admins,
      users,
      households,
      activeHouseholds: activeHouseholdGroups.length,
      recipes,
      recipesManual,
      recipesImported,
      recipesBySource: toBreakdown(
        recipeBySourceGroups.map((group) => ({
          key: group.sourceId === null ? null : (sourceById.get(group.sourceId)?.key ?? null),
          count: group._count._all,
        })),
        (key) => {
          const source = sources.find((s) => s.key === key);
          return source ? source.name : key;
        },
      ),
      plannings,
      planningItems,
      steps,
      detectedTechniques,
      favorites,
      corrections,
      correctionsUnconsumed,
      correctionsRemoval,
      trainingSuggestions,
      trainingSuggestionsByStatus: toBreakdown(
        suggestionStatusGroups.map((group) => ({ key: group.status, count: group._count._all })),
      ),
      trainingSuggestionsBySourceType: toBreakdown(
        suggestionSourceTypeGroups.map((group) => ({
          key: group.sourceType,
          count: group._count._all,
        })),
      ),
    };
  } catch (err) {
    throw err; // see recipe.service.ts's equivalent catch comment
  }
}

/**
 * Builds the admin dashboard's full metrics payload — a `snapshot` of
 * current totals plus `rangeDays` days of daily time series, derived from
 * the `createdAt` columns the `admin_metrics` migration added and from the
 * `AnalyticsEvent` table. `days` is the caller-validated `?days=` value
 * (see `getMetricsSchema`, 7–365).
 */
export async function getMetrics(days: number): Promise<MetricsView> {
  try {
    const now = new Date();
    const since = startOfUtcDay(now, days - 1);

    const [
      snapshot,
      signups,
      recipesCreated,
      planningItemsAdded,
      correctionsSubmitted,
      trainingSuggestions,
      eventRows,
    ] = await Promise.all([
      getSnapshot(),
      prisma.userProfile.findMany({
        where: { createdAt: { gte: since } },
        select: { createdAt: true },
      }),
      prisma.recipe.findMany({ where: { createdAt: { gte: since } }, select: { createdAt: true } }),
      prisma.planningItem.findMany({
        where: { createdAt: { gte: since } },
        select: { createdAt: true },
      }),
      prisma.stepTechStepCorrection.findMany({
        where: { createdAt: { gte: since } },
        select: { createdAt: true },
      }),
      prisma.techStepTrainingSuggestion.findMany({
        where: { createdAt: { gte: since } },
        select: { createdAt: true },
      }),
      prisma.analyticsEvent.findMany({
        where: { createdAt: { gte: since } },
        select: { type: true, createdAt: true },
      }),
    ]);

    const eventsByType = new Map<string, Date[]>();
    for (const row of eventRows) {
      const list = eventsByType.get(row.type);
      if (list) list.push(row.createdAt);
      else eventsByType.set(row.type, [row.createdAt]);
    }
    const events: MetricsEventSeries[] = [...eventsByType.entries()]
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([type, dates]) => ({ type, buckets: bucketByDay(dates, since, days) }));

    return {
      generatedAt: now.toISOString(),
      rangeDays: days,
      snapshot,
      series: {
        signups: bucketByDay(
          signups.map((r) => r.createdAt),
          since,
          days,
        ),
        recipesCreated: bucketByDay(
          recipesCreated.map((r) => r.createdAt),
          since,
          days,
        ),
        planningItemsAdded: bucketByDay(
          planningItemsAdded.map((r) => r.createdAt),
          since,
          days,
        ),
        correctionsSubmitted: bucketByDay(
          correctionsSubmitted.map((r) => r.createdAt),
          since,
          days,
        ),
        trainingSuggestions: bucketByDay(
          trainingSuggestions.map((r) => r.createdAt),
          since,
          days,
        ),
      },
      events,
    };
  } catch (err) {
    throw err; // see recipe.service.ts's equivalent catch comment
  }
}
