import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model, Types } from "mongoose";
import { Request, Response } from "express";
import { ResponseService } from "src/common/service/response.service";
import {
  TrainingPlan,
  TrainingPlanDocument,
} from "src/training-plan/schemas/training-plan.schema";
import {
  Performance,
  PerformanceDocument,
} from "src/performance/schemas/performance.schema";
import { MacroLog, MacroLogDocument } from "src/macros/schemas/macro.schema";
import {
  MacroTarget,
  MacroTargetDocument,
} from "src/macros/schemas/target-macro.schema";
import { WeightLog, WeightLogDocument } from "src/weight/schemas/weight.schema";
import { UpdateGoalDto } from "src/training/dto/update-goal.dto";
import { StrengthService } from "src/strength/strength.service";
import { WorkoutSessionService } from "src/workout-session/workout-session.service";
import {
  Challenges,
  ChallengesDocument,
} from "src/challenges/schemas/challenges.schema";
import {
  ChallengesUser,
  ChallengesUserDocument,
} from "src/challenges/schemas/challengesUser.schema";
import { ChallengeProgressStatus } from "src/common/enums/challenges.enum";
import { buildChallengeWidget } from "src/common/utils/challengeProgress.helper";
import { AchievementsService } from "src/challenges/achievements.service";

@Injectable()
export class ProgressService {
  constructor(
    @InjectModel(TrainingPlan.name)
    private trainingPlanModel: Model<TrainingPlanDocument>,
    @InjectModel(Performance.name)
    private performanceModel: Model<PerformanceDocument>,
    @InjectModel(MacroLog.name) private macroLogModel: Model<MacroLogDocument>,
    @InjectModel(MacroTarget.name)
    private macroTargetModel: Model<MacroTargetDocument>,
    @InjectModel(WeightLog.name)
    private weightLogModel: Model<WeightLogDocument>,
    @InjectModel(Challenges.name)
    private challengesModel: Model<ChallengesDocument>,
    @InjectModel(ChallengesUser.name)
    private challengesUserModel: Model<ChallengesUserDocument>,
    private readonly strengthService: StrengthService,
    private readonly workoutSessionService: WorkoutSessionService,
    private readonly resService: ResponseService,
    private readonly achievementsService: AchievementsService,
  ) {}

  // GET /progress/dashboard
  async getDashboard(req: Request, res: Response, user: any) {
    const lang = req.headers["accept-language"] === "de" ? "de" : "en";
    const userId = new Types.ObjectId(user._id);

    try {
      // capture any completion detected only now (e.g. a habit that ran to its
      // period end) into the achievements collection before we read it.
      await this.achievementsService.reconcileUser(userId);

      const [
        trainingPlans,
        personalRecords,
        macroOverview,
        weightHistory,
        strengthProgression,
        streak,
        trainingFrequency,
        challenges,
        achievements,
      ] = await Promise.all([
        this.fetchTrainingPlans(userId, lang),
        this.fetchPersonalRecords(userId, lang),
        this.fetchMacroOverview(userId),
        this.fetchWeightHistory(userId),
        this.fetchStrengthProgression(userId, lang),
        this.fetchStreak(userId),
        this.workoutSessionService.getRecentFrequencyWeeks(userId),
        this.fetchChallenges(userId, lang),
        this.fetchAchievements(userId, lang),
      ]);

      return this.resService.success(res, "success", req, {
        training_plans: trainingPlans,
        personal_records: personalRecords,
        macros: macroOverview,
        weight: weightHistory,
        strength_progression: strengthProgression,
        streak,
        training_frequency: trainingFrequency,
        challenges,
        achievements,
      });
    } catch (error: any) {
      return this.resService.serverError(
        res,
        "Failed to fetch dashboard",
        req,
        error.message || error,
      );
    }
  }

  // ─── Challenges ───────────────────────────────────────────────────────────
  // The three challenge widgets on the progress page — one per type
  // (habit / performance / volume). When the user has joined several of the
  // same type, the one they joined FIRST wins. Returns a keyed object so the
  // frontend can place each widget without searching the array; a type the user
  // hasn't joined comes back as null.
  private async fetchChallenges(userId: Types.ObjectId, lang: string) {
    // IMPORTANT: a challenge's "failed" (and "completed") status is COMPUTED at
    // read time by the widget and is not necessarily persisted, so we must NOT
    // filter by the stored status — a failed challenge can still be stored as
    // ACTIVE. Fetch every live membership and let the computed status decide.
    const memberships = await this.challengesUserModel
      .find({ user_id: userId, deleted_at: null })
      // earliest joined first -> the "initial" one of each type is picked
      .sort({ started_at: 1, createdAt: 1 })
      .lean();

    const result: Record<string, any> = {
      habit: null,
      performance: null,
      volume: null,
    };
    if (!memberships.length) return result;

    const challenges = await this.challengesModel
      .find({ _id: { $in: memberships.map((m) => m.challenge_id) } })
      .lean();
    const byId = new Map(challenges.map((c) => [String(c._id), c]));

    const now = new Date();

    for (const membership of memberships) {
      const challenge = byId.get(String(membership.challenge_id));
      if (!challenge) continue;

      // fall back to the membership's denormalized mode if the challenge is untyped
      const mode = (challenge as any).type ?? membership.mode;
      if (!mode) continue;

      const widget = buildChallengeWidget(
        { ...challenge, type: mode },
        membership,
        lang,
        now
      );
      const status = widget.status; // live-computed: active / failed / completed

      // surface active or failed only — a completed challenge is done with.
      if (
        status !== ChallengeProgressStatus.ACTIVE &&
        status !== ChallengeProgressStatus.FAILED
      ) {
        continue;
      }

      const existing = result[mode];
      // prefer an ACTIVE challenge for the slot, but fall back to a FAILED one
      // so the user still sees a failure when they have no active challenge of
      // that type. Earliest-joined wins within the same priority.
      if (!existing) {
        result[mode] = widget;
      } else if (
        existing.status !== ChallengeProgressStatus.ACTIVE &&
        status === ChallengeProgressStatus.ACTIVE
      ) {
        result[mode] = widget;
      }
    }

    return result;
  }

  // ─── Achievements ─────────────────────────────────────────────────────────
  // Read straight from the dedicated `achievements` collection (kept fresh by
  // reconcileUser above). One row per challenge, newest first.
  private async fetchAchievements(userId: Types.ObjectId, lang: string) {
    const { items } = await this.achievementsService.listForUser(
      userId,
      lang,
      1,
      100
    );
    return items;
  }

  // ─── Training Plans ───────────────────────────────────────────────────────
  // Returns admin-created plans + plans created by this user
  private async fetchTrainingPlans(userId: Types.ObjectId, lang: string) {
    const plans = await this.trainingPlanModel.aggregate([
      {
        $match: {
          deleted_at: null,
          status: 1,
          $or: [
            { createdByType: "admin" },
            { createdBy: userId, createdByType: "user" },
          ],
        },
      },
      {
        $project: {
          _id: 1,
          name: `$name.${lang}`,
          description: `$description.${lang}`,
          subtitle: `$subtitle.${lang}`,
          attachment: 1,
          createdByType: 1,
          totalDays: { $size: { $ifNull: ["$days", []] } },
          createdAt: 1,
        },
      },
      {
        $addFields: {
          // user plans: newest first; admin plans: keep original (oldest first)
          _sortDate: {
            $cond: [
              { $eq: ["$createdByType", "user"] },
              { $multiply: [{ $toLong: "$createdAt" }, -1] },
              { $toLong: "$createdAt" },
            ],
          },
        },
      },
      { $sort: { createdByType: -1, _sortDate: 1 } },
      { $project: { _sortDate: 0 } },
    ]);
    return plans;
  }

  // ─── Strength Progression ─────────────────────────────────────────────────
  // Same "Strength progression" chart as GET /strength/graph/:exerciseId, built
  // for the user's most-recently-logged exercise over the last month (30d) so it
  // can be embedded in the dashboard / shared profile. Null when nothing logged.
  private async fetchStrengthProgression(userId: Types.ObjectId, lang: string) {
    const exerciseId =
      await this.strengthService.getMostRecentExerciseId(userId);
      console.log("Most recent exerciseId:", exerciseId);
    if (!exerciseId) return null;

    const period = "30d" as const;
    const [graphData, exercise] = await Promise.all([
      this.strengthService.buildStrengthGraphData(exerciseId, userId, period),
      this.strengthService.getExerciseIdentity(exerciseId, lang),
    ]);
    if (!graphData) return null;

    return { exercise, period, ...graphData };
  }

  // ─── Weekly Streak ────────────────────────────────────────────────────────
  // Powers the "streak" card on the progress page (flame + "N weeks"). A streak
  // is a run of consecutive weeks where the user trained on at least
  // `training_frequency` distinct days. Reuses WorkoutSessionService so the
  // logic lives in one place.
  private async fetchStreak(userId: Types.ObjectId) {
    const s = await this.workoutSessionService.getStreakSummary(userId);

    return {
      completed_streak: s.completedStreak, // consecutive finished weeks of goal met (current week excluded)
      training_frequency: s.trainingFrequency, // user's weekly goal (days)
      current_week_workouts: s.currentWeekWorkouts, // days trained this week
      current_week_remaining: s.currentWeekRemaining, // days still needed this week
      current_week_completed: s.currentWeekCompleted, // goal met this week?
      // month_completed_streak: s.monthCompletedWeeks, // completed weeks this month
      // month_total_weeks: s.monthTotalWeeks, // total weeks in current month
      // weeks: s.weeks.map((w) => ({
      //   // one entry per week → drives the progress-bar segments
      //   week_start: w.weekStart,
      //   week_end: w.weekEnd,
      //   days_done: w.daysDone,
      //   required: w.required,
      //   is_completed: w.isCompleted,
      //   is_current_week: w.isCurrentWeek,
      // })),
    };
  }

  // ─── Personal Records ─────────────────────────────────────────────────────
  // All-time BEST set per exercise (not the most recent session).
  // The "best" set is chosen by the exercise category's primary metric:
  //   Weight & Reps / Weighted Bodyweight → highest weight (or added_weight)
  //   Reps Only                           → most reps
  //   Time / Hold Time                    → longest duration
  //   Distance & Time                     → longest distance
  //   Assisted Bodyweight                 → least assistance (least-negative assisted_weight)
  // Because each set only populates the fields relevant to its category, the
  // primary metric is the first non-null value in priority order. Sorting by
  // that score (then by most recent date on ties) yields the record set.
  private async fetchPersonalRecords(userId: Types.ObjectId, lang: string) {
    const records = await this.performanceModel.aggregate([
      { $match: { userId, deleted_at: null } },
      {
        // score = the populated primary metric for this record.
        // assisted_weight is negative, so the least-negative value (least
        // assistance) wins, which is the meaningful record there.
        $addFields: {
          score: {
            $ifNull: [
              "$weight",
              {
                $ifNull: [
                  "$added_weight",
                  {
                    $ifNull: [
                      "$distance",
                      {
                        $ifNull: [
                          "$duration",
                          { $ifNull: ["$assisted_weight", "$reps"] },
                        ],
                      },
                    ],
                  },
                ],
              },
            ],
          },
        },
      },
      // Highest score first; on a tie keep the most recent occurrence of the record.
      { $sort: { exercise_id: 1, score: -1, updatedAt: -1 } },
      {
        $group: {
          _id: "$exercise_id",
          record_date: { $first: "$date" },
          set: {
            $first: {
              reps: "$reps",
              weight: "$weight",
              duration: "$duration",
              distance: "$distance",
              assisted_weight: "$assisted_weight",
              added_weight: "$added_weight",
            },
          },
        },
      },
      {
        $lookup: {
          from: "exercises",
          localField: "_id",
          foreignField: "_id",
          as: "exercise",
        },
      },
      { $unwind: "$exercise" },
      {
        $lookup: {
          from: "exercisecategories",
          localField: "exercise.category",
          foreignField: "_id",
          as: "exercise.category",
        },
      },
      {
        $unwind: {
          path: "$exercise.category",
          preserveNullAndEmptyArrays: true,
        },
      },
      {
        $project: {
          _id: 0,
          category: {
            $cond: [
              { $ifNull: ["$exercise.category._id", false] },
              {
                _id: "$exercise.category._id",
                name: `$exercise.category.name.${lang}`,
              },
              null,
            ],
          },
          exercise: {
            _id: "$exercise._id",
            name: `$exercise.name.${lang}`,
          },
          record_date: 1,
          set: 1,
        },
      },
      // Most recently achieved records first.
      { $sort: { record_date: -1 } },
    ]);
    return records;
  }

  // ─── Macro Overview ───────────────────────────────────────────────────────
  // Today's consumed vs target with fulfillment percentages
  private async fetchMacroOverview(userId: any) {
    const today = new Date();
    const startOfDay = new Date(today);
    startOfDay.setHours(0, 0, 0, 0);
    const endOfDay = new Date(today);
    endOfDay.setHours(23, 59, 59, 999);

    const [target, log] = await Promise.all([
      this.macroTargetModel
        .findOne({
          userId,
          effectiveDate: { $lte: endOfDay },
          deleted_at: null,
        })
        .sort({ effectiveDate: -1 })
        .lean(),
      this.macroLogModel
        .findOne({
          userId,
          date: { $gte: startOfDay, $lte: endOfDay },
          deleted_at: null,
        })
        .lean(),
    ]);

    const targets = {
      calories: target?.calories ?? 0,
      protein: target?.protein ?? 0,
      carbohydrates: target?.carbohydrates ?? 0,
      fats: target?.fats ?? 0,
    };
    const consumed = {
      calories: log?.calories ?? 0,
      protein: log?.protein ?? 0,
      carbohydrates: log?.carbohydrates ?? 0,
      fats: log?.fats ?? 0,
    };

    const pct = (c: number, t: number) =>
      t > 0 ? Math.round((c / t) * 100) : 0;

    const overallPct = Math.round(
      (pct(consumed.calories, targets.calories) +
        pct(consumed.protein, targets.protein) +
        pct(consumed.carbohydrates, targets.carbohydrates) +
        pct(consumed.fats, targets.fats)) /
        4,
    );

    return {
      overall_percent: overallPct,
      calories: {
        target: targets.calories,
        consumed: consumed.calories,
        percent: pct(consumed.calories, targets.calories),
      },
      protein: {
        target: targets.protein,
        consumed: consumed.protein,
        percent: pct(consumed.protein, targets.protein),
      },
      carbohydrates: {
        target: targets.carbohydrates,
        consumed: consumed.carbohydrates,
        percent: pct(consumed.carbohydrates, targets.carbohydrates),
      },
      fats: {
        target: targets.fats,
        consumed: consumed.fats,
        percent: pct(consumed.fats, targets.fats),
      },
    };
  }

  // ─── Weight History ───────────────────────────────────────────────────────
  // Last 30 days in weekly buckets — same format as GET /weight/history?period=30d
  private async fetchWeightHistory(userId: Types.ObjectId) {
    const now = new Date();
    now.setHours(23, 59, 59, 999);
    const start30d = new Date(now);
    start30d.setDate(now.getDate() - 29);
    start30d.setHours(0, 0, 0, 0);

    const [logs, firstEntry, lastEntry] = await Promise.all([
      this.weightLogModel
        .find({
          userId,
          deleted_at: null,
          recordedAt: { $gte: start30d, $lte: now },
        })
        .sort({ recordedAt: 1 })
        .lean(),
      this.weightLogModel
        .findOne({ userId, deleted_at: null })
        .sort({ recordedAt: 1 })
        .lean(),
      this.weightLogModel
        .findOne({ userId, deleted_at: null })
        .sort({ recordedAt: -1 })
        .lean(),
    ]);

    // Build weekly buckets for the 30d window (same as weight service "30d" period)
    const bucketList: Array<{ key: string; label: string }> = [];
    const bucketMap = new Map<string, number[]>();
    const cursor = new Date(start30d);
    while (cursor <= now) {
      const weekNum = this.isoWeek(cursor);
      const key = `${cursor.getFullYear()}-W${weekNum}`;
      if (!bucketMap.has(key)) {
        bucketList.push({ key, label: `Week ${weekNum}` });
        bucketMap.set(key, []);
      }
      cursor.setDate(cursor.getDate() + 1);
    }
    for (const log of logs) {
      const d = new Date(log.recordedAt);
      const key = `${d.getFullYear()}-W${this.isoWeek(d)}`;
      bucketMap.get(key)?.push(log.weight);
    }
    const graph = bucketList.map(({ key, label }) => {
      const values = bucketMap.get(key) ?? [];
      const value = values.length
        ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) /
          10
        : null;
      return { label, value };
    });

    const startingWeight = firstEntry?.weight ?? null;
    const currentWeight = lastEntry?.weight ?? null;
    const periodWeights = logs.map((l) => l.weight);
    const minimumWeight = periodWeights.length
      ? Math.min(...periodWeights)
      : null;
    let changeSinceStart: number | null = null;
    if (startingWeight && currentWeight) {
      changeSinceStart = Math.round(
        ((currentWeight - startingWeight) / startingWeight) * 100,
      );
    }

    return {
      graph,
      stats: { startingWeight, currentWeight, changeSinceStart, minimumWeight },
    };
  }

  private isoWeek(date: Date): number {
    const d = new Date(
      Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()),
    );
    const day = d.getUTCDay() || 7;
    d.setUTCDate(d.getUTCDate() + 4 - day);
    const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
    return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
  }
}
