import type { PrismaClient } from '@prisma/client';

/**
 * Build a compact workout-history summary the AI can use as
 * context for personalized program generation.
 *
 * We DELIBERATELY don't ship raw set logs — that would be tens of
 * KB per request and burn tokens. Instead we compute per-exercise
 * aggregates over a recent window (default 90 days):
 *
 *   - totalSets in window
 *   - distinct workouts
 *   - daysSinceLast
 *   - lastWeight, lastReps (from the most-recent set)
 *   - bestWeight (heaviest set in window)
 *   - estimated 1RM (Epley formula on the heaviest weighted set)
 *   - rpe trend (avg RPE over recent sets, if logged)
 *   - stagnation flag (heaviest weight unchanged for 4+ weeks AND
 *     ≥3 sessions of that exercise in those 4+ weeks)
 *
 * Plus a top-level summary: total workouts, frequency, primary
 * exercise types touched.
 *
 * The output is JSON-stringifiable, ~5-15 KB for a typical user.
 */

export interface HistoryExerciseSummary {
  name: string;
  type: string;
  totalSets: number;
  distinctWorkouts: number;
  daysSinceLast: number;
  lastWeight: number | null;
  lastReps: number | null;
  bestWeight: number | null;
  estimated1RM: number | null;
  avgRpe: number | null;
  stagnant: boolean;
}

export interface HistorySummary {
  windowDays: number;
  totalWorkouts: number;
  workoutsPerWeek: number;
  primaryTypes: string[]; // exercise types by descending volume
  exercises: HistoryExerciseSummary[];
}

/** Epley estimated 1RM: weight * (1 + reps / 30) */
function epley1RM(weight: number, reps: number): number {
  return Math.round(weight * (1 + reps / 30));
}

export async function buildHistorySummary(
  prisma: PrismaClient,
  userId: string,
  windowDays = 90,
): Promise<HistorySummary> {
  const cutoff = new Date(Date.now() - windowDays * 86_400_000);

  // Pull every set log in the window with its exercise + workout
  // date. One query, one result-set walk.
  const sets = await prisma.setLog.findMany({
    where: {
      workout: {
        userId,
        deletedAt: null,
        date: { gte: cutoff },
      },
    },
    select: {
      reps: true,
      weight: true,
      rpe: true,
      exerciseId: true,
      workoutId: true,
      workout: { select: { date: true } },
      exercise: { select: { name: true, type: true } },
    },
    orderBy: { workout: { date: 'desc' } },
  });

  if (sets.length === 0) {
    return {
      windowDays,
      totalWorkouts: 0,
      workoutsPerWeek: 0,
      primaryTypes: [],
      exercises: [],
    };
  }

  const workoutIds = new Set(sets.map((s) => s.workoutId));
  const totalWorkouts = workoutIds.size;
  const weeks = windowDays / 7;
  const workoutsPerWeek = Math.round((totalWorkouts / weeks) * 10) / 10;

  // Group by exercise
  const byExercise = new Map<
    string,
    {
      name: string;
      type: string;
      sets: typeof sets;
    }
  >();
  for (const s of sets) {
    if (!byExercise.has(s.exerciseId)) {
      byExercise.set(s.exerciseId, {
        name: s.exercise.name,
        type: s.exercise.type,
        sets: [],
      });
    }
    byExercise.get(s.exerciseId)!.sets.push(s);
  }

  // Per-exercise summaries
  const now = Date.now();
  const exercises: HistoryExerciseSummary[] = [];
  for (const [, group] of byExercise) {
    const groupSets = group.sets;
    const distinctWorkouts = new Set(groupSets.map((s) => s.workoutId)).size;
    const mostRecent = groupSets[0]; // already date-desc
    const daysSinceLast = Math.floor(
      (now - mostRecent.workout.date.getTime()) / 86_400_000,
    );

    const weightedSets = groupSets.filter(
      (s): s is typeof s & { weight: number; reps: number } =>
        typeof s.weight === 'number' && typeof s.reps === 'number',
    );
    const bestWeightSet = weightedSets.reduce<
      | { weight: number; reps: number }
      | null
    >((best, s) => {
      if (!best || s.weight > best.weight) return s;
      return best;
    }, null);
    const bestWeight = bestWeightSet?.weight ?? null;
    const estimated1RM =
      bestWeightSet != null ? epley1RM(bestWeightSet.weight, bestWeightSet.reps) : null;

    const rpeSets = groupSets.filter(
      (s): s is typeof s & { rpe: number } => typeof s.rpe === 'number',
    );
    const avgRpe =
      rpeSets.length > 0
        ? Math.round(
            (rpeSets.reduce((sum, s) => sum + s.rpe, 0) / rpeSets.length) * 10,
          ) / 10
        : null;

    // Stagnation: best weight in oldest half == best weight in newest half
    // AND ≥3 distinct sessions in the window.
    let stagnant = false;
    if (distinctWorkouts >= 3 && bestWeight != null && weightedSets.length >= 4) {
      const sortedByDate = [...weightedSets].sort(
        (a, b) => a.workout.date.getTime() - b.workout.date.getTime(),
      );
      const mid = Math.floor(sortedByDate.length / 2);
      const oldHalfBest = Math.max(...sortedByDate.slice(0, mid).map((s) => s.weight));
      const newHalfBest = Math.max(...sortedByDate.slice(mid).map((s) => s.weight));
      // No improvement in the new half compared to the old half
      if (newHalfBest <= oldHalfBest) stagnant = true;
    }

    exercises.push({
      name: group.name,
      type: group.type,
      totalSets: groupSets.length,
      distinctWorkouts,
      daysSinceLast,
      lastWeight: mostRecent.weight ?? null,
      lastReps: mostRecent.reps ?? null,
      bestWeight,
      estimated1RM,
      avgRpe,
      stagnant,
    });
  }

  // Sort exercises by total volume (sets) descending so the most
  // important context is first if the model truncates.
  exercises.sort((a, b) => b.totalSets - a.totalSets);

  // Primary types by aggregate sets
  const typeVolume = new Map<string, number>();
  for (const ex of exercises) {
    typeVolume.set(ex.type, (typeVolume.get(ex.type) ?? 0) + ex.totalSets);
  }
  const primaryTypes = Array.from(typeVolume.entries())
    .sort((a, b) => b[1] - a[1])
    .map(([t]) => t);

  return {
    windowDays,
    totalWorkouts,
    workoutsPerWeek,
    primaryTypes,
    exercises,
  };
}

/**
 * Format a HistorySummary as a compact string the LLM can actually
 * use. Aims for <2KB of text even for heavy users.
 */
export function formatHistoryContext(summary: HistorySummary): string {
  if (summary.totalWorkouts === 0) {
    return `\nUSER HISTORY: no workouts logged in the last ${summary.windowDays} days.`;
  }
  const lines: string[] = [];
  lines.push(
    `\nUSER HISTORY (last ${summary.windowDays} days):`,
    `  ${summary.totalWorkouts} workouts (~${summary.workoutsPerWeek}/week)`,
    `  Primary work: ${summary.primaryTypes.slice(0, 4).join(', ')}`,
    '',
    `  Per-exercise activity (descending by volume; weights in user's logged unit):`,
  );
  // Cap at top 30 exercises
  const top = summary.exercises.slice(0, 30);
  for (const ex of top) {
    const bits: string[] = [
      `${ex.totalSets}s/${ex.distinctWorkouts}w`,
      `${ex.daysSinceLast}d ago`,
    ];
    if (ex.bestWeight != null && ex.lastReps != null)
      bits.push(`best ${ex.bestWeight}×${ex.lastReps}`);
    if (ex.estimated1RM != null) bits.push(`~${ex.estimated1RM} 1RM`);
    if (ex.avgRpe != null) bits.push(`avg RPE ${ex.avgRpe}`);
    if (ex.stagnant) bits.push('STAGNANT');
    lines.push(`    - ${ex.name} (${ex.type}): ${bits.join(' · ')}`);
  }
  if (summary.exercises.length > top.length) {
    lines.push(
      `    ...and ${summary.exercises.length - top.length} more exercises with lower volume`,
    );
  }
  lines.push(
    '',
    `  When designing the program, weight recent activity heavily. Address STAGNANT exercises if relevant. Don't propose deload-week-heavy work for someone training infrequently, and don't propose 6-day splits for someone averaging <3 sessions/week.`,
  );
  return lines.join('\n');
}
