import bcrypt from "bcryptjs";
import cloudinary from "../config/cloudinary";
import User from "../models/users";
import Quiz from "../models/quiz";
import QuizResult from "../models/quizResult";
import PsychometricResult from "../models/psychometricResult";

// Save FCM device token for user
export const saveDeviceToken = async (
  userId: string,
  token: string,
  platform: "android" | "ios" | "web",
) => {
  const user = await User.findById(userId);
  if (!user) throw new Error("User not found");
  // Remove duplicates
  user.deviceTokens = user.deviceTokens.filter((dt) => dt.token !== token);
  // Add or update
  user.deviceTokens.push({ token, platform, updatedAt: new Date() });
  await user.save();
};

// Remove FCM device token for user
export const removeDeviceToken = async (userId: string, token: string) => {
  const user = await User.findById(userId);
  if (!user) throw new Error("User not found");
  user.deviceTokens = user.deviceTokens.filter((dt) => dt.token !== token);
  await user.save();
};

export const getActiveUserNames = async (): Promise<string[]> => {
  const users = await User.find({ status: "active" })
    .select("name -_id")
    .sort({ name: 1 })
    .lean();

  return users.map((user) => user.name);
};

const USER_EDITABLE_FIELDS = [
  "name",
  "email",
  "phone",
  "address",
  "department",
  "designation",
  "joining_date",
] as const;

export const getUserById = async (id: string) => {
  const user = await User.findById(id).lean();
  if (!user) return null;
  // Calculate earned points
  const earned_points = user.earned_points || 0;
  const redeemed_points = user.redeemed_points || 0;
  const available_points = earned_points - redeemed_points;

  // Efficient aggregation to count users with higher available_points
  const aggregation = [
    {
      $addFields: {
        available_points: {
          $subtract: [
            { $ifNull: ["$earned_points", 0] },
            { $ifNull: ["$redeemed_points", 0] },
          ],
        },
      },
    },
    { $match: { available_points: { $gt: available_points } } },
    { $count: "higher" },
  ];
  const result = await User.aggregate(aggregation);
  const higherCount = result[0]?.higher || 0;
  const rank = available_points > 0 ? higherCount + 1 : null;
  return { ...user, available_points, rank };
};

export const updateUser = async (id: string, body: Record<string, unknown>) => {
  const update: Record<string, unknown> = {};
  USER_EDITABLE_FIELDS.forEach((f) => {
    if (body[f] !== undefined) update[f] = body[f];
  });

  // Name: required, string, min 2, max 20
  if (update.name !== undefined) {
    const name = String(update.name).trim();
    if (!name || name.length < 2 || name.length > 20) {
      const err = new Error("Name must be 2-20 characters.");
      // @ts-ignore
      err.statusCode = 400;
      throw err;
    }
    update.name = name;
  }

  // Email: required, valid format
  if (update.email !== undefined) {
    const email = String(update.email).trim();
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(email)) {
      const err = new Error("Invalid email format.");
      // @ts-ignore
      err.statusCode = 400;
      throw err;
    }
    update.email = email;
  }

  // Phone: already validated above
  if (update.phone !== undefined && update.phone !== null) {
    const phoneStr = String(update.phone);
    if (!/^[0-9]{10}$/.test(phoneStr)) {
      const err = new Error(
        "Phone number must be exactly 10 digits and numeric only.",
      );
      // @ts-ignore
      err.statusCode = 400;
      throw err;
    }
    update.phone = phoneStr;
  }

  // Address: optional, string, max 200
  if (update.address !== undefined && update.address !== null) {
    const address = String(update.address).trim();
    if (address.length > 200) {
      const err = new Error("Address must be at most 200 characters.");
      // @ts-ignore
      err.statusCode = 400;
      throw err;
    }
    update.address = address;
  }

  // Department: optional, string, max 20
  if (update.department !== undefined && update.department !== null) {
    const department = String(update.department).trim();
    if (department.length > 20) {
      const err = new Error("Department must be at most 20 characters.");
      // @ts-ignore
      err.statusCode = 400;
      throw err;
    }
    update.department = department;
  }

  // Designation: optional, string, max 20
  if (update.designation !== undefined && update.designation !== null) {
    const designation = String(update.designation).trim();
    if (designation.length > 20) {
      const err = new Error("Designation must be at most 20 characters.");
      // @ts-ignore
      err.statusCode = 400;
      throw err;
    }
    update.designation = designation;
  }

  // Joining date: optional, must be valid date
  if (update.joining_date !== undefined && update.joining_date !== null) {
    const date = new Date(update.joining_date as string);
    if (isNaN(date.getTime())) {
      const err = new Error("Joining date must be a valid date.");
      // @ts-ignore
      err.statusCode = 400;
      throw err;
    }
    update.joining_date = date;
  }

  return User.findByIdAndUpdate(id, update, { new: true, runValidators: true });
};

export const uploadAvatar = async (
  id: string,
  fileBuffer: Buffer,
  mimetype: string,
) => {
  const base64 = fileBuffer.toString("base64");
  const dataUri = `data:${mimetype};base64,${base64}`;
  const result = await cloudinary.uploader.upload(dataUri, {
    folder: "avatars",
    transformation: [
      { width: 200, height: 200, crop: "fill", gravity: "face" },
    ],
  });
  return User.findByIdAndUpdate(
    id,
    { avatar: result.secure_url },
    { new: true },
  );
};

export const changePassword = async (
  id: string,
  currentPassword: string,
  newPassword: string,
): Promise<void> => {
  const user = await User.findById(id).select("+password");
  if (!user)
    throw Object.assign(new Error("User not found"), { statusCode: 404 });
  const isMatch = await bcrypt.compare(currentPassword, user.password!);
  if (!isMatch)
    throw Object.assign(new Error("Current password is incorrect"), {
      statusCode: 401,
    });
  user.password = await bcrypt.hash(newPassword, 10);
  await user.save();
};

export const deleteUser = async (id: string): Promise<void> => {
  await User.findByIdAndDelete(id);
};

const getInitials = (name: string): string => {
  const parts = name.trim().split(/\s+/);
  return parts.length >= 2
    ? (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
    : parts[0].substring(0, 2).toUpperCase();
};

export const getLeaderboard = async (userId: string, period: string) => {
  const now = new Date();
  let dateFilter: Date | null = null;

  if (period === "today") {
    dateFilter = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  } else if (period === "weekly") {
    dateFilter = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
  } else if (period === "monthly") {
    dateFilter = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
  }

  const matchStage: Record<string, unknown> = {};
  if (dateFilter) matchStage.submitted_at = { $gte: dateFilter };

  // Efficient aggregation to compute leaderboard with available_points and rank
  const aggregation = [
    {
      $addFields: {
        available_points: {
          $subtract: [
            { $ifNull: ["$earned_points", 0] },
            { $ifNull: ["$redeemed_points", 0] },
          ],
        },
      },
    },
    { $sort: { available_points: -1 as 1 | -1 } },
    {
      $project: {
        _id: 1,
        name: 1,
        avatar: 1,
        earned_points: 1,
        redeemed_points: 1,
        available_points: 1,
      },
    },
  ];
  const users = await User.aggregate(aggregation);

  // Assign rank and build leaderboard
  const leaderboard = users.map((u, idx) => ({
    user_id: u._id,
    name: u.name,
    avatar: u.avatar || null,
    initials: getInitials(u.name),
    earned_points: u.earned_points || 0,
    redeemed_points: u.redeemed_points || 0,
    available_points: u.available_points || 0,
    isCurrentUser: u._id.toString() === userId,
    rank: idx + 1,
  }));
  return leaderboard;
};

export const getDashboard = async (id: string) => {
  const now = new Date();
  const startOfToday = new Date(
    now.getFullYear(),
    now.getMonth(),
    now.getDate(),
  );

  const [
    user,
    quizResults,
    todayResults,
    activeQuizzes,
    psychometricResults,
    totalQuizzes,
  ] = await Promise.all([
    User.findById(id)
      .select(
        "name avatar designation earned_points spin_wheel_points zipline_points redeemed_points currentStreak longestStreak total_points lastCheckInDate status employee_id phone address department createdAt updatedAt role email ",
      )
      .lean(),
    QuizResult.find({ user: id })
      .populate("quiz", "title")
      .select("quiz score total_points submitted_at")
      .sort({ submitted_at: -1 })
      .lean(),
    QuizResult.find({ user: id, submitted_at: { $gte: startOfToday } })
      .select("score")
      .lean(),
    Quiz.find({ status: "active", end_datetime: { $gte: now } })
      .select("title start_datetime end_datetime questions")
      .populate("questions", "points")
      .sort({ start_datetime: 1 })
      .lean(),
    PsychometricResult.find({ user: id })
      .populate("test", "title")
      .select("test personality_type submitted_at")
      .sort({ submitted_at: -1 })
      .lean(),
    Quiz.countDocuments({}),
  ]);

  // Calculate available_points
  const earned_points = user?.earned_points || 0;
  const redeemed_points = user?.redeemed_points || 0;
  const available_points = earned_points - redeemed_points;
  // Efficient aggregation to count users with higher available_points
  const aggregation = [
    {
      $addFields: {
        available_points: {
          $subtract: [
            { $ifNull: ["$earned_points", 0] },
            { $ifNull: ["$redeemed_points", 0] },
          ],
        },
      },
    },
    { $match: { available_points: { $gt: available_points } } },
    { $count: "higher" },
  ];
  const result = await User.aggregate(aggregation);
  const higherCount = result[0]?.higher || 0;
  const rank = available_points > 0 ? higherCount + 1 : null;

  if (!user)
    throw Object.assign(new Error("User not found"), { statusCode: 404 });

  const initials = getInitials(user.name);

  // Points — compute from actual results (source of truth)
  // Subtract redeemed_points from quiz-earned points
  // If you want to keep the calculated quiz_points/redeemed_points from results, you can keep these lines, but for rank, always use the fields above.
  // const quiz_points = quizResults.reduce((sum, r) => sum + r.score, 0);
  // const redeemed_points = user.redeemed_points || 0;
  // const total_points = quiz_points - redeemed_points;
  const points_today = todayResults.reduce((sum, r) => sum + r.score, 0);

  // Sync User model if out of date
  // Optionally sync User model if out of date (optional, not for ranking)
  // if (user.total_points !== available_points) {
  //   await User.findByIdAndUpdate(id, { total_points: available_points });
  // }

  // Quiz stats
  const total_possible_points = quizResults.reduce(
    (sum, r) => sum + r.total_points,
    0,
  );
  const total_active = activeQuizzes.filter(
    (quiz) => new Date(quiz.start_datetime) <= now,
  ).length;
  // Only count submitted active quizzes as 'taken'
  const quizzes_taken = activeQuizzes.filter((quiz) => {
    const quizId = quiz._id.toString();
    return quizResults.some((r) => {
      const rQuiz = r.quiz as unknown as { _id: unknown } | null;
      return rQuiz?._id?.toString() === quizId;
    });
  }).length;

  // Active quizzes with submission status
  const submittedQuizIds = new Set(
    quizResults.map((r) => {
      const quiz = r.quiz as unknown as { _id: unknown } | null;
      return quiz?._id?.toString();
    }),
  );

  const active_quizzes = activeQuizzes
    .filter((quiz) => new Date(quiz.start_datetime) <= now)
    .map((quiz) => {
      const questions = quiz.questions as unknown as { points: number }[];
      const total_points = questions.reduce((sum, q) => sum + q.points, 0);
      const submitted = submittedQuizIds.has(quiz._id.toString());
      return {
        _id: quiz._id,
        title: quiz.title,
        start_datetime: quiz.start_datetime,
        end_datetime: quiz.end_datetime,
        total_points,
        submitted,
      };
    });

  // Recent quizzes (filter out deleted quizzes)
  const recent_quizzes = quizResults
    .filter((r) => r.quiz !== null)
    .slice(0, 5)
    .map((r) => {
      const quiz = r.quiz as unknown as { _id: unknown; title: string };
      return {
        quiz_id: quiz._id,
        quiz_title: quiz.title,
        score: r.score,
        total_points: r.total_points,
        submitted_at: r.submitted_at,
      };
    });

  // Personality results (filter out deleted tests)
  const personality_results = psychometricResults
    .filter((r) => r.test !== null)
    .map((r) => {
      const test = r.test as unknown as { _id: unknown; title: string };
      return {
        test_id: test._id,
        test_title: test.title,
        personality_type: r.personality_type,
        submitted_at: r.submitted_at,
      };
    });

  return {
    user: {
      name: user.name,
      avatar: user.avatar,
      designation: user.designation,
      initials,
    },
    streak: {
      current: user.currentStreak,
      longest: user.longestStreak,
    },
    points: {
      earned_points,
      redeemed_points,
      available_points,
      today: points_today,
      rank,
    },
    quizzes: {
      taken: quizzes_taken,
      total: totalQuizzes,
      total_active,
      percentage:
        total_possible_points > 0
          ? Math.round((available_points / total_possible_points) * 100)
          : 0,
      active: active_quizzes,
      recent: recent_quizzes,
    },
    personality: personality_results,
  };
};
