import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model, PipelineStage, Types } from "mongoose";
import {
  TrainingPlan,
  TrainingPlanDocument,
} from "./schemas/training-plan.schema";
import { CreateTrainingPlanDto } from "./dto/create-training-plan.dto";
import { UpdateTrainingPlanDto } from "./dto/update-training-plan.dto";
import { Request, Response } from "express";
import { ResponseService } from "src/common/service/response.service";
import { RoleEnum } from "src/common/enums/constant.enum";
import {
  WorkoutSession,
  WorkoutSessionDocument,
} from "src/workout-session/schemas/workout-session.schema";

@Injectable()
export class TrainingPlanService {
  constructor(
    @InjectModel(TrainingPlan.name)
    private trainingPlanModel: Model<TrainingPlanDocument>,
    @InjectModel(WorkoutSession.name)
    private workoutSessionModel: Model<WorkoutSessionDocument>,
    private readonly resService: ResponseService,
  ) {}

  private pickLang(
    obj: Record<string, string> | undefined | null,
    lang: string,
  ) {
    if (!obj) return null;
    return obj[lang] ?? obj.en ?? null;
  }

  private formatDuration(totalSeconds: number) {
    const safe = Math.max(0, Math.floor(totalSeconds || 0));
    const hours = Math.floor(safe / 3600);
    const minutes = Math.floor((safe % 3600) / 60);
    return {
      totalSeconds: safe,
      hours,
      minutes,
      // label: `${hours}h ${minutes}min`,
    };
  }

  /** Monday 00:00 (UTC) of the week containing `now`. */
  private startOfWeekUTC(now: Date): Date {
    const d = new Date(
      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()),
    );
    const weekday = d.getUTCDay(); // 0=Sun ... 1=Mon ... 6=Sat
    const daysSinceMonday = weekday === 0 ? 6 : weekday - 1;
    d.setUTCDate(d.getUTCDate() - daysSinceMonday);
    return d;
  }

  /**
   * Derives session progress from completed WorkoutSession history.
   *
   * - Totals (session number + training time) are read from the most recent
   *   completed session, where they are denormalized on save.
   * - The next DAY resets every Monday (start of week): it is chosen from the
   *   number of completed sessions in the current week, cycling through the
   *   plan's days. Session numbers never reset (they are global).
   */
  private async buildSessionProgress(
    plan: any,
    userId: Types.ObjectId,
    lang: string,
  ) {
    const days: any[] = plan.days || [];
    const totalDays = days.length;

    const baseFilter = {
      user: userId,
      trainingPlan: plan._id,
      deleted_at: null,
    };

    const startOfWeek = this.startOfWeekUTC(new Date());

    const [lastCompleted, sessionsThisWeek, doneDayIds] = await Promise.all([
      // Latest completed session carries the denormalized running totals.
      this.workoutSessionModel.findOne(baseFilter).sort({ createdAt: -1 }),
      // How many days already done this week -> drives the next day.
      this.workoutSessionModel.countDocuments({
        ...baseFilter,
        startedAt: { $gte: startOfWeek },
      }),
      // Which days have been completed this week -> used to flag each day.
      this.workoutSessionModel.distinct("dayId", {
        ...baseFilter,
        startedAt: { $gte: startOfWeek },
      }),
    ]);

    const completedSessions = lastCompleted?.sessionNumber || 0;
    const totalDuration = lastCompleted?.totalDurationInSeconds || 0;

    // Week resets to Day 1 on Monday: 0 done this week -> Day 1, etc. (cycling).
    const nextIndex = totalDays > 0 ? sessionsThisWeek % totalDays : 0;
    const nextDay = totalDays > 0 ? days[nextIndex] : null;

    return {
      completedSessions,
      nextSessionNumber: completedSessions + 1,
      trainingTime: this.formatDuration(totalDuration),
      weekStart: startOfWeek,
      // sessionsThisWeek,
      // string ids of days completed in the current week
      doneDayIds: (doneDayIds || []).map((d: any) => d?.toString()),
      nextDay: nextDay
        ? {
            dayId: nextDay._id,
            dayNo: nextIndex + 1,
            name: this.pickLang(nextDay.name, lang),
            exerciseCount: nextDay.exercises?.length || 0,
          }
        : null,
    };
  }

  // Shared aggregation stages to unwind days/exercises, lookup & populate exercise name.
  // When `dayId` is provided, only that single day is processed (the rest are
  // skipped before the exercise lookup runs).
  private exerciseLookupStages(
    lang: string,
    dayId?: Types.ObjectId,
  ): PipelineStage[] {
    return [
      {
        $unwind: {
          path: "$days",
          preserveNullAndEmptyArrays: true,
          includeArrayIndex: "dayIndex",
        },
      },
      ...(dayId
        ? [{ $match: { "days._id": new Types.ObjectId(dayId) } } as PipelineStage]
        : []),
      {
        $unwind: {
          path: "$days.exercises",
          preserveNullAndEmptyArrays: true,
          includeArrayIndex: "exerciseIndex",
        },
      },
      {
        $lookup: {
          from: "exercises",
          let: { exId: "$days.exercises.exercise_id" },
          pipeline: [
            { $match: { $expr: { $eq: ["$_id", "$$exId"] } } },
            // Resolve targeted body parts and localize their names.
            {
              $lookup: {
                from: "bodyparts",
                let: { bpIds: { $ifNull: ["$targetedBodyParts", []] } },
                pipeline: [
                  { $match: { $expr: { $in: ["$_id", "$$bpIds"] } } },
                  {
                    $project: {
                      _id: 1,
                      name: { $ifNull: [`$name.${lang}`, "$name.en"] },
                    },
                  },
                ],
                as: "targetedBodyParts",
              },
            },
            {
              $project: {
                _id: 1,
                name: { $ifNull: [`$name.${lang}`, "$name.en"] },
                description: {
                  $ifNull: [`$description.${lang}`, "$description.en"],
                },
                targetedBodyParts: 1,
              },
            },
          ],
          as: "days.exercises.exercise",
        },
      },
      {
        $addFields: {
          "days.exercises.exercise": {
            $arrayElemAt: ["$days.exercises.exercise", 0],
          },
        },
      },
      // Restore original array order before grouping ($group does not
      // guarantee output order, so days/exercises would come back shuffled)
      { $sort: { dayIndex: 1, exerciseIndex: 1 } },
      // Re-group exercises back into each day (keyed by the day's _id so days
      // with identical names stay separate and each keeps its identifier)
      {
        $group: {
          _id: {
            planId: "$_id",
            dayId: "$days._id",
          },
          planName: { $first: { $ifNull: [`$name.${lang}`, "$name.en"] } },
          planDescription: {
            $first: { $ifNull: [`$description.${lang}`, "$description.en"] },
          },
          planSubtitle: {
            $first: { $ifNull: [`$subtitle.${lang}`, "$subtitle.en"] },
          },
          planAttachment: { $first: "$attachment" },
          status: { $first: "$status" },
          createdAt: { $first: "$createdAt" },
          dayIndex: { $first: "$dayIndex" },
          dayId: { $first: "$days._id" },
          dayName: {
            $first: { $ifNull: [`$days.name.${lang}`, "$days.name.en"] },
          },
          exercises: {
            $push: {
              exercise_id: "$days.exercises.exercise_id",
              name: "$days.exercises.exercise.name",
              description: "$days.exercises.exercise.description",
              targetedBodyParts:
                "$days.exercises.exercise.targetedBodyParts",
              sets: "$days.exercises.sets",
            },
          },
        },
      },
      // Sort days back into their original order before re-grouping
      { $sort: { "_id.planId": 1, dayIndex: 1 } },
      // Re-group days back into the plan
      {
        $group: {
          _id: "$_id.planId",
          name: { $first: "$planName" },
          description: { $first: "$planDescription" },
          subtitle: { $first: "$planSubtitle" },
          attachment: { $first: "$planAttachment" },
          status: { $first: "$status" },
          createdAt: { $first: "$createdAt" },
          days: {
            $push: { _id: "$dayId", name: "$dayName", exercises: "$exercises" },
          },
        },
      },
      { $sort: { createdAt: -1 } },
    ];
  }

  async createPlan(
    body: CreateTrainingPlanDto,
    req: Request,
    res: Response,
    user: any,
  ) {
    try {
      // Prevent duplicate plan names for the same creator (matches any provided language value)
      const nameConditions = Object.entries(body.name || {})
        .filter(([, value]) => value !== undefined && value !== null && value !== "")
        .map(([lang, value]) => ({
          [`name.${lang}`]: {
            $regex: `^${String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`,
            $options: "i",
          },
        }));

      if (nameConditions.length) {
        const existing = await this.trainingPlanModel.findOne({
          deleted_at: null,
          createdBy: new Types.ObjectId(user._id),
          $or: nameConditions,
        });

        if (existing) {
          return this.resService.badRequest(
            res,
            "training_plan_name_already_exists",
            req,
          );
        }
      }

      // Ensure exercise_id is stored as ObjectId (so $lookup against exercises matches)
      const days = (body.days || []).map((day) => ({
        ...day,
        exercises: (day.exercises || []).map((ex) => ({
          ...ex,
          exercise_id: new Types.ObjectId(ex.exercise_id),
        })),
      }));

      const plan = await this.trainingPlanModel.create({
        ...body,
        days,
        createdBy: new Types.ObjectId(user._id),
        createdByType: user.role === RoleEnum.ADMIN ? "admin" : "user",
      });
      return this.resService.created(
        res,
        plan,
        "training_plan_created_successfully",
        req,
      );
    } catch (error: any) {
      return this.resService.serverError(
        res,
        "SOMETHING_WENT_WRONG",
        error.message || error,
      );
    }
  }

  // async getAllPlans(
  //   req: Request,
  //   res: Response,
  //   page: number,
  //   limit: number,
  //   search: string,
  //   status: string
  // ) {
  //   try {
  //     const lang = (req.headers["accept-language"] as string) || "en";
  //     page = Number(page) || 1;
  //     limit = Number(limit) || 10;
  //     const skip = (page - 1) * limit;

  //     const filter: any = { deleted_at: null };
  //     if (status !== undefined && status !== null && status !== "") {
  //       filter["status"] = Number(status);
  //     }
  //     if (search) {
  //       filter.$or = [
  //         { [`name.${lang}`]: { $regex: search, $options: "i" } },
  //         { [`description.${lang}`]: { $regex: search, $options: "i" } },
  //       ];
  //     }

  //     const [data, total] = await Promise.all([
  //       this.trainingPlanModel.aggregate([
  //         { $match: filter },
  //         { $sort: { createdAt: -1 } },
  //         { $skip: skip },
  //         { $limit: limit },
  //         ...this.exerciseLookupStages(lang),
  //       ]),
  //       this.trainingPlanModel.countDocuments(filter),
  //     ]);

  //     return this.resService.success(res, "success", req, {
  //       items: data,
  //       total,
  //       page,
  //       limit,
  //       pages: Math.ceil(total / limit),
  //     });
  //   } catch (error: any) {
  //     return this.resService.serverError(res, "SOMETHING_WENT_WRONG", error.message || error);
  //   }
  // }

  async getAllPlans(
    req: Request,
    res: Response,
    page: number,
    limit: number,
    search: string,
    status: string,
    user: any,
  ) {
    try {
      const rawLang = (req.headers["accept-language"] as string) || "en";
      const lang = ["en", "de"].includes(rawLang) ? rawLang : "en";

      page = Number(page) || 1;
      limit = Number(limit) || 10;

      const skip = (page - 1) * limit;

      const filter: any = {
        deleted_at: null,
      };

      if (status !== undefined && status !== null && status !== "") {
        filter.status = Number(status);
      }

      const andConditions: any[] = [];

      // User can see:
      // 1. Their own plans
      // 2. Admin-created plans
      if (user.role !== RoleEnum.ADMIN) {
        andConditions.push({
          $or: [
            {
              createdBy: new Types.ObjectId(user._id),
            },
            {
              createdByType: "admin",
            },
          ],
        });
      }

      // Search
      if (search) {
        andConditions.push({
          $or: [
            {
              [`name.${lang}`]: {
                $regex: search,
                $options: "i",
              },
            },
            {
              [`description.${lang}`]: {
                $regex: search,
                $options: "i",
              },
            },
          ],
        });
      }

      if (andConditions.length) {
        filter.$and = andConditions;
      }

      const [data, total] = await Promise.all([
        this.trainingPlanModel.aggregate([
          {
            $match: filter,
          },
          {
            $sort: {
              createdAt: -1,
            },
          },
          {
            $skip: skip,
          },
          {
            $limit: limit,
          },
          ...this.exerciseLookupStages(lang),
        ]),
        this.trainingPlanModel.countDocuments(filter),
      ]);

      return this.resService.success(res, "success", req, {
        items: data,
        total,
        page,
        limit,
        pages: Math.ceil(total / limit),
      });
    } catch (error: any) {
      return this.resService.serverError(
        res,
        "SOMETHING_WENT_WRONG",
        error.message || error,
      );
    }
  }

  async getAllAdminPlans(
    req: Request,
    res: Response,
    page: number,
    limit: number,
    search: string,
    status: string,
  ) {
    try {
      const rawLang = (req.headers["accept-language"] as string) || "en";
      const lang = ["en", "de"].includes(rawLang) ? rawLang : "en";

      page = Number(page) || 1;
      limit = Number(limit) || 10;

      const skip = (page - 1) * limit;

      const filter: any = {
        deleted_at: null,
        createdByType: "admin",
      };

      if (status !== undefined && status !== null && status !== "") {
        filter.status = Number(status);
      }

      // Search
      if (search) {
        filter.$or = [
          {
            [`name.${lang}`]: {
              $regex: search,
              $options: "i",
            },
          },
          {
            [`description.${lang}`]: {
              $regex: search,
              $options: "i",
            },
          },
        ];
      }

      const [data, total] = await Promise.all([
        this.trainingPlanModel.aggregate([
          {
            $match: filter,
          },
          {
            $sort: {
              createdAt: -1,
            },
          },
          {
            $skip: skip,
          },
          {
            $limit: limit,
          },
          ...this.exerciseLookupStages(lang),
        ]),
        this.trainingPlanModel.countDocuments(filter),
      ]);

      return this.resService.success(res, "success", req, {
        items: data,
        total,
        page,
        limit,
        pages: Math.ceil(total / limit),
      });
    } catch (error: any) {
      return this.resService.serverError(
        res,
        "SOMETHING_WENT_WRONG",
        error.message || error,
      );
    }
  }

  async getAllPlansWithoutPagination(
    req: Request,
    res: Response,
    search: string,
    status: string,
    user: any,
  ) {
    try {
      const rawLang = (req.headers["accept-language"] as string) || "en";
      const lang = ["en", "de"].includes(rawLang) ? rawLang : "en";

      const filter: any = {
        deleted_at: null,
      };

      if (status !== undefined && status !== null && status !== "") {
        filter.status = Number(status);
      }

      const andConditions: any[] = [];

      // User can see:
      // 1. Their own plans
      // 2. Admin-created plans
      if (user.role !== RoleEnum.ADMIN) {
        andConditions.push({
          $or: [
            {
              createdBy: new Types.ObjectId(user._id),
            },
            {
              createdByType: "admin",
            },
          ],
        });
      }

      // Search
      if (search) {
        andConditions.push({
          $or: [
            {
              [`name.${lang}`]: {
                $regex: search,
                $options: "i",
              },
            },
            {
              [`description.${lang}`]: {
                $regex: search,
                $options: "i",
              },
            },
          ],
        });
      }

      if (andConditions.length) {
        filter.$and = andConditions;
      }

      const data = await this.trainingPlanModel.aggregate([
        {
          $match: filter,
        },
        {
          $sort: {
            createdAt: -1,
          },
        },
        ...this.exerciseLookupStages(lang),
      ]);

      return this.resService.success(res, "success", req, {
        items: data,
        total: data.length,
      });
    } catch (error: any) {
      return this.resService.serverError(
        res,
        "SOMETHING_WENT_WRONG",
        error.message || error,
      );
    }
  }

  async getPlanById(id: string, req: Request, res: Response, user: any) {
    try {
      const rawLang = (req.headers["accept-language"] as string) || "en";
      const lang = ["en", "de"].includes(rawLang) ? rawLang : "en";

      const match: any = {
        _id: new Types.ObjectId(id),
        deleted_at: null,
      };

      // User can access only:
      // - their own plans
      // - admin plans
      if (user.role !== RoleEnum.ADMIN) {
        match.$or = [
          {
            createdBy: new Types.ObjectId(user._id),
          },
          {
            createdByType: "admin",
          },
        ];
      }

      // Raw doc retains day _ids (the aggregation reshapes them away),
      // needed to match the last completed session's dayId.
      const rawPlan = await this.trainingPlanModel.findOne(match).lean();

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

      const sessionProgress = await this.buildSessionProgress(
        rawPlan,
        new Types.ObjectId(user._id),
        lang,
      );

      // Only fetch the day the user has to start next (with its full details),
      // so the aggregation skips every other day.
      const nextDayId = sessionProgress.nextDay?.dayId;
      const data = await this.trainingPlanModel.aggregate([
        {
          $match: match,
        },
        ...this.exerciseLookupStages(lang, nextDayId),
      ]);

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

      // We only return a single (next) day, so expose it as `day` (object)
      // instead of `days` (array).
      const { days, ...plan } = data[0];

      return this.resService.success(res, "success", req, {
        ...plan,
        day: days?.[0] ?? null,
        ...sessionProgress,
      });
    } catch (error: any) {
      return this.resService.serverError(
        res,
        "SOMETHING_WENT_WRONG",
        error.message || error,
      );
    }
  }

  /**
   * Full plan details by id: returns EVERY day with all exercises and sets,
   * unlike `getPlanById` which only returns the single next day.
   */
  async getPlanFullDetailById(
    id: string,
    req: Request,
    res: Response,
    user: any,
  ) {
    try {
      const rawLang = (req.headers["accept-language"] as string) || "en";
      const lang = ["en", "de"].includes(rawLang) ? rawLang : "en";

      const match: any = {
        _id: new Types.ObjectId(id),
        deleted_at: null,
      };

      // Non-admins can access only their own plans or admin plans.
      if (user.role !== RoleEnum.ADMIN) {
        match.$or = [
          {
            createdBy: new Types.ObjectId(user._id),
          },
          {
            createdByType: "admin",
          },
        ];
      }

      // No dayId filter -> every day is unwound, populated and re-grouped.
      const data = await this.trainingPlanModel.aggregate([
        {
          $match: match,
        },
        ...this.exerciseLookupStages(lang),
      ]);

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

      console.log(data , "data")

      return this.resService.success(res, "success", req, data[0]);
    } catch (error: any) {
      return this.resService.serverError(
        res,
        "SOMETHING_WENT_WRONG",
        error.message || error,
      );
    }
  }

  // async updatePlan(id: string, body: UpdateTrainingPlanDto, req: Request, res: Response, user: any) {
  //   try {
  //     const plan = await this.trainingPlanModel.findOne({ _id: id, deleted_at: null });
  //     if (!plan) {
  //       return this.resService.notFound(res, "data_not_found", req);
  //     }

  //     const updated = await this.trainingPlanModel.findByIdAndUpdate(
  //       id,
  //       { ...body },
  //       { new: true }
  //     );
  //     return this.resService.success(res, "training_plan_updated_successfully", req, updated);
  //   } catch (error: any) {
  //     return this.resService.serverError(res, "SOMETHING_WENT_WRONG", error.message || error);
  //   }
  // }

  async updatePlan(
    id: string,
    dto: UpdateTrainingPlanDto,
    req: Request,
    res: Response,
    user: any,
  ) {
    try {
      const plan = await this.trainingPlanModel.findById(id).exec();
      if (!plan) {
        return this.resService.notFound(res, "Plan not found", req);
      }

      if (plan.deleted_at) {
        return this.resService.badRequest(
          res,
          "Cannot update a deleted plan",
          req,
        );
      }

      // Only admin or owner can update
      const isAdmin = user.role === RoleEnum.ADMIN;
      const isOwner = plan.createdBy?.toString() === user._id?.toString();

      if (!isAdmin && !isOwner) {
        return this.resService.forbidden(
          res,
          "You are not authorized to update this plan",
          req,
        );
      }

      // Ensure exercise_id is stored as ObjectId (so $lookup against exercises matches)
      const updatePayload: any = { ...dto };
      if (dto.days) {
        updatePayload.days = dto.days.map((day) => ({
          ...day,
          exercises: (day.exercises || []).map((ex) => ({
            ...ex,
            exercise_id: new Types.ObjectId(ex.exercise_id),
          })),
        }));
      }

      const updatedPlan = await this.trainingPlanModel.findByIdAndUpdate(
        id,
        updatePayload,
        { new: true },
      );

      return this.resService.success(
        res,
        "Plan updated successfully",
        req,
        updatedPlan,
      );
    } catch (error: any) {
      console.error("Error updating plan:", error);

      return this.resService.serverError(
        res,
        "Failed to update plan",
        req,
        error.message || error,
      );
    }
  }

  async deletePlan(id: string, req: Request, res: Response, user: any) {
    try {
      const plan = await this.trainingPlanModel.findOne({
        _id: id,
        deleted_at: null,
      });
      if (!plan) {
        return this.resService.notFound(res, "data_not_found", req);
      }

      // Only admin or owner can delete
      const isAdmin = user.role === RoleEnum.ADMIN;
      const isOwner = plan.createdBy?.toString() === user._id?.toString();

      if (!isAdmin && !isOwner) {
        return this.resService.forbidden(
          res,
          "You are not authorized to delete this plan",
          req,
        );
      }

      await this.trainingPlanModel.findByIdAndUpdate(id, {
        deleted_at: new Date(),
      });
      return this.resService.success(res, "deleted_successfully", req);
    } catch (error: any) {
      return this.resService.serverError(
        res,
        "SOMETHING_WENT_WRONG",
        error.message || error,
      );
    }
  }
}
