import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model, Types } from "mongoose";
import { Request, Response } from "express";
import {
  WorkoutSession,
  WorkoutSessionDocument,
} from "./schemas/workout-session.schema";
import {
  TrainingPlan,
  TrainingPlanDocument,
} from "src/training-plan/schemas/training-plan.schema";
import {
  Exercise,
  ExerciseDocument,
} from "src/exercise/schemas/exercise.schema";
import { User, UserDocument } from "src/user/schemas/user.schema";
import {
  Performance,
  PerformanceDocument,
} from "src/performance/schemas/performance.schema";
import {
  ManualPerformance,
  ManualPerformanceDocument,
} from "src/strength/schemas/manual-performance.schema";
import { ResponseService } from "src/common/service/response.service";
import { CreateWorkoutSessionDto } from "./dto/create-workout-session.dto";
import {
  getWeekStart,
  getWeekEnd,
  getDayName,
  getDayKey,
  getIsoWeek,
} from "src/common/utils/week.util";

@Injectable()
export class WorkoutSessionService {
  constructor(
    @InjectModel(WorkoutSession.name)
    private workoutSessionModel: Model<WorkoutSessionDocument>,
    @InjectModel(TrainingPlan.name)
    private trainingPlanModel: Model<TrainingPlanDocument>,
    @InjectModel(Exercise.name)
    private exerciseModel: Model<ExerciseDocument>,
    @InjectModel(User.name)
    private userModel: Model<UserDocument>,
    @InjectModel(Performance.name)
    private performanceModel: Model<PerformanceDocument>,
    @InjectModel(ManualPerformance.name)
    private manualPerformanceModel: Model<ManualPerformanceDocument>,
    private readonly resService: ResponseService,
  ) {}

  /** Default weekly goal when the user has not set `trained_per_week`. */
  private readonly DEFAULT_TRAINING_FREQUENCY = 3;

  /** An "Assisted Bodyweight" style category (assistance reduces the load). */
  private isAssistedCategory(category: any): boolean {
    const en = category?.name?.en;
    return typeof en === "string" && en.toLowerCase().includes("assist");
  }

  /**
   * Builds the "Training done" summary from the saved exercises.
   * - movedWeight = Σ (reps × weight), EXCLUDING assisted-weight exercises
   *   (their load is assistance, not lifted weight).
   * - repetitions = Σ reps, sets = total set count, exercises = exercise count
   */
  private buildWorkoutSummary(
    exercises: any[],
    durationInSeconds: number,
    assistedExerciseIds: Set<string>,
  ) {
    let totalSets = 0;
    let repetitions = 0;
    let movedWeight = 0;

    for (const exercise of exercises || []) {
      const isAssisted = assistedExerciseIds.has(
        exercise.exercise_id?.toString(),
      );
      for (const set of exercise.sets || []) {
        const values = set?.values || {};
        const reps = Number(values.reps) || 0;
        const weight = Number(values.weight) || 0;
        totalSets += 1;
        repetitions += reps;
        // Assisted exercises do not contribute to moved weight.
        if (!isAssisted) movedWeight += reps * weight;
      }
    }

    const minutes = Math.floor(durationInSeconds / 60);
    const seconds = durationInSeconds % 60;

    return {
      durationInSeconds,
      durationLabel: `${minutes}:${String(seconds).padStart(2, "0")} min`,
      movedWeight: Math.round(movedWeight * 100) / 100,
      exercises: (exercises || []).length,
      sets: totalSets,
      repetitions,
    };
  }

  async createWorkoutSession(
    body: CreateWorkoutSessionDto,
    req: Request,
    res: Response,
    user: any,
  ) {
    try {
      // 1. Validate TrainingPlan exists
      const plan = await this.trainingPlanModel.findOne({
        _id: new Types.ObjectId(body.trainingPlan),
        deleted_at: null,
      });

      if (!plan) {
        return this.resService.notFound(res, "data_not_found", req);
      }

      // 2. Look up the exercises + their categories to detect assisted-weight
      //    exercises (assistance must be stored as a negative value).
      const exerciseIds = (body.exercises || []).map(
        (e) => new Types.ObjectId(e.exercise_id),
      );
      const exerciseDocs = await this.exerciseModel
        .find({ _id: { $in: exerciseIds } })
        .populate("category")
        .lean();

      // exerciseId -> assistance field keys (only assisted exercises are added)
      const assistedFieldKeys = new Map<string, string[]>();
      for (const ex of exerciseDocs as any[]) {
        if (this.isAssistedCategory(ex.category)) {
          const keys = ((ex.category?.fields as any[]) || [])
            .map((f) => f.key)
            .filter((k: string) => k && k !== "reps");
          assistedFieldKeys.set(
            ex._id.toString(),
            keys.length ? keys : ["assisted_weight", "assistance"],
          );
        }
      }
      const assistedExerciseIds = new Set(assistedFieldKeys.keys());

      // 3. Convert exercise_id to ObjectId; for assisted exercises force the
      //    assistance value(s) to be negative.
      const exercises = (body.exercises || []).map((exercise) => {
        const keys = assistedFieldKeys.get(exercise.exercise_id?.toString());

        const sets = (exercise.sets || []).map((set: any) => {
          if (!keys) return set;
          const values = { ...(set?.values || {}) };
          for (const key of keys) {
            const raw = values[key];
            if (raw !== undefined && raw !== null && raw !== "") {
              values[key] = -Math.abs(Number(raw));
            }
          }
          return { ...set, values };
        });

        return {
          ...exercise,
          exercise_id: new Types.ObjectId(exercise.exercise_id),
          sets,
        };
      });

      const userId = new Types.ObjectId(user._id);

      // 3. Calculate duration server-side from startedAt -> endedAt
      //    (not trusted from the client).
      const startedAt = new Date(body.startedAt);
      const endedAt = new Date(body.endedAt);
      const durationInSeconds = Math.max(
        0,
        Math.round((endedAt.getTime() - startedAt.getTime()) / 1000),
      );

      // 4. Carry running totals forward from the last session so the most
      //    recent session always holds the latest progress numbers.
      const lastSession = await this.workoutSessionModel
        .findOne({
          user: userId,
          trainingPlan: plan._id,
          deleted_at: null,
        })
        .sort({ createdAt: -1 });

      const sessionNumber = (lastSession?.sessionNumber || 0) + 1;
      const totalDurationInSeconds =
        (lastSession?.totalDurationInSeconds || 0) + durationInSeconds;

      // 5. Save WorkoutSession with the logged-in user's id
      const workoutSession = await this.workoutSessionModel.create({
        user: userId,
        trainingPlan: plan._id,
        dayId: new Types.ObjectId(body.dayId),
        startedAt,
        endedAt,
        durationInSeconds,
        sessionNumber,
        totalDurationInSeconds,
        notes: body.notes,
        exercises,
      });

      // 6. Build the "Training done" summary and return it with the session.
      const summary = this.buildWorkoutSummary(
        exercises,
        durationInSeconds,
        assistedExerciseIds,
      );

      return this.resService.created(
        res,
        { workoutSession, summary },
        "workout_session_created_successfully",
        req,
      );
    } catch (error: any) {
      return this.resService.serverError(
        res,
        "SOMETHING_WENT_WRONG",
        req,
        error.message || error,
      );
    }
  }

  // ==========================================================================
  // Weekly workout streak
  //
  // A streak is a run of consecutive *successful weeks*, not consecutive days.
  // A week (Monday 00:00:00 → Sunday 23:59:59, UTC) is successful when the
  // number of completed workout sessions in it is >= the user's weekly goal
  // (`trained_per_week`). The streak ends at the first failed week.
  // ==========================================================================

  /**
   * Counts completed (non-deleted) workout sessions for a single user in one
   * week. This is the reusable per-week primitive — handy for "this week's
   * progress" style checks. `calculateWeeklyStreak` does NOT call this in a
   * loop; it uses one bulk aggregation instead (see below) so a long streak
   * still costs a single database round-trip.
   *
   * @param userId    the user's ObjectId
   * @param weekStart Monday 00:00:00.000 UTC (from getWeekStart)
   * @param weekEnd   Sunday 23:59:59.999 UTC (from getWeekEnd)
   */
  async countWorkoutsForWeek(
    userId: Types.ObjectId,
    weekStart: Date,
    weekEnd: Date,
  ): Promise<number> {
    return this.workoutSessionModel.countDocuments({
      user: userId,
      deleted_at: null,
      startedAt: { $gte: weekStart, $lte: weekEnd },
    });
  }

  /**
   * Returns the set of days (`YYYY-MM-DD`, UTC) on which the user did at least
   * one workout. We only care *whether* a day had a workout, not how many
   * sessions — the streak is measured in distinct training days per week.
   *
   * A day counts as "trained" when the user did any of the following on it:
   *   - logged a workout session, OR
   *   - recorded a performance entry, OR
   *   - recorded a manual performance entry.
   * So the streak reflects all the ways the user logs training, not just full
   * workout sessions. One aggregation per collection over the whole history;
   * the per-day status below is then a pure in-memory lookup.
   *
   * Uses `$dateTrunc` (MongoDB 5.0+) in UTC so the day boundary matches
   * `getDayKey`.
   */
  private async getWorkoutDays(
    userId: Types.ObjectId,
    range?: { start?: Date; end?: Date },
  ): Promise<Set<string>> {
    // Optional inclusive date window shared by all three collections (each on
    // its own date field). Omitted → whole history.
    const dateFilter: Record<string, Date> = {};
    if (range?.start) dateFilter.$gte = range.start;
    if (range?.end) dateFilter.$lte = range.end;
    const hasRange = Object.keys(dateFilter).length > 0;

    // One trunc-to-day aggregation shared across all three collections. Each
    // uses its own user/date field, so we parameterise them.
    const dayGroup = (dateField: string) => ({
      _id: {
        $dateTrunc: { date: dateField, unit: "day", timezone: "UTC" },
      },
    });

    const sessionMatch: Record<string, unknown> = {
      user: userId,
      deleted_at: null,
    };
    if (hasRange) sessionMatch.startedAt = dateFilter;

    const perfMatch: Record<string, unknown> = { userId, deleted_at: null };
    if (hasRange) perfMatch.date = dateFilter;

    const [sessionRows, performanceRows, manualRows] = await Promise.all([
      this.workoutSessionModel.aggregate<{ _id: Date }>([
        { $match: sessionMatch },
        { $group: dayGroup("$startedAt") },
      ]),
      this.performanceModel.aggregate<{ _id: Date }>([
        { $match: perfMatch },
        { $group: dayGroup("$date") },
      ]),
      this.manualPerformanceModel.aggregate<{ _id: Date }>([
        { $match: perfMatch },
        { $group: dayGroup("$date") },
      ]),
    ]);

    // Union the day keys — a day trained in more than one collection counts once.
    const days = new Set<string>();
    for (const row of [...sessionRows, ...performanceRows, ...manualRows]) {
      days.add(getDayKey(new Date(row._id)));
    }
    return days;
  }

  /**
   * Builds the full Monday→Sunday breakdown for a single week.
   *
   * Carries a 7-entry `days` array (Monday → Sunday) where every date exposes
   * its day name and whether a workout was done, plus the `isStreak` flag —
   * true when the number of *distinct training days* met the user's weekly
   * training frequency (e.g. trained 4+ days when the frequency is 4).
   */
  private buildWeek(
    weekStart: Date,
    workoutDays: Set<string>,
    trainingFrequency: number,
    currentWeekStartMs: number,
  ) {
    const weekEnd = getWeekEnd(weekStart);

    const days = [];
    let daysDone = 0; // distinct days a workout was done this week

    for (let i = 0; i < 7; i++) {
      const date = new Date(weekStart);
      date.setUTCDate(date.getUTCDate() + i);

      const workoutDone = workoutDays.has(getDayKey(date));
      if (workoutDone) daysDone++;

      days.push({
        date, // day at 00:00:00.000 UTC
        dayName: getDayName(date), // "Monday" ... "Sunday"
        workoutDone, // was a workout done that day
      });
    }

    // The week counts towards a streak when the user trained on at least
    // `trainingFrequency` distinct days (5 days trained with a goal of 4 → yes;
    // only 3 days → no). It is about days trained, not sessions logged.
    const isStreak = daysDone >= trainingFrequency;

    return {
      weekStart,
      weekEnd,
      daysDone, // distinct training days this week
      isStreak, // week met the weekly training goal (in days)
      isCurrentWeek: weekStart.getTime() === currentWeekStartMs,
      days, // Monday → Sunday, every date with its status
    };
  }

  /**
   * Every Monday→Sunday week that overlaps the given month, oldest first.
   * Weeks are never split, so the first week may reach into the previous month
   * and the last week into the next month (e.g. July 2026 → Jun 29…Aug 2).
   */
  private buildMonthWeeks(
    workoutDays: Set<string>,
    trainingFrequency: number,
    year: number,
    month: number, // 0-based (0 = January)
    currentWeekStartMs: number,
  ) {
    const monthStart = new Date(Date.UTC(year, month, 1));
    const monthEnd = new Date(Date.UTC(year, month + 1, 0)); // last day of month

    const weeks = [];
    let cursor = getWeekStart(monthStart);

    // Include a week while its Monday still falls on/before the month's last day.
    while (cursor.getTime() <= monthEnd.getTime()) {
      weeks.push(
        this.buildWeek(
          cursor,
          workoutDays,
          trainingFrequency,
          currentWeekStartMs,
        ),
      );
      cursor = new Date(cursor);
      cursor.setUTCDate(cursor.getUTCDate() + 7);
    }

    return weeks;
  }

  /**
   * "Your stats since start" — scans every week from the user's first workout
   * up to the current week and summarises their *streak weeks* (a week that met
   * the training goal, i.e. `isStreak`).
   *
   *  - longestStreakWeeks:    most consecutive streak weeks in a row
   *  - longestStreakWorkouts: distinct training days within that longest run
   *  - totalStreakWorkouts:   distinct training days across ALL streak weeks
   *
   * A "run" is a maximal block of back-to-back streak weeks (a missed week ends
   * it). Returns `null` unless the user has a genuine multi-week streak (2+
   * weeks in a row) — the card only makes sense then.
   */
  private computeStreakStats(
    workoutDays: Set<string>,
    trainingFrequency: number,
    currentWeekStartMs: number,
  ) {
    if (workoutDays.size === 0) return null;

    // Earliest day the user ever trained → the first week to scan.
    let earliest = "";
    for (const key of workoutDays) {
      if (earliest === "" || key < earliest) earliest = key;
    }

    const currentWeekStart = getWeekStart(new Date());
    let cursor = getWeekStart(new Date(`${earliest}T00:00:00.000Z`));

    let longestStreakWeeks = 0;
    let longestStreakWorkouts = 0;
    let totalStreakWorkouts = 0;

    // Running totals for the current back-to-back run of streak weeks.
    let runWeeks = 0;
    let runWorkouts = 0;

    // Scan every week from the first up to and including the current week.
    while (cursor.getTime() <= currentWeekStart.getTime()) {
      const week = this.buildWeek(
        cursor,
        workoutDays,
        trainingFrequency,
        currentWeekStartMs,
      );

      if (week.isStreak) {
        runWeeks++;
        runWorkouts += week.daysDone;
        totalStreakWorkouts += week.daysDone;

        // Track the longest run; on a tie in weeks keep the one with more workouts.
        if (
          runWeeks > longestStreakWeeks ||
          (runWeeks === longestStreakWeeks &&
            runWorkouts > longestStreakWorkouts)
        ) {
          longestStreakWeeks = runWeeks;
          longestStreakWorkouts = runWorkouts;
        }
      } else {
        // Missed week — the run is broken, start counting fresh.
        runWeeks = 0;
        runWorkouts = 0;
      }

      cursor = new Date(cursor);
      cursor.setUTCDate(cursor.getUTCDate() + 7);
    }

    // Only surfaced when there's a real "X weeks in a row" streak.
    if (longestStreakWeeks < 2) return null;

    return {
      longestStreakWeeks, // e.g. 7 → "7 weeks in a row"
      longestStreakWorkouts, // e.g. 32 → trained 32 times in that streak
      totalStreakWorkouts, // e.g. 42 → total workouts across all streaks
    };
  }

  /**
   * GET /workout-session/streak — weekly streak + a Monday→Sunday breakdown of
   * every week in the month. Optional `?month=1-12&year=YYYY` browse other
   * months (defaults to the current month).
   */
  async getWeeklyStreak(
    req: Request,
    res: Response,
    user: any,
    monthQuery?: string,
    yearQuery?: string,
  ) {
    try {
      const userId = new Types.ObjectId(user._id);

      const userDoc = await this.userModel
        .findById(userId)
        .select("trained_per_week")
        .lean();

      const trainingFrequency =
        userDoc?.trained_per_week && userDoc.trained_per_week > 0
          ? userDoc.trained_per_week
          : this.DEFAULT_TRAINING_FREQUENCY;

      // Which month to render (defaults to the current UTC month/year).
      const now = new Date();

      const parsedYear = Number(yearQuery);
      const year =
        yearQuery && Number.isFinite(parsedYear)
          ? parsedYear
          : now.getUTCFullYear();

      const parsedMonth = Number(monthQuery);
      // month is 1-based in the API; clamp to a valid 1-12 range.
      const monthParam =
        monthQuery && Number.isFinite(parsedMonth)
          ? Math.min(12, Math.max(1, parsedMonth))
          : now.getUTCMonth() + 1;
      const month = monthParam - 1; // 0-based for Date math

      const currentWeekStartMs = getWeekStart(now).getTime();

      // Single aggregation for the whole history; everything below is in-memory.
      const workoutDays = await this.getWorkoutDays(userId);

      const monthWeeks = this.buildMonthWeeks(
        workoutDays,
        trainingFrequency,
        year,
        month,
        currentWeekStartMs,
      );

      // Completed weeks in the month being viewed — i.e. how many of the weeks
      // below met the training goal (matches the `isStreak: true` entries).
      const completedStreak = monthWeeks.filter((w) => w.isStreak).length;

      // "Your stats since start" — all-time streak summary (null until the user
      // has at least one multi-week streak). Independent of the month viewed.
      const stats = this.computeStreakStats(
        workoutDays,
        trainingFrequency,
        currentWeekStartMs,
      );

      // Only expose whether the week is a streak plus the per-day done/not-done
      // flags — no session counts or required/remaining totals.
      const weeks = monthWeeks.map((w) => ({
        weekStart: w.weekStart,
        weekEnd: w.weekEnd,
        isStreak: w.isStreak, // did the week meet the training goal (in days)
        days: w.days.map((d) => ({
          date: d.date,
          dayName: d.dayName,
          workoutDone: d.workoutDone, // was a workout done today
        })),
      }));

      console.log(stats , "stats")

      return this.resService.success(res, "success", req, {
        trainingFrequency,
        completedStreak,
        stats, // "since start" streak stats, or null when < 2 weeks in a row
        year,
        month: monthParam, // 1-based
        weeks, // every week of the month, oldest first
      });
    } catch (error: any) {
      return this.resService.serverError(
        res,
        "SOMETHING_WENT_WRONG",
        req,
        error.message || error,
      );
    }
  }

  /**
   * Reusable streak summary for a user — no HTTP concerns, so other modules
   * (e.g. the progress dashboard) can embed it.
   *
   * Returns the overall streak, this week's progress against the user's weekly
   * goal, and the current month's weeks (for the progress-bar segments: which
   * weeks of the month have been completed).
   */
  async getStreakSummary(userId: Types.ObjectId) {
    const userDoc = await this.userModel
      .findById(userId)
      .select("trained_per_week")
      .lean();

    const trainingFrequency =
      userDoc?.trained_per_week && userDoc.trained_per_week > 0
        ? userDoc.trained_per_week
        : this.DEFAULT_TRAINING_FREQUENCY;

    const now = new Date();
    const currentWeekStart = getWeekStart(now);
    const currentWeekStartMs = currentWeekStart.getTime();

    const workoutDays = await this.getWorkoutDays(userId);

    // This week's progress toward the goal.
    const thisWeek = this.buildWeek(
      currentWeekStart,
      workoutDays,
      trainingFrequency,
      currentWeekStartMs,
    );

    // Current month broken into Monday→Sunday weeks (progress-bar segments).
    const monthWeeks = this.buildMonthWeeks(
      workoutDays,
      trainingFrequency,
      now.getUTCFullYear(),
      now.getUTCMonth(),
      currentWeekStartMs,
    );
    const weeks = monthWeeks.map((w) => ({
      weekStart: w.weekStart,
      weekEnd: w.weekEnd,
      daysDone: w.daysDone, // distinct days trained that week
      required: trainingFrequency, // weekly goal (days)
      isCompleted: w.isStreak, // segment filled when the goal was met
      isCurrentWeek: w.isCurrentWeek,
    }));
    const monthCompletedWeeks = weeks.filter((w) => w.isCompleted).length;

    return {
      completedStreak: monthCompletedWeeks, // completed weeks in the current month
      trainingFrequency, // user's weekly goal
      currentWeekWorkouts: thisWeek.daysDone, // days trained this week
      currentWeekRemaining: Math.max(0, trainingFrequency - thisWeek.daysDone), // days still needed this week
      currentWeekCompleted: thisWeek.isStreak, // goal met this week?
      monthCompletedWeeks, // completed weeks in the current month
      monthTotalWeeks: weeks.length, // total weeks in current month
      weeks, // per-week segments for the current month
    };
  }

  /**
   * Reusable training-frequency bars for the last `weeksWindow` weeks (default
   * 6), oldest → newest, ending at the current week — the exact same graph the
   * "weeks" view of GET /workout-session/training-frequency produces by default
   * (`label`, `week_start`, `week_end`, `sessions`), where `sessions` is the
   * bar height = number of distinct days trained that week.
   *
   * Empty weeks show 0, UTC throughout. No HTTP concerns, so the progress
   * dashboard can embed the same graph.
   */
  async getRecentFrequencyWeeks(userId: Types.ObjectId, weeksWindow = 6) {
    const currentWeekStart = getWeekStart(new Date());
    return this.buildFrequencyWeeks(userId, currentWeekStart, weeksWindow);
  }

  // ==========================================================================
  // Training-frequency screen (bars per week + month calendar + lifetime stats)
  // ==========================================================================

  /**
   * "Your stats since start": total trained days and the average number of days
   * trained per active week (first trained day's week → current week), counting
   * a day once no matter how many sessions / performance / manual-performance
   * entries it holds. Sourced from all three collections (see `getWorkoutDays`).
   */
  private async getLifetimeFrequencyStats(userId: Types.ObjectId) {
    // Union of trained days across workout sessions, performance and manual
    // performance — each day counted once.
    const days = await this.getWorkoutDays(userId);

    const totalDays = days.size;

    // Earliest trained day → start of the active window.
    let earliest = "";
    for (const key of days) {
      if (earliest === "" || key < earliest) earliest = key;
    }
    const firstDate = earliest ? new Date(`${earliest}T00:00:00.000Z`) : null;

    let weeksActive = 0;
    if (firstDate) {
      const firstWeekStart = getWeekStart(firstDate);
      const currentWeekStart = getWeekStart(new Date());
      const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
      weeksActive =
        Math.floor(
          (currentWeekStart.getTime() - firstWeekStart.getTime()) / WEEK_MS,
        ) + 1;
    }

    // average number of days trained per active week
    const averagePerWeek =
      weeksActive > 0 ? Math.round((totalDays / weeksActive) * 10) / 10 : 0;

    // total_sessions = total distinct trained days across all sources.
    return { average_per_week: averagePerWeek, total_sessions: totalDays };
  }

  /**
   * "Weeks" tab — one bar per week for the last `weeksWindow` weeks (default 6),
   * oldest → newest. `sessions` is the bar height = number of distinct days
   * trained that week (two sessions on the same day count once); `label` is the
   * ISO week ("W20"). One aggregation, then in-memory bucketing so empty weeks
   * show 0.
   */
  private async buildFrequencyWeeks(
    userId: Types.ObjectId,
    currentWeekStart: Date,
    weeksWindow: number,
  ) {
    const windowStart = new Date(currentWeekStart);
    windowStart.setUTCDate(windowStart.getUTCDate() - (weeksWindow - 1) * 7);

    // Distinct trained days in the window, unioned across workout sessions,
    // performance and manual performance.
    const days = await this.getWorkoutDays(userId, { start: windowStart });

    const weeks = [];
    const cursor = new Date(windowStart);
    for (let i = 0; i < weeksWindow; i++) {
      const weekStart = new Date(cursor);

      // bar height = number of distinct days trained that week
      let sessions = 0;
      for (let d = 0; d < 7; d++) {
        const date = new Date(weekStart);
        date.setUTCDate(date.getUTCDate() + d);
        if (days.has(getDayKey(date))) sessions++;
      }

      weeks.push({
        label: `W${getIsoWeek(weekStart)}`, // x-axis label
        week_start: weekStart,
        week_end: getWeekEnd(weekStart),
        sessions, // bar height = days trained
      });
      cursor.setUTCDate(cursor.getUTCDate() + 7);
    }
    return weeks;
  }

  /**
   * "Calendar" tab — every day of the given month with whether a workout was
   * done, so trained days can be highlighted. `leading_offset` is how many
   * empty cells precede day 1 in a Monday-first grid.
   */
  private async buildFrequencyCalendar(
    userId: Types.ObjectId,
    year: number,
    month: number, // 0-based
  ) {
    const monthStart = new Date(Date.UTC(year, month, 1));
    const monthEnd = new Date(Date.UTC(year, month + 1, 0, 23, 59, 59, 999));

    // Trained days in the month, unioned across workout sessions, performance
    // and manual performance.
    const trainedDays = await this.getWorkoutDays(userId, {
      start: monthStart,
      end: monthEnd,
    });

    const daysInMonth = monthEnd.getUTCDate();
    const days = [];
    for (let d = 1; d <= daysInMonth; d++) {
      const date = new Date(Date.UTC(year, month, d));
      const weekday = date.getUTCDay() === 0 ? 7 : date.getUTCDay(); // 1=Mon..7=Sun
      days.push({
        date,
        // day: d,
        weekday, // 1 = Monday ... 7 = Sunday
         day_name: getDayName(date),
        workout_done: trainedDays.has(getDayKey(date)), // highlight the day
     });
    }

    const leadingOffset = days.length ? days[0].weekday - 1 : 0;

    return {
      year,
      month: month + 1, // 1-based
      days_in_month: daysInMonth,
      // leading_offset: leadingOffset, // empty cells before day 1 (Mon-first grid)
      days,
    };
  }

  /**
   * GET /workout-session/training-frequency
   *
   * Query params:
   *   view  = "weeks" | "calendar"  (default "weeks")
   *   weeks = window size for the bar chart, 1–52 (default 6) — weeks view
   *   month = 1–12, year = YYYY      (default current month) — calendar view
   *
   * Always returns the shared "stats since start". Only the requested view's
   * data is populated; the other is null.
   */
  async getTrainingFrequency(
    req: Request,
    res: Response,
    user: any,
    viewQuery?: string,
    weeksQuery?: string,
    monthQuery?: string,
    yearQuery?: string,
  ) {
    try {
      const userId = new Types.ObjectId(user._id);
      const view = viewQuery === "calendar" ? "calendar" : "weeks";
      const stats = await this.getLifetimeFrequencyStats(userId);
      const userDoc = await this.userModel
        .findById(userId)
        .select("trained_per_week")
        .lean();
      const training_frequency = userDoc?.trained_per_week ?? null;

      if (view === "calendar") {
        const now = new Date();
        const parsedYear = Number(yearQuery);
        const year =
          yearQuery && Number.isFinite(parsedYear)
            ? parsedYear
            : now.getUTCFullYear();

        const parsedMonth = Number(monthQuery);
        const monthParam =
          monthQuery && Number.isFinite(parsedMonth)
            ? Math.min(12, Math.max(1, parsedMonth))
            : now.getUTCMonth() + 1;

        const calendar = await this.buildFrequencyCalendar(
          userId,
          year,
          monthParam - 1,
        );

        return this.resService.success(res, "success", req, {
          view,
          training_frequency,
          stats,
          weeks: null,
          calendar,
        });
      }

      // weeks view
      const parsedWeeks = Number(weeksQuery);
      const weeksWindow =
        weeksQuery && Number.isFinite(parsedWeeks)
          ? Math.min(52, Math.max(1, Math.trunc(parsedWeeks)))
          : 6;

      const currentWeekStart = getWeekStart(new Date());
      const weeks = await this.buildFrequencyWeeks(
        userId,
        currentWeekStart,
        weeksWindow,
      );

      return this.resService.success(res, "success", req, {
        view,
        weeks_window: weeksWindow,
        training_frequency,
        stats,
        weeks,
        calendar: null,
      });
    } catch (error: any) {
      return this.resService.serverError(
        res,
        "SOMETHING_WENT_WRONG",
        req,
        error.message || error,
      );
    }
  }
  
}
