import { Injectable } from "@nestjs/common";
import { CreateWeightLogDto } from "./dto/create-weight.dto";
import { UpdateWeightLogDto } from "./dto/update-weight.dto";
import { Request, Response } from "express";
import { ResponseService } from "src/common/service/response.service";
import { WeightLogDocument, WeightLog } from "./schemas/weight.schema";
import { InjectModel } from "@nestjs/mongoose";
import { Model, Types } from "mongoose";
import { endOfCurrentDayUtc } from "src/common/constants/timezone.constant";

@Injectable()
export class WeightService {
  constructor(
    @InjectModel(WeightLog.name)
    private weightLogModel: Model<WeightLogDocument>,
    private readonly resService: ResponseService,
  ) {}

  async create(
    dto: CreateWeightLogDto,
    req: Request,
    res: Response,
    user: any,
  ) {
    try {
      // Normalize the recorded date to its day boundaries so we can detect
      // whether a log already exists for that calendar day.
      // const recordedDate = new Date(dto.recordedAt);
      // const startOfDay = new Date(recordedDate);
      // startOfDay.setHours(0, 0, 0, 0);
      // const endOfDay = new Date(recordedDate);
      // endOfDay.setHours(23, 59, 59, 999);

      // const existingLog = await this.weightLogModel.findOne({
      //   userId: new Types.ObjectId(user._id),
      //   deleted_at: null,
      //   recordedAt: { $gte: startOfDay, $lte: endOfDay },
      // });

      const recordedDate = new Date(dto.recordedAt);
      const existingLog = await this.weightLogModel.findOne({
        userId: new Types.ObjectId(user._id),
        deleted_at: null,
        recordedAt: recordedDate,
      });

      if (existingLog) {
        return this.resService.conflict(
          res,
          "Weight already recorded for this date. Please update the existing entry instead.",
          req,
          existingLog,
        );
      }
      const weightLog = await this.weightLogModel.create({
        ...dto,
        userId: new Types.ObjectId(user._id),
      });

      return this.resService.created(
        res,
        weightLog,
        "Weight log created successfully",
        req,
      );
    } catch (error) {
      console.error("Error creating weight log:", error);

      return this.resService.serverError(
        res,
        "Failed to create weight log",
        req,
        error.message || error,
      );
    }
  }

  

  async findAll(
    period: "current" | "last" | "all" = "all",
    req: Request,
    res: Response,
    user: any,
  ) {
    try {
      const query: any = { userId: new Types.ObjectId(user._id) };

      // By default fetch all logs; only constrain by date when a specific
      // month period ("current" or "last") is explicitly requested.
      if (period === "current" || period === "last") {
        const now = new Date();
        const monthOffset = period === "last" ? -1 : 0;
        const startDate = new Date(
          now.getFullYear(),
          now.getMonth() + monthOffset,
          1,
        );
        const endDate = new Date(
          now.getFullYear(),
          now.getMonth() + monthOffset + 1,
          0,
          23,
          59,
          59,
        );
        query.createdAt = { $gte: startDate, $lte: endDate };
      }

      const weightLogs = await this.weightLogModel
        .find(query)
        .sort({ createdAt: 1 });

      return this.resService.success(
        res,
        "Weight logs fetched successfully",
        req,
        weightLogs,
      );
    } catch (error) {
      console.error("Error fetching weight logs:", error);

      return this.resService.serverError(
        res,
        "Failed to fetch weight logs",
        req,
        error.message || error,
      );
    }
  }

  async update(
    id: string,
    dto: UpdateWeightLogDto,
    req: Request,
    res: Response,
    user: any,
  ) {
    try {
      const weightLog = await this.weightLogModel.findOne({
        _id: id,
        deleted_at: null,
      });

      if (!weightLog) {
        return this.resService.notFound(res, "Weight log not found", req);
      }

      weightLog.weight = dto.weight;
      await weightLog.save();

      return this.resService.success(
        res,
        "Weight log updated successfully",
        req,
        weightLog,
      );
    } catch (error) {
      console.error("Error updating weight log:", error);

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

  async remove(id: string, req: Request, res: Response, user: any) {
    try {
      const deletedWeightLog = await this.weightLogModel.findOneAndDelete({
        _id: id,
        userId: new Types.ObjectId(user._id),
      });

      if (!deletedWeightLog) {
        return this.resService.notFound(res, "Weight log not found", req);
      }

      return this.resService.success(
        res,
        "Weight log deleted successfully",
        req,
        {},
      );
    } catch (error) {
      console.error("Error deleting weight log:", error);

      return this.resService.serverError(
        res,
        "Failed to delete weight log",
        req,
        error.message || error,
      );
    }
  }

  async getHistory(
    period: "7d" | "30d" | "3m" | "6m" | "all" = "30d",
    req: Request,
    res: Response,
    user: any,
  ) {
    try {
      // recordedAt is the user's (German) calendar date pinned to UTC midnight,
      // so "today" is resolved in APP_TIMEZONE and all arithmetic stays in UTC
      // date-space to match the stored values.
      const now = endOfCurrentDayUtc();

      let startDate: Date | null = null;

      switch (period) {
        case "7d":
          startDate = new Date(
            Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 6),
          );
          break;
        case "30d":
          startDate = new Date(
            Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 29),
          );
          break;
        case "3m":
          startDate = new Date(
            Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 2, 1),
          );
          break;
        case "6m":
          startDate = new Date(
            Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 5, 1),
          );
          break;
        case "all":
          startDate = null;
          break;
      }

      const query: any = {
        userId: new Types.ObjectId(user._id),
        deleted_at: null,
      };
      if (startDate) {
        query.recordedAt = { $gte: startDate, $lte: now };
      }

      const logs = await this.weightLogModel
        .find(query)
        .sort({ recordedAt: 1 })
        .lean();

      const graph = this.buildGraphData(logs, period, now);

      // Stats are lifetime values, independent of the selected period, so an
      // empty period window still reports the user's real numbers.
      const [firstEntry, lastEntry, lightestEntry] = await Promise.all([
        this.weightLogModel
          .findOne({ userId: new Types.ObjectId(user._id), deleted_at: null })
          .sort({ recordedAt: 1 })
          .lean(),
        this.weightLogModel
          .findOne({ userId: new Types.ObjectId(user._id), deleted_at: null })
          .sort({ recordedAt: -1 })
          .lean(),
        this.weightLogModel
          .findOne({ userId: new Types.ObjectId(user._id), deleted_at: null })
          .sort({ weight: 1 })
          .lean(),
      ]);

      const startingWeight = firstEntry?.weight ?? null;
      const currentWeight = lastEntry?.weight ?? null;
      const minimumWeight = lightestEntry?.weight ?? null;

      let changeSinceStart: number | null = null;
      if (startingWeight && currentWeight) {
        changeSinceStart = Math.round(
          ((currentWeight - startingWeight) / startingWeight) * 100,
        );
      }

      return this.resService.success(
        res,
        "Weight history fetched successfully",
        req,
        {
          graph,
          stats: {
            startingWeight,
            currentWeight,
            changeSinceStart,
            minimumWeight,
          },
        },
      );
    } catch (error) {
      console.error("Error fetching weight history:", error);
      return this.resService.serverError(
        res,
        "Failed to fetch weight history",
        req,
        error.message || error,
      );
    }
  }

  private buildGraphData(
    logs: any[],
    period: string,
    now: Date,
  ): Array<{ label: string; value: number | null }> {
    const MONTH_NAMES = [
      "Jan",
      "Feb",
      "Mar",
      "Apr",
      "May",
      "Jun",
      "Jul",
      "Aug",
      "Sep",
      "Oct",
      "Nov",
      "Dec",
    ];
    const DAY_NAMES = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];

    const bucketList: Array<{ key: string; label: string }> = [];
    const bucketMap = new Map<string, number[]>();

    if (period === "7d") {
      for (let i = 6; i >= 0; i--) {
        const d = new Date(
          Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - i),
        );
        const key = d.toISOString().slice(0, 10);
        bucketList.push({ key, label: DAY_NAMES[d.getUTCDay()] });
        bucketMap.set(key, []);
      }
      for (const log of logs) {
        const key = new Date(log.recordedAt).toISOString().slice(0, 10);
        bucketMap.get(key)?.push(log.weight);
      }
    } else if (period === "30d") {
      const cursor = new Date(
        Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 29),
      );
      while (cursor <= now) {
        const { week, year } = this.getISOWeek(cursor);
        const key = `${year}-W${week}`;
        if (!bucketMap.has(key)) {
          bucketList.push({ key, label: `W${week}` });
          bucketMap.set(key, []);
        }
        cursor.setUTCDate(cursor.getUTCDate() + 1);
      }
      for (const log of logs) {
        const { week, year } = this.getISOWeek(new Date(log.recordedAt));
        const key = `${year}-W${week}`;
        bucketMap.get(key)?.push(log.weight);
      }
    } else if (period === "3m" || period === "6m") {
      const numMonths = period === "3m" ? 3 : 6;
      for (let i = numMonths - 1; i >= 0; i--) {
        const d = new Date(
          Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - i, 1),
        );
        const key = `${d.getUTCFullYear()}-${d.getUTCMonth()}`;
        bucketList.push({ key, label: MONTH_NAMES[d.getUTCMonth()] });
        bucketMap.set(key, []);
      }
      for (const log of logs) {
        const d = new Date(log.recordedAt);
        const key = `${d.getUTCFullYear()}-${d.getUTCMonth()}`;
        bucketMap.get(key)?.push(log.weight);
      }
    } else if (period === "all") {
      if (!logs.length) return [];
      const firstDate = new Date(logs[0].recordedAt);
      const lastDate = new Date(logs[logs.length - 1].recordedAt);
      const monthsDiff =
        (lastDate.getUTCFullYear() - firstDate.getUTCFullYear()) * 12 +
        (lastDate.getUTCMonth() - firstDate.getUTCMonth());

      if (monthsDiff > 24) {
        for (
          let y = firstDate.getUTCFullYear();
          y <= lastDate.getUTCFullYear();
          y++
        ) {
          const key = `${y}`;
          bucketList.push({ key, label: key });
          bucketMap.set(key, []);
        }
        for (const log of logs) {
          const key = `${new Date(log.recordedAt).getUTCFullYear()}`;
          bucketMap.get(key)?.push(log.weight);
        }
      } else {
        let year = firstDate.getUTCFullYear();
        let q = Math.floor(firstDate.getUTCMonth() / 3);
        const endYear = lastDate.getUTCFullYear();
        const endQ = Math.floor(lastDate.getUTCMonth() / 3);
        while (year < endYear || (year === endYear && q <= endQ)) {
          const key = `${year}-Q${q + 1}`;
          bucketList.push({ key, label: `Q${q + 1} ${year}` });
          bucketMap.set(key, []);
          q++;
          if (q > 3) {
            q = 0;
            year++;
          }
        }
        for (const log of logs) {
          const d = new Date(log.recordedAt);
          const quarter = Math.floor(d.getUTCMonth() / 3);
          const key = `${d.getUTCFullYear()}-Q${quarter + 1}`;
          bucketMap.get(key)?.push(log.weight);
        }
      }
    }

    return 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 };
    });
  }

  // ISO-8601 week number + week-year, computed in UTC (recordedAt values are
  // stored at UTC midnight for date-only inputs). The week-year can differ
  // from the calendar year around New Year (e.g. Dec 29 can belong to week 1
  // of the next year), so week keys must use it instead of getFullYear() to
  // avoid duplicate W1/W53 buckets.
  private getISOWeek(date: Date): { week: number; year: number } {
    const d = new Date(
      Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),
    );
    const dayNum = d.getUTCDay() || 7;
    d.setUTCDate(d.getUTCDate() + 4 - dayNum);
    const year = d.getUTCFullYear();
    const yearStart = new Date(Date.UTC(year, 0, 1));
    const week = Math.ceil(
      ((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7,
    );
    return { week, year };
  }
}
