import path from "path";
import express, { Router } from "express";
import dotenv from "dotenv";
import admin from "firebase-admin";
import cors from "cors";
import "dotenv/config";
import { PrismaClient, EnumTransactionStatus, SubscriptionStatus, Prisma, NotesType, NotesLinksStatus } from "@prisma/client";
import cron from "node-cron";
import dayjs from "dayjs";
import pLimit from "p-limit";
import Razorpay from "razorpay";
import asyncHandler from "express-async-handler";
import utc from "dayjs/plugin/utc.js";
import timezone from "dayjs/plugin/timezone.js";
import crypto from "crypto";
import PDFDocument from "pdfkit";
import fs from "fs";
import bcrypt from "bcrypt";
import { v4 } from "uuid";
import nodemailer from "nodemailer";
import jwt from "jsonwebtoken";
import os from "os";
import multer from "multer";
import sharp from "sharp";
import XLSX from "xlsx";
import * as yup from "yup";
import winston from "winston";
import { fileURLToPath } from "url";
dotenv.config();
const serviceAccount = {
  project_id: process.env.FCM_PROJECT_ID,
  private_key_id: process.env.FCM_PROJECT_KEY_ID,
  client_email: process.env.FCM_PROJECT_CLIENT_EMAIL,
  private_key: process.env.FCM_PROJECT_PRIVATE_KEY ? process.env.FCM_PROJECT_PRIVATE_KEY.replace(/\\n/g, "\n") : void 0
};
if (!serviceAccount.project_id) {
  throw new Error("❌ FIREBASE_PROJECT_ID is missing");
}
if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert(
      serviceAccount
    )
  });
}
const sendNotification = async (tokens, title, body, data = {}) => {
  if (!tokens || tokens.length === 0) return;
  if (!admin.apps.length) {
    throw new Error("Firebase Admin not initialized");
  }
  const message = {
    tokens,
    notification: {
      title,
      body
    },
    data
  };
  const response = await admin.messaging().sendEachForMulticast(message);
  const invalidTokens = [];
  response.responses.forEach((res, index) => {
    if (!res.success) {
      const errorCode = res.error?.code;
      if (errorCode === "messaging/registration-token-not-registered" || errorCode === "messaging/invalid-registration-token") {
        invalidTokens.push(tokens[index]);
      }
    }
  });
  return {
    successCount: response.successCount,
    failureCount: response.failureCount,
    invalidTokens
  };
};
const globalForPrisma = globalThis;
const prisma$1 = globalForPrisma.prisma ?? new PrismaClient({
  log: ["error"]
  // datasources: {
  //     db: {
  //         url: process.env.DATABASE_URL, // ensure this includes pool config
  //     },
  // },
});
async function checkDbConnection() {
  try {
    await prisma$1.$connect();
    await prisma$1.$queryRaw`SELECT 1`;
    console.log("✅ Database connected successfully");
  } catch (err) {
    console.error("❌ Database connection failed");
    console.error(err);
    process.exit(1);
  }
}
if (!process.env.RAZORPAY_KEY_ID || !process.env.RAZORPAY_KEY_SECRET) {
  console.warn("RAZORPAY_KEY_ID or RAZORPAY_KEY_SECRET is missing in environment variables");
} else {
  console.log("✅ Razor pay connected successfully");
}
const razorpay = new Razorpay({
  key_id: process.env.RAZORPAY_KEY_ID || "rzp_test_placeholder",
  key_secret: process.env.RAZORPAY_KEY_SECRET || "placeholder_secret"
});
dayjs.extend(utc);
dayjs.extend(timezone);
const IST = "Asia/Kolkata";
const toIST = (date, format = "YYYY-MM-DD HH:mm:ss") => dayjs(date).tz(IST).format(format);
const nowUTC = () => /* @__PURE__ */ new Date();
const tokenExpireTime = () => new Date(Date.now() + 1e3 * 60 * 60 * 24);
function numberToWords(num) {
  if (num === 0) return "Rupees Zero Only";
  const ones = [
    "",
    "One",
    "Two",
    "Three",
    "Four",
    "Five",
    "Six",
    "Seven",
    "Eight",
    "Nine",
    "Ten",
    "Eleven",
    "Twelve",
    "Thirteen",
    "Fourteen",
    "Fifteen",
    "Sixteen",
    "Seventeen",
    "Eighteen",
    "Nineteen"
  ];
  const tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"];
  const convertLessThanOneThousand = (n) => {
    if (n === 0) return "";
    let str = "";
    if (n >= 100) {
      str += ones[Math.floor(n / 100)] + " Hundred ";
      n %= 100;
    }
    if (n > 0) {
      if (str !== "") str += "and ";
      if (n < 20) {
        str += ones[n] + " ";
      } else {
        str += tens[Math.floor(n / 10)] + " ";
        if (n % 10 > 0) {
          str += ones[n % 10] + " ";
        }
      }
    }
    return str;
  };
  let rupees = Math.floor(num);
  let paise = Math.round((num - rupees) * 100);
  let words = "";
  if (rupees > 0) {
    const crore = Math.floor(rupees / 1e7);
    rupees %= 1e7;
    const lakh = Math.floor(rupees / 1e5);
    rupees %= 1e5;
    const thousand = Math.floor(rupees / 1e3);
    rupees %= 1e3;
    if (crore > 0) {
      words += convertLessThanOneThousand(crore) + "Crore ";
    }
    if (lakh > 0) {
      words += convertLessThanOneThousand(lakh) + "Lakh ";
    }
    if (thousand > 0) {
      words += convertLessThanOneThousand(thousand) + "Thousand ";
    }
    if (rupees > 0) {
      words += convertLessThanOneThousand(rupees);
    }
  }
  let result = words.trim();
  if (result !== "") {
    result = "Rupees " + result;
  }
  if (paise > 0) {
    const paiseWords = convertLessThanOneThousand(paise).trim();
    if (result !== "") {
      result += " and " + paiseWords + " Paise";
    } else {
      result = "Paise " + paiseWords;
    }
  }
  if (result !== "") {
    result += " Only";
  }
  return result;
}
const getPaymentStatuses = asyncHandler(async (req, res) => {
  res.status(200).json({
    message: "Payment statuses fetched successfully",
    data: Object.values(EnumTransactionStatus)
  });
});
const listPayments = asyncHandler(async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const skip = (page - 1) * limit;
  const search = req.query.search;
  const startDate = req.query.startDate;
  const endDate = req.query.endDate;
  const planType = req.query.planType;
  const statusFilter = req.query.status;
  const gstTypeFilter = req.query.gstType;
  const isAdmin = req.user?.role === "ADMIN";
  const where = {};
  if (!isAdmin) {
    where.institutionId = req.user?.institutionId;
  } else if (req.query.institutionId) {
    where.institutionId = req.query.institutionId;
  }
  const andConditions = [];
  if (planType) {
    const plans = await prisma$1.subscriptionPlan.findMany({
      where: { planType },
      select: { id: true }
    });
    const planIds = plans.map((p) => p.id);
    const planIdConditions = planIds.map((id) => ({
      metadata: {
        path: "$.planId",
        equals: id
      }
    }));
    andConditions.push({
      OR: [
        {
          transactionReceipt: {
            items: {
              some: {
                subscription: {
                  plan: {
                    planType
                  }
                }
              }
            }
          }
        },
        {
          subscriptionRequest: {
            subscriptionPlan: {
              planType
            }
          }
        },
        ...planIdConditions
      ]
    });
  }
  if (statusFilter) {
    andConditions.push({ status: statusFilter });
  }
  if (gstTypeFilter) {
    if (gstTypeFilter === "NO_GST") {
      andConditions.push({ gstType: null });
    } else {
      andConditions.push({ gstType: gstTypeFilter });
    }
  }
  if (search) {
    const searchWords = search.split(/\s+/).filter(Boolean);
    const searchOR = [
      { failureReason: { contains: search } },
      { paymentMethod: { contains: search } },
      { transactionReceipt: { receiptNo: { contains: search } } },
      { transactionReceipt: { items: { some: { subscription: { plan: { planName: { contains: search } } } } } } },
      ...isAdmin ? [{
        institution: {
          user: {
            OR: [
              { institutionName: { contains: search } },
              { email: { contains: search } }
            ]
          }
        }
      }] : [],
      ...isAdmin && searchWords.length > 1 ? [{
        institution: {
          user: {
            AND: searchWords.map((word) => ({
              OR: [
                { institutionName: { contains: word } },
                { email: { contains: word } }
              ]
            }))
          }
        }
      }] : []
    ];
    const enumValues = Object.values(EnumTransactionStatus);
    const matchedEnum = enumValues.find(
      (value) => value.toLowerCase().includes(search.toLowerCase())
    );
    if (matchedEnum) {
      searchOR.push({
        status: matchedEnum
      });
    }
    const amountSearch = Number(search);
    if (!isNaN(amountSearch)) {
      searchOR.push({
        totalAmount: { equals: amountSearch }
      });
    }
    andConditions.push({ OR: searchOR });
  }
  if (startDate && endDate) {
    andConditions.push({
      createdAt: {
        gte: new Date(startDate),
        lte: new Date(endDate)
      }
    });
  }
  if (andConditions.length > 0) {
    where.AND = andConditions;
  }
  const successWhere = {
    AND: [where, { status: EnumTransactionStatus.SUCCESS }]
  };
  const pendingWhere = {
    AND: [where, { status: EnumTransactionStatus.PENDING }]
  };
  const attentionWhere = {
    AND: [
      where,
      {
        status: {
          in: [
            EnumTransactionStatus.FAILED,
            EnumTransactionStatus.CANCELLED,
            EnumTransactionStatus.EXPIRED
          ]
        }
      }
    ]
  };
  const [
    total,
    transactions,
    successfulPayments,
    pendingPayments,
    attentionPayments,
    totalCollected,
    institutionsWithPayments
  ] = await Promise.all([
    prisma$1.transaction.count({ where }),
    prisma$1.transaction.findMany({
      where,
      include: {
        ...isAdmin && {
          institution: {
            select: {
              user: {
                select: {
                  institutionName: true,
                  email: true
                }
              }
            }
          }
        },
        transactionReceipt: {
          include: {
            items: {
              include: {
                student: { include: { user: true } },
                subscription: { include: { plan: true } }
              }
            }
          }
        },
        subscriptionRequest: {
          include: {
            subscriptionPlan: true,
            student: {
              include: {
                user: true
              }
            }
          }
        }
      },
      orderBy: { createdAt: "desc" },
      skip,
      take: limit
    }),
    prisma$1.transaction.count({ where: successWhere }),
    prisma$1.transaction.count({ where: pendingWhere }),
    prisma$1.transaction.count({ where: attentionWhere }),
    prisma$1.transaction.aggregate({
      where: statusFilter ? where : successWhere,
      _sum: {
        totalAmount: true,
        taxableAmount: true,
        gstAmount: true
      }
    }),
    prisma$1.transaction.findMany({
      where: {
        AND: [
          where,
          {
            institutionId: {
              not: null
            }
          }
        ]
      },
      distinct: ["institutionId"],
      select: {
        institutionId: true
      }
    })
  ]);
  const missingStudentIds = /* @__PURE__ */ new Set();
  const missingPlanIds = /* @__PURE__ */ new Set();
  transactions.forEach((t) => {
    const metadata = t.metadata;
    if (!t.transactionReceipt && metadata) {
      if (!metadata.quoteData && metadata.studentIds) {
        const sIds = Array.isArray(metadata.studentIds) ? metadata.studentIds : String(metadata.studentIds).split(",").map((id) => id.trim()).filter(Boolean);
        sIds.forEach((id) => missingStudentIds.add(id));
      }
      if (!metadata.quoteData && metadata.planId) {
        missingPlanIds.add(metadata.planId);
      }
    }
  });
  const metadataStudentsMap = {};
  if (missingStudentIds.size > 0) {
    const students = await prisma$1.student.findMany({
      where: { id: { in: Array.from(missingStudentIds) } },
      include: { user: true }
    });
    students.forEach((s) => {
      metadataStudentsMap[s.id] = s;
    });
  }
  const metadataPlansMap = {};
  if (missingPlanIds.size > 0) {
    const plans = await prisma$1.subscriptionPlan.findMany({
      where: { id: { in: Array.from(missingPlanIds) } }
    });
    plans.forEach((p) => {
      metadataPlansMap[p.id] = p.planName;
    });
  }
  const paymentsData = transactions.map((t) => {
    const receipt = t.transactionReceipt;
    const items = receipt?.items || [];
    const studentCount = items.length;
    const metadata = t.metadata;
    let userName = "N/A";
    let email = "N/A";
    let phone = "N/A";
    let planName = "N/A";
    if (studentCount > 0) {
      if (studentCount === 1 && items[0]?.student?.user) {
        const u = items[0].student.user;
        userName = `${u.firstName} ${u.lastName}`.trim();
        email = u.email || "N/A";
        phone = u.phone || "N/A";
      } else if (studentCount > 1) {
        userName = `Bulk: ${studentCount} Students`;
        email = "Multiple";
      }
      if (items[0]?.subscription?.plan) {
        planName = items[0].subscription.plan.planName;
      }
    } else if (metadata) {
      if (metadata.quoteData) {
        planName = metadata.quoteData.planName || "N/A";
        const quotes2 = metadata.quoteData.quotes || [];
        if (quotes2.length === 1) {
          userName = quotes2[0].name || "N/A";
          email = quotes2[0].email || "N/A";
        } else if (quotes2.length > 1) {
          userName = `Bulk: ${quotes2.length} Students`;
          email = "Multiple";
        }
      } else {
        if (metadata.planId) {
          planName = metadataPlansMap[metadata.planId] || "N/A";
        }
        if (metadata.studentIds) {
          const sIds = Array.isArray(metadata.studentIds) ? metadata.studentIds : String(metadata.studentIds).split(",").map((id) => id.trim()).filter(Boolean);
          if (sIds.length === 1) {
            const s = metadataStudentsMap[sIds[0]];
            if (s?.user) {
              userName = `${s.user.firstName} ${s.user.lastName}`.trim();
              email = s.user.email || "N/A";
              phone = s.user.phone || "N/A";
            }
          } else if (sIds.length > 1) {
            userName = `Bulk: ${sIds.length} Students`;
            email = "Multiple";
          }
        }
      }
    }
    if (planName === "N/A" && t.subscriptionRequest?.subscriptionPlan?.planName) {
      planName = t.subscriptionRequest.subscriptionPlan.planName;
    }
    if (userName === "N/A" && t.subscriptionRequest?.student?.user) {
      const u = t.subscriptionRequest.student.user;
      userName = `${u.firstName} ${u.lastName}`.trim();
      email = u.email || "N/A";
      phone = u.phone || "N/A";
    }
    return {
      id: t.id,
      transactionId: t.paymentId || t.orderId || t.id,
      ...isAdmin && {
        institution: t.institution?.user?.institutionName || "N/A",
        institutionEmail: t.institution?.user?.email || "N/A"
      },
      receiptNo: receipt?.receiptNo,
      user: {
        name: userName,
        email,
        phone
      },
      amount: Number(t.totalAmount),
      taxableAmount: Number(t.taxableAmount || 0),
      gstRate: Number(t.gstRate || 0),
      gstAmount: Number(t.gstAmount || 0),
      cgstAmount: Number(t.cgstAmount || 0),
      sgstAmount: Number(t.sgstAmount || 0),
      igstAmount: Number(t.igstAmount || 0),
      gstType: t.gstType,
      customerGstNumber: t.customerGstNumber,
      method: t.paymentMethod,
      date: toIST(t.createdAt),
      status: t.status,
      plan: planName
    };
  });
  res.status(200).json({
    message: "Payments fetched successfully",
    data: paymentsData,
    meta: {
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
      summary: {
        totalTransactions: total,
        successfulPayments,
        pendingPayments,
        attentionPayments,
        institutionCount: institutionsWithPayments.length,
        totalCollected: Number(totalCollected._sum.totalAmount || 0),
        totalTaxableCollected: Number(totalCollected._sum.totalAmount || 0) - Number(totalCollected._sum.gstAmount || 0),
        totalGstCollected: Number(totalCollected._sum.gstAmount || 0)
      }
    }
  });
});
const getPaymentDetails = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const transaction = await prisma$1.transaction.findUnique({
    where: { id },
    include: {
      institution: {
        select: {
          user: {
            select: {
              institutionName: true,
              email: true,
              gstNumber: true,
              institutionAddress: true
            }
          }
        }
      },
      transactionReceipt: {
        include: {
          items: {
            include: {
              student: { include: { user: true } },
              subscription: { include: { plan: true } }
            }
          }
        }
      }
    }
  });
  if (!transaction) {
    res.status(404).json({ message: "Transaction not found" });
    return;
  }
  const receipt = transaction.transactionReceipt;
  const items = receipt?.items || [];
  let planName = items[0]?.subscription?.plan?.planName || "N/A";
  const planId = transaction.metadata?.planId;
  if (planId) {
    const plan = await prisma$1.subscriptionPlan.findUnique({
      where: { id: planId }
    });
    planName = plan.planName;
  }
  let studentList = items.map((item) => ({
    id: item.student?.id,
    name: item.student?.user ? `${item.student.user.firstName} ${item.student.user.lastName}` : "N/A",
    email: item.student?.user?.email,
    amount: Number(item.amount)
  }));
  if (!receipt && transaction.metadata) {
    const metadata = transaction?.metadata;
    const quoteData = metadata?.quoteData;
    if (quoteData?.quotes) {
      studentList = (quoteData.quotes || []).map((q) => ({
        id: q.studentId,
        name: q.name || "N/A",
        email: q.email || "N/A",
        amount: Number(q.chargedAmount || 0)
      }));
    } else {
      const metadataStudentIds = metadata?.studentIds || [];
      const studentIds = Array.isArray(metadataStudentIds) ? metadataStudentIds : typeof metadataStudentIds === "string" ? metadataStudentIds.split(",").map((studentId) => studentId.trim()).filter(Boolean) : [];
      if (studentIds.length > 0) {
        const students = await prisma$1.student.findMany({
          where: { id: { in: studentIds } },
          include: { user: true }
        });
        studentList = students.map((s) => ({
          id: s.id,
          name: s.user ? `${s.user.firstName} ${s.user.lastName}` : "N/A",
          email: s.user?.email,
          amount: Number(transaction.totalAmount) / studentIds.length
        }));
      }
    }
  }
  const data = {
    id: transaction.id,
    amount: Number(transaction.totalAmount),
    taxableAmount: Number(transaction.taxableAmount || 0),
    gstRate: Number(transaction.gstRate || 0),
    gstAmount: Number(transaction.gstAmount || 0),
    cgstAmount: Number(transaction.cgstAmount || 0),
    sgstAmount: Number(transaction.sgstAmount || 0),
    igstAmount: Number(transaction.igstAmount || 0),
    gstType: transaction.gstType,
    customerGstNumber: transaction.customerGstNumber,
    institution: transaction.institution?.user?.institutionName || "N/A",
    institutionEmail: transaction.institution?.user?.email || "N/A",
    institutionGstNumber: transaction.customerGstNumber || transaction.institution?.user?.gstNumber || null,
    institutionAddress: transaction.institution?.user?.institutionAddress || "N/A",
    studentCount: studentList.length,
    planName,
    status: transaction.status,
    failureReason: transaction.failureReason,
    date: toIST(transaction.createdAt),
    endDate: items[0]?.subscription?.endDate,
    paymentMethod: transaction.paymentMethod,
    transactionId: transaction.paymentId || transaction.orderId,
    receiptNo: receipt?.receiptNo,
    students: studentList
  };
  res.status(200).json({
    message: "Payment details fetched successfully",
    data
  });
});
const getBulkPaymentQuote = asyncHandler(async (req, res) => {
  const { studentId, studentIds, planId } = req.body;
  const rules = [
    {
      isValid: !planId,
      message: "Plan ID is required"
    },
    {
      isValid: !studentId && !studentIds,
      message: "Either studentId or studentIds must be provided"
    },
    {
      isValid: studentId && studentIds,
      message: "Provide either studentId or studentIds, not both"
    },
    {
      isValid: studentIds && !Array.isArray(studentIds),
      message: "studentIds must be an array"
    },
    {
      isValid: studentIds && Array.isArray(studentIds) && studentIds.length === 0,
      message: "studentIds array cannot be empty"
    }
  ];
  for (const rule of rules) {
    if (rule.isValid) {
      res.status(400).json({ message: rule.message });
      return;
    }
  }
  const targetStudentIds = studentIds ? studentIds : [studentId];
  const plan = await prisma$1.subscriptionPlan.findUnique({
    where: { id: planId, isActive: true, isDeleted: false }
  });
  if (!plan) {
    res.status(404).json({ message: "Subscription plan not found" });
    return;
  }
  const students = await prisma$1.student.findMany({
    where: { id: { in: targetStudentIds } },
    include: {
      user: { select: { firstName: true, lastName: true, email: true } },
      subscriptions: { where: { isCurrent: true }, include: { plan: { select: { planName: true, duration: true, priceAfterDiscount: true, gstPercentage: true } } } }
    }
  });
  const quotes2 = students.map((student) => {
    const currentSub = student.subscriptions[0];
    let credit = 0;
    let remainingDays = 0;
    if (currentSub && currentSub.status === "ACTIVE" && currentSub.endDate) {
      const now = dayjs();
      const end = dayjs(currentSub.endDate);
      const start = dayjs(currentSub.startDate);
      if (end.isAfter(now)) {
        remainingDays = end.diff(now, "day");
        const totalDays = end.diff(start, "day") || 1;
        const dailyRate = Number(currentSub.amount) / totalDays;
        credit = Math.max(0, remainingDays * dailyRate);
      }
    }
    const planPrice = Number(plan.priceAfterDiscount);
    const gstAmount = planPrice * Number(plan.gstPercentage) / 100;
    const finalPrice = planPrice;
    const chargedAmount = Math.max(0, finalPrice - credit);
    return {
      studentId: student.id,
      name: `${student.user.firstName} ${student.user.lastName}`,
      email: student.user.email,
      currentPlan: currentSub?.planId || "TRIAL",
      currentPlanName: currentSub?.plan?.planName || "TRIAL",
      currentPlanStatus: currentSub?.status || "TRIAL",
      creditValue: Number(credit.toFixed(2)),
      planPrice: Number(planPrice.toFixed(2)),
      gstAmount: Number(gstAmount.toFixed(2)),
      totalPrice: Number(finalPrice.toFixed(2)),
      chargedAmount: Number(chargedAmount.toFixed(2)),
      remainingDays
    };
  });
  const grandTotal = quotes2.reduce((acc, q) => acc + q.chargedAmount, 0);
  res.status(200).json({
    message: "Bulk payment quote generated",
    data: {
      quotes: quotes2,
      grandTotal: Number(grandTotal.toFixed(2)),
      planName: plan.planName,
      planDuration: plan.duration
    }
  });
});
const createRazorpayOrder = asyncHandler(async (req, res) => {
  const { amount, currency = "INR", studentIds, studentId, planId, quoteData, subscriptionRequestId } = req.body;
  const rules = [
    {
      isValid: !planId,
      message: "Plan ID is required"
    },
    {
      isValid: !studentId && !studentIds,
      message: "Either studentId or studentIds must be provided"
    },
    {
      isValid: studentId && studentIds,
      message: "Provide either studentId or studentIds, not both"
    },
    {
      isValid: studentIds && !Array.isArray(studentIds),
      message: "studentIds must be an array"
    },
    {
      isValid: studentIds && Array.isArray(studentIds) && studentIds.length === 0,
      message: "studentIds array cannot be empty"
    }
  ];
  for (const rule of rules) {
    if (rule.isValid) {
      res.status(400).json({ message: rule.message });
      return;
    }
  }
  const targetStudentIds = studentIds ? studentIds : [studentId];
  const students = await prisma$1.student.findMany({
    where: { id: { in: targetStudentIds } }
  });
  if (students.length < 1 || targetStudentIds?.length < 1 || targetStudentIds?.length !== students.length) {
    res.status(400).json({ message: "Give the correct student id or ids" });
    return;
  }
  if (!Number.isFinite(Number(amount)) || Number(amount) <= 0) {
    res.status(400).json({ message: "Valid payment amount is required" });
    return;
  }
  if (req.user?.role === "STUDENT") {
    const student = await prisma$1.student.findUnique({ where: { userId: req.user.id } });
    if (!student) {
      res.status(404).json({ message: "Student not found" });
      return;
    }
    if (targetStudentIds.length !== 1 || targetStudentIds[0] !== student.id) {
      res.status(403).json({ message: "Students can only pay for themselves" });
      return;
    }
    if (subscriptionRequestId) {
      const subRequest = await prisma$1.subscriptionRequest.findFirst({
        where: { id: subscriptionRequestId, studentId: student.id, status: { in: ["PENDING"] } }
      });
      if (!subRequest) {
        res.status(404).json({ message: "Valid subscription request not found" });
        return;
      }
      if (subRequest.expiryDate && /* @__PURE__ */ new Date() > subRequest.expiryDate) {
        res.status(400).json({ message: "Subscription request has expired" });
        return;
      }
    }
  }
  try {
    const institutionId = req.user?.institutionId;
    if (!req.user?.id) {
      res.status(404).json({ message: "Logined user not found" });
      return;
    }
    const institution = await prisma$1.institution.findFirst({
      where: { id: institutionId },
      select: {
        user: {
          select: {
            state: true,
            gstNumber: true
          }
        }
      }
    });
    if (!institution) {
      res.status(404).json({ message: "Institution not found" });
      return;
    }
    const plan = await prisma$1.subscriptionPlan.findUnique({
      where: { id: planId }
    });
    if (!plan) {
      res.status(404).json({ message: "Subscription plan not found" });
      return;
    }
    const customerGstNumber = institution.user.gstNumber;
    const isIntraState = String(institution.user.state ?? "").trim().toUpperCase() === "TAMIL NADU";
    const totalAmount = Number(amount);
    const gstRate = Number(plan.gstPercentage || 0);
    const taxableAmount = +(totalAmount / (1 + gstRate / 100)).toFixed(2);
    const gstAmount = +(totalAmount - taxableAmount).toFixed(2);
    let cgstAmount = 0;
    let sgstAmount = 0;
    let igstAmount = 0;
    let gstType = "CGST_SGST";
    if (isIntraState) {
      cgstAmount = +(gstAmount / 2).toFixed(2);
      sgstAmount = +(gstAmount / 2).toFixed(2);
    } else {
      igstAmount = gstAmount;
      gstType = "IGST";
    }
    const options = {
      amount: Math.round(Number(amount) * 100),
      // amount in the smallest currency unit
      currency,
      receipt: `rcpt_${Date.now()}`
    };
    const order = await razorpay.orders.create(options);
    await prisma$1.transaction.create({
      data: {
        totalAmount: Number(amount),
        taxableAmount,
        gstRate,
        gstAmount,
        cgstAmount,
        sgstAmount,
        igstAmount,
        gstType,
        customerGstNumber,
        createdById: req.user?.id,
        institutionId: req.user?.institutionId || null,
        paymentMethod: "",
        orderId: order.id,
        status: "PENDING",
        ...subscriptionRequestId && { subscriptionRequestId },
        metadata: {
          studentIds: targetStudentIds,
          // Expecting array
          planId,
          quoteData: quoteData ? quoteData : {}
          // Persist full quote data for itemized view
        }
      }
    });
    res.status(200).json({
      message: "Razorpay order created",
      data: order
    });
  } catch (error) {
    console.log(error);
    res.status(500).json({ message: error?.error?.description || error?.message || "Failed to create Razorpay order" });
  }
});
const executeBulkSubscriptionPaymentProcess = async (params) => {
  const { orderId } = params;
  const order = await razorpay.orders.fetch(orderId);
  const payments = await razorpay.orders.fetchPayments(order.id);
  if (order.status == "created") {
    throw new Error("Order only created. Not paid");
  }
  if (order.status !== "paid") {
    throw new Error("Order has not paid");
  }
  const payment = payments.items.find((p) => p.status === "captured");
  if (!payment) {
    throw new Error("Captured payment not found");
  }
  return await prisma$1.$transaction(async (tx) => {
    const claimResult = await tx.transaction.updateMany({
      where: { orderId, status: "PENDING" },
      data: { status: "PROCESSING" }
    });
    let transaction = await tx.transaction.findFirst({ where: { orderId } });
    if (!transaction) throw new Error("Transaction not found");
    if (claimResult.count === 0) {
      if (transaction.status === "SUCCESS") return transaction;
      throw new Error(`Transaction is in ${transaction.status} state, cannot process`);
    }
    const { id: paymentId, method: paymentMethod } = payment;
    const { subscriptionRequestId, institutionId } = transaction;
    const institution = await tx.institution.findUnique({
      where: { id: institutionId },
      select: {
        user: { select: { gstNumber: true, state: true } }
      }
    });
    if (!institution) {
      throw new Error("Institution not found");
    }
    const metadata = transaction.metadata;
    if (!metadata?.planId) {
      throw new Error("Plan ID missing");
    }
    if (!Array.isArray(metadata?.studentIds) || metadata?.studentIds.length < 1) {
      throw new Error("Student IDs missing");
    }
    const { studentIds, planId } = transaction.metadata;
    const plan = await prisma$1.subscriptionPlan.findUnique({
      where: { id: planId }
    });
    if (!Array.isArray(studentIds) || studentIds.length < 1) {
      throw new Error("Students not found");
    }
    if (!plan) {
      throw new Error("Subscription plan not found");
    }
    let grandTotal = 0;
    const studentQuotes = [];
    for (const sId of studentIds) {
      const student = await tx.student.findUnique({
        where: { id: sId },
        include: {
          user: true,
          subscriptions: { where: { isCurrent: true, institutionId } },
          institutions: { where: { institutionId, deletedAt: null, isVerified: true }, take: 1 }
        }
      });
      if (!student) continue;
      const currentSub = student.subscriptions[0];
      const quoteForStudent = metadata?.quoteData?.quotes?.find((q) => q.studentId === sId);
      let credit = 0;
      let finalPrice = Number(plan.priceAfterDiscount);
      let chargedAmount = 0;
      if (quoteForStudent) {
        credit = Number(quoteForStudent.creditValue || 0);
        finalPrice = Number(quoteForStudent.totalPrice || finalPrice);
        chargedAmount = Number(quoteForStudent.chargedAmount || 0);
      } else {
        if (currentSub && currentSub.status === "ACTIVE" && currentSub.endDate) {
          const now = dayjs();
          const end = dayjs(currentSub.endDate);
          const start = dayjs(currentSub.startDate);
          if (end.isAfter(now)) {
            const remainingDays = end.diff(now, "day");
            const totalDays = end.diff(start, "day") || 1;
            const dailyRate = Number(currentSub.amount) / totalDays;
            credit = Math.max(0, remainingDays * dailyRate);
          }
        }
        chargedAmount = Math.max(0, finalPrice - credit);
      }
      grandTotal += chargedAmount;
      studentQuotes.push({
        studentId: sId,
        institutionId,
        previousSubscriptionId: currentSub?.id || null,
        studentInstitutionId: student.institutions[0]?.id || null,
        email: student.user.email,
        credit,
        chargedAmount,
        finalPrice
      });
    }
    const customerGstNumber = institution?.user?.gstNumber || null;
    const isIntraState = String(institution?.user?.state ?? "").trim().toUpperCase() === "TAMIL NADU";
    const totalAmount = Number(grandTotal);
    const gstRate = Number(plan.gstPercentage || 0);
    const taxableAmount = +(totalAmount / (1 + gstRate / 100)).toFixed(2);
    const gstAmount = +(totalAmount - taxableAmount).toFixed(2);
    let cgstAmount = 0;
    let sgstAmount = 0;
    let igstAmount = 0;
    let gstType = "CGST_SGST";
    if (isIntraState) {
      cgstAmount = +(gstAmount / 2).toFixed(2);
      sgstAmount = +(gstAmount - cgstAmount).toFixed(2);
    } else {
      igstAmount = gstAmount;
      gstType = "IGST";
    }
    transaction = await tx.transaction.update({
      where: { id: transaction.id },
      data: {
        paymentId,
        paymentMethod,
        signature: null,
        taxableAmount,
        gstRate,
        gstAmount,
        cgstAmount,
        sgstAmount,
        igstAmount,
        gstType,
        customerGstNumber,
        totalAmount: grandTotal,
        // Update with calculated total
        status: "SUCCESS"
      }
    });
    const sequence = await tx.referenceSequence.findFirst({
      where: { type: "RECEIPT", isActive: true, endedAt: null }
    });
    if (!sequence) {
      throw new Error("Sequence not found");
    }
    const updatedSequence = await tx.referenceSequence.update({
      where: { id: sequence.id },
      data: { lastCount: { increment: 1 } }
    });
    const receiptNo = `${updatedSequence.prefix}${updatedSequence.lastCount}`;
    const transactionReceipt = await tx.transactionReceipt.create({
      data: {
        transactionId: transaction.id,
        institutionId,
        receiptNo,
        totalAmount: grandTotal,
        status: "PAID",
        issuedAt: /* @__PURE__ */ new Date()
      }
    });
    for (const quote of studentQuotes) {
      await tx.subscription.updateMany({
        where: { studentId: quote.studentId, institutionId, isCurrent: true },
        data: { isCurrent: false }
      });
      const newSub = await tx.subscription.create({
        data: {
          studentId: quote.studentId,
          planId: plan.id,
          email: quote.email,
          amount: quote.finalPrice,
          chargedAmount: quote.chargedAmount,
          creditUsed: quote.credit,
          startDate: /* @__PURE__ */ new Date(),
          endDate: dayjs().add(plan.duration, "day").toDate(),
          status: "ACTIVE",
          isCurrent: true,
          action: "UPGRADE",
          institutionId: quote.institutionId,
          previousSubscriptionId: quote.previousSubscriptionId,
          studentInstitutionId: quote.studentInstitutionId
        }
      });
      await tx.transactionReceiptItem.create({
        data: {
          receiptId: transactionReceipt.id,
          subscriptionId: newSub.id,
          studentId: quote.studentId,
          amount: quote.chargedAmount
        }
      });
    }
    if (subscriptionRequestId) {
      const request = await tx.subscriptionRequest.findUnique({
        where: { id: subscriptionRequestId }
      });
      if (request) {
        const currentSub = await tx.subscription.findFirst({
          where: { studentId: request.studentId, institutionId, isCurrent: true }
        });
        if (currentSub) {
          await tx.subscriptionRequest.update({
            where: { id: subscriptionRequestId },
            data: {
              status: "PAID",
              paidAt: /* @__PURE__ */ new Date(),
              razorpayOrderId: order.id,
              razorpayPaymentId: paymentId,
              subscriptionId: currentSub.id
            }
          });
        }
      }
    }
    return transaction;
  });
};
const verifyAndProcessBulkPayment = asyncHandler(async (req, res) => {
  const { razorpay_order_id, razorpay_payment_id, razorpay_signature } = req.body;
  if (!razorpay_order_id || !razorpay_payment_id || !razorpay_signature) {
    res.status(400).json({ message: "Missing required verification fields" });
    return;
  }
  const secret = process.env.RAZORPAY_KEY_SECRET || "placeholder_secret";
  const body = razorpay_order_id + "|" + razorpay_payment_id;
  const expectedSignature = crypto.createHmac("sha256", secret).update(body.toString()).digest("hex");
  if (expectedSignature !== razorpay_signature) {
    res.status(400).json({ message: "Invalid payment signature" });
    return;
  }
  const payment = await razorpay.payments.fetch(razorpay_payment_id);
  if (payment.status !== "captured") {
    res.status(400).json({
      message: `Payment not successful. Current status: ${payment.status}`
    });
    return;
  }
  try {
    const result = await executeBulkSubscriptionPaymentProcess({ orderId: razorpay_order_id });
    res.status(200).json({
      message: "Payment verified and subscriptions updated",
      data: {
        id: result?.id,
        amount: result?.totalAmount,
        status: result?.status
      }
    });
    return;
  } catch (error) {
    res.status(500).json({ message: error.message || "Failed to process bulk payment after verification" });
  }
});
const razorpayWebhook = asyncHandler(async (req, res) => {
  const signature = req.headers["x-razorpay-signature"];
  const secret = process.env.RAZORPAY_WEBHOOK_SECRET || "placeholder_secret";
  const expectedSignature = crypto.createHmac("sha256", secret).update(req.rawBody).digest("hex");
  if (expectedSignature !== signature) {
    res.status(400).json({ message: "Invalid webhook signature" });
    return;
  }
  const event = req.body.event;
  console.log(`Razorpay Webhook Event: ${event}`);
  if (event === "order.paid") {
    const orderId = req.body.payload.order.entity.id;
    try {
      await executeBulkSubscriptionPaymentProcess({ orderId });
      console.log(`Successfully processed payment via webhook for Order: ${orderId}`);
    } catch (err) {
      console.error("Error processing webhook payment:", err);
    }
  } else if (event === "payment.failed") {
    const orderId = req.body.payload.payment.entity.order_id;
    const errorDescription = req.body.payload.payment.entity.error_description;
    await prisma$1.transaction.updateMany({
      where: { orderId, status: "PENDING" },
      data: {
        status: "FAILED",
        failureReason: errorDescription || "Payment failed",
        paymentId: req.body.payload.payment.entity.id
      }
    });
  } else if (event === "refund.processed") {
    const paymentId = req.body.payload.payment.entity.id;
    const amountRefunded = req.body.payload.refund.entity.amount;
    const totalAmount = req.body.payload.payment.entity.amount;
    const transactionStatus = amountRefunded < totalAmount ? "PARTIAL_REFUND" : "REFUNDED";
    await prisma$1.transaction.updateMany({
      where: { paymentId },
      data: {
        status: transactionStatus,
        failureReason: `Refund processed: ${amountRefunded / 100}`
      }
    });
    if (transactionStatus === "REFUNDED") {
      const transaction = await prisma$1.transaction.findFirst({
        where: { paymentId },
        include: { transactionReceipt: { include: { items: true } } }
      });
      if (transaction) {
        const subscriptionIds = transaction.transactionReceipt ? transaction.transactionReceipt.items.map((i) => i.subscriptionId) : [];
        await prisma$1.subscription.updateMany({
          where: { id: { in: subscriptionIds } },
          data: { status: "EXPIRED", isCurrent: false }
        });
      }
    }
  }
  res.status(200).json({ status: "ok" });
});
const cancelPayment = asyncHandler(async (req, res) => {
  const { orderId, reason } = req.body;
  if (!orderId) {
    res.status(400).json({ message: "Order ID is required" });
    return;
  }
  const result = await prisma$1.transaction.updateMany({
    where: { orderId, status: "PENDING" },
    data: {
      status: "CANCELLED",
      failureReason: reason || "User cancelled payment process"
    }
  });
  if (result.count === 0) {
    const existing = await prisma$1.transaction.findFirst({ where: { orderId } });
    if (existing) {
      res.status(400).json({ message: `Payment cannot be cancelled (status: ${existing.status})` });
      return;
    }
  }
  res.status(200).json({ message: "Payment cancellation tracked" });
});
const downloadPaymentReceipt = asyncHandler(async (req, res) => {
  try {
    let money = function(val) {
      return `Rs.${val.toFixed(2)}`;
    }, normalizeMoney = function(val) {
      return `${val.toFixed(2)}`;
    };
    const { id } = req.params;
    const transaction = await prisma$1.transaction.findUnique({
      where: { id },
      include: {
        institution: {
          include: {
            user: {
              select: {
                institutionName: true,
                gstNumber: true,
                institutionAddress: true
              }
            }
          }
        },
        transactionReceipt: {
          include: {
            items: {
              include: {
                subscription: { include: { plan: true } }
              }
            }
          }
        }
      }
    });
    if (!transaction) {
      res.status(404).json({ message: "Transaction not found" });
      return;
    }
    const receipt = transaction.transactionReceipt;
    const metadata = transaction.metadata;
    const gstRate = Number(transaction.gstRate || 0);
    const toAmount = (value) => Number(value || 0);
    const reIndexItems = (items) => items.map((item, index) => ({ no: index + 1, ...item }));
    const groupItemsByDescription = (items) => {
      const grouped = /* @__PURE__ */ new Map();
      for (const item of items) {
        const description = item.description || "Subscription Plan";
        const current = grouped.get(description) || { description, quantity: 0, total: 0 };
        current.quantity += 1;
        current.total += item.amount;
        grouped.set(description, current);
      }
      return reIndexItems(
        Array.from(grouped.values()).map((item) => {
          const total = gstRate > 0 ? item.total / (1 + gstRate / 100) : item.total;
          return {
            description: item.description,
            quantity: item.quantity,
            unitPrice: item.quantity > 0 ? total / item.quantity : total,
            total
          };
        })
      );
    };
    const getStudentCount = (studentIds) => {
      if (Array.isArray(studentIds)) return studentIds.length;
      if (typeof studentIds === "string") return studentIds.split(",").filter(Boolean).length;
      return 0;
    };
    let pdfItems = groupItemsByDescription(
      (receipt?.items || []).map((item) => ({
        description: item.subscription?.plan?.planName || "Subscription Plan",
        amount: toAmount(item.amount || item.subscription?.chargedAmount)
      }))
    );
    if (pdfItems.length === 0 && metadata) {
      const quoteData = metadata?.quoteData;
      if (quoteData) {
        pdfItems = groupItemsByDescription(
          (quoteData.quotes || []).map((quote) => ({
            description: quoteData.planName || "Subscription Plan",
            amount: toAmount(quote.chargedAmount)
          }))
        );
      } else {
        const studentIds = metadata?.studentIds || [];
        const planId = metadata?.planId;
        let planName = "Subscription Plan";
        if (planId) {
          const plan = await prisma$1.subscriptionPlan.findUnique({ where: { id: planId } });
          planName = plan?.planName || planName;
        }
        const quantity = getStudentCount(studentIds) || 1;
        const total = gstRate > 0 ? Number(transaction.taxableAmount || 0) : Number(transaction.totalAmount || 0);
        pdfItems = reIndexItems([{
          description: planName,
          quantity,
          unitPrice: quantity > 0 ? total / quantity : total,
          total
        }]);
      }
    }
    ;
    if (pdfItems.length === 0) {
      const total = gstRate > 0 ? Number(transaction.taxableAmount || 0) : Number(transaction.totalAmount || 0);
      pdfItems = reIndexItems([{
        description: "Subscription Payment",
        quantity: 1,
        unitPrice: total,
        total
      }]);
    }
    const platformUser = await prisma$1.user.findFirst({
      where: {
        role: "ADMIN",
        email: "admin@gmail.com"
      },
      select: {
        institutionAddress: true,
        phone: true,
        gstNumber: true
      }
    });
    if (!platformUser) {
      throw new Error("Platform Admin not found");
    }
    const gstAmount = Number(transaction.gstAmount || 0);
    const cgstAmount = Number(transaction.cgstAmount || 0);
    const sgstAmount = Number(transaction.sgstAmount || 0);
    const igstAmount = Number(transaction.igstAmount || 0);
    const grandTotal = Number(receipt?.totalAmount || transaction.totalAmount || 0);
    const netTotal = gstAmount > 0 ? Number(transaction.taxableAmount || 0) : grandTotal;
    const hasGst = gstRate > 0 && gstAmount > 0;
    const gstType = transaction.gstType;
    const data = {
      receiptNo: receipt?.receiptNo || transaction.orderId || transaction.id,
      receiptDate: dayjs(receipt?.createdAt || transaction.createdAt).format("DD-MM-YYYY HH:mm A"),
      from: {
        name: "Exam Infra",
        address: platformUser.institutionAddress || "N/A",
        phone: platformUser.phone || "N/A",
        gst: platformUser.gstNumber || "N/A",
        sac: "998313"
      },
      to: {
        name: transaction.institution?.user.institutionName || "N/A",
        address: transaction.institution?.user.institutionAddress || "N/A",
        gst: transaction.customerGstNumber || transaction.institution?.user.gstNumber || "N/A"
      },
      items: pdfItems,
      netTotal,
      gstRate,
      gstAmount,
      cgstAmount,
      sgstAmount,
      igstAmount,
      gstType,
      hasGst,
      grandTotal
    };
    const doc = new PDFDocument({
      size: "A4",
      margin: 0
    });
    const colors = {
      primary: "#1976d2",
      text: "#1f2937",
      subText: "#6b7280",
      border: "#e5e7eb",
      light: "#f5f7fa",
      white: "#ffffff",
      success: "#16a34a",
      textLight: "#ffffff",
      textMuted: "#cbd5e1"
    };
    const drawPageDecoration = () => {
      doc.save();
      doc.rect(0, 0, 595, 842).fill("#eef1f5");
      doc.roundedRect(18, 18, 559, 806, 8).fill(colors.white);
      doc.restore();
    };
    drawPageDecoration();
    doc.on("pageAdded", () => {
      drawPageDecoration();
    });
    const safeReceiptFileName = String(data.receiptNo).replace(/[^a-zA-Z0-9_-]/g, "-");
    const logoPath = path.resolve(process.cwd(), "public", "logo.png");
    res.setHeader("Content-Type", "application/pdf");
    res.setHeader(
      "Content-Disposition",
      `inline; filename="receipt-${safeReceiptFileName}.pdf"`
    );
    doc.pipe(res);
    doc.roundedRect(20, 20, 555, 78, 10).fill(colors.primary);
    if (fs.existsSync(logoPath)) {
      doc.roundedRect(36, 34, 42, 42, 6).fill(colors.white);
      doc.image(logoPath, 40, 38, {
        fit: [34, 34],
        align: "center",
        valign: "center"
      });
    }
    doc.fillColor(colors.textLight).font("Helvetica-Bold").fontSize(20).text(data.from.name, fs.existsSync(logoPath) ? 88 : 36, 46);
    doc.fontSize(18).text("RECEIPT", 440, 46);
    doc.font("Helvetica").fontSize(9).fillColor(colors.textMuted).text("Payment Confirmation", 440, 68);
    const infoY = 110;
    doc.roundedRect(32, infoY, 530, 52, 5).fill(colors.light);
    doc.fillColor(colors.text);
    doc.font("Helvetica-Bold").fontSize(8).text("Receipt No", 45, infoY + 10);
    doc.font("Helvetica").fontSize(10).text(data.receiptNo, 45, infoY + 24);
    doc.font("Helvetica-Bold").fontSize(8).text("Date", 430, infoY + 10);
    doc.font("Helvetica").fontSize(10).text(data.receiptDate, 430, infoY + 24);
    const sectionY = 170;
    doc.font("Helvetica-Bold").fontSize(12);
    const leftNameHeight = doc.heightOfString(data.from.name, { width: 190 });
    doc.font("Helvetica").fontSize(9);
    const leftAddressHeight = doc.heightOfString(data.from.address, { width: 190 });
    const leftPhoneHeight = doc.heightOfString(`Phone: ${data.from.phone}`, { width: 190 });
    const leftGstHeight = doc.heightOfString(`GSTIN: ${data.from.gst}`, { width: 190 });
    const leftSacHeight = doc.heightOfString(`SAC: ${data.from.sac}`, { width: 190 });
    const leftContentHeight = 18 + leftNameHeight + 8 + leftAddressHeight + 6 + leftPhoneHeight + 4 + leftGstHeight + 4 + leftSacHeight + 18;
    doc.font("Helvetica-Bold").fontSize(9);
    const rightTitleHeight = doc.heightOfString("Billed To", { width: 190 });
    doc.font("Helvetica-Bold").fontSize(12);
    const rightNameHeight = doc.heightOfString(data.to.name, { width: 190 });
    doc.font("Helvetica").fontSize(9);
    const rightAddressHeight = doc.heightOfString(data.to.address, { width: 190 });
    const rightGstHeight = doc.heightOfString(`GSTIN: ${data.to.gst}`, { width: 190 });
    const rightContentHeight = 10 + rightTitleHeight + 8 + rightNameHeight + 6 + rightAddressHeight + 6 + rightGstHeight + 18;
    const cardHeight = Math.max(100, leftContentHeight, rightContentHeight);
    doc.roundedRect(32, sectionY, 250, cardHeight, 5).fill(colors.light);
    let currentLeftY = sectionY + 18;
    doc.fillColor(colors.text).font("Helvetica-Bold").fontSize(12).text(data.from.name, 42, currentLeftY);
    currentLeftY += leftNameHeight + 8;
    doc.font("Helvetica").fontSize(9).fillColor(colors.subText).text(data.from.address, 42, currentLeftY, {
      width: 190
    });
    currentLeftY += leftAddressHeight + 6;
    doc.text(`Phone: ${data.from.phone}`, 42, currentLeftY);
    currentLeftY += leftPhoneHeight + 4;
    doc.text(`GSTIN: ${data.from.gst}`, 42, currentLeftY);
    currentLeftY += leftGstHeight + 4;
    doc.text(`SAC: ${data.from.sac}`, 42, currentLeftY);
    doc.roundedRect(312, sectionY, 250, cardHeight, 5).fill(colors.light);
    let currentRightY = sectionY + 10;
    doc.fillColor(colors.subText).font("Helvetica-Bold").fontSize(9).text("BILLED TO", 322, currentRightY);
    currentRightY += rightTitleHeight + 8;
    doc.fillColor(colors.text).font("Helvetica-Bold").fontSize(12).text(data.to.name, 322, currentRightY);
    currentRightY += rightNameHeight + 6;
    doc.font("Helvetica").fontSize(9).fillColor(colors.subText).text(data.to.address, 322, currentRightY, {
      width: 190
    });
    currentRightY += rightAddressHeight + 6;
    doc.text(`GSTIN: ${data.to.gst}`, 322, currentRightY);
    const tableTop = sectionY + cardHeight + 15;
    const cols = {
      no: 42,
      desc: 80,
      qty: 355,
      unit: 420,
      total: 500
    };
    doc.rect(32, tableTop, 530, 28).fill(colors.primary);
    doc.fillColor(colors.white).font("Helvetica-Bold").fontSize(9);
    doc.text("#", cols.no, tableTop + 9);
    doc.text("Description", cols.desc, tableTop + 9);
    doc.text("Qty", cols.qty, tableTop + 9);
    doc.text("Price", cols.unit, tableTop + 9);
    doc.text("Total", cols.total, tableTop + 9);
    let rowY = tableTop + 28;
    data.items.forEach((item, index) => {
      doc.font("Helvetica").fontSize(9);
      const descHeight = doc.heightOfString(item.description, { width: 220 });
      const rowHeight = Math.max(30, descHeight + 16);
      doc.rect(32, rowY, 530, rowHeight).fill(index % 2 === 0 ? "#fafafa" : "#ffffff");
      doc.fillColor(colors.text).font("Helvetica").fontSize(9);
      const verticalOffset = (rowHeight - 9) / 2;
      doc.text(item.no.toString(), cols.no, rowY + verticalOffset);
      doc.text(item.description, cols.desc, rowY + 8, {
        width: 220
      });
      doc.text(item.quantity.toString(), cols.qty, rowY + verticalOffset);
      doc.text(
        item.unitPrice.toFixed(2),
        cols.unit,
        rowY + verticalOffset
      );
      doc.text(
        item.total.toFixed(2),
        cols.total,
        rowY + verticalOffset
      );
      rowY += rowHeight;
    });
    let totalsY = rowY + 14;
    const taxRows = data.hasGst ? data.gstType === "IGST" ? [{ label: `IGST (${data.gstRate}%)`, value: data.igstAmount || data.gstAmount }] : [
      { label: `CGST (${data.gstRate / 2}%)`, value: data.cgstAmount },
      { label: `SGST (${data.gstRate / 2}%)`, value: data.sgstAmount }
    ] : [{ label: "GST", value: "No GST" }];
    const amountInWords = numberToWords(data.grandTotal);
    doc.font("Helvetica").fontSize(9);
    const amountInWordsHeight = doc.heightOfString(amountInWords, { width: 170 });
    const totalsBoxHeight = 70 + taxRows.length * 20;
    const totalSectionHeight = totalsBoxHeight + 24 + amountInWordsHeight;
    if (totalsY + totalSectionHeight > 760) {
      doc.addPage();
      totalsY = 40;
    }
    doc.roundedRect(372, totalsY, 190, totalsBoxHeight, 5).fill(colors.light);
    doc.fillColor(colors.text).font("Helvetica").fontSize(9);
    let totalLineY = totalsY + 14;
    doc.text("Taxable Amount", 385, totalLineY);
    doc.text(
      normalizeMoney(data.netTotal),
      470,
      totalLineY,
      {
        align: "right",
        width: 70
      }
    );
    totalLineY += 20;
    taxRows.forEach((row) => {
      doc.text(row.label, 385, totalLineY);
      const valStr = typeof row.value === "string" ? row.value : normalizeMoney(row.value);
      doc.text(
        valStr,
        470,
        totalLineY,
        {
          align: "right",
          width: 70
        }
      );
      totalLineY += 20;
    });
    doc.moveTo(385, totalLineY).lineTo(542, totalLineY).strokeColor("#d1d5db").stroke();
    totalLineY += 10;
    doc.font("Helvetica-Bold").fontSize(11).fillColor(colors.primary);
    doc.text("Grand Total", 385, totalLineY);
    doc.text(
      money(data.grandTotal),
      458,
      totalLineY,
      {
        align: "right",
        width: 82
      }
    );
    doc.fillColor(colors.text).font("Helvetica-Bold").fontSize(9).text("Amount in Words:", 372, totalsY + totalsBoxHeight + 12, {
      align: "right",
      width: 170
    });
    doc.font("Helvetica").fontSize(9).fillColor(colors.subText).text(amountInWords, 372, totalsY + totalsBoxHeight + 24, {
      align: "right",
      width: 170
    });
    doc.fillColor("#9ca3af").font("Helvetica").fontSize(8).text(
      "Thank you for your business",
      0,
      795,
      {
        align: "center"
      }
    );
    doc.end();
  } catch (error) {
    if (!res.headersSent) {
      res.status(500).json({ message: error.message });
    }
  }
});
const paymentReportsPDF = asyncHandler(async (req, res) => {
  const { search, status: statusFilter, planType, startDate, endDate } = req.query;
  const isAdmin = req.user?.role === "ADMIN";
  const where = {};
  if (!isAdmin) {
    where.institutionId = req.user?.institutionId;
  }
  const andConditions = [];
  if (planType) {
    const plans = await prisma$1.subscriptionPlan.findMany({
      where: { planType },
      select: { id: true }
    });
    const planIds = plans.map((p) => p.id);
    const planIdConditions = planIds.map((id) => ({
      metadata: {
        path: "$.planId",
        equals: id
      }
    }));
    andConditions.push({
      OR: [
        {
          transactionReceipt: {
            items: {
              some: {
                subscription: {
                  plan: {
                    planType
                  }
                }
              }
            }
          }
        },
        {
          subscriptionRequest: {
            subscriptionPlan: {
              planType
            }
          }
        },
        ...planIdConditions
      ]
    });
  }
  if (statusFilter) {
    andConditions.push({ status: statusFilter });
  }
  if (search) {
    const searchOR = [
      { failureReason: { contains: search } },
      { paymentMethod: { contains: search } },
      { transactionReceipt: { receiptNo: { contains: search } } },
      ...isAdmin ? [{ institution: { user: { OR: [{ institutionName: { contains: search } }, { email: { contains: search } }] } } }] : []
    ];
    andConditions.push({ OR: searchOR });
  }
  if (startDate && endDate) {
    andConditions.push({
      createdAt: { gte: new Date(startDate), lte: new Date(endDate) }
    });
  }
  if (andConditions.length > 0) {
    where.AND = andConditions;
  }
  const transactions = await prisma$1.transaction.findMany({
    where,
    include: {
      institution: { select: { user: { select: { institutionName: true, email: true } } } },
      transactionReceipt: {
        include: {
          items: {
            include: {
              student: { include: { user: true } },
              subscription: { include: { plan: true } }
            }
          }
        }
      },
      subscriptionRequest: {
        include: {
          subscriptionPlan: true,
          student: {
            include: {
              user: true
            }
          }
        }
      }
    },
    orderBy: { createdAt: "desc" }
  });
  const formatMoney = (n) => {
    const formatted = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR", maximumFractionDigits: 2 }).format(n || 0);
    return formatted.replace("₹", "INR ");
  };
  const missingStudentIds = /* @__PURE__ */ new Set();
  const missingPlanIds = /* @__PURE__ */ new Set();
  transactions.forEach((t) => {
    const metadata = t.metadata;
    if (!t.transactionReceipt && metadata) {
      if (!metadata.quoteData && metadata.studentIds) {
        const sIds = Array.isArray(metadata.studentIds) ? metadata.studentIds : String(metadata.studentIds).split(",").map((id) => id.trim()).filter(Boolean);
        sIds.forEach((id) => missingStudentIds.add(id));
      }
      if (!metadata.quoteData && metadata.planId) {
        missingPlanIds.add(metadata.planId);
      }
    }
  });
  const metadataStudentsMap = {};
  if (missingStudentIds.size > 0) {
    const students = await prisma$1.student.findMany({
      where: { id: { in: Array.from(missingStudentIds) } },
      include: { user: true }
    });
    students.forEach((s) => {
      metadataStudentsMap[s.id] = s;
    });
  }
  const metadataPlansMap = {};
  if (missingPlanIds.size > 0) {
    const plans = await prisma$1.subscriptionPlan.findMany({
      where: { id: { in: Array.from(missingPlanIds) } }
    });
    plans.forEach((p) => {
      metadataPlansMap[p.id] = p.planName;
    });
  }
  const formatStudentDisplay = (studentName, studentEmail) => {
    const displayName = (studentName || "").trim();
    const displayEmail = (studentEmail || "").trim();
    if (!displayName) return displayEmail || "N/A";
    return displayEmail ? `${displayName}
${displayEmail}` : displayName;
  };
  const rows = transactions.map((t, index) => {
    const receipt = t.transactionReceipt;
    const items = receipt?.items || [];
    const studentCount = items.length;
    const metadata = t.metadata;
    let userName = "N/A";
    let planName = "N/A";
    if (studentCount > 0) {
      if (studentCount === 1 && items[0]?.student?.user) {
        const u = items[0].student.user;
        userName = formatStudentDisplay(`${u.firstName} ${u.lastName}`.trim(), u.email);
      } else if (studentCount > 1) {
        const entries = items.map((item) => ({
          name: item.student?.user ? `${item.student.user.firstName} ${item.student.user.lastName}`.trim() : null,
          email: item.student?.user?.email?.trim() || null
        })).filter((entry) => entry.name || entry.email);
        const names = entries.map((entry) => entry.name).filter(Boolean);
        const emails = entries.map((entry) => entry.email).filter(Boolean);
        if (names.length > 0 && names.length <= 3) {
          userName = emails.length > 0 ? `${names.join(", ")}
${emails.join(", ")}` : names.join(", ");
        } else if (names.length > 3) {
          userName = `${names.slice(0, 2).join(", ")} +${names.length - 2} more`;
        } else {
          userName = `${studentCount} Students`;
        }
      }
      planName = items[0]?.subscription?.plan?.planName || "N/A";
    } else if (metadata) {
      if (metadata.quoteData) {
        planName = metadata.quoteData.planName || "N/A";
        const quotes2 = metadata.quoteData.quotes || [];
        if (quotes2.length === 1) {
          userName = formatStudentDisplay(quotes2[0].name, quotes2[0].email);
        } else if (quotes2.length > 1) {
          const entries = quotes2.map((q) => ({
            name: q.name?.trim() || null,
            email: q.email?.trim() || null
          })).filter((entry) => entry.name || entry.email);
          const names = entries.map((entry) => entry.name).filter(Boolean);
          const emails = entries.map((entry) => entry.email).filter(Boolean);
          if (names.length > 0 && names.length <= 3) {
            userName = emails.length > 0 ? `${names.join(", ")}
${emails.join(", ")}` : names.join(", ");
          } else if (names.length > 3) {
            userName = `${names.slice(0, 2).join(", ")} +${names.length - 2} more`;
          } else {
            userName = `${quotes2.length} Students`;
          }
        }
      } else {
        if (metadata.planId) {
          planName = metadataPlansMap[metadata.planId] || "N/A";
        }
        if (metadata.studentIds) {
          const sIds = Array.isArray(metadata.studentIds) ? metadata.studentIds : String(metadata.studentIds).split(",").map((id) => id.trim()).filter(Boolean);
          if (sIds.length === 1) {
            const s = metadataStudentsMap[sIds[0]];
            if (s?.user) userName = formatStudentDisplay(`${s.user.firstName} ${s.user.lastName}`.trim(), s.user.email);
          } else if (sIds.length > 1) {
            const entries = sIds.map((id) => {
              const s = metadataStudentsMap[id];
              return {
                name: s?.user ? `${s.user.firstName} ${s.user.lastName}`.trim() : null,
                email: s?.user?.email?.trim() || null
              };
            }).filter((entry) => entry.name || entry.email);
            const names = entries.map((entry) => entry.name).filter(Boolean);
            const emails = entries.map((entry) => entry.email).filter(Boolean);
            if (names.length > 0 && names.length <= 3) {
              userName = emails.length > 0 ? `${names.join(", ")}
${emails.join(", ")}` : names.join(", ");
            } else if (names.length > 3) {
              userName = `${names.slice(0, 2).join(", ")} +${names.length - 2} more`;
            } else {
              userName = `${sIds.length} Students`;
            }
          }
        }
      }
    }
    if (planName === "N/A" && t.subscriptionRequest?.subscriptionPlan?.planName) {
      planName = t.subscriptionRequest.subscriptionPlan.planName;
    }
    if (userName === "N/A" && t.subscriptionRequest?.student?.user) {
      const u = t.subscriptionRequest.student.user;
      userName = formatStudentDisplay(`${u.firstName} ${u.lastName}`.trim(), u.email);
    }
    const institution = t.institution?.user?.institutionName || "N/A";
    const receiptNo = receipt?.receiptNo || t.orderId || "N/A";
    const method = t.paymentMethod || "N/A";
    const amount = formatMoney(Number(t.totalAmount || 0));
    const taxable = formatMoney(Number(t.taxableAmount || 0));
    let gstInfo = "No GST";
    if (t.gstRate && Number(t.gstRate) > 0) {
      if (t.gstType === "IGST") {
        gstInfo = `IGST ${t.gstRate}%
${formatMoney(Number(t.igstAmount || t.gstAmount))}`;
      } else {
        gstInfo = `CGST/SGST ${t.gstRate}%
${formatMoney(Number(t.cgstAmount || 0))} + ${formatMoney(Number(t.sgstAmount || 0))}`;
      }
    }
    const dateStr = dayjs(t.createdAt).format("DD MMM YYYY, hh:mm A");
    const statusStr = String(t.status || "UNKNOWN");
    return [String(index + 1), institution, `${receiptNo}
${method}`, userName, planName, amount, `Taxable: ${taxable}
${gstInfo}`, dateStr, statusStr];
  });
  const headers = ["S.No", "Institution", "Receipt/Method", "Student", "Plan", "Amount", "Tax Details", "Date", "Status"];
  const colWidths = [35, 95, 95, 130, 75, 70, 95, 95, 55];
  const doc = new PDFDocument({ size: "A4", layout: "landscape", margin: 0 });
  res.setHeader("Content-Type", "application/pdf");
  res.setHeader("Content-Disposition", `attachment; filename=Payment_History_Report_${dayjs().format("YYYY-MM-DD")}.pdf`);
  doc.pipe(res);
  const primary = "#1976d2";
  const white = "#ffffff";
  const altRow = "#f5f7fa";
  const textColor = "#1f2937";
  const headerTextMuted = "#cbd5e1";
  const PW = doc.page.width;
  const PH = doc.page.height;
  const margin = 24;
  const drawPageBg = () => {
    doc.save();
    doc.rect(0, 0, PW, PH).fill("#eef1f5");
    doc.roundedRect(margin - 6, margin - 6, PW - (margin - 6) * 2, PH - (margin - 6) * 2, 8).fill(white);
    doc.restore();
  };
  drawPageBg();
  doc.on("pageAdded", () => {
    drawPageBg();
  });
  const logoPath = path.resolve(process.cwd(), "public", "logo.png");
  const hasLogo = fs.existsSync(logoPath);
  const headerH = 66;
  doc.roundedRect(margin, margin, PW - margin * 2, headerH, 10).fill(primary);
  if (hasLogo) {
    doc.roundedRect(margin + 14, margin + 12, 40, 40, 6).fill(white);
    try {
      doc.image(logoPath, margin + 18, margin + 16, { fit: [32, 32], align: "center", valign: "center" });
    } catch (_) {
    }
  }
  const textStartX = hasLogo ? margin + 64 : margin + 16;
  doc.fillColor(white).font("Helvetica-Bold").fontSize(20).text("Exam Infra", textStartX, margin + 14);
  doc.font("Helvetica").fontSize(9).fillColor(headerTextMuted).text("Payment History Report", textStartX, margin + 38);
  doc.font("Helvetica-Bold").fontSize(9).fillColor(white).text(`Generated: ${dayjs().format("DD MMM YYYY, hh:mm A")}`, PW - margin - 230, margin + 14, { width: 220, align: "right" });
  doc.font("Helvetica").fontSize(9).fillColor(headerTextMuted).text(`Total Records: ${rows.length}`, PW - margin - 230, margin + 36, { width: 220, align: "right" });
  const tableTop = margin + headerH + 14;
  const tableLeft = margin;
  const tableWidth = PW - margin * 2;
  const baseTotal = colWidths.reduce((a, b) => a + b, 0);
  const scaledWidths = colWidths.map((w) => w / baseTotal * tableWidth);
  const headerRowH = 26;
  const rowPad = 6;
  const cellFontSize = 8;
  doc.rect(tableLeft, tableTop, tableWidth, headerRowH).fill(primary);
  doc.font("Helvetica-Bold").fontSize(9).fillColor(white);
  let cx = tableLeft;
  headers.forEach((h, i) => {
    doc.text(h, cx + 5, tableTop + (headerRowH - 9) / 2, { width: scaledWidths[i] - 10, align: "left" });
    cx += scaledWidths[i];
  });
  let rowY = tableTop + headerRowH;
  const drawTableHeader = (y) => {
    doc.rect(tableLeft, y, tableWidth, headerRowH).fill(primary);
    doc.font("Helvetica-Bold").fontSize(9).fillColor(white);
    let hx = tableLeft;
    headers.forEach((h, i) => {
      doc.text(h, hx + 5, y + (headerRowH - 9) / 2, { width: scaledWidths[i] - 10, align: "left" });
      hx += scaledWidths[i];
    });
    return y + headerRowH;
  };
  rows.forEach((row, idx) => {
    doc.font("Helvetica").fontSize(cellFontSize);
    const rowH = Math.max(28, ...row.map(
      (cell, ci) => doc.heightOfString(String(cell), { width: scaledWidths[ci] - 10 }) + rowPad * 2
    ));
    if (rowY + rowH > PH - margin - 20) {
      doc.addPage();
      rowY = margin + 8;
      rowY = drawTableHeader(rowY);
    }
    doc.rect(tableLeft, rowY, tableWidth, rowH).fill(idx % 2 === 0 ? altRow : white);
    doc.moveTo(tableLeft, rowY + rowH).lineTo(tableLeft + tableWidth, rowY + rowH);
    let rx = tableLeft;
    row.forEach((cell, ci) => {
      let fillColor = textColor;
      const textHeight = doc.heightOfString(String(cell), { width: scaledWidths[ci] - 10 });
      const verticalOffset = (rowH - textHeight) / 2;
      doc.font("Helvetica").fontSize(cellFontSize).fillColor(fillColor);
      doc.text(String(cell), rx + 5, rowY + verticalOffset, { width: scaledWidths[ci] - 10 });
      rx += scaledWidths[ci];
    });
    rowY += rowH;
  });
  doc.rect(tableLeft, tableTop, tableWidth, rowY - tableTop);
  doc.end();
});
const PAYMENT_TIMEOUT_MINUTES = 30;
const MAX_CONCURRENT_PAYMENTS = 5;
let isProcessingPayments = false;
const updateTransactionStatus = async (id, status, failureReason) => {
  await prisma$1.transaction.update({
    where: { id },
    data: {
      status,
      failureReason
    }
  });
};
const processTransaction = async (txn) => {
  if (!txn.orderId) return;
  try {
    console.log(`🔎 Checking Razorpay Order: ${txn.orderId}`);
    const { items = [] } = await razorpay.orders.fetchPayments(txn.orderId);
    const capturedPayment = items.find((payment) => payment.status === "captured");
    if (capturedPayment) {
      console.log(`✅ Payment captured for ${txn.orderId}`);
      await executeBulkSubscriptionPaymentProcess({ orderId: txn.orderId });
      return;
    }
    const authorizedPayment = items.find((payment) => payment.status === "authorized");
    if (authorizedPayment) {
      console.log(`⌛ Payment authorized but not captured yet: ${txn.orderId}`);
      return;
    }
    const failedPayment = items.find((payment) => payment.status === "failed");
    if (failedPayment) {
      console.log(`❌ Payment failed: ${txn.orderId}`);
      await updateTransactionStatus(txn.id, "FAILED", `Razorpay payment ${failedPayment.status}`);
      return;
    }
    if (items.length === 0) {
      console.log(`⌛ No payment attempts. Expiring ${txn.orderId}`);
      await updateTransactionStatus(txn.id, "EXPIRED", "Checkout session expired or abandoned");
      return;
    }
    console.log(`ℹ️ Payment exists but is in '${items[0].status}' state for ${txn.orderId}`);
  } catch (error) {
    console.error(`❌ Error processing transaction ${txn.id}`, error);
  }
};
const processAbandonedPayments = async () => {
  if (isProcessingPayments) {
    console.log("⚠️ Payment cleanup already running. Skipping...");
    return;
  }
  isProcessingPayments = true;
  try {
    console.log("💳 Running abandoned payment cleanup...");
    const expiredBefore = dayjs().subtract(PAYMENT_TIMEOUT_MINUTES, "minute").toDate();
    const pendingTransactions = await prisma$1.transaction.findMany({
      where: {
        status: "PENDING",
        orderId: {
          not: null
        },
        createdAt: {
          lt: expiredBefore
        }
      },
      select: {
        id: true,
        orderId: true
      }
    });
    console.log(`🔍 [${dayjs().format("YYYY-MM-DD HH:mm:ss")}] Found ${pendingTransactions.length} pending transactions`);
    const limit = pLimit(MAX_CONCURRENT_PAYMENTS);
    await Promise.all(
      pendingTransactions.map(
        (txn) => limit(() => processTransaction(txn))
      )
    );
    console.log("✅ Payment cleanup completed.");
  } catch (error) {
    console.error("❌ Payment cleanup cron error:", error);
  } finally {
    isProcessingPayments = false;
  }
};
let isProcessingSubscriptions = false;
const processExpiredSubscriptions = async () => {
  if (isProcessingSubscriptions) {
    console.log("⚠️ Subscription expiration job already running. Skipping...");
    return;
  }
  isProcessingSubscriptions = true;
  try {
    console.log("📦 Running subscription expiration check...");
    const now = nowUTC();
    const { count } = await prisma$1.subscription.updateMany({
      where: {
        status: SubscriptionStatus.ACTIVE,
        isCurrent: true,
        endDate: {
          lt: now
        }
      },
      data: {
        status: SubscriptionStatus.EXPIRED,
        updatedAt: now
        // if your schema has updatedAt without @updatedAt
      }
    });
    console.log(`✅ [${dayjs(now).format("YYYY-MM-DD HH:mm:ss")}] Expired ${count} subscriptions`);
  } catch (error) {
    console.error("❌ Subscription expiration cron failed:", error);
  } finally {
    isProcessingSubscriptions = false;
  }
};
console.log("✅ Cron service initialized");
const cleanupExpiredUsersAndTokens = async () => {
};
cron.schedule(
  "0 */5 * * *",
  async () => {
    await cleanupExpiredUsersAndTokens();
  },
  { timezone: "Asia/Kolkata" }
);
cron.schedule(
  "*/30 * * * *",
  async () => {
    await processExpiredSubscriptions();
  },
  { timezone: "Asia/Kolkata" }
);
cron.schedule(
  "*/30 * * * *",
  async () => {
    await processAbandonedPayments();
  },
  { timezone: "Asia/Kolkata" }
);
async function sendEmail(to, subject, html) {
  const host = process.env.SMTP_HOST;
  const port2 = process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : void 0;
  const user = process.env.SMTP_USER;
  const pass = process.env.SMTP_PASS;
  const from = `"Exam Infra Support" <${process.env.FROM_EMAIL || user}>`;
  if (!to || !to.includes("@")) {
    console.warn(`⚠️ Invalid recipient email: ${to}`);
    return { success: false, error: "Invalid recipient email address" };
  }
  if (!host || !port2 || !user || !pass || !from) {
    console.warn("⚠️ SMTP not configured — email not sent");
    return { success: false, error: "SMTP not configured on server" };
  }
  const transporter = nodemailer.createTransport({
    host,
    port: port2,
    secure: port2 === 465,
    auth: { user, pass }
  });
  try {
    await transporter.sendMail({ from, to, subject, html });
    console.log(`✅ Email sent to ${to}`);
    return { success: true };
  } catch (err) {
    console.error("❌ Failed to send email:", err);
    return { success: false, error: err?.message || "Failed to send email" };
  }
}
const verificationTemplate = ({
  firstName,
  companyName,
  verifyUrl
}) => `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Email Verification</title>
  <style>
    body {
      margin: 0;
      padding: 0;
      background: #f4f4f4;
      font-family: Arial, sans-serif;
    }
    .container {
      max-width: 600px;
      margin: 30px auto;
      background: #ffffff;
      padding: 0;
      border-radius: 10px;
      overflow: hidden;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }
    .header {
      background: #e63946;
      padding: 30px;
      text-align: center;
      color: #ffffff;
    }
    .header img {
      width: 60px;
    }
    .content {
      padding: 30px;
      text-align: left;
      color: #333333;
    }
    .btn {
      display: inline-block;
      background: #e63946;
      color: #ffffff !important;
      padding: 12px 24px;
      border-radius: 5px;
      text-decoration: none;
      font-size: 16px;
      margin: 20px 0;
    }
    .footer {
      text-align: center;
      padding: 20px;
      color: #777777;
      font-size: 13px;
    }
    .social-icons img {
      width: 24px;
      margin: 0 8px;
    }
  </style>
</head>

<body>
  <div class="container">
    <div class="header">
      <img src="https://cdn-icons-png.flaticon.com/512/561/561127.png" />
    </div>

    <div class="content">
      <h2>Email verification</h2>

      <p>Hi ${firstName},</p>

      <p>
        You're almost set to start enjoying <strong>${companyName}</strong>.  
        Simply click the button below to verify your email address and get started.
        The link expires in <strong>24 hours</strong>.
      </p>

      <a href="${verifyUrl}" class="btn">Verify my email address</a>

      <hr />

      <div class="social-icons">
        <img src="https://cdn-icons-png.flaticon.com/512/2111/2111393.png" />
        <img src="https://cdn-icons-png.flaticon.com/512/733/733579.png" />
        <img src="https://cdn-icons-png.flaticon.com/512/1384/1384014.png" />
        <img src="https://cdn-icons-png.flaticon.com/512/733/733558.png" />
      </div>

      <div class="footer">
        Tamilnadu, India. 
        <br />
        <a href="#">Privacy Policy</a> | <a href="#">Contact Details</a>
      </div>
    </div>
  </div>
</body>
</html>
`;
const forgotPasswordTemplate = ({
  companyName,
  verifyUrl
}) => `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <meta name="x-apple-disable-message-reformatting" />
  <title>Reset Password</title>
  <link href="https://fonts.googleapis.com/css2?family=Albert+Sans:wght@500;800&display=swap" rel="stylesheet" />
  <style>
    body {
      margin: 0;
      padding: 0;
      background-color: #e0e0e0;
      font-family: 'Albert Sans', BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }
    table {
      border-collapse: collapse;
      width: 100%;
    }
    img {
      border: 0;
      max-width: 100%;
    }
    a {
      text-decoration: none;
      color: inherit;
    }
    .container {
      max-width: 600px;
      margin: 0 auto;
      background: #f8f8f8;
      border-radius: 18px;
      overflow: hidden;
    }
    .header {
      background: #586ce0;
      padding: 40px 50px;
      color: #fff;
      border-radius: 18px 18px 0 0;
    }
    .header h1 {
      font-size: 28px;
      margin: 0;
      line-height: 1.3;
    }
    .logo {
      font-weight: 800;
      font-size: 20px;
      margin-bottom: 20px;
    }
    .content {
      padding: 40px 50px;
      background: #f8f8f8;
    }
    .content h2 {
      font-size: 26px;
      font-weight: 800;
      color: #191919;
      margin: 0 0 20px;
      line-height: 1.3;
    }
    .content p {
      font-size: 14px;
      color: #333;
      line-height: 1.6;
      margin: 0 0 20px;
    }
    .btn {
      display: inline-block;
      background: #586ce0;
      color: #fff !important;
      font-weight: 800;
      font-size: 12px;
      text-transform: uppercase;
      letter-spacing: 2px;
      border-radius: 40px;
      padding: 14px 30px;
      text-align: center;
    }
    .footer {
      text-align: center;
      padding: 30px 20px;
      font-size: 12px;
      color: #888;
    }
    @media (max-width: 480px) {
      .container,
      .content,
      .header {
        padding: 20px;
      }
      .content h2 {
        font-size: 22px;
      }
    }
  </style>
</head>
<body>
  <table role="presentation" width="100%">
    <tr>
      <td align="center" bgcolor="#E0E0E0" style="padding: 40px 10px;">
        <table class="container" role="presentation">
          <tr>
            <td class="header">
              <div class="logo">ExamInfra</div>
            </td>
          </tr>
          <tr>
            <td class="content">
              <h2>Forgot your password?<br />It happens to the best of us.</h2>
              <p>To reset your password, click the button below. The link will self-destruct after 24 hours.</p>
              <a href="${verifyUrl}" class="btn" target="_blank">Reset Password</a>
              <p>If you have any questions or need further assistance, please contact our support team by replying to this email or visiting our support page.</p>
            </td>
          </tr>
          <tr>
            <td class="footer">
              © 2025 ExamInfra Inc. All Rights Reserved
            </td>
          </tr>
        </table>
      </td>
    </tr>
  </table>
</body>
</html>

`;
function signToken(user, sessionToken) {
  const JWT_SECRET = process.env.JWT_SECRET;
  return jwt.sign(
    {
      id: user.id,
      email: user.email,
      role: user.role,
      sessionToken,
      institutionId: user.institutionId
    },
    JWT_SECRET,
    { expiresIn: "1d" }
  );
}
const getHost = () => {
  const ip = Object.values(os.networkInterfaces()).flat().find((i) => i.family === "IPv4" && !i.internal)?.address || "localhost";
  const port2 = process.env.PORT || 5173;
  return `http://${ip}:${port2}`;
};
const generateReferralCode = async (length = 8) => {
  const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
  while (true) {
    let code = "";
    for (let i = 0; i < length; i++) {
      code += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    const existingStaff = await prisma$1.staff.findFirst({
      where: { referralCode: code },
      select: { id: true }
    });
    if (!existingStaff) {
      return code;
    }
  }
};
const toPublicPath = (filePath) => {
  if (!filePath) return null;
  const normalized = filePath.replace(/\\/g, "/");
  const uploadsIndex = normalized.lastIndexOf("/uploads/");
  if (uploadsIndex !== -1) {
    return normalized.substring(uploadsIndex);
  }
  if (normalized.startsWith("uploads/")) {
    return "/" + normalized;
  }
  return null;
};
const registerStudent = asyncHandler(async (req, res) => {
  const { firstName, lastName, email, phone, password } = req.body;
  {
    res.status(201).json({
      message: "Temporarily blocked from registration."
    });
    return;
  }
});
const registerInstitution = asyncHandler(async (req, res) => {
  const { contactPerson, institutionName, institutionAddress, email, phone, password, state, pincode } = req.body;
  const isEmpty = (value) => value === void 0 || value === null || value.toString().trim() === "";
  const requiredFields = [
    { key: "institutionName", message: "Institution name is required" },
    { key: "institutionAddress", message: "Institution address is required" },
    { key: "contactPerson", message: "Contact person is required" },
    { key: "email", message: "Email is required" },
    { key: "password", message: "Password is required" },
    { key: "state", message: "State is required" },
    { key: "pincode", message: "Pincode is required" }
  ];
  for (const field of requiredFields) {
    if (isEmpty(req.body[field.key])) {
      res.status(400).json({ message: field.message });
      return;
    }
    if (field.key === "phone" && req.body[field.key].length !== 10) {
      res.status(400).json({ message: "Phone number must be 10 digits" });
      return;
    }
    if (field.key === "pincode" && req.body[field.key].length !== 6) {
      res.status(400).json({ message: "Pincode must be 6 digits" });
      return;
    }
  }
  const exists = await prisma$1.user.findFirst({ where: { email } });
  if (exists) {
    res.status(400).json({ message: "Email already registered" });
    return;
  }
  const hash = await bcrypt.hash(password, 10);
  const user = await prisma$1.user.create({
    data: {
      institutionName: String(institutionName).trim(),
      institutionAddress,
      contactPerson,
      email,
      phone,
      password: hash,
      state,
      pincode,
      role: "INSTITUTION",
      isVerified: false,
      expiresAt: tokenExpireTime()
    }
  });
  const tokenStr = v4();
  await prisma$1.token.create({
    data: {
      userId: user.id,
      token: tokenStr,
      type: "VERIFY",
      expiresAt: tokenExpireTime()
    }
  });
  const verifyUrl = `${process.env.BASE_URL}/verify/${tokenStr}`;
  await sendEmail(
    email,
    "Verify your email",
    verificationTemplate({ firstName: institutionName || "", companyName: "ExamInfra", verifyUrl })
  );
  res.status(201).json({
    message: "Registered. Check your email to verify your account."
  });
});
const verifyEmail = asyncHandler(async (req, res) => {
  const { token } = req.params;
  await prisma$1.$transaction(async (tx) => {
    const dbToken = await tx.token.findFirst({
      where: {
        token,
        type: { in: ["VERIFY", "STUDENTVERIFY"] },
        expiresAt: {
          gt: /* @__PURE__ */ new Date()
        }
      }
    });
    if (!dbToken) {
      const err = new Error("Invalid or expired token");
      err.statusCode = 400;
      throw err;
    }
    const user = await tx.user.findUnique({
      where: { id: dbToken.userId }
    });
    if (!user) {
      const err = new Error("User not found");
      err.statusCode = 404;
      throw err;
    }
    let institutionId = null;
    if (user.createdById) {
      const institution = await tx.institution.findUnique({
        where: { userId: user.createdById },
        select: { id: true }
      });
      if (institution) {
        institutionId = institution.id;
      } else {
        const staff = await tx.staff.findUnique({
          where: { userId: user.createdById },
          select: { institutionId: true }
        });
        institutionId = staff?.institutionId ?? null;
      }
    }
    if (user.role !== "INSTITUTION" && !institutionId) {
      const err = new Error("Institution not found. Please contact admin for more details.");
      err.statusCode = 404;
      throw err;
    }
    await tx.user.update({
      where: { id: user.id },
      data: {
        isVerified: true,
        expiresAt: null
      }
    });
    if (user.role === "INSTITUTION") {
      await tx.institution.upsert({
        where: { userId: user.id },
        update: {},
        create: { userId: user.id }
      });
    }
    if (user.role === "STAFF") {
      await tx.staff.upsert({
        where: { userId: user.id },
        update: {},
        create: { userId: user.id, institutionId }
      });
    }
    if (user.role === "STUDENT") {
      if (!dbToken.studentInstitutionId || !dbToken.institutionId) {
        const err = new Error("Institution not found. Please contact admin for more details.");
        err.statusCode = 404;
        throw err;
      } else {
        institutionId = dbToken.institutionId;
      }
      const student = await tx.student.upsert({
        where: { userId: user.id },
        update: {},
        create: { userId: user.id },
        include: { user: true }
      });
      await tx.studentInstitution.updateMany({
        where: { studentId: student.id },
        data: {
          isPrimary: false
        }
      });
      await tx.studentInstitution.update({
        where: { id: dbToken.studentInstitutionId },
        data: {
          isPrimary: true,
          isVerified: true,
          verificationExpiresAt: null
        }
      });
      const existingSubscripion = await tx.subscription.findFirst({
        where: {
          institutionId,
          studentInstitutionId: dbToken.studentInstitutionId,
          email: student.user.email,
          isCurrent: true
        },
        orderBy: {
          createdAt: "desc"
        }
      });
      if (!existingSubscripion) {
        const trialPlan = await tx.subscriptionPlan.findFirst({
          where: {
            planType: "trial",
            isActive: true
          },
          orderBy: {
            createdAt: "desc"
          }
        });
        if (!trialPlan) {
          const err = new Error("Trial plan not found");
          err.statusCode = 404;
          throw err;
        }
        const startDate = /* @__PURE__ */ new Date();
        const endDate = /* @__PURE__ */ new Date();
        endDate.setDate(endDate.getDate() + trialPlan.duration);
        await tx.subscription.create({
          data: {
            student: {
              connect: {
                id: student.id
              }
            },
            email: student.user.email,
            amount: 0,
            chargedAmount: 0,
            creditUsed: 0,
            startDate,
            endDate,
            status: "ACTIVE",
            action: "TRIAL",
            isCurrent: true,
            plan: {
              connect: {
                id: trialPlan.id
              }
            },
            institution: {
              connect: {
                id: institutionId
              }
            },
            studentInstitution: {
              connect: {
                id: dbToken.studentInstitutionId
              }
            }
          }
        });
      } else {
        await tx.subscription.update({
          where: {
            id: existingSubscripion.id
          },
          data: {
            studentId: student.id,
            institutionId,
            planId: existingSubscripion.planId
          }
        });
      }
    }
    await tx.token.delete({
      where: { id: dbToken.id }
    });
  });
  res.status(201).json({
    success: true,
    message: "Email verified successfully!"
  });
});
const login = asyncHandler(async (req, res) => {
  const { email, password } = req.body;
  if (!email) {
    res.status(400).json({ message: "Email is required" });
    return;
  }
  if (!password) {
    res.status(400).json({ message: "Password is required" });
    return;
  }
  const user = await prisma$1.user.findFirst({
    where: {
      email: {
        equals: String(email ?? "").trim().toLowerCase(),
        not: null
      },
      isDeleted: false
    }
  });
  if (!user) {
    res.status(404).json({ message: "User not found, Please register first" });
    return;
  }
  if (user.role === "STUDENT") {
    res.status(403).json({ message: "Invalid user" });
    return;
  }
  if (!user.isVerified) {
    let institutionId = null;
    if (user.role === "STAFF") {
      const staff = await prisma$1.staff.findUnique({
        where: { userId: user.id },
        select: { institutionId: true }
      });
      institutionId = staff?.institutionId ?? null;
      if (!institutionId && user.createdById) {
        const institution = await prisma$1.institution.findUnique({
          where: { userId: user.createdById },
          select: { id: true }
        });
        institutionId = institution?.id ?? null;
      }
    } else if (user.role === "INSTITUTION") {
      const institution = await prisma$1.institution.findUnique({
        where: { userId: user.id },
        select: { id: true }
      });
      institutionId = institution?.id ?? null;
    }
    const verifyToken = await prisma$1.token.findFirst({
      where: {
        userId: user.id,
        type: "VERIFY"
      },
      orderBy: { createdAt: "desc" }
    });
    const shouldResendVerification = !verifyToken || verifyToken && /* @__PURE__ */ new Date() > verifyToken.expiresAt;
    if (shouldResendVerification) {
      const tokenStr = v4();
      await prisma$1.token.create({
        data: {
          userId: user.id,
          token: tokenStr,
          type: "VERIFY",
          institutionId: institutionId ?? void 0,
          expiresAt: tokenExpireTime()
        }
      });
      const verifyUrl = `${process.env.BASE_URL}/verify/${tokenStr}`;
      const displayName = user.institutionName || [user.firstName, user.lastName].filter(Boolean).join(" ") || "there";
      const emailRes = await sendEmail(
        user.email,
        "Verify your email",
        verificationTemplate({ firstName: displayName, companyName: "ExamInfra", verifyUrl })
      );
      if (!emailRes.success) {
        res.status(500).json({ message: "Failed to send verification email. Please try again later." });
        return;
      }
      res.status(403).json({ message: "Your verification link has expired. A new link has been sent to your email." });
      return;
    }
    res.status(403).json({ message: "Email not verified" });
    return;
  }
  const match = await bcrypt.compare(password, user.password);
  if (!match) {
    res.status(400).json({ message: "Invalid credentials" });
    return;
  }
  const { firstName, lastName, role, phone, id, isVerified, institutionName, institutionAddress, logoPic, profilePic, contactPerson, createdAt, updatedAt, state, pincode } = user;
  let response = {
    id,
    firstName,
    lastName,
    role,
    phone,
    email,
    state,
    pincode,
    profilePic: profilePic ? `${getHost()}${profilePic}` : "",
    isVerified,
    institutionName,
    institutionAddress,
    logoPic: logoPic ? `${getHost()}${logoPic}` : "",
    contactPerson,
    createdAt: toIST(createdAt),
    updatedAt: toIST(updatedAt),
    institutionId: null
  };
  if (role === "STAFF") {
    const staff = await prisma$1.staff.findUnique({
      where: { userId: id },
      include: { role: true }
    });
    response.institutionId = staff?.institutionId;
    const institution = await prisma$1.user.findUnique({ where: { id: user.createdById } });
    response.institutionName = institution?.institutionName;
    response.institutionAddress = institution?.institutionAddress;
    response.logoPic = institution?.logoPic ? `${getHost()}${institution?.logoPic}` : "";
    response.contactPerson = institution?.contactPerson;
    response.permissions = staff?.role.permissions;
    response.referralCode = staff?.referralCode;
  } else if (role === "INSTITUTION") {
    const institution = await prisma$1.institution.findUnique({
      where: { userId: id }
    });
    if (!institution) {
      res.status(404).json({ message: "Institution not found" });
      return;
    }
    response.institutionId = institution?.id;
  }
  const sessionToken = crypto.randomBytes(32).toString("hex");
  await prisma$1.user.update({
    where: { id: user.id },
    data: { sessionToken }
  });
  const token = signToken({ ...user, institutionId: response.institutionId }, sessionToken);
  res.json({
    message: "Login Successfully",
    token,
    user: response
  });
});
const studentLogin = asyncHandler(async (req, res) => {
  const { email, password } = req.body;
  if (!email) {
    res.status(400).json({ message: "Email is required" });
    return;
  }
  if (!password) {
    res.status(400).json({ message: "Password is required" });
    return;
  }
  const user = await prisma$1.user.findFirst({ where: { email, isDeleted: false } });
  if (!user) {
    res.status(401).json({ message: "User not found, Please register first" });
    return;
  }
  if (user.role !== "STUDENT") {
    res.status(401).json({ message: "Invalid user" });
    return;
  }
  const match = await bcrypt.compare(String(password).trim(), user.password);
  if (!match) {
    res.status(400).json({ message: "Invalid credentials" });
    return;
  }
  const student = await prisma$1.student.findUnique({
    where: { userId: user.id },
    include: {
      institutions: {
        take: 1,
        where: { deletedAt: null, isPrimary: true },
        include: {
          exam: { select: { id: true, examName: true } },
          institution: {
            select: {
              user: {
                select: { institutionName: true, logoPic: true, institutionAddress: true }
              }
            }
          },
          subscriptions: {
            where: { isCurrent: true }
          }
        }
      }
    }
  });
  const studentTrialPlan = await prisma$1.subscription.findFirst({
    where: { studentId: student?.id, action: "TRIAL" },
    orderBy: { createdAt: "asc" }
  });
  if (!student) {
    res.status(401).json({ message: "Student not found" });
    return;
  }
  if (!student.institutions[0]) {
    res.status(403).json({ message: "Primary institution not found" });
    return;
  }
  if (!student.institutions[0].isVerified) {
    const verifyToken = await prisma$1.token.findFirst({
      where: {
        userId: user.id,
        type: "VERIFY",
        studentInstitutionId: student.institutions[0].id
      },
      orderBy: { createdAt: "desc" }
    });
    const shouldResendVerification = !verifyToken || verifyToken && /* @__PURE__ */ new Date() > verifyToken.expiresAt;
    if (shouldResendVerification) {
      const tokenStr = v4();
      await prisma$1.token.create({
        data: {
          userId: user.id,
          token: tokenStr,
          institutionId: student.institutions[0].institutionId,
          studentInstitutionId: student.institutions[0].id,
          type: "VERIFY",
          expiresAt: tokenExpireTime()
        }
      });
      const verifyUrl = `${process.env.BASE_URL}/verify/${tokenStr}`;
      const emailRes = await sendEmail(
        user.email,
        "Verify your email",
        verificationTemplate({ firstName: user.firstName, companyName: "ExamInfra", verifyUrl })
      );
      if (!emailRes.success) {
        res.status(500).json({ message: "Failed to send verification email. Please try again later." });
        return;
      }
      res.status(403).json({ message: "Your verification link has expired. A new link has been sent to your email." });
      return;
    }
    res.status(403).json({ message: "Email not verified" });
    return;
  }
  const { firstName, lastName, role, phone, id } = user;
  let response = { id, firstName, lastName, role, phone, email };
  const sessionToken = crypto.randomBytes(32).toString("hex");
  await prisma$1.user.update({
    where: { id: user.id },
    data: { sessionToken }
  });
  const primaryInst = student.institutions[0];
  const tokenStudentInstId = primaryInst?.id;
  const token = signToken({ ...user, institutionId: tokenStudentInstId }, sessionToken);
  const createdByUser = await prisma$1.user.findUnique({ where: { id: user.createdById } });
  response.isInstitution = createdByUser?.role === "INSTITUTION" ? true : false;
  response.institutionName = createdByUser?.institutionName ?? "";
  response.institutionAddress = createdByUser?.institutionAddress ?? "";
  response.institutionLogo = createdByUser?.logoPic ? `${getHost()}${createdByUser?.logoPic}` : "";
  res.json({
    message: "Login Successfully",
    token,
    user: {
      ...response,
      language: student?.language || primaryInst?.language || "",
      mediumOfExam: student?.language ? primaryInst?.language || "English" : "English",
      examId: primaryInst?.exam?.id ?? "",
      examName: primaryInst?.exam?.examName ?? "",
      studentInstitutionId: primaryInst ? primaryInst.id : null,
      // institutions: student.institutions.map(inst => ({
      //     id: inst.institutionId,
      //     name: inst.institution?.user?.institutionName ?? "",
      //     logo: inst.institution?.user?.logoPic ? `${getHost()}${inst.institution?.user?.logoPic}` : "",
      //     address: inst.institution?.user?.institutionAddress ?? "",
      //     isPrimary: inst.isPrimary,
      //     examId: inst.examsId,
      //     examName: inst.exam?.examName ?? "",
      //     language: inst.language ?? "",
      // })),
      subscription: primaryInst.subscriptions[0] ? {
        ...primaryInst.subscriptions[0],
        amount: Number(primaryInst.subscriptions[0].amount),
        chargedAmount: Number(primaryInst.subscriptions[0].chargedAmount),
        creditUsed: Number(primaryInst.subscriptions[0].creditUsed),
        startDate: primaryInst.subscriptions[0].startDate ? toIST(primaryInst.subscriptions[0].startDate) : null,
        endDate: primaryInst.subscriptions[0].endDate ? toIST(primaryInst.subscriptions[0].endDate) : null,
        trialEndsAt: studentTrialPlan?.endDate ? toIST(studentTrialPlan.endDate) : null,
        createdAt: toIST(primaryInst.subscriptions[0].createdAt)
      } : "",
      profilePic: user.profilePic ? `${getHost()}${user.profilePic}` : ""
    }
  });
});
const changePassword = asyncHandler(async (req, res) => {
  const { oldPassword, newPassword } = req.body;
  const userId = req.user.id;
  const user = await prisma$1.user.findUnique({ where: { id: userId } });
  if (!user) {
    res.status(404).json({ message: "User not found" });
    return;
  }
  const match = await bcrypt.compare(oldPassword, user.password);
  if (!match) {
    res.status(400).json({ message: "Old password incorrect" });
    return;
  }
  const hash = await bcrypt.hash(newPassword, 10);
  await prisma$1.user.update({
    where: { id: userId },
    data: { password: hash }
  });
  res.json({ message: "Password changed" });
});
const forgotPassword = asyncHandler(async (req, res) => {
  const { email } = req.body;
  if (!email) {
    res.status(400).json({ message: "Email is required" });
    return;
  }
  const user = await prisma$1.user.findFirst({ where: { email } });
  if (!user) {
    res.status(404).json({ message: "No user with that email" });
    return;
  }
  const tokenStr = v4();
  await prisma$1.token.create({
    data: {
      userId: user.id,
      token: tokenStr,
      type: "RESET",
      expiresAt: tokenExpireTime()
    }
  });
  const verifyUrl = `${process.env.BASE_URL}/forgot-password/${tokenStr}`;
  const emailRes = await sendEmail(user.email, "Password reset", forgotPasswordTemplate({ companyName: "ExamInfra", verifyUrl }));
  if (!emailRes.success) {
    res.status(500).json({ message: "Failed to send password reset email. Please try again later." });
    return;
  }
  res.json({ message: "Password reset email sent" });
});
const resetPassword = asyncHandler(async (req, res) => {
  const { token } = req.params;
  const { password } = req.body;
  const dbToken = await prisma$1.token.findFirst({ where: { token, type: "RESET", expiresAt: { gt: /* @__PURE__ */ new Date() } } });
  if (!dbToken) {
    res.status(400).json({ message: "Invalid or expired token" });
    return;
  }
  const user = await prisma$1.user.findUnique({ where: { id: dbToken.userId } });
  if (!user) {
    res.status(404).json({ message: "User not found" });
    return;
  }
  const hash = await bcrypt.hash(password, 10);
  await prisma$1.user.update({
    where: { id: user.id },
    data: { password: hash }
  });
  await prisma$1.token.delete({ where: { id: dbToken.id } });
  res.json({ message: "Password reset successful" });
});
const registerAdmin = asyncHandler(async (req, res) => {
  const { firstName, lastName, email, password } = req.body;
  if (!firstName) {
    res.status(400).json({ message: "First name is required" });
    return;
  }
  if (!email) {
    res.status(400).json({ message: "Email is required" });
    return;
  }
  if (!password) {
    res.status(400).json({ message: "Password is required" });
    return;
  }
  const exists = await prisma$1.user.findFirst({ where: { email } });
  if (exists) {
    res.status(400).json({ message: "Email already registered" });
    return;
  }
  const hash = await bcrypt.hash(password, 10);
  const admin2 = await prisma$1.user.create({
    data: {
      firstName,
      lastName,
      email,
      password: hash,
      role: "ADMIN",
      isVerified: true
    }
  });
  res.status(201).json({
    message: "Admin registered successfully",
    user: {
      id: admin2.id,
      firstName,
      lastName,
      email,
      role: "ADMIN",
      isVerified: true
    }
  });
});
const getMe = asyncHandler(async (req, res) => {
  const userId = req.user.id;
  const user = await prisma$1.user.findUnique({ where: { id: userId } });
  if (!user) {
    res.status(401).json({ tokenValid: false, message: "Your session has expired. Please login again." });
    return;
  }
  res.json({
    tokenValid: true,
    message: "Valid token",
    data: {
      id: user.id,
      firstName: user.firstName,
      lastName: user.lastName,
      email: user.email,
      role: user.role
    }
  });
});
async function protect(req, res, next) {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith("Bearer ")) {
    return res.status(401).json({ message: "Not authorized" });
  }
  const token = auth.split(" ")[1];
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    const user = await prisma$1.user.findUnique({ where: { id: payload.id, isDeleted: false } });
    if (!user) {
      return res.status(401).json({ message: "User not found or session expired" });
    }
    req.user = {
      ...user,
      institutionId: payload.institutionId
    };
    if (user.role === "STAFF") {
      const staff = await prisma$1.staff.findUnique({ where: { userId: user.id } });
      if (!staff) {
        return res.status(401).json({ message: "Staff not found" });
      }
    }
    if (user.role === "STUDENT") {
      const student = await prisma$1.student.findUnique({ where: { userId: user.id } });
      if (!student) {
        res.status(401).json({ message: "Student not found" });
        return;
      }
      const headerStudentInstId = req.headers["x-student-institution-id"];
      if (!headerStudentInstId || !req.user) return res.status(401).json({ message: "institutionId not found in the request" });
      ;
      console.log(12);
      const studentInstitution = await prisma$1.studentInstitution.findUnique({
        where: { id: headerStudentInstId, deletedAt: null },
        select: { institutionId: true }
      });
      if (!studentInstitution || !studentInstitution.institutionId) {
        res.status(401).json({ message: "Student institution not found" });
        return;
      }
      req.user.studentInstitutionId = headerStudentInstId;
      req.user.institutionId = studentInstitution.institutionId;
    }
    next();
  } catch (err) {
    return res.status(401).json({ message: "Invalid or expired token" });
  }
}
function permit(...allowed) {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ message: "Not authorized" });
    let allowedRoles = allowed.map((role) => role.toUpperCase());
    if (!allowedRoles.includes(req.user.role.toUpperCase())) return res.status(403).json({ message: "Forbidden" });
    next();
  };
}
function onlyAdmin(req, res, next) {
  if (req.user.role !== "ADMIN") {
    res.status(403).json({ message: "Admin only route" });
    return;
  }
  next();
}
const router$s = express.Router();
router$s.post("/register", registerStudent);
router$s.post("/admin-register", registerAdmin);
router$s.post("/institution-register", registerInstitution);
router$s.get("/verify/:token", verifyEmail);
router$s.post("/user-login", login);
router$s.post("/login", studentLogin);
router$s.post("/change-password", protect, changePassword);
router$s.post("/forgot-password", forgotPassword);
router$s.post("/reset-password/:token", resetPassword);
router$s.get("/me", protect, getMe);
const updateUserDetails = asyncHandler(async (req, res) => {
  const userId = req.user.id;
  const { firstName, lastName, phone, contactPerson, institutionName, institutionAddress, gstNumber, state, pincode, mediumOfExam, language } = req.body;
  const actualMediumOfExam = mediumOfExam !== void 0 ? mediumOfExam : language;
  const updateData = {};
  if (firstName && firstName.trim() !== "") updateData.firstName = firstName.trim();
  if (lastName && lastName.trim() !== "") updateData.lastName = lastName.trim();
  if (phone && phone.trim() !== "") updateData.phone = phone.trim();
  if (contactPerson && contactPerson.trim() !== "") updateData.contactPerson = contactPerson.trim();
  if (institutionName && institutionName.trim() !== "") updateData.institutionName = institutionName.trim();
  if (institutionAddress && institutionAddress.trim() !== "") updateData.institutionAddress = institutionAddress.trim();
  if (state && state.trim() !== "") updateData.state = state.trim();
  if (pincode && pincode.trim() !== "") updateData.pincode = pincode.trim();
  const currentUser = await prisma$1.user.findUnique({ where: { id: userId }, select: { details: true, role: true } });
  let updatedDetails = currentUser?.details || {};
  let detailsChanged = false;
  if (actualMediumOfExam !== void 0 && currentUser?.role === "STUDENT") {
    const studentInstitutionId = req.user?.studentInstitutionId;
    if (studentInstitutionId) {
      await prisma$1.studentInstitution.update({
        where: { id: studentInstitutionId },
        data: { language: actualMediumOfExam }
      });
    }
  } else if (actualMediumOfExam !== void 0) {
    if (typeof updatedDetails !== "object" || updatedDetails === null) updatedDetails = {};
    updatedDetails.mediumOfExam = actualMediumOfExam;
    detailsChanged = true;
  }
  if (detailsChanged) {
    updateData.details = updatedDetails;
  }
  const updatedUser = await prisma$1.user.update({
    where: { id: userId },
    data: updateData
  });
  let returnedMediumOfExam = updatedUser.details?.mediumOfExam || null;
  let returnedLanguage = "";
  if (updatedUser.role === "STUDENT") {
    const studentInstitutionId = req.user?.studentInstitutionId;
    if (studentInstitutionId) {
      const studentInst = await prisma$1.studentInstitution.findUnique({ where: { id: studentInstitutionId }, include: { student: true } });
      returnedMediumOfExam = studentInst?.student?.language ? studentInst?.language || "English" : "English";
      returnedLanguage = studentInst?.student?.language || studentInst?.language || "";
    }
  }
  let response = {
    language: returnedLanguage,
    mediumOfExam: returnedMediumOfExam,
    id: updatedUser.id,
    email: updatedUser.email,
    firstName: updatedUser.firstName,
    lastName: updatedUser.lastName,
    phone: updatedUser.phone,
    role: updatedUser.role,
    state: updatedUser.state,
    pincode: updatedUser.pincode,
    isVerified: updatedUser.isVerified,
    contactPerson: updatedUser.contactPerson ?? "",
    institutionName: updatedUser.institutionName,
    institutionAddress: updatedUser.institutionAddress,
    logoPic: updatedUser.logoPic ? `${getHost()}${updatedUser.logoPic}` : "",
    profilePic: updatedUser.profilePic ? `${getHost()}${updatedUser.profilePic}` : "",
    createdAt: toIST(updatedUser.createdAt),
    updatedAt: toIST(updatedUser.updatedAt)
  };
  if (updatedUser.role === "STAFF") {
    const institution = await prisma$1.user.findUnique({ where: { id: updatedUser.createdById } });
    response.institutionName = institution?.institutionName;
    response.institutionAddress = institution?.institutionAddress;
    response.logoPic = institution?.logoPic ? `${getHost()}${institution?.logoPic}` : "";
  }
  if (updatedUser.role === "INSTITUTION" || updatedUser.role === "ADMIN") {
    if (gstNumber && gstNumber.trim() !== "") {
      await prisma$1.user.update({
        where: { id: updatedUser.id },
        data: { gstNumber: gstNumber.trim() }
      });
      response.gstNumber = gstNumber.trim();
    }
  }
  res.status(200).json({
    message: "Updated successfully",
    data: response
  });
});
const listUsers = asyncHandler(async (req, res) => {
  const currentUserId = req.user?.id;
  const roleFilter = req.query.role?.trim();
  const search = req.query.search?.trim();
  const isVerified = req.query.isVerified?.trim();
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const skip = (page - 1) * limit;
  const where = {
    id: { not: currentUserId },
    isDeleted: false
  };
  if (roleFilter && roleFilter.toUpperCase() !== "ALL") {
    where.role = roleFilter.toUpperCase();
  }
  if (isVerified) {
    where.isVerified = isVerified === "true";
  }
  if (search) {
    const searchWords = search.split(/\s+/).filter(Boolean);
    where.OR = [
      { firstName: { contains: search } },
      { lastName: { contains: search } },
      { email: { contains: search } },
      { institutionName: { contains: search } },
      { contactPerson: { contains: search } },
      ...searchWords.length > 1 ? [{
        AND: searchWords.map((word) => ({
          OR: [
            { firstName: { contains: word } },
            { lastName: { contains: word } },
            { institutionName: { contains: word } },
            { contactPerson: { contains: word } }
          ]
        }))
      }] : []
    ];
  }
  const [total, users] = await Promise.all([
    prisma$1.user.count({ where }),
    prisma$1.user.findMany({
      where,
      select: {
        id: true,
        email: true,
        firstName: true,
        lastName: true,
        phone: true,
        role: true,
        state: true,
        pincode: true,
        isVerified: true,
        institutionName: true,
        institutionAddress: true,
        contactPerson: true,
        profilePic: true,
        logoPic: true,
        gstNumber: true,
        createdAt: true
      },
      orderBy: { institutionName: "asc" },
      skip,
      take: limit
    })
  ]);
  const formattedUsers = users.map(async (u) => {
    if (u.role === "INSTITUTION") {
      const [studentCount, staffCount] = await Promise.all([
        prisma$1.user.count({ where: { role: "STUDENT", createdById: u.id, isDeleted: false } }),
        prisma$1.user.count({ where: { role: "STAFF", createdById: u.id, isDeleted: false } })
      ]);
      return {
        ...u,
        createdAt: toIST(u.createdAt),
        studentCount,
        staffCount
      };
    }
    return {
      ...u,
      createdAt: toIST(u.createdAt)
    };
  });
  const finalUsers = await Promise.all(formattedUsers);
  res.status(200).json({
    message: "Successfully fetched.",
    data: finalUsers,
    meta: {
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit)
    }
  });
});
const getUserProfile = asyncHandler(async (req, res) => {
  const userId = req.params.id;
  const user = await prisma$1.user.findUnique({
    where: { id: userId, isDeleted: false },
    select: {
      id: true,
      email: true,
      firstName: true,
      lastName: true,
      phone: true,
      role: true,
      isVerified: true,
      institutionName: true,
      institutionAddress: true,
      contactPerson: true,
      profilePic: true,
      logoPic: true,
      state: true,
      pincode: true,
      details: true,
      student: {
        select: {
          id: true,
          referral: true,
          language: true,
          institutions: {
            where: { deletedAt: null },
            select: { id: true, language: true, isPrimary: true }
          }
        }
      }
    }
  });
  if (!user) {
    res.status(404).json({ message: "User not found" });
    return;
  }
  const { student, details, ...userData } = user;
  const activeInstitutionId = req.user?.studentInstitutionId;
  let targetInst = user.student?.institutions?.find((i) => i.id === activeInstitutionId);
  if (!targetInst) targetInst = user.student?.institutions?.find((i) => i.isPrimary);
  if (!targetInst) targetInst = user.student?.institutions?.[0];
  res.status(200).json({
    message: "Successfully fetched.",
    data: {
      ...userData,
      language: user.student?.language || targetInst?.language || "",
      mediumOfExam: user.student?.language ? targetInst?.language || "English" : "English"
    }
  });
});
const deleteUser = asyncHandler(async (req, res) => {
  res.status(402).send({ message: "Under maintanence" });
  return;
});
const checkEmail = asyncHandler(async (req, res) => {
  const { email } = req.query;
  if (!email) {
    res.status(400).json({ message: "Email is required" });
    return;
  }
  const user = await prisma$1.user.findFirst({
    where: { email }
  });
  res.status(200).json({ exists: !!user });
});
const uploadUserImage = asyncHandler(async (req, res) => {
  const user = req.user;
  const existing = await prisma$1.user.findFirst({
    where: { id: user.id }
  });
  if (!existing) {
    res.status(404).json({ message: "User not found" });
    return;
  }
  const filesMap = req.files;
  const logoPicFile = filesMap?.logoPic?.[0];
  const profilePicFile = filesMap?.profilePic?.[0];
  const updates = {};
  if (logoPicFile) {
    updates.logoPic = toPublicPath(logoPicFile.path);
  }
  if (profilePicFile) {
    updates.profilePic = toPublicPath(profilePicFile.path);
  }
  if (Object.keys(updates).length === 0) {
    res.status(400).json({ message: "No files uploaded" });
    return;
  }
  await prisma$1.user.update({
    where: { id: user.id },
    data: updates
  });
  res.status(200).json({
    message: "Image uploaded successfully",
    data: { logoPic: updates.logoPic ? `${getHost()}${updates.logoPic}` : existing.logoPic, profilePic: updates.profilePic ? `${getHost()}${updates.profilePic}` : existing.profilePic }
  });
});
const ALLOWED_EXT = [".jpg", ".jpeg", ".png", ".webp"];
const UPLOAD_ROOT = "uploads";
const ensureDir = (subDir) => {
  const uploadPath = path.join(
    process.cwd(),
    UPLOAD_ROOT,
    subDir ?? ""
  );
  if (!fs.existsSync(uploadPath)) {
    fs.mkdirSync(uploadPath, { recursive: true });
  }
  return uploadPath;
};
const createStorage = (subDir) => multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, ensureDir(subDir));
  },
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname).toLowerCase();
    const baseName = path.basename(file.originalname, ext).trim().replace(/\s+/g, "_").replace(/[^a-zA-Z0-9._-]/g, "");
    const uploadPath = ensureDir(subDir);
    let finalName = `${baseName}${ext}`;
    let counter = 1;
    while (fs.existsSync(path.join(uploadPath, finalName))) {
      finalName = `${baseName}_(${counter})${ext}`;
      counter++;
    }
    cb(null, finalName);
  }
});
const fileFilter = (req, file, cb) => {
  const ext = path.extname(file.originalname).toLowerCase();
  if (!ALLOWED_EXT.includes(ext)) {
    return cb(new Error("INVALID_FILE_TYPE"));
  }
  cb(null, true);
};
const imageUpload = (subDir) => multer({
  storage: createStorage(subDir),
  fileFilter,
  limits: { fileSize: 5 * 1024 * 1024 }
});
const syllabusFileFilter = (req, file, cb) => {
  const ext = path.extname(file.originalname).toLowerCase();
  if (ext !== ".pdf") {
    return cb(new Error("INVALID_FILE_TYPE_ONLY_PDF_ALLOWED"));
  }
  cb(null, true);
};
const syllabusUpload = multer({
  storage: createStorage("syllabus"),
  fileFilter: syllabusFileFilter,
  limits: { fileSize: 10 * 1024 * 1024 }
});
const notificationFileFilter = (req, file, cb) => {
  const ext = path.extname(file.originalname).toLowerCase();
  const allowed = [".pdf", ".jpg", ".jpeg", ".png", ".webp"];
  if (!allowed.includes(ext)) {
    return cb(new Error("INVALID_FILE_TYPE"));
  }
  cb(null, true);
};
const notificationUpload = multer({
  storage: createStorage("notification"),
  fileFilter: notificationFileFilter,
  limits: { fileSize: 10 * 1024 * 1024 }
});
const excelFileFilter = (req, file, cb) => {
  const ext = path.extname(file.originalname).toLowerCase();
  const allowed = [".xlsx", ".xls"];
  if (!allowed.includes(ext)) {
    return cb(new Error("INVALID_FILE_TYPE_ONLY_EXCEL_ALLOWED"));
  }
  cb(null, true);
};
const excelUpload = multer({
  storage: createStorage("excel"),
  fileFilter: excelFileFilter,
  limits: { fileSize: 5 * 1024 * 1024 }
});
const notesFileFilter = (req, file, cb) => {
  const ext = path.extname(file.originalname).toLowerCase();
  if (ext !== ".pdf") {
    return cb(new Error("INVALID_FILE_TYPE_ONLY_PDF_ALLOWED"));
  }
  cb(null, true);
};
const notesUpload = multer({
  storage: createStorage("notespdf"),
  fileFilter: notesFileFilter,
  limits: { fileSize: 3 * 1024 * 1024 }
});
const optimizeImage = async (req, res, next) => {
  try {
    const filesMap = req.files;
    if (!filesMap) return next();
    const files = Object.values(filesMap).flat();
    if (!files.length) return next();
    for (const file of files) {
      const ext = path.extname(file.filename).toLowerCase();
      const inputPath = file.path;
      const tempPath = inputPath + ".tmp";
      const image = sharp(inputPath).resize({
        width: 1600,
        withoutEnlargement: true
      });
      if (ext === ".jpg" || ext === ".jpeg") {
        await image.jpeg({ quality: 80 }).toFile(tempPath);
      } else if (ext === ".png") {
        await image.png({ compressionLevel: 8 }).toFile(tempPath);
      } else if (ext === ".webp") {
        await image.webp({ quality: 80 }).toFile(tempPath);
      } else {
        continue;
      }
      fs.unlinkSync(inputPath);
      fs.renameSync(tempPath, inputPath);
    }
    next();
  } catch (err) {
    next(err);
  }
};
const router$r = express.Router();
router$r.put("/", protect, updateUserDetails);
router$r.get("/", protect, listUsers);
router$r.get("/check-email", protect, checkEmail);
router$r.get("/:id", protect, getUserProfile);
router$r.delete("/:id", protect, deleteUser);
router$r.post("/upload", protect, imageUpload("profiles").fields([
  { name: "logoPic", maxCount: 1 },
  { name: "profilePic", maxCount: 1 }
]), optimizeImage, uploadUserImage);
const createExam = asyncHandler(async (req, res) => {
  const { examName, isPublished } = req.body;
  const user = req.user;
  if (!examName) {
    res.status(400).json({ message: "examName is required" });
    return;
  }
  const existing = await prisma$1.exam.findFirst({
    where: { institutionId: user.institutionId, examName: { equals: examName } }
  });
  if (existing) {
    res.status(400).json({ message: "examName already exists" });
    return;
  }
  const exam = await prisma$1.exam.create({
    data: {
      examName,
      institutionId: user.institutionId,
      isPublished: typeof isPublished === "boolean" ? isPublished : void 0
    }
  });
  res.status(201).json({
    message: "Exam created",
    data: {
      ...exam,
      createdAt: toIST(exam.createdAt),
      updatedAt: toIST(exam.updatedAt)
    }
  });
});
const getExams = asyncHandler(async (req, res) => {
  const search = req.query.search || "";
  const page = Number(req.query.page || 1);
  const limit = Number(req.query.limit || 10);
  const skip = (page - 1) * limit;
  const user = req.user;
  let institutionId = user.role === "ADMIN" && req.query.institutionId ? req.query.institutionId : user.institutionId;
  if (user.role === "ADMIN" && req.query.institutionId) {
    const instRec = await prisma$1.institution.findFirst({
      where: { userId: req.query.institutionId },
      select: { id: true }
    });
    if (instRec) {
      institutionId = instRec.id;
    }
  }
  const where = {
    institutionId,
    examName: { contains: search }
  };
  if (user.role === "STUDENT") {
    where.isPublished = true;
  } else if (req.query.isPublished !== void 0) {
    where.isPublished = req.query.isPublished === "true";
  }
  const [total, exams] = await Promise.all([
    prisma$1.exam.count({ where }),
    prisma$1.exam.findMany({
      where,
      include: {
        institution: {
          include: { user: { select: { institutionName: true } } }
        },
        _count: {
          select: {
            practiceTests: { where: { institutionId } },
            mockTests: { where: { institutionId } }
          }
        }
      },
      orderBy: { examName: "asc" },
      skip,
      take: limit
    })
  ]);
  const updatedExams = exams.map((exam) => {
    const { _count, institutionId: examInstId, institution, ...rest } = exam;
    let examName = exam.examName;
    let sharedFromName = null;
    if (examInstId && examInstId !== institutionId) {
      sharedFromName = institution?.user?.institutionName || "another institution";
      examName = `${exam.examName} (shared from ${sharedFromName})`;
    }
    return user.role === "STUDENT" ? {
      id: exam.id,
      examName,
      baseExamName: exam.examName,
      sharedFromName,
      isPublished: exam.isPublished
    } : {
      ...rest,
      examName,
      baseExamName: exam.examName,
      sharedFromName,
      createdAt: toIST(exam.createdAt),
      updatedAt: toIST(exam.updatedAt),
      testCount: _count.practiceTests,
      mockTestCount: _count.mockTests
    };
  });
  res.status(200).json({
    data: updatedExams,
    meta: {
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit)
    }
  });
});
const updateExam = asyncHandler(async (req, res) => {
  const user = req.user;
  const { id } = req.params;
  const { examName, isPublished } = req.body;
  const updateData = {};
  if (!examName || examName.trim() === "") {
    res.status(400).json({ message: "No valid fields to update" });
    return;
  }
  updateData.examName = examName;
  if (typeof isPublished === "boolean") {
    updateData.isPublished = isPublished;
  }
  if (Object.keys(updateData).length === 0) {
    res.status(400).json({ message: "No valid fields to update" });
    return;
  }
  const updated = await prisma$1.exam.update({
    where: { id, institutionId: user.institutionId },
    data: updateData
  });
  res.status(200).json({
    message: "Exam updated successfully",
    data: {
      ...updated,
      createdAt: toIST(updated.createdAt),
      updatedAt: toIST(updated.updatedAt)
    }
  });
});
const deleteExam = asyncHandler(async (req, res) => {
  const user = req.user;
  const { id } = req.params;
  const exam = await prisma$1.exam.findUnique({
    where: { id, institutionId: user.institutionId },
    include: {
      _count: {
        select: {
          practiceTests: true,
          mockTests: true,
          oldQuestionPapers: true
        }
      }
    }
  });
  if (!exam) {
    res.status(404).json({ message: "Exam not found" });
    return;
  }
  if (exam._count.practiceTests > 0) {
    res.status(400).json({ message: "Exam cannot be deleted as it has Practice Tests" });
    return;
  }
  if (exam._count.mockTests > 0) {
    res.status(400).json({ message: "Exam cannot be deleted as it has Mock Tests" });
    return;
  }
  if (exam._count.oldQuestionPapers > 0) {
    res.status(400).json({ message: "Exam cannot be deleted as it has PYQ (Previous Question Papers)" });
    return;
  }
  await prisma$1.exam.delete({
    where: { id, institutionId: user.institutionId }
  });
  res.status(201).json({ message: "Exam deleted successfully" });
});
const router$q = Router();
router$q.post("/", protect, createExam);
router$q.get("/", protect, getExams);
router$q.put("/:id", protect, updateExam);
router$q.delete("/:id", protect, deleteExam);
const createStaff = asyncHandler(async (req, res) => {
  const { firstName, lastName, email, phone, password, roleId, designation, subjects } = req.body;
  if (!firstName) {
    res.status(400).json({ message: "First name is required" });
    return;
  }
  if (!email) {
    res.status(400).json({ message: "Email is required" });
    return;
  }
  if (!password) {
    res.status(400).json({ message: "Password is required" });
    return;
  }
  if (!roleId) {
    res.status(400).json({ message: "Role ID is required" });
    return;
  }
  const exists = await prisma$1.user.findFirst({ where: { email, isDeleted: false } });
  if (exists) {
    res.status(400).json({ message: "Email already used" });
    return;
  }
  const hash = await bcrypt.hash(password, 10);
  const user = req.user;
  const adminId = user.id;
  const referralCode = await generateReferralCode();
  const result = await prisma$1.$transaction(async (tx) => {
    const newUser = await tx.user.create({
      data: {
        firstName,
        lastName,
        email,
        phone,
        password: hash,
        role: "STAFF",
        createdById: adminId,
        expiresAt: tokenExpireTime()
      }
    });
    const staffInfo = await tx.staff.create({
      data: {
        userId: newUser.id,
        roleId,
        designation,
        subjects: {
          connect: (subjects || []).map((id) => ({ id }))
        },
        institutionId: user.institutionId,
        referralCode
      }
    });
    const tokenStr = v4();
    await tx.token.create({
      data: {
        userId: newUser.id,
        token: tokenStr,
        institutionId: user.institutionId,
        type: "VERIFY",
        expiresAt: tokenExpireTime()
      }
    });
    return { userId: newUser.id, staffId: staffInfo.id, token: tokenStr };
  });
  const verifyUrl = `${process.env.BASE_URL}/verify/${result.token}`;
  const emailRes = await sendEmail(
    email,
    "Verify your email",
    verificationTemplate({ firstName, companyName: "ExamInfra", verifyUrl })
  );
  if (!emailRes.success) {
    res.status(500).json({ message: "Failed to send verification email. Please try again later." });
    return;
  }
  res.status(201).json({
    message: "Staff created successfully. Verification email sent.",
    data: { userId: result.userId, staffId: result.staffId }
  });
});
const listStaffs = asyncHandler(async (req, res) => {
  const user = req.user;
  const search = req.query.search?.trim();
  const isVerified = req.query.isVerified?.trim();
  const roleId = req.query.roleId?.trim();
  const subjectId = req.query.subjectId?.trim();
  const regStartDate = req.query.regStartDate?.trim();
  const regEndDate = req.query.regEndDate?.trim();
  const page = Math.max(parseInt(req.query.page) || 1, 1);
  const limit = Math.max(parseInt(req.query.limit) || 10, 1);
  const skip = (page - 1) * limit;
  const institutionId = user.role === "ADMIN" ? req.query.institutionId : user.institutionId;
  const where = {
    user: { isDeleted: false }
  };
  if (institutionId) {
    where.institutionId = institutionId;
  }
  if (search) {
    const searchWords = search.split(/\s+/).filter(Boolean);
    where.OR = [
      { user: { firstName: { contains: search } } },
      { user: { lastName: { contains: search } } },
      { user: { email: { contains: search } } },
      { user: { phone: { contains: search } } },
      { role: { roleName: { contains: search } } },
      { subjects: { some: { subjectName: { contains: search } } } },
      ...searchWords.length > 1 ? [{
        user: {
          AND: searchWords.map((word) => ({
            OR: [
              { firstName: { contains: word } },
              { lastName: { contains: word } }
            ]
          }))
        }
      }] : []
    ];
  }
  if (isVerified) {
    where.user.isVerified = isVerified === "true";
  }
  if (roleId) {
    where.roleId = roleId;
  }
  if (subjectId) {
    where.subjects = {
      some: { id: subjectId }
    };
  }
  if (regStartDate || regEndDate) {
    where.user.createdAt = {};
    const convertToISOFormat = (dateStr) => {
      const parts = dateStr.split("/");
      if (parts.length === 3) {
        const [day, month, year] = parts;
        return `${year}-${month}-${day}`;
      }
      return dateStr;
    };
    if (regStartDate) {
      const formattedStartDate = convertToISOFormat(regStartDate);
      where.user.createdAt.gte = /* @__PURE__ */ new Date(`${formattedStartDate}T00:00:00.000Z`);
    }
    if (regEndDate) {
      const formattedEndDate = convertToISOFormat(regEndDate);
      where.user.createdAt.lte = /* @__PURE__ */ new Date(`${formattedEndDate}T23:59:59.999Z`);
    }
  }
  const [total, staffs] = await Promise.all([
    prisma$1.staff.count({ where }),
    prisma$1.staff.findMany({
      where,
      orderBy: { user: { firstName: "asc" } },
      include: {
        user: true,
        role: true,
        // exams: true,
        subjects: true
      },
      skip,
      take: limit
    })
  ]);
  const structuredList = staffs.map((staff) => {
    const userData = staff.user || {};
    return {
      id: staff.id,
      firstName: userData.firstName ?? "",
      lastName: userData.lastName ?? "",
      email: userData.email ?? "",
      phone: userData.phone ?? "",
      designation: staff.designation ?? "",
      createdBy: userData.createdById ?? "",
      roleId: staff.role?.id || staff.roleId,
      roleName: staff.role?.roleName || "",
      referralCode: staff.referralCode || "",
      // exams: staff.exams,
      subjects: staff.subjects,
      isVerified: userData.isVerified,
      createdAt: toIST(staff.createdAt),
      updatedAt: toIST(staff.updatedAt)
    };
  });
  res.status(200).json({
    message: "Successfully fetched staff list",
    data: structuredList,
    meta: {
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit)
    }
  });
});
const updateStaff = asyncHandler(async (req, res) => {
  const staffId = req.params.id;
  const { firstName, lastName, phone, designation, roleId, subjects } = req.body;
  const user = req.user;
  const staffRecord = await prisma$1.staff.findFirst({
    where: { id: staffId, institutionId: user.institutionId, user: { isDeleted: false } }
  });
  if (!staffRecord) {
    res.status(404).json({ message: "Staff record not found" });
    return;
  }
  if (firstName || lastName || phone) {
    const userUpdates = {};
    if (firstName) userUpdates.firstName = firstName;
    if (lastName) userUpdates.lastName = lastName;
    if (phone) userUpdates.phone = phone;
    await prisma$1.user.update({
      where: { id: staffRecord.userId },
      data: userUpdates
    });
  }
  const staffUpdates = {};
  if (designation) staffUpdates.designation = designation;
  if (roleId) staffUpdates.roleId = roleId;
  if (subjects) {
    staffUpdates.subjects = {
      set: subjects.map((id) => ({ id }))
    };
  }
  const updatedStaff = await prisma$1.staff.update({
    where: { id: staffId },
    data: staffUpdates,
    include: {
      user: true,
      role: true,
      subjects: true
    }
  });
  res.status(200).json({
    message: "Staff updated successfully",
    data: {
      id: updatedStaff.id,
      firstName: updatedStaff.user.firstName,
      lastName: updatedStaff.user.lastName,
      email: updatedStaff.user.email,
      phone: updatedStaff.user.phone,
      designation: updatedStaff.designation,
      roleId: updatedStaff.roleId,
      subjects: updatedStaff.subjects
    }
  });
});
const deleteStaff = asyncHandler(async (req, res) => {
  const staffId = req.params.id;
  const user = req.user;
  const staffRecord = await prisma$1.staff.findFirst({
    where: { id: staffId, institutionId: user.institutionId, user: { isDeleted: false } },
    select: {
      userId: true,
      user: true
    }
  });
  if (!staffRecord) {
    res.status(400).json({ message: "Staff not found" });
    return;
  }
  const userRecord = staffRecord.user;
  const userId = staffRecord.userId;
  if (userId === user.id) {
    res.status(400).json({ message: "You can not delete yourself" });
    return;
  }
  await prisma$1.user.update({
    where: { id: userId },
    data: {
      oldEmail: userRecord.email,
      details: {
        email: userRecord.email,
        phone: userRecord.phone,
        firstName: userRecord.firstName,
        lastName: userRecord.lastName,
        createdAt: userRecord.createdAt,
        updatedAt: userRecord.updatedAt,
        createdById: userRecord.createdById,
        role: userRecord.role,
        isVerified: userRecord.isVerified
      },
      email: null,
      phone: null,
      isDeleted: true,
      sessionToken: null
    }
  });
  res.status(200).json({
    message: "Staff deleted successfully",
    deletedStaffId: staffId,
    deletedUserId: userId
  });
});
const verifyReferralCode = asyncHandler(async (req, res) => {
  const { code } = req.params;
  const user = req.user;
  const staff = await prisma$1.staff.findFirst({
    where: {
      referralCode: code,
      institutionId: user.institutionId,
      user: { isDeleted: false, isVerified: true }
    },
    include: {
      user: {
        select: {
          firstName: true,
          lastName: true,
          id: true
        }
      }
    }
  });
  if (!staff) {
    res.status(404).json({ message: "Invalid Referral Code" });
    return;
  }
  res.status(200).json({
    message: "Valid Code",
    data: {
      staffName: `${staff.user.firstName} ${staff.user.lastName}`,
      staffUserId: staff.user.id
    }
  });
});
const router$p = express.Router();
router$p.post("/", protect, permit("Admin", "Staff", "Institution"), createStaff);
router$p.get("/", protect, permit("Admin", "Staff", "Institution"), listStaffs);
router$p.get("/verify-referral/:code", protect, permit("Admin", "Staff", "Institution"), verifyReferralCode);
router$p.put("/:id", protect, permit("Admin", "Staff", "Institution"), updateStaff);
router$p.delete("/:id", protect, permit("Admin", "Staff", "Institution"), deleteStaff);
const allowedLanguages = [
  "Assamese",
  "Bengali",
  "Bodo",
  "Dogri",
  "English",
  "Gujarati",
  "Hindi",
  "Kannada",
  "Kashmiri",
  "Kondan",
  "Maithili",
  "Malayalam",
  "Manipuri",
  "Marathi",
  "Nepali",
  "Oriya",
  "Punjabi",
  "Sanskrit",
  "Santhali",
  "Sindhi",
  "Tamil",
  "Telugu",
  "Urdu"
];
const isValidBulkUploadLanguage = (value) => {
  const normalized = value.trim().toLowerCase();
  return allowedLanguages.some((lang) => lang.toLowerCase() === normalized);
};
const getLanguages = asyncHandler(async (_req, res) => {
  res.status(200).json({
    message: "Supported student languages fetched successfully.",
    data: allowedLanguages
  });
});
const createStudent = asyncHandler(async (req, res) => {
  const { firstName, lastName, email, phone, password, exams, language, referralId } = req.body;
  const user = req.user;
  const userInstitutionId = user.institutionId;
  const validations = [
    { field: firstName, message: "First name is required" },
    { field: lastName, message: "Last name is required" },
    { field: email, message: "Email is required" },
    { field: phone, message: "Phone number is required" },
    { field: exams, message: "Exams is required" },
    { field: language, message: "Language is required" }
  ];
  for (const { field, message } of validations) {
    if (!field) {
      res.status(400).json({ message });
      return;
    }
  }
  if (phone && !/^[0-9]{10}$/.test(String(phone).trim())) {
    res.status(400).json({ message: "Phone number should be 10 digits" });
    return;
  }
  const exists = await prisma$1.user.findFirst({
    where: { email },
    select: {
      id: true,
      role: true,
      student: {
        select: {
          id: true,
          institutions: {
            where: { deletedAt: null },
            select: {
              institutionId: true
            }
          }
        }
      }
    }
  });
  if (exists && exists.student?.institutions) {
    const currentInstMatch = exists.student.institutions.some(
      (i) => i.institutionId === userInstitutionId
    );
    if (currentInstMatch) {
      res.status(400).json({ message: "Email already exists in this institution" });
      return;
    }
    if (exists.student.institutions.length > 0) {
      res.status(400).json({
        message: "Email already exists in another institution"
      });
      return;
    }
  }
  if (referralId) {
    const referralStaff = await prisma$1.staff.findFirst({
      where: {
        userId: String(referralId).trim(),
        institutionId: userInstitutionId,
        user: { isDeleted: false, isVerified: true }
      }
    });
    if (!referralStaff) {
      res.status(400).json({ message: "Invalid or unverified referral code" });
      return;
    }
  }
  const normalizedPhone = String(phone).trim();
  const rawPassword = password && String(password).trim() ? String(password).trim() : normalizedPhone;
  const hash = await bcrypt.hash(rawPassword, 10);
  const result = await prisma$1.$transaction(async (tx) => {
    let newUser = null;
    let newStudent = null;
    const tokenStr = v4();
    if (!exists) {
      newUser = await tx.user.create({
        data: {
          firstName,
          lastName,
          email,
          phone: normalizedPhone,
          password: hash,
          role: "STUDENT",
          isVerified: false,
          createdById: user.id
        }
      });
      newStudent = await tx.student.create({
        data: {
          userId: newUser.id,
          referralId: referralId || null
        }
      });
    } else {
      if (exists.role && exists.role !== "STUDENT") {
        const err = new Error("Email already in use");
        err.statusCode = 400;
        throw err;
      }
      if (!exists.student) {
        const err = new Error("Email already in use");
        err.statusCode = 400;
        throw err;
      }
      await tx.user.update({
        where: { id: exists.id },
        data: {
          firstName,
          lastName,
          phone: normalizedPhone,
          password: hash
        }
      });
      newUser = exists;
      newStudent = exists.student;
    }
    if (!newStudent || !newStudent.id) {
      const err = new Error("Student not found");
      err.statusCode = 400;
      throw err;
    }
    if (!newUser || !newUser.id) {
      const err = new Error("User not found");
      err.statusCode = 400;
      throw err;
    }
    const studentInstitution = await tx.studentInstitution.create({
      data: {
        studentId: newStudent.id,
        institutionId: userInstitutionId,
        createdById: user.id,
        isPrimary: true,
        isVerified: false,
        examsId: exams || null,
        verificationExpiresAt: tokenExpireTime()
      }
    });
    await tx.token.create({
      data: {
        userId: newUser.id,
        token: tokenStr,
        institutionId: userInstitutionId,
        studentInstitutionId: studentInstitution.id,
        type: "VERIFY",
        expiresAt: tokenExpireTime()
      }
    });
    return { userId: newUser.id, studentId: newStudent.id, token: tokenStr };
  });
  const verifyUrl = `${process.env.BASE_URL}/verify/${result.token}`;
  const emailRes = await sendEmail(
    email,
    "Verify your email",
    verificationTemplate({ firstName, companyName: "ExamInfra", verifyUrl })
  );
  if (!emailRes.success) {
    res.status(500).json({ message: "Failed to send verification email. Please try again later." });
    return;
  }
  res.status(201).json({
    message: "Student created successfully. Verification email sent.",
    data: { userId: result.userId, studentId: result.studentId }
  });
});
const listRegisterStudents = asyncHandler(async (req, res) => {
  const user = req.user;
  const institutionId = user.institutionId;
  const search = req.query.search?.trim();
  const isVerified = req.query.isVerified?.trim();
  const planStatus = req.query.planStatus?.trim();
  const planType = req.query.planType?.trim();
  const examId = req.query.examId?.trim();
  const language = req.query.language?.trim();
  const planId = req.query.planId?.trim();
  const planName = req.query.planName?.trim();
  const subscriptionAction = req.query.subscriptionAction?.trim();
  const noSubscription = req.query.noSubscription?.trim();
  const regStartDate = req.query.regStartDate?.trim();
  const regEndDate = req.query.regEndDate?.trim();
  const page = Math.max(parseInt(req.query.page) || 1, 1);
  const limit = Math.max(parseInt(req.query.limit) || 10, 1);
  const skip = (page - 1) * limit;
  const whereClause = {
    institutionId,
    deletedAt: null
  };
  if (search) {
    const searchWords = search.split(/\s+/).filter(Boolean);
    whereClause.student = {
      user: {
        OR: [
          { firstName: { contains: search } },
          { lastName: { contains: search } },
          { email: { contains: search } },
          { phone: { contains: search } },
          ...searchWords.length > 1 ? [{
            AND: searchWords.map((word) => ({
              OR: [
                { firstName: { contains: word } },
                { lastName: { contains: word } }
              ]
            }))
          }] : []
        ]
      }
    };
  }
  if (isVerified) {
    whereClause.isVerified = isVerified === "true";
  }
  if (examId) {
    whereClause.examsId = examId;
  }
  if (language) {
    whereClause.language = language;
  }
  if (noSubscription === "true") {
    whereClause.subscriptions = {
      none: { isCurrent: true }
    };
  } else if (planStatus || planId || subscriptionAction || planName || planType) {
    const subConditions = { isCurrent: true };
    if (planId) {
      subConditions.planId = planId;
    }
    if (planStatus) {
      subConditions.status = planStatus.toUpperCase();
    }
    if (subscriptionAction) {
      subConditions.action = subscriptionAction.toUpperCase();
    }
    if (planName) {
      subConditions.plan = subConditions.plan || {};
      subConditions.plan.planName = planName;
    }
    if (planType) {
      subConditions.plan = subConditions.plan || {};
      subConditions.plan.planType = planType;
    }
    whereClause.subscriptions = {
      some: subConditions
    };
  }
  if (regStartDate || regEndDate) {
    whereClause.createdAt = {};
    const convertToISOFormat = (dateStr) => {
      const parts = dateStr.split("/");
      if (parts.length === 3) {
        const [day, month, year] = parts;
        return `${year}-${month}-${day}`;
      }
      return dateStr;
    };
    if (regStartDate) {
      const formattedStartDate = convertToISOFormat(regStartDate);
      whereClause.createdAt.gte = /* @__PURE__ */ new Date(`${formattedStartDate}T00:00:00.000Z`);
    }
    if (regEndDate) {
      const formattedEndDate = convertToISOFormat(regEndDate);
      whereClause.createdAt.lte = /* @__PURE__ */ new Date(`${formattedEndDate}T23:59:59.999Z`);
    }
  }
  const [studentInstitutions, total] = await Promise.all([
    prisma$1.studentInstitution.findMany({
      where: whereClause,
      select: {
        id: true,
        institutionId: true,
        isVerified: true,
        examsId: true,
        exam: true,
        language: true,
        student: {
          include: {
            user: {
              select: {
                id: true,
                firstName: true,
                lastName: true,
                email: true,
                phone: true,
                isVerified: true,
                createdById: true
              }
            },
            referral: {
              select: {
                firstName: true,
                lastName: true,
                id: true
              }
            }
          }
        },
        subscriptions: { where: { isCurrent: true }, include: { plan: true } },
        subscriptionRequests: { where: { status: "PENDING" }, orderBy: { createdAt: "desc" }, take: 1, include: { subscriptionPlan: { select: { id: true, planName: true } } } },
        createdAt: true
      },
      skip,
      take: limit,
      orderBy: { student: { user: { firstName: "asc" } } }
    }),
    prisma$1.studentInstitution.count({
      where: whereClause
    })
  ]);
  const structuredList = studentInstitutions.map((student, i) => {
    const { id: studentInstitutionId, institutionId: institutionId2, subscriptions, subscriptionRequests, isVerified: isVerified2, student: { id, referral, user: user2, language: motherTongue }, exam, examsId, language: instLanguage, createdAt: studentInstitutionCreatedAt } = student;
    return {
      studentInstitutionId,
      institutionId: institutionId2,
      id,
      userId: user2?.id ?? "",
      firstName: user2?.firstName ?? "",
      lastName: user2?.lastName ?? "",
      phone: user2?.phone ?? "",
      email: user2?.email ?? "",
      currentSubscriptionPlanId: subscriptions[0]?.id,
      subscription: subscriptions[0] ? {
        ...subscriptions[0],
        startDate: subscriptions[0].startDate ? toIST(subscriptions[0].startDate) : null,
        endDate: subscriptions[0].endDate ? toIST(subscriptions[0].endDate) : null,
        trialEndsAt: subscriptions[0].endDate ? toIST(subscriptions[0].endDate) : null,
        createdAt: toIST(subscriptions[0].createdAt)
        // plan:null
      } : null,
      referral: `${referral?.firstName ?? ""} ${referral?.lastName ?? ""}`,
      createdAt: toIST(studentInstitutionCreatedAt),
      examsId,
      examName: exam?.examName ?? "",
      language: motherTongue || instLanguage || "",
      isVerified: isVerified2 ?? false,
      subscriptionPlan: {
        id: subscriptions[0]?.plan?.id,
        planName: subscriptions[0]?.plan?.planName,
        planType: subscriptions[0]?.plan?.planType,
        duration: subscriptions[0]?.plan?.duration,
        isActive: subscriptions[0]?.plan?.isActive
      },
      subscriptionRequest: subscriptionRequests[0] ? {
        id: subscriptionRequests[0].id,
        status: subscriptionRequests[0].status,
        planName: subscriptionRequests[0].subscriptionPlan?.planName || "N/A",
        createdAt: toIST(subscriptionRequests[0].createdAt)
      } : null
    };
  });
  res.status(201).json({
    message: "Successfully fetched student list.",
    data: structuredList,
    meta: {
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit)
    }
  });
});
const updateOwnStudentProfile = asyncHandler(async (req, res) => {
  const user = req.user;
  const userInstitutionId = user.institutionId;
  const studentInstitutionId = user.studentInstitutionId;
  if (!studentInstitutionId) {
    res.status(404).json({ message: "Institution not found." });
    return;
  }
  const { exams, institutionId } = req.body;
  const studentInstitution = await prisma$1.studentInstitution.findFirst({
    where: { id: studentInstitutionId }
  });
  if (!studentInstitution) {
    res.status(404).json({ message: "Student not found in this institution." });
    return;
  }
  const updateData = {};
  if (exams) {
    const examExists = await prisma$1.exam.findFirst({
      where: { id: exams, institutionId: userInstitutionId }
    });
    if (!examExists) {
      res.status(400).json({ message: "Exam not found in database." });
      return;
    }
    updateData.examsId = exams;
  }
  await prisma$1.$transaction(async (tx) => {
    if (Object.keys(updateData).length > 0) {
      await tx.studentInstitution.update({
        where: { id: studentInstitutionId },
        data: updateData
      });
    }
    if (institutionId) {
      const targetInstitution = await prisma$1.studentInstitution.findUnique({
        where: { id: institutionId, deletedAt: null }
      });
      if (!targetInstitution) {
        res.status(400).json({
          message: "Invalid institution."
        });
        return;
      }
      await tx.studentInstitution.updateMany({
        where: { studentId: studentInstitution.studentId, deletedAt: null },
        data: { isPrimary: false }
      });
      await tx.studentInstitution.update({
        where: { id: targetInstitution.id },
        data: { isPrimary: true }
      });
    }
  });
  const updatedRecord = await prisma$1.studentInstitution.findUnique({
    where: { id: institutionId ?? studentInstitution.id },
    select: { id: true, examsId: true }
  });
  res.status(200).json({
    message: "Updated successfully.",
    data: {
      studentInstitutionId: updatedRecord?.id,
      userId: studentInstitution.studentId,
      exams: updatedRecord?.examsId ?? ""
    }
  });
});
const saveFcmToken = asyncHandler(async (req, res) => {
  const user = req.user;
  if (!user) {
    res.status(401).json({ message: "Unauthorized" });
    return;
  }
  const userId = user.id;
  const { fcmToken } = req.body;
  const student = await prisma$1.student.findUnique({ where: { userId, user: { isDeleted: false } } });
  if (!student) {
    res.status(404).json({ message: "Student not found." });
    return;
  }
  await prisma$1.student.update({
    where: { id: student.id },
    data: { fcmToken }
  });
  res.status(201).json({ message: "FCM token saved successfully.", fcmToken });
});
const updateStudentByAdmin = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const { firstName, lastName, email, phone, exams, language, referralId } = req.body;
  const user = req.user;
  const studentRecord = await prisma$1.student.findUnique({
    where: { id, user: { isDeleted: false } },
    include: { user: true, institutions: { where: { institutionId: user.institutionId, deletedAt: null } } }
  });
  if (!studentRecord || !studentRecord.institutions) {
    res.status(404).json({ message: "Student not found" });
    return;
  }
  const result = await prisma$1.$transaction(async (tx) => {
    await tx.user.update({
      where: { id: studentRecord.userId },
      data: {
        firstName,
        lastName,
        email,
        phone
      }
    });
    const updatedStudent = await tx.student.update({
      where: { id },
      data: {
        referralId,
        language
      },
      include: { user: true }
    });
    const studentInstitution = await tx.studentInstitution.findFirst({
      where: { studentId: id, institutionId: user.institutionId, deletedAt: null }
    });
    let updatedStudentInstitution = null;
    if (studentInstitution) {
      updatedStudentInstitution = await tx.studentInstitution.update({
        where: { id: studentInstitution.id },
        data: {
          examsId: exams
        },
        select: { id: true }
      });
    }
    return { student: updatedStudent, studentInstitutionId: updatedStudentInstitution?.id };
  });
  res.status(201).json({
    message: "Updated successfully",
    data: {
      ...result.student,
      studentInstitutionId: result.studentInstitutionId,
      createdAt: toIST(result.student.createdAt),
      updatedAt: toIST(result.student.updatedAt)
    }
  });
});
const deleteStudent = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const studentRecord = await prisma$1.studentInstitution.findFirst({
    where: { studentId: id, institutionId: user.institutionId, deletedAt: null }
  });
  if (!studentRecord) {
    res.status(404).json({ message: "Student not found" });
    return;
  }
  await prisma$1.studentInstitution.update({
    where: { id: studentRecord.id },
    data: {
      deletedAt: /* @__PURE__ */ new Date()
    }
  });
  res.status(201).json({
    message: "Student deleted successfully"
  });
});
const getStudentById = asyncHandler(async (req, res) => {
  const user = req.user;
  const { id } = req.params;
  const institutionId = user.institutionId;
  const studentInstitution = await prisma$1.studentInstitution.findFirst({
    where: { studentId: id, institutionId, deletedAt: null },
    select: {
      id: true,
      student: {
        include: {
          user: {
            select: {
              id: true,
              firstName: true,
              lastName: true,
              email: true,
              phone: true,
              isVerified: true,
              createdById: true
            }
          },
          subscriptions: { where: { isCurrent: true, institutionId }, include: { plan: true } }
        }
      },
      exam: true,
      examsId: true,
      language: true
    }
  });
  const student = studentInstitution?.student;
  if (!studentInstitution || !student) {
    res.status(404).json({ message: "Student not found" });
    return;
  }
  const structured = {
    studentInstitutionId: studentInstitution.id,
    id: student?.id,
    userId: student?.user?.id ?? "",
    firstName: student?.user?.firstName ?? "",
    lastName: student?.user?.lastName ?? "",
    phone: student?.user?.phone ?? "",
    email: student?.user?.email ?? "",
    subscription: student?.subscriptions[0] ? {
      ...student?.subscriptions[0],
      startDate: student?.subscriptions[0].startDate ? toIST(student?.subscriptions[0].startDate) : null,
      endDate: student?.subscriptions[0].endDate ? toIST(student?.subscriptions[0].endDate) : null,
      trialEndsAt: student?.subscriptions[0].endDate ? toIST(student?.subscriptions[0].endDate) : null,
      createdAt: toIST(student?.subscriptions[0].createdAt)
    } : null,
    examsId: studentInstitution?.examsId,
    examName: studentInstitution.exam?.examName ?? "",
    language: student?.language || studentInstitution?.language,
    isVerified: student?.user?.isVerified ?? false,
    subscriptionPlan: {
      id: student?.subscriptions[0].plan.id,
      planName: student?.subscriptions[0].plan.planName,
      planType: student?.subscriptions[0].plan.planType,
      duration: student?.subscriptions[0].plan.duration,
      isActive: student?.subscriptions[0].plan.isActive
    }
  };
  res.status(200).json({
    message: "Student details fetched successfully",
    data: structured
  });
});
const getMyInstitutions = asyncHandler(async (req, res) => {
  const user = req.user;
  const userId = user.id;
  const studentInstitution = await prisma$1.studentInstitution.findMany({
    where: { deletedAt: null, isVerified: true, student: { user: { id: userId } } },
    include: { institution: { include: { user: { select: { institutionName: true } } } } },
    orderBy: { createdAt: "desc" }
  });
  if (studentInstitution.filter((si) => si.isPrimary).length > 1) {
    await prisma$1.studentInstitution.updateMany({
      where: { studentId: studentInstitution[0].studentId, deletedAt: null },
      data: { isPrimary: false }
    });
    await prisma$1.studentInstitution.update({
      where: { id: studentInstitution[0].id },
      data: { isPrimary: true }
    });
  }
  const structured = [...studentInstitution].map((i) => {
    return {
      institutionId: i.id,
      institutionName: i?.institution?.user?.institutionName,
      isPrimary: i.isPrimary
    };
  });
  res.status(200).json({
    message: "Student details fetched successfully",
    data: structured
  });
});
const getOwnSubscription = asyncHandler(async (req, res) => {
  const user = req.user;
  const studentInstitutionId = user.studentInstitutionId;
  const studentInstitution = await prisma$1.studentInstitution.findFirst({
    where: { deletedAt: null, isVerified: true, id: studentInstitutionId },
    include: { subscriptions: { where: { isCurrent: true }, take: 1, orderBy: { createdAt: "desc" } } }
  });
  if (!studentInstitution) {
    res.status(404).json({ message: "Student not found" });
    return;
  }
  if (studentInstitution.subscriptions.length === 0) {
    res.status(404).json({ message: "No current subscription found" });
    return;
  }
  res.status(200).json({
    message: "Student details fetched successfully",
    data: studentInstitution.subscriptions[0]
  });
});
const downloadStudentUploadTemplate = asyncHandler(async (req, res) => {
  const templateData = [
    {
      "First Name": "John",
      "Last Name": "Doe",
      "Email": "johndoe@gmail.com",
      "Phone": "9876543210",
      "Exam": "TNPSC",
      "Language": "English",
      "Referral Code (Optional)": "ABC123"
    }
  ];
  const workbook = XLSX.utils.book_new();
  const worksheet = XLSX.utils.json_to_sheet(templateData);
  const objectKeys = Object.keys(templateData[0]);
  worksheet["!cols"] = objectKeys.map((key) => {
    let maxLen = key.length;
    for (const row of templateData) {
      const cellValue = row[key];
      if (cellValue) {
        const cellLen = String(cellValue).length;
        if (cellLen > maxLen) {
          maxLen = cellLen;
        }
      }
    }
    return {
      wch: maxLen + 4
    };
  });
  XLSX.utils.book_append_sheet(workbook, worksheet, "Students Template");
  const buffer = XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
  res.setHeader("Content-Disposition", "attachment; filename=student-upload-template.xlsx");
  res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  res.send(buffer);
});
const previewBulkUploadStudents = asyncHandler(async (req, res) => {
  const user = req.user;
  const userInstitutionId = user?.institutionId;
  if (!userInstitutionId) {
    res.status(400).json({ message: "User is not linked to a valid institution." });
    return;
  }
  if (!req.file) {
    res.status(400).json({ message: "Please upload an Excel file" });
    return;
  }
  try {
    const workbook = XLSX.readFile(req.file.path);
    const sheetName = workbook.SheetNames[0];
    const rawRows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]);
    if (rawRows.length === 0) {
      res.status(400).json({ message: "The uploaded sheet is empty" });
      return;
    }
    const institutionExams = await prisma$1.exam.findMany({
      where: { institutionId: userInstitutionId },
      select: { id: true, examName: true, isPublished: true }
    });
    const examMap = new Map(institutionExams.map((e) => [e.examName.toLowerCase().trim(), { id: e.id, isPublished: e.isPublished }]));
    const emailsInSheet = rawRows.map((r) => String(r["Email"] || "").trim().toLowerCase()).filter(Boolean);
    const referralCodesInSheet = rawRows.map((r) => r["Referral Code (Optional)"] ? String(r["Referral Code (Optional)"]).trim() : "").filter(Boolean);
    const existingUsers = await prisma$1.user.findMany({
      where: { email: { in: emailsInSheet } },
      select: {
        email: true,
        role: true,
        student: {
          select: {
            id: true,
            institutions: {
              where: { deletedAt: null },
              select: {
                institutionId: true
              }
            }
          }
        }
      }
    });
    const uniqueReferralCodes = Array.from(new Set(referralCodesInSheet));
    const staffByReferralCode = /* @__PURE__ */ new Map();
    if (uniqueReferralCodes.length > 0) {
      const staffRecords = await prisma$1.staff.findMany({
        where: {
          referralCode: { in: uniqueReferralCodes },
          institutionId: userInstitutionId,
          user: { isDeleted: false, isVerified: true }
        },
        include: { user: { select: { id: true, firstName: true, lastName: true } } }
      });
      for (const staff of staffRecords) {
        if (staff.referralCode) {
          staffByReferralCode.set(staff.referralCode.trim(), {
            userId: staff.user.id,
            firstName: staff.user.firstName ?? "",
            lastName: staff.user.lastName ?? ""
          });
        }
      }
    }
    const existingEmailMap = new Map(existingUsers.map((u) => [u.email.toLowerCase(), u]));
    const processedRows = [];
    let totalValid = 0;
    let totalInvalid = 0;
    const seenEmailsInSheet = /* @__PURE__ */ new Set();
    for (const row of rawRows) {
      const errors = [];
      const firstName = row["First Name"] ? String(row["First Name"]).trim() : "";
      const lastName = row["Last Name"] ? String(row["Last Name"]).trim() : "";
      const email = row["Email"] ? String(row["Email"]).trim().toLowerCase() : "";
      const phone = row["Phone"] ? String(row["Phone"]).trim() : "";
      const examName = row["Exam"] ? String(row["Exam"]).trim() : "";
      const language = row["Language"] ? String(row["Language"]).trim() : "";
      const referralCode = row["Referral Code (Optional)"] ? String(row["Referral Code (Optional)"]).trim() : "";
      let referralId = null;
      if (referralCode) {
        const referralStaff = staffByReferralCode.get(referralCode);
        if (!referralStaff) {
          errors.push("Invalid referral code or unverified referrer");
        } else {
          referralId = referralStaff.userId;
        }
      }
      if (!firstName) errors.push("First name is required");
      if (!lastName) errors.push("Last name is required");
      if (!email) errors.push("Email is required");
      if (!phone) errors.push("Phone number is required");
      if (!examName) errors.push("Exam is required");
      if (!language) {
        errors.push("Language is required");
      } else if (!isValidBulkUploadLanguage(language)) {
        errors.push(`Invalid language`);
      }
      if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
        errors.push("Invalid email format");
      }
      if (phone && !/^\d{10}$/.test(phone)) {
        errors.push("Phone number should be 10 digits and contain only numbers.");
      }
      if (email) {
        if (seenEmailsInSheet.has(email)) {
          errors.push("Duplicate email within this Excel sheet");
        } else {
          seenEmailsInSheet.add(email);
        }
      }
      if (email && !errors.includes("Duplicate email within this Excel sheet")) {
        const dbMatch = existingEmailMap.get(email);
        if (dbMatch) {
          if (dbMatch.role && dbMatch.role !== "STUDENT") {
            errors.push("Email already in use");
          } else if (dbMatch.student?.institutions) {
            const currentInstMatch = dbMatch.student.institutions.some(
              (i) => i.institutionId === userInstitutionId
            );
            if (currentInstMatch) {
              errors.push("Email already exists in this institution");
            } else if (dbMatch.student.institutions.length > 0) {
              errors.push("Email already in use");
            }
          }
        }
      }
      let matchedExamId = null;
      if (examName) {
        const matched = examMap.get(examName.toLowerCase());
        if (!matched) {
          errors.push(`Exam '${examName}' does not exist in this institution`);
        } else if (!matched.isPublished) {
          errors.push(`Exam '${examName}' is not published`);
        } else {
          matchedExamId = matched.id;
        }
      }
      const isValid = errors.length === 0;
      if (isValid) totalValid++;
      else totalInvalid++;
      processedRows.push({
        firstName,
        lastName,
        email,
        phone: phone || null,
        examName,
        examsId: matchedExamId,
        language,
        referralCode: referralCode || null,
        referralId: referralId || null,
        isValid,
        errors
      });
    }
    res.status(200).json({
      message: "Bulk upload preview completed successfully.",
      meta: {
        totalRows: rawRows.length,
        validRows: totalValid,
        invalidRows: totalInvalid
      },
      data: processedRows
    });
  } finally {
    if (req.file && req.file.path && fs.existsSync(req.file.path)) {
      fs.unlinkSync(req.file.path);
    }
  }
});
const confirmBulkUploadStudents = asyncHandler(async (req, res) => {
  const { students } = req.body;
  const user = req.user;
  const userInstitutionId = user?.institutionId;
  if (!userInstitutionId) {
    res.status(400).json({ message: "User is not linked to a valid institution." });
    return;
  }
  if (!Array.isArray(students) || students.length === 0) {
    res.status(400).json({ message: "No student data provided for confirmation" });
    return;
  }
  const summary = {
    successCount: 0,
    failedCount: 0,
    errors: []
  };
  for (const studentItem of students) {
    const { firstName, lastName, email, phone, examsId, language, referralId } = studentItem;
    try {
      if (!firstName || !lastName || !email || !phone || !examsId || !language) {
        throw new Error("Missing required data parameters mapping fields.");
      }
      const examRecord = await prisma$1.exam.findFirst({ where: { id: examsId, institutionId: userInstitutionId }, select: { id: true, examName: true, isPublished: true } });
      if (!examRecord) {
        throw new Error("Exam not found in database.");
      }
      if (!examRecord.isPublished) {
        throw new Error("Exam is not published");
      }
      if (!isValidBulkUploadLanguage(String(language))) {
        throw new Error(`Invalid language`);
      }
      const sanitizedEmail = String(email).trim().toLowerCase();
      const normalizedPhone = String(phone).trim();
      let resolvedReferralId = null;
      if (referralId) {
        const referralStaff = await prisma$1.staff.findFirst({
          where: {
            userId: String(referralId).trim(),
            institutionId: userInstitutionId,
            user: { isDeleted: false, isVerified: true }
          }
        });
        if (!referralStaff) {
          throw new Error("Invalid or unverified referral code");
        }
        resolvedReferralId = String(referralId).trim();
      }
      if (!/^\d{10}$/.test(normalizedPhone)) {
        throw new Error("Phone number must be 10 digits and contain only numbers.");
      }
      await prisma$1.$transaction(async (tx) => {
        const dbUser = await tx.user.findFirst({
          where: { email: sanitizedEmail },
          select: {
            id: true,
            role: true,
            student: {
              select: {
                id: true,
                institutions: {
                  where: { deletedAt: null },
                  select: {
                    institutionId: true
                  }
                }
              }
            }
          }
        });
        if (dbUser) {
          if (dbUser.role && dbUser.role !== "STUDENT") {
            throw new Error("Email already in use");
          }
          if (dbUser.student?.institutions) {
            const currentInstMatch = dbUser.student.institutions.some(
              (i) => i.institutionId === userInstitutionId
            );
            if (currentInstMatch) {
              throw new Error("Email already in use");
            }
          }
        }
        let targetUserId = dbUser?.id;
        let targetStudentId = dbUser?.student?.id;
        if (!dbUser) {
          const hash = await bcrypt.hash(normalizedPhone, 10);
          const createdUser = await tx.user.create({
            data: {
              firstName: String(firstName).trim(),
              lastName: String(lastName).trim(),
              email: sanitizedEmail,
              phone: normalizedPhone,
              password: hash,
              role: "STUDENT",
              isVerified: false,
              createdById: user.id,
              createdAt: nowUTC()
            }
          });
          const createdStudent = await tx.student.create({
            data: {
              userId: createdUser.id,
              referralId: resolvedReferralId,
              language,
              createdAt: nowUTC()
            }
          });
          targetUserId = createdUser.id;
          targetStudentId = createdStudent.id;
        } else {
          const hash = await bcrypt.hash(normalizedPhone, 10);
          await tx.user.update({
            where: { id: dbUser.id },
            data: {
              firstName: String(firstName).trim(),
              lastName: String(lastName).trim(),
              phone: normalizedPhone,
              password: hash
            }
          });
          await tx.student.update({
            where: { id: dbUser.student.id },
            data: {
              language
            }
          });
        }
        if (dbUser) {
          await tx.studentInstitution.updateMany({
            where: {
              studentId: targetStudentId,
              institutionId: userInstitutionId
            },
            data: { isPrimary: false }
          });
        }
        const studentInstitution = await tx.studentInstitution.create({
          data: {
            studentId: targetStudentId,
            institutionId: userInstitutionId,
            createdById: user.id,
            isPrimary: true,
            isVerified: false,
            examsId,
            verificationExpiresAt: tokenExpireTime(),
            createdAt: nowUTC()
          }
        });
        const tokenStr = v4();
        await tx.token.create({
          data: {
            userId: targetUserId,
            token: tokenStr,
            institutionId: userInstitutionId,
            studentInstitutionId: studentInstitution.id,
            type: "VERIFY",
            expiresAt: tokenExpireTime()
          }
        });
        const verifyUrl = `${process.env.BASE_URL}/verify/${tokenStr}`;
        const emailRes = await sendEmail(
          sanitizedEmail,
          "Verify your email",
          verificationTemplate({ firstName, companyName: "ExamInfra", verifyUrl })
        );
        if (!emailRes.success) {
          throw new Error("Failed to send verification email. Please check SMTP/Network configuration.");
        }
      });
      summary.successCount++;
    } catch (error) {
      summary.failedCount++;
      summary.errors.push({
        email: studentItem.email || "UNKNOWN",
        reason: error.message || "Unknown error processing record sequence."
      });
    }
  }
  res.status(200).json({
    message: "Bulk upload confirmation processing phase completed.",
    summary
  });
});
const router$o = express.Router();
const permitAdmin = permit("Admin", "Staff", "Institution");
const permitStudent = permit("Student");
router$o.get("/template", protect, downloadStudentUploadTemplate);
router$o.post("/preview-bulk", protect, permitAdmin, excelUpload.single("file"), previewBulkUploadStudents);
router$o.post("/confirm-bulk", protect, permitAdmin, confirmBulkUploadStudents);
router$o.put("/", protect, permitStudent, updateOwnStudentProfile);
router$o.post("/fcm-token", protect, saveFcmToken);
router$o.get("/my-institutions", protect, permitStudent, getMyInstitutions);
router$o.get("/subscription", protect, permitStudent, getOwnSubscription);
router$o.get("/", protect, permitAdmin, listRegisterStudents);
router$o.post("/", protect, permitAdmin, createStudent);
router$o.get("/:id", protect, permitAdmin, getStudentById);
router$o.put("/:id", protect, permitAdmin, updateStudentByAdmin);
router$o.delete("/:id", protect, permitAdmin, deleteStudent);
const createQuestion = asyncHandler(async (req, res) => {
  const user = req.user;
  const {
    subject,
    exams,
    topic,
    questionText,
    correctAnswer,
    marks,
    difficulty,
    explanation,
    language,
    options: optionsReq
  } = req.body;
  const validations = [
    { field: questionText, message: "Question text is required" },
    { field: subject, message: "Subject is required" },
    { field: topic, message: "Topic is required" },
    { field: correctAnswer, message: "Correct answer is required" },
    { field: marks, message: "Marks is required" },
    { field: difficulty, message: "Difficulty is required" }
  ];
  for (const item of validations) {
    if (!item.field) {
      res.status(400).json({
        success: false,
        message: item.message
      });
      return;
    }
  }
  if (questionText && questionText.trim() !== "") {
    const existingQuestions = await prisma$1.questionBank.findMany({
      where: {
        questionText: { equals: questionText.trim() },
        subjectId: subject,
        institutionId: user.institutionId
      }
    });
    const duplicate = existingQuestions.find((q) => {
      const sameQuestion = q.questionText.toLowerCase().trim() === questionText.toLowerCase().trim();
      if (!sameQuestion || !Array.isArray(q.options)) {
        return false;
      }
      const dbOptions = q.options.map((opt) => opt.option?.toLowerCase().trim()).sort();
      const uploadedOptions = optionsReq.map((opt) => opt.toLowerCase().trim()).sort();
      return JSON.stringify(dbOptions) === JSON.stringify(uploadedOptions);
    });
    if (duplicate) {
      const topicMsg = duplicate.topic ? ` under the topic "${duplicate.topic}"` : "";
      res.status(400).json({
        success: false,
        message: `A Question with same options already exists in the selected subject${topicMsg}.`
      });
      return;
    }
  }
  const filesMap = req.files;
  const questionImageFile = filesMap?.questionImage?.[0] || null;
  const optionImageFiles = filesMap?.optionImage || [];
  const explanationImageFile = filesMap?.explanationImage?.[0] || null;
  const questionImage = questionImageFile ? toPublicPath(questionImageFile.path) : null;
  const explanationImage = explanationImageFile ? toPublicPath(explanationImageFile.path) : null;
  const rawOptions = Array.isArray(req.body.options) ? req.body.options : [req.body.options];
  let optionImageIndexes = [];
  if (req.body.optionImageIndexes) {
    if (Array.isArray(req.body.optionImageIndexes)) {
      optionImageIndexes = req.body.optionImageIndexes.map((i) => parseInt(i, 10));
    } else {
      optionImageIndexes = [parseInt(req.body.optionImageIndexes, 10)];
    }
  }
  const imageMap = {};
  if (optionImageIndexes.length === optionImageFiles.length) {
    optionImageFiles.forEach((file, i) => {
      const optionIndex = optionImageIndexes[i];
      imageMap[optionIndex] = file;
    });
  } else if (optionImageFiles.length > 0) {
    optionImageFiles.forEach((file, i) => {
      imageMap[i] = file;
    });
  }
  const options = rawOptions.map((text, index) => {
    const img = imageMap[index];
    return {
      option: text || "",
      optionImage: img ? toPublicPath(img.path) : null
    };
  });
  if (!questionText && !questionImage || correctAnswer < 0 || correctAnswer >= options.length) {
    res.status(400).json({ success: false, message: "Invalid question data" });
    return;
  }
  const createdById = user.id;
  const newQuestion = await prisma$1.questionBank.create({
    data: {
      createdById,
      subjectId: subject,
      topic,
      language: language || "English",
      questionText,
      questionImage,
      options,
      correctAnswer: parseInt(correctAnswer),
      marks: parseFloat(marks || "1"),
      difficulty: difficulty || "Medium",
      explanation,
      explanationImage,
      institutionId: user.institutionId
    }
  });
  res.status(201).json({
    success: true,
    message: "Question created successfully",
    data: {
      ...newQuestion,
      createdAt: toIST(newQuestion.createdAt),
      updatedAt: toIST(newQuestion.updatedAt),
      questionImage: newQuestion.questionImage ? `${getHost()}${newQuestion.questionImage}` : "",
      options: newQuestion.options.map((option) => ({
        ...option,
        optionImage: option.optionImage ? `${getHost()}${option.optionImage}` : ""
      })),
      explanationImage: newQuestion.explanationImage ? `${getHost()}${newQuestion.explanationImage}` : ""
    }
  });
});
const listQuestions = asyncHandler(async (req, res) => {
  const user = req.user;
  const { search, exam, difficulty, language, subject, topic, page = "1", limit = "10", isAlreadyUsed, mockTestId, questionUsage } = req.query;
  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);
  const skip = (pageNum - 1) * limitNum;
  const where = {
    institutionId: user.institutionId
  };
  if (search) {
    where.questionText = { contains: search };
  }
  if (difficulty) where.difficulty = difficulty;
  if (subject) where.subjectId = { in: Array.isArray(subject) ? subject : [subject] };
  if (topic) where.topic = { in: Array.isArray(topic) ? topic : [topic] };
  if (language) where.language = { contains: language };
  const requestExamIds = Array.isArray(exam) ? exam : exam ? [exam] : [];
  if (isAlreadyUsed !== void 0) {
    if (requestExamIds.length > 0) {
      if (isAlreadyUsed === "1") {
        where.AND = [
          ...where.AND || [],
          {
            OR: [
              { practiceTests: { some: { examId: { in: requestExamIds } } } },
              { testQuestions: { some: { test: { practiceTests: { some: { examId: { in: requestExamIds } } } } } } }
            ]
          }
        ];
      } else if (isAlreadyUsed === "0") {
        where.AND = [
          ...where.AND || [],
          { practiceTests: { none: { examId: { in: requestExamIds } } } },
          { testQuestions: { none: { test: { practiceTests: { some: { examId: { in: requestExamIds } } } } } } }
        ];
      }
    } else {
      const currentTestId = req.query.testId;
      if (isAlreadyUsed === "1") {
        where.testQuestions = { some: {} };
      } else if (isAlreadyUsed === "0") {
        if (currentTestId) {
          where.testQuestions = { none: { testId: { not: currentTestId } } };
        } else {
          where.testQuestions = { none: {} };
        }
      }
    }
  }
  if (questionUsage) {
    if (questionUsage === "available") {
      if (requestExamIds.length > 0) {
        where.mockTestQuestions = {
          none: {
            mockTest: { examId: { in: requestExamIds } }
          }
        };
      } else {
        where.mockTestQuestions = { none: {} };
      }
    } else if (questionUsage === "used") {
      if (requestExamIds.length > 0) {
        where.mockTestQuestions = {
          some: {
            mockTest: { examId: { in: requestExamIds } }
          }
        };
      } else {
        where.mockTestQuestions = { some: {} };
      }
    }
  }
  const [total, questions] = await Promise.all([
    prisma$1.questionBank.count({ where }),
    prisma$1.questionBank.findMany({
      where,
      include: {
        createdBy: { select: { firstName: true, lastName: true } },
        practiceTests: {
          select: { id: true, examId: true },
          where: requestExamIds.length > 0 ? { examId: { in: requestExamIds } } : void 0
        },
        testQuestions: {
          select: { testId: true }
        },
        // Include mock test usage for the given exam
        mockTestQuestions: {
          select: {
            mockTestId: true,
            mockTest: { select: { id: true, title: true, examId: true } }
          },
          where: requestExamIds.length > 0 ? { mockTest: { examId: { in: requestExamIds } } } : void 0
        },
        _count: {
          select: { practiceTests: true, testQuestions: true }
        }
      },
      orderBy: { createdAt: "desc" },
      skip,
      take: limitNum
    })
  ]);
  const testIdsToFetch = /* @__PURE__ */ new Set();
  questions.forEach((q) => {
    q.testQuestions?.forEach((tq) => testIdsToFetch.add(tq.testId));
  });
  const testTitles = await prisma$1.test.findMany({
    where: { id: { in: Array.from(testIdsToFetch) } },
    select: { id: true, title: true }
  });
  const testMap = new Map(testTitles.map((t) => [t.id, t.title]));
  const currentMockTestId = mockTestId;
  const updatedQuestions = questions.map((question) => {
    const testsInSpecificExam = question.practiceTests.map((t) => t.id);
    const totalTestsCount = question._count.practiceTests;
    let isAlreadyUsedVal = 0;
    if (testsInSpecificExam.length > 0) {
      isAlreadyUsedVal = 1;
    } else if (totalTestsCount === 0) {
      isAlreadyUsedVal = 2;
    }
    const usedInTests = question.testQuestions?.reduce((acc, tq) => {
      const title = testMap.get(tq.testId);
      if (title) {
        acc.push({ id: tq.testId, title });
      }
      return acc;
    }, []) || [];
    const usedInMockTests = (question.mockTestQuestions || []).reduce((acc, mtq) => {
      if (mtq.mockTest && (!currentMockTestId || mtq.mockTest.id !== currentMockTestId)) {
        acc.push({ id: mtq.mockTest.id, title: mtq.mockTest.title });
      }
      return acc;
    }, []);
    return {
      ...question,
      createdAt: toIST(question.createdAt),
      updatedAt: toIST(question.updatedAt),
      tests: testsInSpecificExam,
      usedInTests,
      usedInMockTests,
      questionImage: question.questionImage ? `${getHost()}${question.questionImage}` : "",
      options: question.options.map((option) => ({
        ...option,
        optionImage: option.optionImage ? `${getHost()}${option.optionImage}` : ""
      })),
      explanationImage: question.explanationImage ? `${getHost()}${question.explanationImage}` : "",
      isAlreadyUsed: isAlreadyUsedVal,
      createdBy: question.createdBy ? `${question.createdBy.firstName} ${question.createdBy.lastName}` : "Unknown"
    };
  });
  res.status(200).json({
    message: "Successfully fetched questions",
    data: updatedQuestions,
    meta: {
      total,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(total / limitNum)
    }
  });
});
const updateQuestion = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const updates = req.body;
  const filesMap = req.files;
  const user = req.user;
  const existingQuestion = await prisma$1.questionBank.findFirst({
    where: { id, institutionId: user.institutionId }
  });
  if (!existingQuestion) {
    res.status(404).json({ message: "Question not found" });
    return;
  }
  if (updates.questionText && updates.questionText.trim() !== "") {
    const finalSubjectId = updates.subjectId || existingQuestion.subjectId;
    const existingQuestions = await prisma$1.questionBank.findMany({
      where: {
        id: { not: id },
        questionText: updates.questionText.trim(),
        subjectId: finalSubjectId,
        institutionId: user.institutionId
      }
    });
    const duplicate = existingQuestions.find((q) => {
      const sameQuestion = q.questionText.toLowerCase().trim() === updates.questionText.toLowerCase().trim();
      if (!sameQuestion || !Array.isArray(q.options)) {
        return false;
      }
      const dbOptions = q.options.map((opt) => opt.option?.toLowerCase().trim()).sort();
      const uploadedOptions = updates.options.map((opt) => opt?.toLowerCase().trim()).sort();
      return JSON.stringify(dbOptions) === JSON.stringify(uploadedOptions);
    });
    if (duplicate) {
      const topicMsg = duplicate.topic ? ` under the topic "${duplicate.topic}"` : "";
      res.status(400).json({
        success: false,
        message: `A Question with same options already exists in the selected subject${topicMsg}.`
      });
      return;
    }
  }
  const fileDelete = (filePathUrl) => {
    if (!filePathUrl) return;
    const oldFileName = path.basename(filePathUrl || "");
    if (!oldFileName) return;
    const oldFilePath = path.join(process.cwd(), "uploads", oldFileName);
    if (fs.existsSync(oldFilePath)) {
      try {
        fs.unlinkSync(oldFilePath);
      } catch (e) {
        console.error("Error deleting file:", e);
      }
    }
  };
  if (filesMap?.questionImage?.[0]) {
    if (existingQuestion.questionImage) fileDelete(existingQuestion.questionImage);
    updates.questionImage = toPublicPath(filesMap.questionImage[0].path);
  } else if (updates.questionImage === "" || updates.questionImage === "null" || updates.questionImage === null) {
    if (existingQuestion.questionImage) fileDelete(existingQuestion.questionImage);
    updates.questionImage = null;
  }
  if (filesMap?.explanationImage?.[0]) {
    if (existingQuestion.explanationImage) fileDelete(existingQuestion.explanationImage);
    updates.explanationImage = toPublicPath(filesMap.explanationImage[0].path);
  } else if (updates.explanationImage === "" || updates.explanationImage === "null" || updates.explanationImage === null) {
    if (existingQuestion.explanationImage) fileDelete(existingQuestion.explanationImage);
    updates.explanationImage = null;
  }
  if (updates.options) {
    const rawOptions = Array.isArray(updates.options) ? updates.options : [updates.options];
    const optionImageFiles = filesMap?.optionImage || [];
    let optionImageIndexes = [];
    if (req.body.optionImageIndexes) {
      optionImageIndexes = Array.isArray(req.body.optionImageIndexes) ? req.body.optionImageIndexes.map((i) => parseInt(i, 10)) : [parseInt(req.body.optionImageIndexes, 10)];
    }
    const uploadedImageMap = {};
    if (optionImageIndexes.length === optionImageFiles.length) {
      optionImageFiles.forEach((file, i) => {
        uploadedImageMap[optionImageIndexes[i]] = file;
      });
    }
    let existingOptionImages = {};
    if (req.body.existingOptionImages) {
      try {
        existingOptionImages = JSON.parse(req.body.existingOptionImages);
      } catch (e) {
      }
    }
    const newOptions = rawOptions.map((text, index) => {
      const uploadedFile = uploadedImageMap[index];
      const existingUrl = existingOptionImages[index];
      return {
        option: text || "",
        optionImage: uploadedFile ? toPublicPath(uploadedFile.path) : existingUrl || null
      };
    });
    const oldOptions = existingQuestion.options;
    if (oldOptions) {
      const newImageUrls = new Set(newOptions.map((o) => o.optionImage).filter(Boolean));
      oldOptions.forEach((oldOpt) => {
        if (oldOpt.optionImage && !newImageUrls.has(oldOpt.optionImage)) fileDelete(oldOpt.optionImage);
      });
    }
    updates.options = newOptions;
  }
  if (updates.exams) {
    const examArray = Array.isArray(updates.exams) ? updates.exams : [updates.exams];
    updates.exams = {
      set: examArray.map((eid) => ({ id: eid }))
    };
  }
  const allowedFields = [
    "questionText",
    "questionImage",
    "explanation",
    "explanationImage",
    "options",
    "exams",
    "topic",
    "difficulty",
    "language",
    "correctAnswer",
    "marks"
  ];
  const newUpdate = {};
  for (const field of allowedFields) {
    if (updates[field] !== void 0) {
      newUpdate[field] = updates[field];
    }
  }
  if (updates.correctAnswer !== void 0) newUpdate.correctAnswer = parseInt(updates.correctAnswer);
  if (updates.marks !== void 0) newUpdate.marks = parseFloat(updates.marks);
  const { subject: sub, ..._rest } = updates;
  if (sub) newUpdate.subjectId = sub;
  const updatedQuestion = await prisma$1.questionBank.update({
    where: { id },
    data: newUpdate
  });
  res.status(200).json({
    message: "Question updated successfully",
    data: {
      ...updatedQuestion,
      createdAt: toIST(updatedQuestion.createdAt),
      updatedAt: toIST(updatedQuestion.updatedAt),
      questionImage: updatedQuestion.questionImage ? `${getHost()}${updatedQuestion.questionImage}` : "",
      options: updatedQuestion.options.map((option) => ({
        ...option,
        optionImage: option.optionImage ? `${getHost()}${option.optionImage}` : ""
      })),
      explanationImage: updatedQuestion.explanationImage ? `${getHost()}${updatedQuestion.explanationImage}` : ""
    }
  });
});
const deleteQuestion = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const question = await prisma$1.questionBank.findFirst({
    where: { id, institutionId: user.institutionId },
    include: {
      practiceTests: {
        select: { id: true }
      }
    }
  });
  if (!question) {
    res.status(404).json({ message: "Question not found" });
    return;
  }
  if (question.practiceTests.length > 0) {
    res.status(400).json({
      message: "Question cannot be deleted as it is used in practice tests"
    });
    return;
  }
  await prisma$1.questionBank.delete({ where: { id } });
  res.status(201).json({
    message: "Question deleted successfully",
    deletedId: id
  });
});
const getRandomQuestion = asyncHandler(async (req, res) => {
  const user = req.user;
  const examId = typeof req.query.examId === "string" ? req.query.examId : typeof req.query.exam_id === "string" ? req.query.exam_id : void 0;
  const exam = typeof req.query.exam === "string" ? req.query.exam : void 0;
  const subjectId = typeof req.query.subjectId === "string" ? req.query.subjectId : typeof req.query.subject_id === "string" ? req.query.subject_id : void 0;
  const subject = typeof req.query.subject === "string" ? req.query.subject : void 0;
  const practiceTestId = typeof req.query.practiceTestId === "string" ? req.query.practiceTestId : typeof req.query.practice_test_id === "string" ? req.query.practice_test_id : void 0;
  const practiceTest = typeof req.query.practiceTest === "string" ? req.query.practiceTest : void 0;
  let studentLanguage;
  let studentExamId;
  if (user.studentInstitutionId) {
    const studentInstitution = await prisma$1.studentInstitution.findUnique({
      where: { id: user.studentInstitutionId },
      select: { language: true, examsId: true }
    });
    studentLanguage = studentInstitution?.language ?? void 0;
    studentExamId = studentInstitution?.examsId ?? void 0;
  }
  const currentExamId = examId || exam || studentExamId;
  const currentSubjectId = subjectId || subject;
  const currentPracticeTestId = practiceTestId || practiceTest;
  const where = {
    institutionId: user.institutionId,
    questionText: { not: null },
    AND: [
      {
        OR: [
          { questionImage: null },
          { questionImage: "" }
        ]
      }
    ]
  };
  if (currentSubjectId) {
    where.subjectId = currentSubjectId;
  }
  const ptFilter = currentPracticeTestId ? { id: currentPracticeTestId, publish: true } : currentExamId ? { examId: currentExamId, publish: true } : { publish: true };
  where.AND.push({
    OR: [
      {
        practiceTests: {
          some: ptFilter
        }
      },
      {
        testQuestions: {
          some: {
            test: {
              practiceTests: {
                some: ptFilter
              }
            }
          }
        }
      }
    ]
  });
  if (studentLanguage) {
    where.language = { equals: studentLanguage };
  }
  const candidates = await prisma$1.questionBank.findMany({
    where,
    select: {
      id: true,
      questionText: true
    }
  });
  const validCandidates = candidates.filter((item) => {
    return typeof item.questionText === "string" && item.questionText.length <= 100;
  });
  if (validCandidates.length === 0) {
    res.status(404).json({ message: "No matching question found" });
    return;
  }
  const randomQuestionId = validCandidates[Math.floor(Math.random() * validCandidates.length)].id;
  const question = await prisma$1.questionBank.findUnique({
    where: { id: randomQuestionId }
  });
  if (!question) {
    res.status(404).json({ message: "No matching question found" });
    return;
  }
  const rawOptions = question.options;
  res.status(200).json({
    message: "Random question fetched successfully",
    data: {
      ...question,
      options: typeof rawOptions === "string" ? JSON.parse(rawOptions) : rawOptions
    }
  });
});
const previewBulkUploadByExcel = asyncHandler(async (req, res) => {
  try {
    const user = req.user;
    if (!req.file) {
      res.status(400).json({ message: "No file uploaded" });
      return;
    }
    const filePath = req.file.path;
    const workbook = XLSX.readFile(filePath);
    const data = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]);
    fs.unlinkSync(filePath);
    if (!data || data.length === 0) {
      res.status(400).json({ message: "Excel file is empty" });
      return;
    }
    const normalize2 = (value) => String(value)?.trim()?.toLowerCase() || "";
    const where = {
      institutionId: user.institutionId
    };
    if (user.role === "STAFF") {
      where.staffs = { some: { userId: user.id } };
    }
    const subjects = await prisma$1.subject.findMany({
      where,
      select: { id: true, subjectName: true, topics: true }
    });
    const existingQuestions = (await prisma$1.questionBank.findMany({
      where: {
        institutionId: user.institutionId,
        OR: data.filter((row) => row.questionText).map((row) => {
          const subjectObj = subjects.find((s) => s.subjectName?.trim().toLowerCase() === (row.subject || "").toString()?.trim().toLowerCase());
          return {
            questionText: row.questionText?.trim(),
            ...subjectObj ? { subjectId: subjectObj.id } : {}
          };
        })
      },
      select: {
        questionText: true,
        topic: true,
        options: true,
        subject: { select: { id: true, subjectName: true } }
      }
    })).map((question) => ({
      ...question,
      questionText: question.questionText?.trim().toLowerCase(),
      topic: question.topic?.trim().toLowerCase(),
      subjectName: question.subject?.subjectName?.trim().toLowerCase(),
      subjectId: question.subject?.id
    }));
    const seenKeys = /* @__PURE__ */ new Set();
    const previewData = data.map((row, i) => {
      const errors = [];
      if (!row.questionText) errors.push("Missing questionText.");
      if (!row.subject) errors.push("Missing subject name.");
      if (row.correctAnswer === void 0) errors.push("Missing correctAnswer.");
      const subjectObj = subjects.find((s) => s.subjectName?.trim().toLowerCase() === (row.subject || "").toString()?.trim().toLowerCase());
      if (row.subject && !subjectObj) errors.push(`Subject '${row.subject}' not found.`);
      const topicObj = Array.isArray(subjectObj?.topics) ? subjectObj?.topics.some((t) => t?.trim().toLowerCase() === (row.topic || "").toString()?.trim().toLowerCase()) : null;
      if (row.topic && !topicObj) errors.push(`Topic '${row.topic}' not found.`);
      const options = [];
      for (let j = 1; j <= 4; j++) {
        const opt = row[`option${j}`];
        if (opt) options.push(opt);
      }
      if (options.length < 2) errors.push("At least 2 options required.");
      const key = [
        normalize2(row.subject),
        normalize2(row.questionText),
        options.map(normalize2).sort().join("-")
      ].join("-");
      if (seenKeys.has(key)) {
        errors.push(`Duplicate question in the file.`);
      } else {
        seenKeys.add(key);
      }
      const correctAnswerIndex = parseInt(row.correctAnswer) - 1;
      if (isNaN(correctAnswerIndex) || correctAnswerIndex < 0 || correctAnswerIndex >= options.length) errors.push("Invalid correctAnswer index.");
      const uploadedOptions = options.map((opt) => String(opt)?.trim()?.toLowerCase()).sort();
      const duplicate = existingQuestions.find((q) => {
        const sameQuestion = q.questionText.toLowerCase().trim() === row.questionText?.trim().toLowerCase();
        if (!sameQuestion || !Array.isArray(q.options)) {
          return false;
        }
        const dbOptions = q.options.map((opt) => String(opt.option)?.trim()?.toLowerCase()).sort();
        return JSON.stringify(dbOptions) === JSON.stringify(uploadedOptions);
      });
      if (duplicate) {
        const topicMsg = duplicate.topic ? ` under the topic "${duplicate.topic}"` : "";
        errors.push(`A Question with same options already exists in the selected subject${topicMsg}.`);
      }
      return {
        rowNumber: i + 1,
        questionText: row.questionText || "",
        subjectName: row.subject || "",
        subjectId: subjectObj?.id || null,
        topic: row.topic || "General",
        language: row.language || "English",
        options,
        correctAnswer: row.correctAnswer,
        marks: row.marks || 1,
        difficulty: row.difficulty || "Medium",
        explanation: row.explanation || "",
        isValid: errors.length === 0,
        errors
      };
    });
    res.status(200).json({
      message: "Preview data generated",
      data: {
        totalRows: data.length,
        validCount: previewData.filter((d) => d.isValid).length,
        invalidCount: previewData.filter((d) => !d.isValid).length,
        questions: previewData
      }
    });
  } catch (error) {
    if (req.file?.path) setTimeout(() => fs.unlinkSync(req.file.path), 100);
    res.status(500).json({ message: error.message || "Preview failed" });
  }
});
const confirmBulkUploadByExcel = asyncHandler(async (req, res) => {
  const { questions, batchInfo } = req.body;
  if (!questions?.length) {
    res.status(400).json({ message: "No questions provided" });
    return;
  }
  const user = req.user;
  const createdById = user.id;
  const results = await prisma$1.$transaction(async (tx) => {
    let createdCount = 0;
    for (const q of questions) {
      const finalSubject = batchInfo?.subject ? batchInfo.subject : q.subjectId;
      if (q.questionText && q.questionText.trim() !== "") {
        const existingQuestions = await tx.questionBank.findMany({
          where: {
            questionText: q.questionText.trim(),
            subjectId: finalSubject,
            institutionId: user.institutionId
          }
        });
        const uploadedOptions = q.options.map((opt) => String(opt)?.trim()?.toLowerCase()).sort();
        const duplicate = existingQuestions.find((existingQ) => {
          if (!Array.isArray(existingQ.options)) return false;
          const dbOptions = existingQ.options.map((opt) => String(opt.option)?.trim()?.toLowerCase()).sort();
          return JSON.stringify(dbOptions) === JSON.stringify(uploadedOptions);
        });
        if (duplicate) continue;
      }
      const options = q.options.map((opt) => ({ option: opt, optionImage: null }));
      batchInfo?.exams?.length > 0 ? batchInfo.exams : q.examIds || [];
      await tx.questionBank.create({
        data: {
          createdById,
          subjectId: finalSubject,
          topic: q.topic,
          language: q.language,
          questionText: q.questionText,
          options,
          correctAnswer: parseInt(q.correctAnswer) - 1,
          marks: parseFloat(q.marks || "1"),
          difficulty: q.difficulty || "Medium",
          explanation: q.explanation,
          institutionId: user.institutionId
        }
      });
      createdCount++;
    }
    return createdCount;
  });
  res.status(201).json({ message: `${results} Questions created successfully`, count: results });
});
const downloadTemplateByExcel = asyncHandler(async (req, res) => {
  const templateData = [{
    questionText: "Which is the capital of India?",
    option1: "Mumbai",
    option2: "New Delhi",
    option3: "Chennai",
    option4: "Kolkata",
    correctAnswer: 2,
    subject: "History",
    topic: "General",
    language: "English",
    marks: 1,
    difficulty: "Easy",
    explanation: "New Delhi is the capital of India."
  }];
  const worksheet = XLSX.utils.json_to_sheet(templateData);
  const workbook = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(workbook, worksheet, "Questions");
  const buffer = XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
  res.setHeader("Content-Disposition", "attachment; filename=question_bank_template.xlsx");
  res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  res.send(buffer);
});
const getQuestionById = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const question = await prisma$1.questionBank.findFirst({
    where: {
      id,
      OR: [
        { institutionId: user.institutionId },
        { institutionId: null }
      ]
    },
    include: {
      subject: true
    }
  });
  if (!question) {
    res.status(404).json({ success: false, message: "Question not found" });
    return;
  }
  res.status(200).json({ success: true, data: question });
});
const router$n = express.Router();
router$n.get("/template", protect, downloadTemplateByExcel);
router$n.post("/preview-bulk", protect, excelUpload.single("file"), previewBulkUploadByExcel);
router$n.post("/confirm-bulk", protect, confirmBulkUploadByExcel);
router$n.route("/").post(
  protect,
  imageUpload("question_bank").fields([
    { name: "questionImage", maxCount: 1 },
    { name: "optionImage", maxCount: 4 },
    { name: "explanationImage", maxCount: 1 }
  ]),
  optimizeImage,
  createQuestion
).get(protect, listQuestions);
router$n.route("/:id").get(protect, getQuestionById).put(
  protect,
  imageUpload("question_bank").fields([
    { name: "questionImage", maxCount: 1 },
    { name: "optionImage", maxCount: 4 },
    { name: "explanationImage", maxCount: 1 }
  ]),
  optimizeImage,
  updateQuestion
).delete(protect, deleteQuestion);
router$n.route("/random").get(protect, getRandomQuestion);
const submitExam = asyncHandler(async (req, res) => {
  const user = req.user;
  user.id;
  const institutionId = user.institutionId;
  const studentInstitutionId = user.studentInstitutionId;
  let { testId, testType, answers } = req.body;
  if (!testType) {
    testType = "PracticeTest";
  }
  if (!testId || !answers) {
    res.status(400).json({ message: "testId & answers are required" });
    return;
  }
  let test = null;
  let questions = [];
  if (testType === "OldQuestionPaper") {
    test = await prisma$1.oldQuestionPaper.findUnique({
      where: { id: testId, institutionId },
      include: {
        questions: {
          orderBy: {
            createdAt: "asc"
          }
        }
      }
    });
  } else {
    test = await prisma$1.practiceTest.findUnique({
      where: { id: testId, institutionId },
      include: {
        questions: {
          orderBy: {
            createdAt: "asc"
          }
        }
      }
    });
  }
  if (!test) {
    res.status(404).json({ message: "Test not found" });
    return;
  }
  if (testType === "PracticeTest" && test.testId) {
    questions = await prisma$1.questionBank.findMany({
      where: {
        testQuestions: { some: { testId: test.testId } }
      },
      orderBy: {
        createdAt: "asc"
      }
    });
  } else {
    questions = test.questions;
  }
  let totalMarks = 0;
  let obtainedMarks = 0;
  let correctCount = 0;
  let wrongCount = 0;
  const validationDetails = [];
  questions.forEach((q) => {
    const questionId = q.id;
    const selectedAnswerIndex = answers[questionId];
    const correctIndex = q.correctAnswer;
    const mark = q.marks || 1;
    totalMarks += mark;
    const isCorrect = selectedAnswerIndex != null && selectedAnswerIndex == correctIndex;
    if (isCorrect) {
      correctCount++;
      obtainedMarks += mark;
    } else {
      wrongCount++;
    }
    const optionsArray = Array.isArray(q.options) ? q.options : [];
    validationDetails.push({
      questionId,
      questionText: q.questionText,
      questionImage: q.questionImage ? `${getHost()}${q.questionImage}` : "",
      options: optionsArray.map((option) => ({
        option: String(option.option) ?? "",
        optionImage: option.optionImage ? `${getHost()}${option.optionImage}` : ""
      })),
      correctAnswerIndex: correctIndex,
      difficulty: q.difficulty,
      selectedAnswerIndex: selectedAnswerIndex ?? null,
      isCorrect,
      explanation: q.explanation ?? "",
      explanationImage: q.explanationImage ? `${getHost()}${q.explanationImage}` : ""
    });
  });
  const studentInstitution = await prisma$1.studentInstitution.findFirst({
    where: { id: studentInstitutionId }
  });
  if (!studentInstitution) {
    res.status(404).json({ message: "Student record not found" });
    return;
  }
  const computedPercentage = totalMarks > 0 ? obtainedMarks / totalMarks * 100 : 0;
  const resultData = {
    studentId: studentInstitution.studentId,
    testType,
    totalQuestions: questions.length,
    totalMarks,
    obtainedMarks,
    percentage: parseFloat(computedPercentage.toFixed(2)),
    answers: validationDetails.map((q) => ({
      questionId: q.questionId,
      selectedAnswerIndex: q.selectedAnswerIndex,
      isCorrect: q.isCorrect
    })),
    institutionId,
    studentInstitutionId
  };
  if (testType === "OldQuestionPaper") {
    resultData.oldQuestionPaperId = testId;
  } else {
    resultData.practiceTestId = testId;
  }
  await prisma$1.examResult.create({
    data: resultData
  });
  if (testType === "PracticeTest" && test.marks !== totalMarks) {
    await prisma$1.practiceTest.update({
      where: { id: testId },
      data: { marks: totalMarks }
    });
  }
  res.status(200).json({
    message: "Test submitted successfully",
    result: {
      totalMarks,
      obtainedMarks,
      correctCount,
      wrongCount,
      percentage: computedPercentage.toFixed(2),
      validationDetails
    }
  });
});
const getExamResults = asyncHandler(async (req, res) => {
  const user = req.user;
  const search = req.query.search?.trim();
  const examId = req.query.examId;
  const examType = req.query.examType;
  const subjectId = req.query.subjectId;
  const testTitle = req.query.testTitle?.trim();
  const startDate = req.query.startDate;
  const endDate = req.query.endDate;
  const sortBy = req.query.sortBy || "date";
  const sortOrder = req.query.sortOrder?.toLowerCase() === "asc" ? "asc" : "desc";
  const page = parseInt(req.query.page || "1");
  const limit = parseInt(req.query.limit || "10");
  const skip = (page - 1) * limit;
  let studentSearchCondition = void 0;
  if (search) {
    const searchWords = search.split(/\s+/).filter(Boolean);
    studentSearchCondition = {
      user: {
        OR: [
          { firstName: { contains: search } },
          { lastName: { contains: search } },
          { email: { contains: search } },
          ...searchWords.length > 1 ? [{
            AND: searchWords.map((word) => ({
              OR: [
                { firstName: { contains: word } },
                { lastName: { contains: word } }
              ]
            }))
          }] : []
        ]
      }
    };
  }
  let dateCondition = void 0;
  if (startDate || endDate) {
    dateCondition = {};
    if (startDate) dateCondition.gte = new Date(startDate);
    if (endDate) {
      const endOfDay = new Date(endDate);
      endOfDay.setHours(23, 59, 59, 999);
      dateCondition.lte = endOfDay;
    }
  }
  let examResultsRaw = [];
  let mockResultsRaw = [];
  let examWhereClause = { institutionId: user.institutionId };
  if (studentSearchCondition) {
    examWhereClause.studentInstitution = { student: studentSearchCondition };
  }
  if (dateCondition) examWhereClause.createdAt = dateCondition;
  if (examType === "PRACTICE") examWhereClause.practiceTestId = { not: null };
  if (examType === "OQP") examWhereClause.oldQuestionPaperId = { not: null };
  if (subjectId) {
    examWhereClause.practiceTest = {
      test: {
        subjectId
      }
    };
  }
  const examOrConditions = [];
  if (examId) {
    examOrConditions.push({ practiceTest: { examId } }, { oldQuestionPaper: { examId } });
  }
  if (testTitle) {
    examOrConditions.push(
      { practiceTest: { title: { contains: testTitle } } },
      { oldQuestionPaper: { title: { contains: testTitle } } }
    );
  }
  if (examOrConditions.length > 0) examWhereClause.OR = examOrConditions;
  let mockWhereClause = { studentInstitution: { institutionId: user.institutionId } };
  if (studentSearchCondition) {
    mockWhereClause.studentInstitution.student = studentSearchCondition;
  }
  if (dateCondition) mockWhereClause.createdAt = dateCondition;
  if (examId) mockWhereClause.mockTest = { examId };
  if (testTitle) {
    mockWhereClause.mockTest = {
      ...mockWhereClause.mockTest,
      title: { contains: testTitle }
    };
  }
  const shouldFetchExams = !examType || examType === "PRACTICE" || examType === "OQP";
  const shouldFetchMocks = (!examType || examType === "MOCK") && !subjectId;
  await Promise.all([
    shouldFetchExams ? (async () => {
      examResultsRaw = await prisma$1.examResult.findMany({
        where: examWhereClause,
        include: {
          studentInstitution: {
            select: {
              student: { include: { user: { select: { firstName: true, lastName: true, email: true } } } }
            }
          },
          practiceTest: {
            select: {
              title: true,
              exam: true,
              test: {
                select: {
                  subject: { select: { id: true, subjectName: true } }
                }
              }
            }
          },
          oldQuestionPaper: { select: { title: true, exam: true } }
        }
      });
    })() : Promise.resolve(),
    shouldFetchMocks ? (async () => {
      mockResultsRaw = await prisma$1.mockTestResult.findMany({
        where: mockWhereClause,
        include: {
          studentInstitution: {
            select: {
              student: { include: { user: { select: { firstName: true, lastName: true, email: true } } } }
            }
          },
          mockTest: { select: { title: true, exam: true, questionCount: true } }
        }
      });
    })() : Promise.resolve()
  ]);
  const formattedExams = examResultsRaw.map((res2) => {
    const student = res2.studentInstitution?.student;
    const totalMarks = res2.totalMarks || 0;
    const obtainedMarks = res2.obtainedMarks || 0;
    const percentage = totalMarks > 0 ? parseFloat((obtainedMarks / totalMarks * 100).toFixed(2)) : 0;
    return {
      id: res2.id,
      testTitle: res2.practiceTest?.title || res2.oldQuestionPaper?.title || "Deleted Test",
      studentName: student?.user ? `${student.user.firstName} ${student.user.lastName}`.trim() : "Deleted Student",
      email: student?.user?.email || "",
      examName: res2.practiceTest?.exam?.examName || res2.oldQuestionPaper?.exam?.examName || "Deleted Exam",
      subjectName: res2.practiceTest?.test?.subject?.subjectName || null,
      examType: res2.practiceTest ? "PRACTICE" : "OQP",
      createdAt: res2.createdAt,
      totalQuestions: res2.totalQuestions,
      obtainedMarks,
      totalMarks,
      percentage
    };
  });
  const formattedMocks = mockResultsRaw.map((res2) => {
    const student = res2.studentInstitution?.student;
    const totalMarks = res2.totalMarks || 0;
    const obtainedMarks = res2.obtainedMarks || 0;
    const percentage = totalMarks > 0 ? parseFloat((obtainedMarks / totalMarks * 100).toFixed(2)) : 0;
    return {
      id: res2.id,
      testTitle: res2.mockTest?.title || "Deleted Test",
      studentName: student?.user ? `${student.user.firstName} ${student.user.lastName}`.trim() : "Deleted Student",
      email: student?.user?.email || "",
      examName: res2.mockTest?.exam?.examName || "Deleted Exam",
      subjectName: null,
      examType: "MOCK",
      createdAt: res2.createdAt,
      totalQuestions: res2.mockTest?.questionCount || 0,
      obtainedMarks,
      totalMarks,
      percentage
    };
  });
  const combinedResults = [...formattedExams, ...formattedMocks];
  combinedResults.sort((a, b) => {
    let valueA;
    let valueB;
    switch (sortBy) {
      case "title":
        valueA = a.testTitle.toLowerCase();
        valueB = b.testTitle.toLowerCase();
        return sortOrder === "asc" ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
      case "score":
      case "percentage":
        valueA = a.percentage;
        valueB = b.percentage;
        break;
      case "date":
      default:
        valueA = a.createdAt.getTime();
        valueB = b.createdAt.getTime();
        break;
    }
    if (valueA < valueB) return sortOrder === "asc" ? -1 : 1;
    if (valueA > valueB) return sortOrder === "asc" ? 1 : -1;
    return 0;
  });
  const total = combinedResults.length;
  const paginatedResults = combinedResults.slice(skip, skip + limit).map((res2) => ({
    ...res2,
    createdAt: toIST(res2.createdAt),
    percentage: `${res2.percentage.toFixed(2)}%`
  }));
  res.status(200).json({
    message: "Test results fetched successfully",
    data: paginatedResults,
    meta: {
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit)
    }
  });
});
const getLastTest = asyncHandler(async (req, res) => {
  const user = req.user;
  const studentInstitutionId = user.studentInstitutionId;
  const { type: reqType } = req.query;
  const testType = reqType === "PYQ" ? "PYQ" : "PRACTICE";
  const responseData = {
    testTitle: "",
    createdAt: "",
    obtainedMarks: 0,
    totalMarks: 0,
    correctCount: 0,
    wrongCount: 0,
    skippedCount: 0,
    percentage: "0.00",
    validationDetails: []
  };
  const student = await prisma$1.student.findFirst({
    where: {
      userId: user.id
    }
  });
  if (!student) {
    res.status(404).json({
      message: "Student not found"
    });
    return;
  }
  const studentInstitution = studentInstitutionId ? await prisma$1.studentInstitution.findUnique({
    where: { id: studentInstitutionId },
    select: { examsId: true, language: true, student: { select: { language: true } } }
  }) : null;
  const motherTongue = studentInstitution?.student?.language || studentInstitution?.language || "";
  const filterLanguage = studentInstitution?.student?.language ? studentInstitution?.language || "English" : "English";
  const enrolledExamId = studentInstitution?.examsId ?? null;
  let questions = [];
  let testAnswers = [];
  if (testType === "PYQ") {
    const oldQuestionPaperResult = await prisma$1.examResult.findFirst({
      where: {
        testType: "OldQuestionPaper",
        studentInstitutionId,
        ...enrolledExamId ? {
          oldQuestionPaper: {
            examId: enrolledExamId
          }
        } : {}
      },
      include: {
        oldQuestionPaper: {
          include: {
            questions: true
          }
        }
      },
      orderBy: {
        createdAt: "desc"
      }
    });
    if (!oldQuestionPaperResult) {
      res.status(404).json({
        message: "No PYQ test found for this exam"
      });
      return;
    }
    const oldQuestionPaperTitle = oldQuestionPaperResult.oldQuestionPaper?.title || "";
    const oldQuestionPaperYear = oldQuestionPaperResult.oldQuestionPaper?.year;
    responseData.testTitle = oldQuestionPaperYear ? `${oldQuestionPaperTitle} (${oldQuestionPaperYear})` : oldQuestionPaperTitle;
    responseData.year = oldQuestionPaperYear || "";
    responseData.createdAt = oldQuestionPaperResult.createdAt;
    questions = oldQuestionPaperResult.oldQuestionPaper?.questions || [];
    testAnswers = Array.isArray(oldQuestionPaperResult.answers) ? oldQuestionPaperResult.answers : [];
  } else {
    const practiceTestFilter = {};
    if (enrolledExamId) practiceTestFilter.examId = enrolledExamId;
    {
      practiceTestFilter.OR = [];
      {
        practiceTestFilter.OR.push(
          { language: filterLanguage, questions: { some: { subject: { language: null } } } },
          { test: { language: filterLanguage, subject: { language: null } } }
        );
      }
      if (motherTongue) {
        practiceTestFilter.OR.push(
          { questions: { some: { subject: { language: { not: null, equals: motherTongue } } } } },
          { test: { subject: { language: { not: null, equals: motherTongue } } } }
        );
      }
    }
    const practiceTestResult = await prisma$1.examResult.findFirst({
      where: {
        studentInstitutionId,
        testType: "PracticeTest",
        practiceTest: practiceTestFilter
      },
      include: {
        practiceTest: {
          include: {
            questions: true,
            test: {
              include: {
                testQuestions: {
                  include: { question: true }
                }
              }
            }
          }
        }
      },
      orderBy: {
        createdAt: "desc"
      }
    });
    if (!practiceTestResult) {
      res.status(404).json({
        message: "No practice test found for this exam"
      });
      return;
    }
    responseData.testTitle = practiceTestResult.practiceTest?.title || "";
    responseData.createdAt = practiceTestResult.createdAt;
    questions = practiceTestResult.practiceTest?.questions || [];
    if (questions.length === 0 && practiceTestResult.practiceTest?.test?.testQuestions) {
      questions = practiceTestResult.practiceTest.test.testQuestions.map((tq) => tq.question);
    }
    testAnswers = Array.isArray(practiceTestResult.answers) ? practiceTestResult.answers : [];
  }
  questions.forEach((q) => {
    const questionId = q.id;
    const selectedAnswer = testAnswers.find((t) => t.questionId === questionId);
    const selectedAnswerIndex = selectedAnswer?.selectedAnswerIndex !== void 0 && selectedAnswer?.selectedAnswerIndex !== null ? Number(selectedAnswer.selectedAnswerIndex) : null;
    const correctIndex = q.correctAnswer;
    const mark = q.marks || 1;
    responseData.totalMarks += mark;
    let isCorrect = false;
    if (selectedAnswerIndex === null) {
      responseData.skippedCount++;
    } else if (selectedAnswerIndex === correctIndex) {
      isCorrect = true;
      responseData.correctCount++;
      responseData.obtainedMarks += mark;
    } else {
      responseData.wrongCount++;
    }
    const optionsArray = Array.isArray(q.options) ? q.options : [];
    responseData.validationDetails.push({
      questionId,
      questionText: q.questionText,
      questionImage: q.questionImage ? `${getHost()}${q.questionImage}` : "",
      options: optionsArray.map((option) => ({
        option: option?.option || "",
        optionImage: option?.optionImage ? `${getHost()}${option.optionImage}` : ""
      })),
      correctAnswerIndex: correctIndex,
      difficulty: q.difficulty,
      selectedAnswerIndex,
      isCorrect,
      explanation: q.explanation || "",
      explanationImage: q.explanationImage ? `${getHost()}${q.explanationImage}` : ""
    });
  });
  responseData.percentage = responseData.totalMarks > 0 ? (responseData.obtainedMarks / responseData.totalMarks * 100).toFixed(2) : "0.00";
  res.status(200).json({
    message: "Last test fetched successfully",
    result: responseData
  });
});
const scoreReportsPDF = asyncHandler(async (req, res) => {
  const user = req.user;
  let institutionNameFromRequest = null;
  try {
    if (user?.institutionId) {
      const inst = await prisma$1.institution.findUnique({
        where: { id: user.institutionId },
        select: { user: { select: { institutionName: true, firstName: true, email: true } } }
      });
      institutionNameFromRequest = inst?.user?.institutionName || inst?.user?.firstName || inst?.user?.email || null;
    }
  } catch (e) {
    institutionNameFromRequest = null;
  }
  const search = req.query.search?.trim();
  const examId = req.query.examId;
  const examType = req.query.examType;
  const subjectId = req.query.subjectId;
  const startDate = req.query.startDate;
  const endDate = req.query.endDate;
  const sortBy = req.query.sortBy || "date";
  const sortOrder = req.query.sortOrder?.toLowerCase() === "asc" ? "asc" : "desc";
  let whereClause = {
    institutionId: user.institutionId
  };
  if (search) {
    whereClause.student = {
      user: {
        OR: [
          { firstName: { contains: search } },
          { lastName: { contains: search } },
          { email: { contains: search } }
        ]
      }
    };
  }
  if (examType === "PRACTICE") {
    whereClause.practiceTestId = { not: null };
  } else if (examType === "OQP") {
    whereClause.oldQuestionPaperId = { not: null };
  }
  if (subjectId) {
    whereClause.practiceTest = {
      test: {
        subjectId
      }
    };
  }
  if (examId) {
    whereClause.OR = [
      { practiceTest: { examId } },
      { oldQuestionPaper: { examId } }
    ];
  }
  if (startDate || endDate) {
    whereClause.createdAt = {};
    if (startDate) whereClause.createdAt.gte = new Date(startDate);
    if (endDate) {
      const endOfDay = new Date(endDate);
      endOfDay.setHours(23, 59, 59, 999);
      whereClause.createdAt.lte = endOfDay;
    }
  }
  const shouldFetchExams = !examType || examType === "PRACTICE" || examType === "OQP";
  const shouldFetchMocks = (!examType || examType === "MOCK") && !subjectId;
  let examResults = [];
  let mockResults = [];
  await Promise.all([
    shouldFetchExams ? (async () => {
      examResults = await prisma$1.examResult.findMany({
        where: whereClause,
        include: {
          studentInstitution: {
            select: {
              student: { include: { user: { select: { firstName: true, lastName: true, email: true } } } }
            }
          },
          practiceTest: {
            select: {
              title: true,
              exam: true,
              test: {
                select: {
                  subject: {
                    select: {
                      subjectName: true
                    }
                  }
                }
              }
            }
          },
          oldQuestionPaper: { select: { title: true, exam: true } }
        }
      });
    })() : Promise.resolve(),
    shouldFetchMocks ? (async () => {
      let mockWhereClause = { studentInstitution: { institutionId: user.institutionId } };
      if (search) {
        mockWhereClause.studentInstitution = {
          student: whereClause.student
        };
      }
      if (examId) mockWhereClause.mockTest = { examId };
      if (startDate || endDate) mockWhereClause.createdAt = whereClause.createdAt;
      mockResults = await prisma$1.mockTestResult.findMany({
        where: mockWhereClause,
        include: {
          studentInstitution: {
            select: {
              student: { include: { user: { select: { firstName: true, lastName: true, email: true } } } }
            }
          },
          mockTest: { select: { title: true, exam: true } }
        }
      });
    })() : Promise.resolve()
  ]);
  const combinedResults = [...examResults, ...mockResults].sort((a, b) => {
    const totalA = a.totalMarks || 0;
    const totalB = b.totalMarks || 0;
    const pctA = totalA > 0 ? a.obtainedMarks / totalA * 100 : 0;
    const pctB = totalB > 0 ? b.obtainedMarks / totalB * 100 : 0;
    const titleA = (a.practiceTest?.title || a.oldQuestionPaper?.title || a.mockTest?.title || "").toLowerCase();
    const titleB = (b.practiceTest?.title || b.oldQuestionPaper?.title || b.mockTest?.title || "").toLowerCase();
    let valueA;
    let valueB;
    switch (sortBy) {
      case "title":
        return sortOrder === "asc" ? titleA.localeCompare(titleB) : titleB.localeCompare(titleA);
      case "score":
      case "percentage":
        valueA = pctA;
        valueB = pctB;
        break;
      case "date":
      default:
        valueA = new Date(a.createdAt).getTime();
        valueB = new Date(b.createdAt).getTime();
        break;
    }
    if (valueA < valueB) return sortOrder === "asc" ? -1 : 1;
    if (valueA > valueB) return sortOrder === "asc" ? 1 : -1;
    return 0;
  });
  const rows = combinedResults.map((result, index) => {
    const isMock = !!result.mockTest;
    const student = result.studentInstitution?.student;
    const studentName = student?.user ? `${student.user.firstName} ${student.user.lastName}`.trim() : "Deleted Student";
    const email = student?.user ? student.user.email : "";
    let testTitle = "Deleted Test";
    let examName = "Deleted Exam";
    let typeStr = "";
    if (isMock) {
      testTitle = result.mockTest?.title || "Deleted Mock";
      examName = result.mockTest?.exam?.examName || "Deleted Exam";
      typeStr = "MOCK TEST";
    } else {
      const practiceTest = result.practiceTest;
      const oldQuestionPaper = result.oldQuestionPaper;
      testTitle = practiceTest?.title || oldQuestionPaper?.title || "Deleted Test";
      examName = practiceTest?.exam?.examName || oldQuestionPaper?.exam?.examName || "Deleted Exam";
      if (practiceTest) {
        const subjectName = practiceTest.test?.subject?.subjectName;
        typeStr = subjectName ? `PRACTICE TEST
(${subjectName})` : "PRACTICE TEST";
      } else {
        typeStr = "PYQ";
      }
    }
    const dateStr = dayjs(result.createdAt).format("DD MMM YYYY, hh:mm A");
    const scoreStr = `${result.obtainedMarks} / ${result.totalMarks}`;
    const percentage = result.totalMarks > 0 ? (result.obtainedMarks / result.totalMarks * 100).toFixed(1) : "0.0";
    return [
      `${index + 1}`,
      `${studentName}
(${email})`,
      examName,
      typeStr,
      testTitle,
      dateStr,
      scoreStr,
      `${percentage}%`
    ];
  });
  const doc = new PDFDocument({ size: "A4", layout: "landscape", margin: 0 });
  res.setHeader("Content-Type", "application/pdf");
  res.setHeader("Content-Disposition", `attachment; filename=Reports_Scores_${dayjs().format("YYYY-MM-DD")}.pdf`);
  doc.pipe(res);
  try {
    doc.registerFont("NotoSansTamil", path.join(process.cwd(), "node_modules", "@expo-google-fonts", "noto-sans-tamil", "400Regular", "NotoSansTamil_400Regular.ttf"));
    doc.registerFont("NotoSansDevanagari", path.join(process.cwd(), "node_modules", "@expo-google-fonts", "noto-sans-devanagari", "400Regular", "NotoSansDevanagari_400Regular.ttf"));
    const getFontForText = (text, isBold = false) => {
      const hasTamil = /[\u0B80-\u0BFF]/.test(text);
      const hasHindi = /[\u0900-\u097F]/.test(text);
      if (hasTamil) return "NotoSansTamil";
      if (hasHindi) return "NotoSansDevanagari";
      return isBold ? "Helvetica-Bold" : "Helvetica";
    };
    const primary = "#1976d2";
    const white = "#ffffff";
    const altRow = "#f5f7fa";
    const textColor = "#1f2937";
    const headerTextMuted = "#cbd5e1";
    const PW = doc.page.width;
    const PH = doc.page.height;
    const margin = 24;
    const drawPageBg = () => {
      doc.save();
      doc.rect(0, 0, PW, PH).fill("#eef1f5");
      doc.roundedRect(margin - 6, margin - 6, PW - (margin - 6) * 2, PH - (margin - 6) * 2, 8).fill(white);
      doc.restore();
    };
    drawPageBg();
    doc.on("pageAdded", () => {
      drawPageBg();
    });
    const logoPath = path.resolve(process.cwd(), "public", "logo.png");
    const hasLogo = fs.existsSync(logoPath);
    const headerH = 66;
    doc.roundedRect(margin, margin, PW - margin * 2, headerH, 10).fill(primary);
    if (hasLogo) {
      doc.roundedRect(margin + 14, margin + 12, 40, 40, 6).fill(white);
      try {
        doc.image(logoPath, margin + 18, margin + 16, { fit: [32, 32], align: "center", valign: "center" });
      } catch (imgError) {
        console.error("Failed to render logo image:", imgError);
      }
    }
    const textStartX = hasLogo ? margin + 64 : margin + 16;
    doc.fillColor(white).font("Helvetica-Bold").fontSize(20).text("Exam Infra", textStartX, margin + 14);
    doc.font("Helvetica").fontSize(9).fillColor(headerTextMuted).text("Reports & Scores Summary", textStartX, margin + 38);
    if (institutionNameFromRequest) {
      doc.font(getFontForText(institutionNameFromRequest)).fontSize(9).fillColor(white).text(`Institution: ${institutionNameFromRequest}`, textStartX, margin + 52);
    }
    doc.font("Helvetica-Bold").fontSize(9).fillColor(white).text(`Generated: ${dayjs().format("DD MMM YYYY, hh:mm A")}`, PW - margin - 230, margin + 14, { width: 220, align: "right" });
    doc.font("Helvetica").fontSize(9).fillColor(headerTextMuted).text(`Total Records: ${rows.length}`, PW - margin - 230, margin + 36, { width: 220, align: "right" });
    const tableTop = margin + headerH + 14;
    const tableLeft = margin;
    const tableWidth = PW - margin * 2;
    const baseWidths = [35, 145, 105, 100, 120, 110, 55, 80];
    const baseTotal = baseWidths.reduce((a, b) => a + b, 0);
    const colWidths = baseWidths.map((w) => w / baseTotal * tableWidth);
    const headers = ["S.No", "Student Info", "Exam Name", "Test Type", "Test Title", "Date Completed", "Score", "Percentage"];
    const headerRowH = 26;
    const rowPad = 6;
    const cellFontSize = 8.5;
    doc.rect(tableLeft, tableTop, tableWidth, headerRowH).fill(primary);
    doc.font("Helvetica-Bold").fontSize(9).fillColor(white);
    let cx = tableLeft;
    headers.forEach((h, i) => {
      const textH = doc.heightOfString(h, { width: colWidths[i] - 10 });
      const vOffset = (headerRowH - textH) / 2;
      doc.text(h, cx + 5, tableTop + vOffset, { width: colWidths[i] - 10, align: "left" });
      cx += colWidths[i];
    });
    let rowY = tableTop + headerRowH;
    const drawTableHeader = (y) => {
      doc.rect(tableLeft, y, tableWidth, headerRowH).fill(primary);
      doc.font("Helvetica-Bold").fontSize(9).fillColor(white);
      let hx = tableLeft;
      headers.forEach((h, i) => {
        const textH = doc.heightOfString(h, { width: colWidths[i] - 10 });
        const vOffset = (headerRowH - textH) / 2;
        doc.text(h, hx + 5, y + vOffset, { width: colWidths[i] - 10, align: "left" });
        hx += colWidths[i];
      });
      return y + headerRowH;
    };
    rows.forEach((row, idx) => {
      const rowH = Math.max(28, ...row.map((cell, ci) => {
        doc.font(getFontForText(String(cell))).fontSize(cellFontSize);
        return doc.heightOfString(String(cell), { width: colWidths[ci] - 10 }) + rowPad * 2;
      }));
      if (rowY + rowH > PH - margin - 20) {
        doc.addPage();
        rowY = margin + 8;
        rowY = drawTableHeader(rowY);
      }
      doc.rect(tableLeft, rowY, tableWidth, rowH).fill(idx % 2 === 0 ? altRow : white);
      doc.moveTo(tableLeft, rowY + rowH).lineTo(tableLeft + tableWidth, rowY + rowH);
      let rx = tableLeft;
      row.forEach((cell, ci) => {
        doc.font(getFontForText(String(cell))).fontSize(cellFontSize).fillColor(textColor);
        const textH = doc.heightOfString(String(cell), { width: colWidths[ci] - 10 });
        const vOffset = (rowH - textH) / 2;
        doc.text(String(cell), rx + 5, rowY + vOffset, { width: colWidths[ci] - 10 });
        rx += colWidths[ci];
      });
      rowY += rowH;
    });
    doc.rect(tableLeft, tableTop, tableWidth, rowY - tableTop);
    doc.end();
  } catch (error) {
    console.error("Error generating PDF stream:", error);
    doc.destroy();
    if (!res.headersSent) {
      res.status(500).json({ message: "Failed to generate PDF report" });
    }
  }
});
const createPracticeTest = asyncHandler(async (req, res) => {
  const {
    exam,
    language,
    title,
    questions,
    duration,
    marks,
    difficulty,
    publish
  } = req.body;
  const user = req.user;
  const userId = user.id;
  if (!title) {
    res.status(400).json({ success: false, message: "Title is required" });
    return;
  }
  if (!exam) {
    res.status(400).json({ success: false, message: "Exam is required" });
    return;
  }
  const existTest = await prisma$1.practiceTest.findFirst({ where: { title: { equals: title }, examId: exam, institutionId: user.institutionId } });
  if (existTest) {
    res.status(400).json({ success: false, message: "Practice test title already exists" });
    return;
  }
  if (!marks) {
    res.status(400).json({ success: false, message: "Marks is required" });
    return;
  }
  if (!duration) {
    res.status(400).json({ success: false, message: "Duration is required" });
    return;
  }
  if (!questions?.length) {
    res.status(400).json({ success: false, message: "Questions are required" });
    return;
  }
  const newTest = await prisma$1.practiceTest.create({
    data: {
      createdById: userId,
      examId: exam,
      language: language || "English",
      title,
      duration,
      marks: parseFloat(marks),
      difficulty: difficulty || "Medium",
      publish: publish === true || publish === "true",
      testId: req.body.testId || null,
      institutionId: user.institutionId
    }
  });
  res.status(201).json({
    success: true,
    message: "PracticeTest created successfully",
    data: {
      ...newTest,
      createdAt: toIST(newTest.createdAt),
      updatedAt: toIST(newTest.updatedAt)
    }
  });
});
const listPracticeTest = asyncHandler(async (req, res) => {
  const user = req.user;
  const { search, exam, difficulty, publish, language, page = "1", limit = "10" } = req.query;
  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);
  const skip = (pageNum - 1) * limitNum;
  const where = {
    institutionId: user.institutionId
  };
  if (search) {
    where.title = { contains: search };
  }
  if (language) {
    where.language = language;
  }
  if (exam) {
    const examIds = Array.isArray(exam) ? exam : [exam];
    where.examId = { in: examIds };
  }
  if (difficulty) {
    where.difficulty = difficulty;
  }
  if (publish !== void 0) {
    where.publish = String(publish) === "true";
  }
  const [total, tests] = await Promise.all([
    prisma$1.practiceTest.count({ where }),
    prisma$1.practiceTest.findMany({
      where,
      include: {
        exam: { select: { examName: true } },
        test: {
          include: {
            testQuestions: true,
            referenceInstitution: { select: { user: { select: { institutionName: true, firstName: true } } } }
          }
        },
        questions: { select: { id: true } },
        _count: { select: { examResults: true } }
      },
      orderBy: { title: "asc" },
      skip,
      take: limitNum
    })
  ]);
  const updatedTests = tests.map((test) => ({
    id: test.id,
    title: test.title,
    examId: test.examId,
    exam: test.exam?.examName,
    language: test.language,
    duration: test.duration,
    marks: test.marks,
    difficulty: test.difficulty,
    publish: test.publish,
    questions: Array.from(/* @__PURE__ */ new Set([
      ...test.test?.testQuestions.map((tq) => tq.questionId) || [],
      ...test.questions?.map((q) => q.id) || []
    ])),
    tests: test.test ? [test.test.id] : [],
    testId: test.testId,
    referenceSourceId: test.test?.referenceSourceId,
    referenceInstitution: test.test?.referenceInstitution,
    hasResults: (test._count?.examResults ?? 0) > 0,
    createdAt: toIST(test.createdAt),
    updatedAt: toIST(test.updatedAt)
  }));
  res.status(200).json({
    message: "Successfully fetched practice tests",
    data: updatedTests,
    meta: {
      total,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(total / limitNum)
    }
  });
});
const practiceTestById = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const test = await prisma$1.practiceTest.findFirst({
    where: { id, institutionId: user.institutionId },
    include: {
      test: {
        include: {
          testQuestions: {
            include: {
              question: {
                select: {
                  id: true,
                  questionText: true,
                  options: true,
                  correctAnswer: true,
                  topic: true,
                  language: true,
                  marks: true,
                  difficulty: true
                }
              }
            }
          }
        }
      },
      createdBy: { select: { firstName: true, lastName: true } }
    }
  });
  if (!test) {
    res.status(404).json({ message: "Practice test not found" });
    return;
  }
  const updatedTest = {
    ...test,
    createdAt: toIST(test.createdAt),
    updatedAt: toIST(test.updatedAt),
    questions: test.test?.testQuestions.map((tq) => ({
      ...tq.question
    })) || [],
    totalQuestions: test.test?.testQuestions.length || 0,
    createdBy: test.createdBy ? `${test.createdBy.firstName} ${test.createdBy.lastName}` : "Unknown"
  };
  res.status(200).json({
    message: "Successfully fetched practice test",
    data: updatedTest
  });
});
const updatePracticeTest = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const { questions, testId, ...updates } = req.body;
  const existing = await prisma$1.practiceTest.findFirst({
    where: { id, institutionId: user.institutionId }
  });
  if (!existing) {
    res.status(404).json({ message: "Practice test not found" });
    return;
  }
  const allowedFields = ["title", "language", "duration", "marks", "difficulty", "publish"];
  const data = {};
  for (const field of allowedFields) {
    if (updates[field] !== void 0) {
      data[field] = updates[field];
    }
  }
  if (updates.exam) {
    data.examId = updates.exam;
  }
  if (testId) {
    data.testId = testId;
  }
  if (data.marks) data.marks = parseFloat(data.marks);
  if (data.publish !== void 0) data.publish = data.publish === "true" || data.publish === true;
  const resolvedTestId = testId || existing.testId;
  if (resolvedTestId) {
    const questionCount = await prisma$1.testQuestion.count({ where: { testId: resolvedTestId } });
    data.marks = questionCount;
  }
  const updatedTest = await prisma$1.practiceTest.update({
    where: { id },
    data
  });
  res.status(200).json({
    message: "Practice Test updated successfully",
    data: {
      ...updatedTest,
      createdAt: toIST(updatedTest.createdAt),
      updatedAt: toIST(updatedTest.updatedAt)
    }
  });
});
const deletePracticeTest = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const existing = await prisma$1.practiceTest.findFirst({
    where: { id, institutionId: user.institutionId }
  });
  if (!existing) {
    res.status(404).json({ message: "Practice test not found" });
    return;
  }
  const hasResults = await prisma$1.examResult.findFirst({
    where: { practiceTestId: id }
  });
  if (hasResults) {
    res.status(400).json({ message: "Cannot unmap this test because students have already taken it." });
    return;
  }
  await prisma$1.practiceTest.delete({
    where: { id }
  });
  res.status(200).json({
    message: "PracticeTest deleted successfully",
    deletedId: id
  });
});
const studentPracticeTestList = asyncHandler(async (req, res) => {
  const user = req.user;
  user.id;
  const studentInstitutionId = user.studentInstitutionId;
  const institutionId = user.institutionId;
  const { search, attempted, exam, difficulty, language, subjectId, page = "1", limit = "10" } = req.query;
  const fetchAll = limit === "all";
  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);
  const safePageNum = isNaN(pageNum) ? 1 : pageNum;
  const safeLimitNum = isNaN(limitNum) ? 10 : limitNum;
  const skip = (safePageNum - 1) * safeLimitNum;
  const studentInstitution = await prisma$1.studentInstitution.findUnique({
    where: { id: studentInstitutionId, deletedAt: null },
    include: {
      examResults: {
        select: {
          practiceTestId: true
        }
      },
      subscriptions: { where: { isCurrent: true } },
      exam: true,
      student: { include: { user: { select: { details: true } } } }
    }
  });
  if (!studentInstitution || !studentInstitution.institutionId) {
    res.status(404).json({ message: "Student record not found" });
    return;
  }
  const attemptedTestIds = new Set(studentInstitution.examResults.map((r) => r.practiceTestId).filter(Boolean));
  const where = {
    publish: true,
    institutionId
  };
  if (search) {
    where.title = { contains: search };
  }
  if (exam) {
    where.examId = exam;
  } else if (studentInstitution.examsId) {
    where.examId = studentInstitution.examsId;
  }
  const motherTongue = studentInstitution.student?.language || studentInstitution.language;
  const mediumOfExam = studentInstitution.student?.language ? studentInstitution.language : studentInstitution.student?.user?.details?.mediumOfExam || "English";
  const filterLanguage = language || mediumOfExam;
  if (filterLanguage || motherTongue) {
    where.AND = where.AND || [];
    const languageOr = [];
    if (motherTongue) {
      languageOr.push(
        { test: { subject: { language: { not: null, equals: motherTongue } } } },
        { questions: { some: { subject: { language: { not: null, equals: motherTongue } } } } }
      );
    }
    if (filterLanguage) {
      languageOr.push(
        { test: { subject: { language: null } }, language: filterLanguage },
        { testId: null, questions: { some: { subject: { language: null } } }, language: filterLanguage },
        { testId: null, questions: { none: {} }, language: filterLanguage }
      );
    }
    if (languageOr.length > 0) {
      where.AND.push({ OR: languageOr });
    }
  }
  if (difficulty) {
    where.difficulty = difficulty;
  }
  if (subjectId) {
    where.test = {
      subjectId
    };
  }
  if (attempted === "true") {
    where.id = { in: Array.from(attemptedTestIds) };
  } else if (attempted === "false") {
    where.id = { notIn: Array.from(attemptedTestIds) };
  }
  const [total, tests] = await Promise.all([
    prisma$1.practiceTest.count({ where }),
    prisma$1.practiceTest.findMany({
      where,
      include: {
        exam: { select: { examName: true } },
        test: {
          select: {
            subjectId: true,
            subject: { select: { subjectName: true } },
            _count: { select: { testQuestions: true } }
          }
        }
      },
      orderBy: { title: "asc" },
      ...!fetchAll && {
        skip,
        take: safeLimitNum
      }
    })
  ]);
  res.status(200).json({
    message: tests.length > 0 ? "Successfully fetched practice tests" : "No practice test available",
    data: tests.map((test) => ({
      id: test.id,
      exam: test.exam?.examName,
      language: test.language,
      title: test.title,
      duration: test.duration,
      marks: test.marks,
      difficulty: test.difficulty,
      subjectId: test.test?.subjectId || null,
      subject: test.test?.subject?.subjectName || null,
      totalQuestions: test.test?._count.testQuestions || 0,
      attempted: attemptedTestIds.has(test.id)
    })),
    meta: {
      total,
      page: fetchAll ? 1 : safePageNum,
      limit: fetchAll ? total : safeLimitNum,
      totalPages: fetchAll ? 1 : Math.ceil(total / safeLimitNum)
    }
  });
});
const getTestById$1 = asyncHandler(async (req, res) => {
  const user = req.user;
  const studentInstitutionId = user.studentInstitutionId;
  const testId = req.params.id;
  const { page = "1", limit = "10" } = req.query;
  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);
  const skip = (pageNum - 1) * limitNum;
  const studentInstitution = await prisma$1.studentInstitution.findUnique({
    where: { id: studentInstitutionId },
    include: {
      subscriptions: { where: { isCurrent: true } },
      student: { include: { user: { select: { details: true } } } }
    }
  });
  if (!studentInstitution) {
    res.status(404).json({ message: "Student record not found" });
    return;
  }
  const practiceTest = await prisma$1.practiceTest.findFirst({
    where: { id: testId, institutionId: user.institutionId },
    include: {
      test: {
        include: {
          testQuestions: {
            include: {
              question: {
                select: {
                  id: true,
                  questionText: true,
                  questionImage: true,
                  options: true
                }
              }
            },
            orderBy: {
              createdAt: "asc"
            }
          }
        }
      }
    }
  });
  if (!practiceTest) {
    res.status(404).json({ message: "Practice Test not found" });
    return;
  }
  const mappedQuestions = practiceTest.test?.testQuestions.map((tq) => tq.question) || [];
  const totalQuestions = mappedQuestions.length;
  const paginatedQuestions = mappedQuestions.slice(skip, skip + limitNum);
  const formattedExam = {
    testid: practiceTest.id,
    title: practiceTest.title,
    duration: practiceTest.duration,
    marks: practiceTest.marks,
    difficulty: practiceTest.difficulty,
    questions: paginatedQuestions.map((q) => ({
      id: q.id,
      questionText: q.questionText,
      questionImage: q.questionImage ? `${getHost()}${q.questionImage}` : "",
      options: q.options.map((opt) => ({
        option: String(opt?.option) || "",
        optionImage: opt.optionImage ? `${getHost()}${opt.optionImage}` : ""
      }))
    }))
  };
  res.status(200).json({
    message: "PracticeTest fetched successfully",
    data: formattedExam,
    meta: {
      total: totalQuestions,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(totalQuestions / limitNum)
    }
  });
});
const getPracticeTestQuestions = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const { page = "1", limit = "10", search, examId } = req.query;
  const user = req.user;
  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);
  const skip = (pageNum - 1) * limitNum;
  const test = await prisma$1.practiceTest.findFirst({
    where: { id, institutionId: user.institutionId },
    select: { id: true, exam: { select: { examName: true } } }
  });
  if (!test) {
    res.status(404).json({ message: "Practice test not found" });
    return;
  }
  const practiceTestObj = await prisma$1.practiceTest.findUnique({
    where: { id },
    select: { testId: true }
  });
  const where = {
    OR: [
      { practiceTests: { some: { id } } }
    ]
  };
  if (practiceTestObj?.testId) {
    where.OR.push({ testQuestions: { some: { testId: practiceTestObj.testId } } });
  }
  if (search) {
    where.questionText = { contains: search };
  }
  if (examId) {
    where.exams = { some: { id: examId } };
  }
  const [total, questions] = await Promise.all([
    prisma$1.questionBank.count({ where }),
    prisma$1.questionBank.findMany({
      where,
      include: {
        subject: { select: { subjectName: true } }
      },
      orderBy: { createdAt: "desc" },
      skip,
      take: limitNum
    })
  ]);
  const formattedQuestions = questions.map((q) => ({
    ...q,
    createdAt: toIST(q.createdAt),
    updatedAt: toIST(q.updatedAt),
    exam: q.exams?.[0]?.examName || "",
    subject: q.subject?.subjectName || "",
    questionImage: q.questionImage ? `${getHost()}${q.questionImage}` : "",
    options: q.options.map((option) => ({
      ...option,
      optionImage: option.optionImage ? `${getHost()}${option.optionImage}` : ""
    })),
    explanationImage: q.explanationImage ? `${getHost()}${q.explanationImage}` : ""
  }));
  res.status(200).json({
    message: "Successfully fetched practice test questions",
    data: {
      testId: test.id,
      exam: test.exam?.examName
    },
    questions: formattedQuestions,
    meta: {
      page: pageNum,
      limit: limitNum,
      total,
      totalPages: Math.ceil(total / limitNum)
    }
  });
});
const getStudentSubjects = asyncHandler(async (req, res) => {
  const user = req.user;
  const institutionId = user.institutionId;
  const search = req.query.search?.trim();
  const page = Math.max(parseInt(req.query.page) || 1, 1);
  const limit = Math.max(parseInt(req.query.limit) || 10, 1);
  const skip = (page - 1) * limit;
  if (!institutionId) {
    res.status(400).json({ message: "Institution ID is missing" });
    return;
  }
  const studentInstitution = await prisma$1.studentInstitution.findUnique({
    where: { id: user.studentInstitutionId },
    include: { student: { include: { user: { select: { details: true } } } } }
  });
  if (!studentInstitution) {
    res.status(404).json({ message: "Student record not found" });
    return;
  }
  const motherTongue = studentInstitution.student?.language || studentInstitution.language;
  const mediumOfExam = studentInstitution.student?.language ? studentInstitution.language : studentInstitution.student?.user?.details?.mediumOfExam || "English";
  const practiceTestFilter = {
    institutionId,
    publish: true
  };
  if (studentInstitution.examsId) {
    practiceTestFilter.examId = studentInstitution.examsId;
  }
  const practiceTestFilterMedium = { ...practiceTestFilter, language: mediumOfExam };
  const practiceTestFilterMotherTongue = { ...practiceTestFilter };
  const where = {
    institutionId,
    OR: [
      {
        language: motherTongue || void 0,
        OR: [
          { questions: { some: { practiceTests: { some: practiceTestFilterMotherTongue } } } },
          { tests: { some: { practiceTests: { some: practiceTestFilterMotherTongue } } } }
        ]
      },
      {
        language: null,
        OR: [
          { questions: { some: { practiceTests: { some: practiceTestFilterMedium } } } },
          { tests: { some: { practiceTests: { some: practiceTestFilterMedium } } } }
        ]
      }
    ]
  };
  if (search) {
    where.subjectName = { contains: search };
  }
  const [total, subjects] = await Promise.all([
    prisma$1.subject.count({ where }),
    prisma$1.subject.findMany({
      where,
      select: {
        id: true,
        subjectName: true,
        topics: true,
        createdAt: true
      },
      orderBy: { subjectName: "asc" },
      skip,
      take: limit
    })
  ]);
  res.status(200).json({
    message: "Subjects fetched successfully",
    data: subjects,
    meta: {
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit)
    }
  });
});
const mapTestToExam = asyncHandler(async (req, res) => {
  const user = req.user;
  const { testId, examId } = req.body;
  if (!testId || !examId) {
    res.status(400).json({ success: false, message: "Test ID and Exam ID are required" });
    return;
  }
  const test = await prisma$1.test.findUnique({
    where: { id: testId },
    include: { testQuestions: true }
  });
  if (!test) {
    res.status(404).json({ success: false, message: "Test not found" });
    return;
  }
  const existingMappedTest = await prisma$1.practiceTest.findFirst({
    where: {
      examId,
      testId
    }
  });
  if (existingMappedTest) {
    res.status(400).json({ success: false, message: "This Test is already mapped to this Exam." });
    return;
  }
  const newPracticeTest = await prisma$1.practiceTest.create({
    data: {
      title: test.title || "Test",
      examId,
      duration: 30,
      marks: test.testQuestions.length,
      language: test.language || "English",
      createdById: user.id,
      institutionId: user.institutionId,
      testId
    }
  });
  res.status(200).json({
    success: true,
    message: "Test mapped successfully!",
    data: {
      ...newPracticeTest,
      createdAt: toIST(newPracticeTest.createdAt),
      updatedAt: toIST(newPracticeTest.updatedAt)
    }
  });
});
const getStudentStats = asyncHandler(
  async (req, res) => {
    const user = req.user;
    const isAdmin = user.role === "ADMIN";
    const institutionId = isAdmin ? req.query.institutionId : user.institutionId;
    const examId = req.query.examId;
    const search = req.query.search?.trim() || "";
    const subscriptionStatus = req.query.subscriptionStatus;
    const planStatus = (req.query.planStatus || subscriptionStatus)?.trim();
    const planType = req.query.planType?.trim();
    const planName = req.query.planName?.trim();
    const isVerified = req.query.isVerified?.trim();
    const language = req.query.language?.trim();
    const regStartDate = req.query.regStartDate?.trim();
    const regEndDate = req.query.regEndDate?.trim();
    const page = parseInt(req.query.page || "1");
    const limit = parseInt(req.query.limit || "10");
    const skip = (page - 1) * limit;
    const statsWhere = {
      deletedAt: null,
      student: { user: { isDeleted: false } }
    };
    if (institutionId) statsWhere.institutionId = institutionId;
    if (examId) statsWhere.examsId = examId;
    const where = { ...statsWhere };
    if (search) {
      const searchWords = search.split(/\s+/).filter(Boolean);
      where.student = {
        user: {
          isDeleted: false,
          OR: [
            { firstName: { contains: search } },
            { lastName: { contains: search } },
            { email: { contains: search } },
            { phone: { contains: search } },
            ...searchWords.length > 1 ? [{
              AND: searchWords.map((word) => ({
                OR: [
                  { firstName: { contains: word } },
                  { lastName: { contains: word } }
                ]
              }))
            }] : []
          ]
        }
      };
    }
    if (isVerified) {
      where.isVerified = isVerified === "true";
    }
    if (language) {
      where.language = language;
    }
    if (planStatus || planName || planType) {
      const subConditions = { isCurrent: true };
      if (planStatus && planStatus !== "ALL" && planStatus !== "UNPAID") {
        subConditions.status = planStatus.toUpperCase();
      }
      if (planName || planType) {
        subConditions.plan = {};
        if (planName) subConditions.plan.planName = planName;
        if (planType) subConditions.plan.planType = planType;
      }
      where.subscriptions = {
        some: subConditions
      };
    }
    if (regStartDate || regEndDate) {
      where.createdAt = {};
      const convertToISOFormat = (dateStr) => {
        const parts = dateStr.split("/");
        if (parts.length === 3) {
          const [day, month, year] = parts;
          return `${year}-${month}-${day}`;
        }
        return dateStr;
      };
      if (regStartDate) {
        const formattedStartDate = convertToISOFormat(regStartDate);
        where.createdAt.gte = /* @__PURE__ */ new Date(`${formattedStartDate}T00:00:00.000Z`);
      }
      if (regEndDate) {
        const formattedEndDate = convertToISOFormat(regEndDate);
        where.createdAt.lte = /* @__PURE__ */ new Date(`${formattedEndDate}T23:59:59.999Z`);
      }
    }
    const [statsRecords, students, totalStudents] = await Promise.all([
      prisma$1.studentInstitution.findMany({
        where: statsWhere,
        select: {
          isVerified: true,
          subscriptions: {
            where: { isCurrent: true },
            select: { status: true }
          }
        }
      }),
      prisma$1.studentInstitution.findMany({
        where,
        include: {
          student: {
            include: {
              user: {
                select: {
                  firstName: true,
                  lastName: true,
                  email: true,
                  phone: true
                }
              }
            }
          },
          exam: { select: { examName: true } },
          subscriptions: {
            where: { isCurrent: true },
            select: {
              id: true,
              status: true,
              amount: true,
              startDate: true,
              endDate: true,
              plan: {
                select: {
                  id: true,
                  planName: true,
                  planType: true,
                  duration: true
                }
              }
            }
          }
        },
        orderBy: { createdAt: "desc" },
        skip,
        take: limit
      }),
      prisma$1.studentInstitution.count({ where })
    ]);
    let totalEnrolled = statsRecords.length;
    let verifiedCount = 0;
    let pendingCount = 0;
    let paidCount = 0;
    let expiredCount = 0;
    statsRecords.forEach((record) => {
      if (record.isVerified) {
        verifiedCount++;
      } else {
        pendingCount++;
      }
      const currentSub = record.subscriptions?.[0];
      if (currentSub) {
        if (currentSub.status === "ACTIVE") {
          paidCount++;
        } else if (currentSub.status === "EXPIRED") {
          expiredCount++;
        }
      }
    });
    const data = students.map((si) => {
      const latestSub = si.subscriptions?.[0] ?? null;
      const status = latestSub?.status && latestSub?.status !== "UNPAID" ? latestSub.status : null;
      return {
        id: si.id,
        studentName: si.student?.user ? `${si.student.user.firstName} ${si.student.user.lastName}`.trim() : "—",
        email: si.student?.user?.email || "—",
        phone: si.student?.user?.phone || "—",
        examName: si.exam?.examName || "—",
        language: si.language || "—",
        isVerified: si.isVerified,
        registrationDate: si.createdAt,
        subscriptionStatus: status,
        planName: latestSub?.plan?.planName || "—",
        expiryDate: latestSub?.endDate || null,
        subscription: latestSub ? {
          status: latestSub.status,
          amount: latestSub.amount,
          startDate: latestSub.startDate,
          endDate: latestSub.endDate,
          trialEndsAt: latestSub.endDate
        } : null,
        subscriptionPlan: latestSub?.plan ? {
          planName: latestSub.plan.planName,
          planType: latestSub.plan.planType,
          duration: latestSub.plan.duration
        } : null
      };
    });
    res.status(200).json({
      message: "Exam student stats fetched successfully",
      stats: {
        totalEnrolled,
        verifiedCount,
        pendingCount,
        paidCount,
        expiredCount
      },
      data,
      meta: {
        total: totalStudents,
        page,
        limit,
        totalPages: Math.ceil(totalStudents / limit)
      }
    });
  }
);
const getStudentPerformance = asyncHandler(
  async (req, res) => {
    const { studentId } = req.params;
    const search = req.query.search?.trim() || "";
    const testType = req.query.testType;
    const subjectId = req.query.subjectId;
    const startDate = req.query.startDate;
    const endDate = req.query.endDate;
    const examId = req.query.examId;
    const sortBy = req.query.sortBy || "date";
    const sortOrder = req.query.sortOrder?.toLowerCase() === "asc" ? "asc" : "desc";
    let subjectName = "All Subjects";
    if (subjectId) {
      const subjectData = await prisma$1.subject.findUnique({
        where: { id: subjectId },
        select: { subjectName: true }
      });
      if (subjectData) {
        subjectName = subjectData.subjectName;
      }
    }
    const page = parseInt(req.query.page || "1");
    const limit = parseInt(req.query.limit || "10");
    const skip = (page - 1) * limit;
    const enrollments = await prisma$1.studentInstitution.findMany({
      where: {
        OR: [
          { id: studentId },
          { studentId }
        ]
      },
      include: {
        student: {
          include: {
            user: {
              select: {
                firstName: true,
                lastName: true,
                email: true,
                phone: true
              }
            }
          }
        },
        exam: {
          select: {
            id: true,
            examName: true
          }
        }
      },
      orderBy: {
        createdAt: "desc"
      }
    });
    if (enrollments.length === 0) {
      res.status(404).json({ message: "Student enrollment not found" });
      return;
    }
    let activeEnrollment = enrollments[0];
    const enrolledExams = enrollments.map((e) => e.exam).filter((exam) => !!exam);
    let dateClause = void 0;
    if (startDate || endDate) {
      dateClause = {};
      if (startDate) {
        dateClause.gte = /* @__PURE__ */ new Date(`${startDate}T00:00:00.000Z`);
      }
      if (endDate) {
        dateClause.lte = /* @__PURE__ */ new Date(`${endDate}T23:59:59.999Z`);
      }
    }
    const dynamicTestType = subjectId ? "PracticeTest" : testType;
    const shouldFetchExams = !dynamicTestType || dynamicTestType === "ALL" || dynamicTestType === "PracticeTest" || dynamicTestType === "OldQuestionPaper";
    const shouldFetchMocks = (!dynamicTestType || dynamicTestType === "ALL" || dynamicTestType === "MockTest") && !subjectId;
    let rawExams = [];
    let rawMocks = [];
    const examWhere = { studentInstitutionId: { in: enrollments.map((e) => e.id) } };
    if (dynamicTestType && dynamicTestType !== "ALL") {
      examWhere.testType = dynamicTestType;
    } else if (subjectId) {
      examWhere.testType = "PracticeTest";
    }
    if (dateClause) examWhere.createdAt = dateClause;
    const mockWhere = { studentInstitutionId: { in: enrollments.map((e) => e.id) } };
    if (dateClause) mockWhere.createdAt = dateClause;
    if (examId) {
      examWhere.OR = [
        { practiceTest: { examId } },
        { oldQuestionPaper: { examId } }
      ];
      mockWhere.mockTest = { examId };
    }
    await Promise.all([
      shouldFetchExams ? (async () => {
        rawExams = await prisma$1.examResult.findMany({
          where: examWhere,
          include: {
            practiceTest: { select: { title: true, exam: { select: { id: true, examName: true } } } },
            oldQuestionPaper: { select: { title: true, exam: { select: { id: true, examName: true } } } }
          },
          orderBy: { createdAt: "desc" }
        });
      })() : Promise.resolve(),
      shouldFetchMocks ? (async () => {
        rawMocks = await prisma$1.mockTestResult.findMany({
          where: mockWhere,
          include: {
            mockTest: { select: { title: true, questionCount: true, exam: { select: { id: true, examName: true } } } }
          },
          orderBy: { createdAt: "desc" }
        });
      })() : Promise.resolve()
    ]);
    const allQuestionIds = /* @__PURE__ */ new Set();
    rawExams.forEach((r) => {
      const answers = Array.isArray(r.answers) ? r.answers : [];
      answers.forEach((ans) => {
        if (ans.questionId) allQuestionIds.add(ans.questionId);
      });
    });
    rawMocks.forEach((r) => {
      const answers = Array.isArray(r.answers) ? r.answers : [];
      answers.forEach((ans) => {
        if (ans.questionId) allQuestionIds.add(ans.questionId);
      });
    });
    const allQuestions = await prisma$1.questionBank.findMany({
      where: { id: { in: Array.from(allQuestionIds) } },
      include: { subject: { select: { subjectName: true } } }
    });
    const questionInfoMap = new Map(allQuestions.map((q) => [
      q.id,
      { marks: q.marks || 1, subjectId: q.subjectId, subjectName: q.subject?.subjectName || "Unknown" }
    ]));
    const processedResults = [];
    const mapResultItem = (r, isMock) => {
      if (subjectId && (isMock || r.testType !== "PracticeTest")) return;
      let newObtainedMarks = 0;
      let newTotalMarks = 0;
      let newTotalQuestions = 0;
      const subjectsInTest = /* @__PURE__ */ new Set();
      const answers = Array.isArray(r.answers) ? r.answers : [];
      for (const ans of answers) {
        const qInfo = questionInfoMap.get(ans.questionId);
        if (qInfo) {
          subjectsInTest.add(qInfo.subjectName);
          if (subjectId && qInfo.subjectId !== subjectId) {
            continue;
          }
          newTotalQuestions++;
          newTotalMarks += qInfo.marks;
          if (ans.isCorrect) {
            newObtainedMarks += qInfo.marks;
          }
        }
      }
      if (subjectId && newTotalQuestions === 0) return;
      let finalSubjectName = "";
      if (subjectId) {
        finalSubjectName = subjectName;
      } else {
        finalSubjectName = Array.from(subjectsInTest).join(", ");
        if (!finalSubjectName) {
          finalSubjectName = isMock ? "Mock Test Stack" : r.testType === "OldQuestionPaper" ? "Old Question Paper" : "Mixed";
        }
      }
      const defaultTitle = isMock ? r.mockTest?.title || "Deleted Mock" : r.practiceTest?.title || r.oldQuestionPaper?.title || "Deleted Test";
      const defaultQuestionsCount = isMock ? r.mockTest?.questionCount || 0 : r.totalQuestions;
      const rowExam = isMock ? r.mockTest?.exam : r.practiceTest?.exam || r.oldQuestionPaper?.exam;
      const rowExamName = rowExam?.examName || activeEnrollment.exam?.examName || "—";
      const rowExamId = rowExam?.id || activeEnrollment.examsId || "";
      const obtainedMarks = subjectId ? newObtainedMarks : r.obtainedMarks;
      const totalMarks = subjectId ? newTotalMarks : r.totalMarks;
      const scorePercentage = totalMarks > 0 ? obtainedMarks / totalMarks * 100 : 0;
      processedResults.push({
        id: r.id,
        testTitle: defaultTitle,
        displaySubjectName: finalSubjectName,
        testType: isMock ? "MockTest" : r.testType,
        obtainedMarks,
        totalMarks,
        totalQuestions: subjectId ? newTotalQuestions : defaultQuestionsCount,
        scorePercentage: Math.round(scorePercentage * 100) / 100,
        createdAt: r.createdAt,
        examName: rowExamName,
        examId: rowExamId
      });
    };
    rawExams.forEach((r) => mapResultItem(r, false));
    rawMocks.forEach((r) => mapResultItem(r, true));
    let filteredResults = processedResults;
    if (search) {
      const searchLower = search.toLowerCase();
      filteredResults = filteredResults.filter((r) => r.testTitle.toLowerCase().includes(searchLower));
    }
    filteredResults.sort((a, b) => {
      let comparison = 0;
      switch (sortBy) {
        case "testTitle":
          comparison = a.testTitle.localeCompare(b.testTitle);
          break;
        case "score":
        case "percentage":
          comparison = a.scorePercentage - b.scorePercentage;
          break;
        case "date":
        default:
          comparison = a.createdAt.getTime() - b.createdAt.getTime();
          break;
      }
      return sortOrder === "asc" ? comparison : -comparison;
    });
    const total = filteredResults.length;
    const paginatedResults = filteredResults.slice(skip, skip + limit);
    const data = paginatedResults.map((r) => ({
      id: r.id,
      testTitle: r.testTitle,
      subjectName: r.displaySubjectName,
      testType: r.testType,
      obtainedMarks: r.obtainedMarks,
      totalMarks: r.totalMarks,
      totalQuestions: r.totalQuestions,
      scorePercentage: r.scorePercentage,
      createdAt: r.createdAt,
      examName: r.examName,
      examId: r.examId
    }));
    res.status(200).json({
      student: {
        name: `${activeEnrollment.student.user.firstName} ${activeEnrollment.student.user.lastName}`.trim(),
        email: activeEnrollment.student.user.email || "—",
        phone: activeEnrollment.student.user.phone || "—",
        examName: activeEnrollment.exam?.examName || "—",
        examId: activeEnrollment.examsId
      },
      enrolledExams,
      data,
      meta: {
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit)
      }
    });
  }
);
const studentPerformancePDF = asyncHandler(async (req, res) => {
  const { studentId } = req.params;
  const search = req.query.search?.trim() || "";
  const testType = req.query.testType;
  const subjectId = req.query.subjectId;
  const startDate = req.query.startDate;
  const endDate = req.query.endDate;
  const examId = req.query.examId;
  const sortBy = req.query.sortBy || "date";
  const sortOrder = req.query.sortOrder?.toLowerCase() === "asc" ? "asc" : "desc";
  const enrollments = await prisma$1.studentInstitution.findMany({
    where: { OR: [{ id: studentId }, { studentId }] },
    include: {
      student: { include: { user: { select: { firstName: true, lastName: true, email: true, phone: true } } } },
      exam: { select: { id: true, examName: true } }
    },
    orderBy: { createdAt: "desc" }
  });
  if (enrollments.length === 0) {
    res.status(404).json({ message: "Student enrollment not found" });
    return;
  }
  const activeEnrollment = enrollments[0];
  let institutionNameFromEnrollment = null;
  try {
    const inst = await prisma$1.institution.findUnique({
      where: { id: activeEnrollment.institutionId },
      select: {
        user: {
          select: {
            institutionName: true,
            firstName: true,
            email: true
          }
        }
      }
    });
    institutionNameFromEnrollment = inst?.user?.institutionName || inst?.user?.firstName || inst?.user?.email || null;
  } catch (e) {
    institutionNameFromEnrollment = null;
  }
  if (subjectId) {
    const subjectData = await prisma$1.subject.findUnique({ where: { id: subjectId }, select: { subjectName: true } });
    if (subjectData) subjectData.subjectName;
  }
  let dateClause = void 0;
  if (startDate || endDate) {
    dateClause = {};
    if (startDate) dateClause.gte = /* @__PURE__ */ new Date(`${startDate}T00:00:00.000Z`);
    if (endDate) dateClause.lte = /* @__PURE__ */ new Date(`${endDate}T23:59:59.999Z`);
  }
  const dynamicTestType = subjectId ? "PracticeTest" : testType;
  const shouldFetchExams = !dynamicTestType || dynamicTestType === "ALL" || dynamicTestType === "PracticeTest" || dynamicTestType === "OldQuestionPaper";
  const shouldFetchMocks = (!dynamicTestType || dynamicTestType === "ALL" || dynamicTestType === "MockTest") && !subjectId;
  let rawExams = [];
  let rawMocks = [];
  const examWhere = { studentInstitutionId: { in: enrollments.map((e) => e.id) } };
  if (dynamicTestType && dynamicTestType !== "ALL") examWhere.testType = dynamicTestType;
  if (dateClause) examWhere.createdAt = dateClause;
  const mockWhere = { studentInstitutionId: { in: enrollments.map((e) => e.id) } };
  if (dateClause) mockWhere.createdAt = dateClause;
  if (examId) {
    examWhere.OR = [{ practiceTest: { examId } }, { oldQuestionPaper: { examId } }];
    mockWhere.mockTest = { examId };
  }
  await Promise.all([
    shouldFetchExams ? (async () => {
      rawExams = await prisma$1.examResult.findMany({
        where: examWhere,
        include: {
          practiceTest: {
            select: {
              title: true,
              exam: { select: { id: true, examName: true } },
              test: {
                select: {
                  subject: {
                    select: {
                      subjectName: true
                    }
                  }
                }
              }
            }
          },
          oldQuestionPaper: { select: { title: true, exam: { select: { id: true, examName: true } } } }
        },
        orderBy: { createdAt: "desc" }
      });
    })() : Promise.resolve(),
    shouldFetchMocks ? (async () => {
      rawMocks = await prisma$1.mockTestResult.findMany({
        where: mockWhere,
        include: { mockTest: { select: { title: true, questionCount: true, exam: { select: { id: true, examName: true } } } } },
        orderBy: { createdAt: "desc" }
      });
    })() : Promise.resolve()
  ]);
  const allQuestionIds = /* @__PURE__ */ new Set();
  [...rawExams, ...rawMocks].forEach((r) => {
    (Array.isArray(r.answers) ? r.answers : []).forEach((ans) => {
      if (ans.questionId) allQuestionIds.add(ans.questionId);
    });
  });
  const allQuestions = await prisma$1.questionBank.findMany({
    where: { id: { in: Array.from(allQuestionIds) } },
    include: { subject: { select: { subjectName: true } } }
  });
  const questionInfoMap = new Map(allQuestions.map((q) => [q.id, { marks: q.marks || 1, subjectId: q.subjectId, subjectName: q.subject?.subjectName || "Unknown" }]));
  const processedResults = [];
  const mapItem = (r, isMock) => {
    if (subjectId && (isMock || r.testType !== "PracticeTest")) return;
    let newObtained = 0, newTotal = 0, newQuestions = 0;
    const subjectsInTest = /* @__PURE__ */ new Set();
    const answers = Array.isArray(r.answers) ? r.answers : [];
    for (const ans of answers) {
      const qInfo = questionInfoMap.get(ans.questionId);
      if (qInfo) {
        subjectsInTest.add(qInfo.subjectName);
        if (subjectId && qInfo.subjectId !== subjectId) continue;
        newQuestions++;
        newTotal += qInfo.marks;
        if (ans.isCorrect) newObtained += qInfo.marks;
      }
    }
    if (subjectId && newQuestions === 0) return;
    const rowExam = isMock ? r.mockTest?.exam : r.practiceTest?.exam || r.oldQuestionPaper?.exam;
    const obtainedMarks = subjectId ? newObtained : r.obtainedMarks;
    const totalMarks = subjectId ? newTotal : r.totalMarks;
    const pct = totalMarks > 0 ? obtainedMarks / totalMarks * 100 : 0;
    const subjectName2 = !isMock && r.practiceTest ? r.practiceTest.test?.subject?.subjectName : null;
    processedResults.push({
      testTitle: isMock ? r.mockTest?.title || "Deleted Mock" : r.practiceTest?.title || r.oldQuestionPaper?.title || "Deleted Test",
      testType: isMock ? "MockTest" : r.testType,
      examName: rowExam?.examName || activeEnrollment.exam?.examName || "—",
      obtainedMarks,
      totalMarks,
      scorePercentage: Math.round(pct * 100) / 100,
      createdAt: r.createdAt,
      subjectName: subjectName2
    });
  };
  rawExams.forEach((r) => mapItem(r, false));
  rawMocks.forEach((r) => mapItem(r, true));
  let filtered = processedResults;
  if (search) {
    const sl = search.toLowerCase();
    filtered = filtered.filter((r) => r.testTitle.toLowerCase().includes(sl));
  }
  filtered.sort((a, b) => {
    let cmp = 0;
    switch (sortBy) {
      case "testTitle":
        cmp = a.testTitle.localeCompare(b.testTitle);
        break;
      case "score":
      case "percentage":
        cmp = a.scorePercentage - b.scorePercentage;
        break;
      default:
        cmp = a.createdAt.getTime() - b.createdAt.getTime();
        break;
    }
    return sortOrder === "asc" ? cmp : -cmp;
  });
  const studentName = `${activeEnrollment.student.user.firstName} ${activeEnrollment.student.user.lastName}`.trim();
  const studentEmail = activeEnrollment.student.user.email || "—";
  const studentPhone = activeEnrollment.student.user.phone || "—";
  activeEnrollment.exam?.examName || "—";
  const typeLabels = { PracticeTest: "PRACTICE", MockTest: "MOCK", OldQuestionPaper: "PYQ" };
  const rows = filtered.map((r, i) => {
    let typeStr = typeLabels[r.testType] || r.testType;
    if (r.testType === "PracticeTest" && r.subjectName) {
      typeStr = `PRACTICE TEST
(${r.subjectName})`;
    } else if (r.testType === "MockTest") {
      typeStr = "MOCK TEST";
    } else if (r.testType === "OldQuestionPaper") {
      typeStr = "PYQ";
    }
    return [
      `${i + 1}`,
      r.examName,
      typeStr,
      r.testTitle,
      dayjs(r.createdAt).format("DD MMM YYYY, hh:mm A"),
      `${r.obtainedMarks} / ${r.totalMarks}`,
      `${r.scorePercentage.toFixed(1)}%`
    ];
  });
  const doc = new PDFDocument({ size: "A4", layout: "landscape", margin: 0 });
  res.setHeader("Content-Type", "application/pdf");
  res.setHeader("Content-Disposition", `attachment; filename=Student_Performance_${dayjs().format("YYYY-MM-DD")}.pdf`);
  doc.pipe(res);
  try {
    doc.registerFont("NotoSansTamil", path.join(process.cwd(), "node_modules", "@expo-google-fonts", "noto-sans-tamil", "400Regular", "NotoSansTamil_400Regular.ttf"));
    doc.registerFont("NotoSansDevanagari", path.join(process.cwd(), "node_modules", "@expo-google-fonts", "noto-sans-devanagari", "400Regular", "NotoSansDevanagari_400Regular.ttf"));
    const getFontForText = (text, isBold = false) => {
      const hasTamil = /[\u0B80-\u0BFF]/.test(text);
      const hasHindi = /[\u0900-\u097F]/.test(text);
      if (hasTamil) return "NotoSansTamil";
      if (hasHindi) return "NotoSansDevanagari";
      return isBold ? "Helvetica-Bold" : "Helvetica";
    };
    const primary = "#1976d2";
    const white = "#ffffff";
    const altRow = "#f5f7fa";
    const textColor = "#1f2937";
    const mutedText = "#cbd5e1";
    const margin = 24;
    const PW = doc.page.width;
    const PH = doc.page.height;
    const drawBg = () => {
      doc.save();
      doc.rect(0, 0, PW, PH).fill("#eef1f5");
      doc.roundedRect(margin - 6, margin - 6, PW - (margin - 6) * 2, PH - (margin - 6) * 2, 8).fill(white);
      doc.restore();
    };
    drawBg();
    doc.on("pageAdded", drawBg);
    const logoPath = path.resolve(process.cwd(), "public", "logo.png");
    const hasLogo = fs.existsSync(logoPath);
    const headerH = 66;
    doc.roundedRect(margin, margin, PW - margin * 2, headerH, 10).fill(primary);
    if (hasLogo) {
      doc.roundedRect(margin + 14, margin + 12, 40, 40, 6).fill(white);
      try {
        doc.image(logoPath, margin + 18, margin + 16, { fit: [32, 32], align: "center", valign: "center" });
      } catch {
      }
    }
    const textX = hasLogo ? margin + 64 : margin + 16;
    doc.fillColor(white).font("Helvetica-Bold").fontSize(20).text("Exam Infra", textX, margin + 14);
    doc.font("Helvetica").fontSize(9).fillColor(mutedText).text("Student Performance Report", textX, margin + 38);
    if (institutionNameFromEnrollment) {
      doc.font(getFontForText(institutionNameFromEnrollment)).fontSize(9).fillColor(white).text(`Institution: ${institutionNameFromEnrollment}`, textX, margin + 52);
    }
    doc.font("Helvetica-Bold").fontSize(9).fillColor(white).text(`Generated: ${dayjs().format("DD MMM YYYY, hh:mm A")}`, PW - margin - 230, margin + 14, { width: 220, align: "right" });
    doc.font("Helvetica").fontSize(9).fillColor(mutedText).text(`Total Records: ${rows.length}`, PW - margin - 230, margin + 36, { width: 220, align: "right" });
    const infoY = margin + headerH + 6;
    const infoH = 24;
    doc.rect(margin, infoY, PW - margin * 2, infoH).fill("#e8f0fe");
    doc.font(getFontForText(studentName, true)).fontSize(8.5).fillColor("#1e3a5f").text(`Student: ${studentName}`, margin + 10, infoY + 6, { continued: true }).font("Helvetica").fillColor("#374151").text(`   ${studentEmail}   •   ${studentPhone}`);
    const tableTop = infoY + infoH + 8;
    const tableLeft = margin;
    const tableWidth = PW - margin * 2;
    const baseWidths = [35, 90, 100, 160, 120, 70, 70];
    const baseTotal = baseWidths.reduce((a, b) => a + b, 0);
    const colWidths = baseWidths.map((w) => w / baseTotal * tableWidth);
    const headers = ["S.No", "Exam", "Test Type", "Test Title", "Date", "Score", "Percentage"];
    const headerRowH = 26;
    const cellFontSize = 8.5;
    const rowPad = 6;
    const drawTableHeader = (y) => {
      doc.rect(tableLeft, y, tableWidth, headerRowH).fill(primary);
      doc.font("Helvetica-Bold").fontSize(9).fillColor(white);
      let hx = tableLeft;
      headers.forEach((h, i) => {
        const th = doc.heightOfString(h, { width: colWidths[i] - 10 });
        doc.text(h, hx + 5, y + (headerRowH - th) / 2, { width: colWidths[i] - 10, align: "left" });
        hx += colWidths[i];
      });
      return y + headerRowH;
    };
    let rowY = drawTableHeader(tableTop);
    rows.forEach((row, idx) => {
      const rowH = Math.max(28, ...row.map((cell, ci) => {
        doc.font(getFontForText(String(cell))).fontSize(cellFontSize);
        return doc.heightOfString(String(cell), { width: colWidths[ci] - 10 }) + rowPad * 2;
      }));
      if (rowY + rowH > PH - margin - 20) {
        doc.addPage();
        rowY = margin + 8;
        rowY = drawTableHeader(rowY);
      }
      doc.rect(tableLeft, rowY, tableWidth, rowH).fill(idx % 2 === 0 ? altRow : white);
      let rx = tableLeft;
      row.forEach((cell, ci) => {
        doc.font(getFontForText(String(cell))).fontSize(cellFontSize).fillColor(textColor);
        const th = doc.heightOfString(String(cell), { width: colWidths[ci] - 10 });
        doc.text(String(cell), rx + 5, rowY + (rowH - th) / 2, { width: colWidths[ci] - 10 });
        rx += colWidths[ci];
      });
      rowY += rowH;
    });
    doc.end();
  } catch (err) {
    console.error("studentPerformancePDF - PDF Generation Error:", err);
    try {
      if (!res.headersSent) {
        res.status(500).json({ message: "Failed to generate PDF report" });
      }
    } catch (resErr) {
      console.error("Failed to send 500 error response:", resErr);
    }
    try {
      doc.destroy();
    } catch (docErr) {
      console.error("Failed to destroy doc:", docErr);
    }
  }
});
const studentReportsPDF = asyncHandler(async (req, res) => {
  const user = req.user;
  const isAdmin = user.role === "ADMIN";
  const institutionId = isAdmin ? req.query.institutionId : user.institutionId;
  const examId = req.query.examId;
  const search = req.query.search?.trim() || "";
  const planStatus = req.query.planStatus?.trim();
  const planType = req.query.planType?.trim();
  const planName = req.query.planName?.trim();
  const isVerified = req.query.isVerified?.trim();
  const language = req.query.language?.trim();
  const regStartDate = req.query.regStartDate?.trim();
  const regEndDate = req.query.regEndDate?.trim();
  let institutionNameFromRequest = null;
  if (institutionId) {
    const institutionRecord = await prisma$1.institution.findUnique({
      where: { id: institutionId },
      select: {
        user: {
          select: {
            institutionName: true,
            firstName: true,
            email: true
          }
        }
      }
    });
    institutionNameFromRequest = institutionRecord?.user?.institutionName || institutionRecord?.user?.firstName || institutionRecord?.user?.email || null;
  }
  const statsWhere = {};
  if (institutionId) statsWhere.institutionId = institutionId;
  if (examId) statsWhere.examsId = examId;
  const where = { ...statsWhere };
  if (search) {
    where.student = {
      user: {
        OR: [
          { firstName: { contains: search } },
          { lastName: { contains: search } },
          { email: { contains: search } },
          { phone: { contains: search } }
        ]
      }
    };
  }
  if (isVerified) {
    where.isVerified = isVerified === "true";
  }
  if (language) {
    where.language = language;
  }
  if (planStatus || planName || planType) {
    if (planStatus === "UNPAID") {
      where.subscriptions = { none: { isCurrent: true } };
    } else {
      const subConditions = { isCurrent: true };
      if (planStatus && planStatus !== "ALL") {
        subConditions.status = planStatus.toUpperCase();
      }
      if (planName || planType) {
        subConditions.plan = {};
        if (planName) subConditions.plan.planName = planName;
        if (planType) subConditions.plan.planType = planType;
      }
      where.subscriptions = {
        some: subConditions
      };
    }
  }
  if (regStartDate || regEndDate) {
    where.student = where.student || {};
    where.student.createdAt = {};
    const convertToISOFormat = (dateStr) => {
      const parts = dateStr.split("/");
      if (parts.length === 3) {
        const [day, month, year] = parts;
        return `${year}-${month}-${day}`;
      }
      return dateStr;
    };
    if (regStartDate) {
      const formattedStartDate = convertToISOFormat(regStartDate);
      where.student.createdAt.gte = /* @__PURE__ */ new Date(`${formattedStartDate}T00:00:00.000Z`);
    }
    if (regEndDate) {
      const formattedEndDate = convertToISOFormat(regEndDate);
      where.student.createdAt.lte = /* @__PURE__ */ new Date(`${formattedEndDate}T23:59:59.999Z`);
    }
  }
  const studentInstitutions = await prisma$1.studentInstitution.findMany({
    where,
    include: {
      student: {
        include: {
          user: {
            select: {
              firstName: true,
              lastName: true,
              email: true,
              phone: true
            }
          }
        }
      },
      exam: { select: { examName: true } },
      institution: {
        select: {
          user: {
            select: {
              institutionName: true,
              firstName: true,
              email: true
            }
          }
        }
      },
      subscriptions: {
        where: { isCurrent: true },
        select: {
          status: true,
          endDate: true,
          plan: { select: { planName: true } }
        }
      }
    },
    orderBy: { createdAt: "desc" }
  });
  const rows = studentInstitutions.map((si, index) => {
    const latestSub = si.subscriptions?.[0] ?? null;
    const studentName = si.student?.user ? `${si.student.user.firstName} ${si.student.user.lastName}`.trim() : "—";
    const studentEmail = si.student?.user?.email || "—";
    const studentInfo = `${studentName || "—"}
${studentEmail}`;
    const status = latestSub?.status && latestSub?.status !== "UNPAID" ? latestSub.status : "";
    return [
      `${index + 1}`,
      studentInfo,
      si.student?.user?.phone || "—",
      si.exam?.examName || "—",
      si.language || "—",
      si.isVerified ? "Verified" : "Pending",
      status,
      latestSub?.plan?.planName || "—",
      latestSub?.endDate ? dayjs(latestSub.endDate).format("DD/MM/YYYY hh:mm A") : "—",
      dayjs(si.createdAt).format("DD/MM/YYYY hh:mm A")
    ];
  });
  const doc = new PDFDocument({ size: "A4", layout: "landscape", margin: 24 });
  res.setHeader("Content-Type", "application/pdf");
  res.setHeader("Content-Disposition", `attachment; filename=Exam_Student_Report_${dayjs().format("YYYY-MM-DD")}.pdf`);
  doc.pipe(res);
  const white = "#ffffff";
  const primary = "#1976d2";
  const textColor = "#1f2937";
  const altRow = "#f5f7fa";
  const mutedText = "#cbd5e1";
  const margin = 24;
  const headerH = 66;
  const tableTop = margin + headerH + 14;
  const tableWidth = doc.page.width - margin * 2;
  const baseWidths = [50, 200, 85, 105, 80, 65, 85, 95, 75, 85];
  const totalBaseWidth = baseWidths.reduce((sum, value) => sum + value, 0);
  const columnWidths = baseWidths.map((value) => Math.round(value / totalBaseWidth * tableWidth));
  columnWidths[columnWidths.length - 1] = tableWidth - columnWidths.slice(0, -1).reduce((sum, value) => sum + value, 0);
  const headers = [
    "S.No",
    "Student Info",
    "Phone",
    "Exam",
    "Language",
    "Verified",
    "Status",
    "Plan",
    "Expiry",
    "Registered"
  ];
  const logoPath = path.resolve(process.cwd(), "public", "logo.png");
  const hasLogo = fs.existsSync(logoPath);
  const drawPageBackground = () => {
    doc.save();
    doc.rect(0, 0, doc.page.width, doc.page.height).fill("#eef1f5");
    doc.roundedRect(margin - 6, margin - 6, doc.page.width - (margin - 6) * 2, doc.page.height - (margin - 6) * 2, 8).fill(white);
    doc.restore();
  };
  const drawHeader = () => {
    doc.roundedRect(margin, margin, tableWidth, headerH, 10).fill(primary);
    if (hasLogo) {
      doc.roundedRect(margin + 14, margin + 13, 40, 40, 6).fill(white);
      try {
        doc.image(logoPath, margin + 18, margin + 17, { fit: [32, 32], align: "center", valign: "center" });
      } catch (imgError) {
        console.error("Failed to render logo image:", imgError);
      }
    }
    const textStartX = hasLogo ? margin + 64 : margin + 20;
    doc.font("Helvetica-Bold").fontSize(20).fillColor(white).text("Exam Infra", textStartX, margin + 14);
    doc.font("Helvetica").fontSize(9).fillColor(mutedText).text("Student Report", textStartX, margin + 38);
    if (institutionNameFromRequest) {
      doc.font("Helvetica").fontSize(9).fillColor(white).text(`Institution: ${institutionNameFromRequest}`, textStartX, margin + 52);
    }
    doc.font("Helvetica-Bold").fontSize(9).fillColor(white).text(`Generated: ${dayjs().format("DD MMM YYYY, hh:mm A")}`, doc.page.width - margin - 240, margin + 14, { width: 220, align: "right" });
    doc.font("Helvetica").fontSize(9).fillColor(mutedText).text(`Total Records: ${rows.length}`, doc.page.width - margin - 240, margin + 36, { width: 220, align: "right" });
  };
  const drawTableHeader = (y) => {
    doc.rect(margin, y, tableWidth, 28).fill(primary);
    doc.font("Helvetica-Bold").fontSize(9).fillColor(white);
    let x = margin;
    headers.forEach((header, idx) => {
      doc.text(header, x + 8, y + 8, { width: columnWidths[idx] - 16, align: "left" });
      x += columnWidths[idx];
    });
    return y + 28;
  };
  drawPageBackground();
  drawHeader();
  let rowY = drawTableHeader(tableTop);
  const rowFontSize = 8.5;
  const rowPadding = 6;
  const maxY = doc.page.height - margin - 20;
  doc.on("pageAdded", () => {
    drawPageBackground();
    rowY = drawTableHeader(tableTop);
  });
  rows.forEach((row, index) => {
    doc.font("Helvetica").fontSize(rowFontSize);
    const rowHeight = Math.max(
      28,
      ...row.map((cell, idx) => {
        return doc.heightOfString(String(cell), { width: columnWidths[idx] - 16 }) + rowPadding * 2;
      })
    );
    if (rowY + rowHeight > maxY) {
      doc.addPage();
    }
    doc.rect(margin, rowY, tableWidth, rowHeight).fill(index % 2 === 0 ? altRow : white);
    doc.moveTo(margin, rowY + rowHeight).lineTo(margin + tableWidth, rowY + rowHeight);
    let x = margin;
    row.forEach((cell, idx) => {
      doc.fillColor(textColor).font("Helvetica").fontSize(rowFontSize);
      doc.text(String(cell), x + 8, rowY + rowPadding, {
        width: columnWidths[idx] - 16,
        align: "left"
      });
      x += columnWidths[idx];
    });
    rowY += rowHeight;
  });
  doc.rect(margin, tableTop, tableWidth, rowY - tableTop);
  doc.end();
});
const getInstitutionsList = asyncHandler(
  async (req, res) => {
    const search = req.query.search?.trim() || "";
    const page = parseInt(req.query.page || "1");
    const limitStr = req.query.limit;
    const limit = limitStr !== void 0 ? parseInt(limitStr) : 10;
    const skip = limit > 0 ? (page - 1) * limit : 0;
    const where = {};
    if (search) {
      where.user = {
        ...where.user || {},
        OR: [
          { institutionName: { contains: search } },
          { firstName: { contains: search } },
          { email: { contains: search } }
        ]
      };
    }
    const isVerifiedStr = req.query.isVerified;
    if (isVerifiedStr !== void 0) {
      where.user = {
        ...where.user || {},
        isVerified: isVerifiedStr === "true"
      };
    }
    const [institutions, total] = await Promise.all([
      prisma$1.institution.findMany({
        where,
        select: {
          id: true,
          user: { select: { institutionName: true, firstName: true, email: true } }
        },
        skip,
        take: limit > 0 ? limit : void 0,
        orderBy: { createdAt: "desc" }
      }),
      prisma$1.institution.count({ where })
    ]);
    const data = institutions.map((inst) => ({
      id: inst.id,
      name: inst.user?.institutionName || inst.user?.firstName || inst.user?.email || "—"
    }));
    res.status(200).json({
      data,
      meta: { total, page, limit, totalPages: Math.ceil(total / limit) }
    });
  }
);
const formatDateShort$1 = (date) => {
  return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
};
const getWeekRangeLabel$1 = (weekIndex, fromDate) => {
  const startOfWeek = new Date(fromDate);
  startOfWeek.setDate(fromDate.getDate() + weekIndex * 7);
  const endOfWeek = new Date(startOfWeek);
  endOfWeek.setDate(startOfWeek.getDate() + 6);
  return `From ${formatDateShort$1(startOfWeek)} to ${formatDateShort$1(endOfWeek)}`;
};
const getWeekLabelForDate$1 = (createdDate, fromDate) => {
  const diffTime = Math.abs(createdDate.getTime() - fromDate.getTime());
  const diffDays = Math.ceil(diffTime / (1e3 * 60 * 60 * 24));
  const weekIndex = Math.min(Math.ceil(diffDays / 7), 13) - 1;
  return getWeekRangeLabel$1(Math.max(0, weekIndex), fromDate);
};
const getStudentPracticeWeeklyTrend = asyncHandler(async (req, res) => {
  const user = req.user;
  let studentId = req.params.studentId;
  if (!studentId) {
    const viewStudentId = req.query.viewStudentId;
    if (viewStudentId && ["ADMIN", "INSTITUTION", "STAFF"].includes(user.role.toUpperCase())) {
      studentId = viewStudentId;
    } else {
      studentId = user.studentInstitutionId;
    }
  }
  if (!studentId) {
    res.status(400).json({ message: "Student institution ID missing or invalid student" });
    return;
  }
  const ninetyDaysAgo = /* @__PURE__ */ new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
  ninetyDaysAgo.setHours(0, 0, 0, 0);
  const examId = req.query.examId;
  const enrollments = await prisma$1.studentInstitution.findMany({
    where: {
      OR: [
        { id: studentId },
        { studentId }
      ]
    },
    include: { exam: true },
    orderBy: { createdAt: "desc" }
  });
  if (enrollments.length === 0) {
    res.status(404).json({ message: "Student profile records not found" });
    return;
  }
  let activeEnrollment = enrollments[0];
  const targetIds = enrollments.map((e) => e.id);
  const currentExamId = examId || activeEnrollment.examsId;
  const subjectId = req.query.subjectId;
  const filterBySubject = subjectId && subjectId !== "all";
  const whereClause = {
    studentInstitutionId: { in: targetIds },
    practiceTestId: { not: null },
    createdAt: { gte: ninetyDaysAgo },
    practiceTest: {
      examId: currentExamId
    }
  };
  let subjectName = void 0;
  if (filterBySubject) {
    const subject = await prisma$1.subject.findUnique({
      where: { id: subjectId },
      select: { subjectName: true }
    });
    if (subject) {
      subjectName = subject.subjectName;
    }
  }
  const results = await prisma$1.examResult.findMany({
    where: whereClause,
    orderBy: { createdAt: "asc" }
  });
  let questionInfoMap = null;
  if (filterBySubject) {
    const allQuestionIds = /* @__PURE__ */ new Set();
    results.forEach((r) => {
      const answers = Array.isArray(r.answers) ? r.answers : [];
      answers.forEach((ans) => {
        if (ans.questionId) allQuestionIds.add(ans.questionId);
      });
    });
    if (allQuestionIds.size > 0) {
      const questions = await prisma$1.questionBank.findMany({
        where: { id: { in: Array.from(allQuestionIds) } },
        select: { id: true, subjectId: true, marks: true }
      });
      questionInfoMap = new Map(questions.map((q) => [q.id, { subjectId: q.subjectId, marks: q.marks || 1 }]));
    }
  }
  const weeklyDataMap = {};
  const orderedWeeks = [];
  for (let w = 0; w < 13; w++) {
    const label = getWeekRangeLabel$1(w, ninetyDaysAgo);
    weeklyDataMap[label] = [];
    orderedWeeks.push(label);
  }
  results.forEach((r) => {
    const weekLabel = getWeekLabelForDate$1(new Date(r.createdAt), ninetyDaysAgo);
    if (!weeklyDataMap[weekLabel]) return;
    let scorePercentage;
    if (filterBySubject && questionInfoMap) {
      const answers = Array.isArray(r.answers) ? r.answers : [];
      let subjectObtained = 0;
      let subjectTotal = 0;
      for (const ans of answers) {
        const qInfo = questionInfoMap.get(ans.questionId);
        if (qInfo && qInfo.subjectId === subjectId) {
          subjectTotal += qInfo.marks;
          if (ans.isCorrect) subjectObtained += qInfo.marks;
        }
      }
      if (subjectTotal === 0) return;
      scorePercentage = subjectObtained / subjectTotal * 100;
    } else {
      const totalMarks = r.totalMarks || 100;
      scorePercentage = r.obtainedMarks / totalMarks * 100;
    }
    weeklyDataMap[weekLabel].push(scorePercentage);
  });
  const graphData = orderedWeeks.map((week) => {
    const scores = weeklyDataMap[week];
    if (scores.length === 0) {
      return { week, percentage: null };
    }
    const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
    return { week, percentage: Math.round(avg) };
  });
  res.status(200).json({
    message: "Practice test report fetched successfully",
    period: {
      from: ninetyDaysAgo.toISOString().split("T")[0],
      to: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
      totalDays: 90
    },
    data: {
      examId: activeEnrollment?.examsId || "",
      examName: activeEnrollment?.exam?.examName || "—",
      ...subjectId && subjectId !== "all" ? { subjectId, subjectName } : {},
      graphData
    }
  });
});
const getStudentPracticeSubjectAvg = asyncHandler(async (req, res) => {
  const { studentId } = req.params;
  const sortParam = req.query.sort?.toLowerCase();
  const sortOrder = sortParam === "asc" ? "asc" : "desc";
  const page = Math.max(1, parseInt(req.query.page) || 1);
  const limit = Math.max(1, parseInt(req.query.limit) || 10);
  const startIndex = (page - 1) * limit;
  const endIndex = page * limit;
  const examIdQuery = req.query.examId;
  const ninetyDaysAgo = /* @__PURE__ */ new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
  ninetyDaysAgo.setHours(0, 0, 0, 0);
  const enrollments = await prisma$1.studentInstitution.findMany({
    where: {
      OR: [
        { id: studentId },
        { studentId }
      ]
    },
    include: { exam: true },
    orderBy: { createdAt: "desc" }
  });
  if (enrollments.length === 0) {
    res.status(404).json({ message: "Student profile records not found" });
    return;
  }
  let studentInst = enrollments[0];
  const targetIds = enrollments.map((e) => e.id);
  const currentExamId = examIdQuery || studentInst.examsId;
  const language = studentInst.language || "English";
  let availableSubjects = [];
  if (currentExamId) {
    availableSubjects = await prisma$1.subject.findMany({
      where: {
        OR: [
          {
            questions: {
              some: {
                practiceTests: {
                  some: {
                    examId: currentExamId,
                    language: { in: [language, language.toLowerCase(), language.toUpperCase()] }
                  }
                }
              }
            }
          },
          {
            questions: {
              some: {
                testQuestions: {
                  some: {
                    test: {
                      practiceTests: {
                        some: {
                          examId: currentExamId,
                          language: { in: [language, language.toLowerCase(), language.toUpperCase()] }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        ]
      },
      select: {
        id: true,
        subjectName: true,
        institutionId: true,
        institution: { select: { user: { select: { institutionName: true } } } }
      }
    });
  }
  const results = await prisma$1.examResult.findMany({
    where: {
      studentInstitutionId: { in: targetIds },
      practiceTestId: { not: null },
      createdAt: { gte: ninetyDaysAgo },
      practiceTest: {
        examId: currentExamId
      }
    }
  });
  const allQuestionIds = /* @__PURE__ */ new Set();
  results.forEach((r) => {
    const answers = Array.isArray(r.answers) ? r.answers : [];
    answers.forEach((ans) => {
      if (ans.questionId) allQuestionIds.add(ans.questionId);
    });
  });
  const allQuestions = await prisma$1.questionBank.findMany({
    where: { id: { in: Array.from(allQuestionIds) } },
    include: { subject: { select: { subjectName: true } } }
  });
  const questionInfoMap = new Map(allQuestions.map((q) => [
    q.id,
    { marks: q.marks || 1, subjectName: q.subject?.subjectName || "Unknown" }
  ]));
  const subjectStats = {};
  const subjectIdMap = {};
  const user = req.user;
  let publishedSubjects = [];
  if (currentExamId) {
    publishedSubjects = await prisma$1.subject.findMany({
      where: {
        OR: [
          {
            questions: {
              some: {
                practiceTests: {
                  some: {
                    examId: currentExamId,
                    publish: true,
                    language: { in: [language, language.toLowerCase(), language.toUpperCase()] }
                  }
                }
              }
            }
          },
          {
            questions: {
              some: {
                testQuestions: {
                  some: {
                    test: {
                      practiceTests: {
                        some: {
                          examId: currentExamId,
                          publish: true,
                          language: { in: [language, language.toLowerCase(), language.toUpperCase()] }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        ]
      },
      select: { subjectName: true }
    });
  }
  const publishedSubjectNames = new Set(publishedSubjects.map((s) => s.subjectName));
  availableSubjects.forEach((s) => {
    const isShared = user.role !== "ADMIN" && s.institutionId !== user.institutionId;
    const sharedInstitutionName = isShared ? s.institution?.user?.institutionName : null;
    if (publishedSubjectNames.has(s.subjectName)) {
      subjectStats[s.subjectName] = { totalMarks: 0, obtainedMarks: 0, hasSubmissions: false };
    }
    subjectIdMap[s.subjectName] = { id: s.id, isShared, sharedInstitutionName };
  });
  results.forEach((r) => {
    const answers = Array.isArray(r.answers) ? r.answers : [];
    answers.forEach((ans) => {
      const qInfo = questionInfoMap.get(ans.questionId);
      if (qInfo) {
        if (!subjectStats[qInfo.subjectName]) {
          subjectStats[qInfo.subjectName] = { totalMarks: 0, obtainedMarks: 0, hasSubmissions: false };
        }
        subjectStats[qInfo.subjectName].hasSubmissions = true;
        subjectStats[qInfo.subjectName].totalMarks += qInfo.marks;
        if (ans.isCorrect) {
          subjectStats[qInfo.subjectName].obtainedMarks += qInfo.marks;
        }
      }
    });
  });
  let fullChartData = Object.entries(subjectStats).map(([subject, stats]) => {
    const subInfo = subjectIdMap[subject] || { id: null, isShared: false, sharedInstitutionName: null };
    if (!stats.hasSubmissions) {
      return {
        subject,
        subjectId: subInfo.id,
        isShared: subInfo.isShared,
        sharedInstitutionName: subInfo.sharedInstitutionName,
        percentage: null,
        hasSubmissions: false
      };
    }
    const percentage = stats.totalMarks > 0 ? stats.obtainedMarks / stats.totalMarks * 100 : 0;
    return {
      subject,
      subjectId: subInfo.id,
      isShared: subInfo.isShared,
      sharedInstitutionName: subInfo.sharedInstitutionName,
      percentage: Math.round(percentage * 100) / 100,
      hasSubmissions: true
    };
  });
  fullChartData.sort((a, b) => {
    const aMissing = a.percentage === null || a.percentage === void 0;
    const bMissing = b.percentage === null || b.percentage === void 0;
    if (aMissing && bMissing) return 0;
    if (sortOrder === "asc") {
      if (aMissing) return -1;
      if (bMissing) return 1;
      return a.percentage - b.percentage;
    } else {
      if (aMissing) return 1;
      if (bMissing) return -1;
      return b.percentage - a.percentage;
    }
  });
  const totalSubjects = fullChartData.length;
  const totalPages = Math.ceil(totalSubjects / limit);
  const paginatedChartData = fullChartData.slice(startIndex, endIndex);
  res.status(200).json({
    message: "Subjects report fetched successfully",
    timeframe: "Last 90 Days",
    pagination: {
      totalSubjects,
      totalPages,
      currentPage: page,
      limit
    },
    data: {
      examId: studentInst?.examsId || "",
      examName: studentInst?.exam?.examName || "—",
      currentSortOrder: sortOrder,
      chartData: paginatedChartData
    }
  });
});
const getStudentMockWeeklyTrend = asyncHandler(async (req, res) => {
  const user = req.user;
  let studentId = req.params.studentId;
  if (!studentId) {
    const viewStudentId = req.query.viewStudentId;
    if (viewStudentId && ["ADMIN", "INSTITUTION", "STAFF"].includes(user.role.toUpperCase())) {
      studentId = viewStudentId;
    } else {
      studentId = user.studentInstitutionId;
    }
  }
  if (!studentId) {
    res.status(400).json({ message: "Student institution ID missing or invalid student" });
    return;
  }
  const ninetyDaysAgo = /* @__PURE__ */ new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
  ninetyDaysAgo.setHours(0, 0, 0, 0);
  const examId = req.query.examId;
  const enrollments = await prisma$1.studentInstitution.findMany({
    where: {
      OR: [
        { id: studentId },
        { studentId }
      ]
    },
    include: { exam: true },
    orderBy: { createdAt: "desc" }
  });
  if (enrollments.length === 0) {
    res.status(404).json({ message: "Student profile records not found" });
    return;
  }
  const targetIds = enrollments.map((e) => e.id);
  const currentExamId = examId || enrollments[0].examsId;
  const activeEnrollment = enrollments.find((e) => e.examsId === currentExamId) || enrollments[0];
  const results = await prisma$1.mockTestResult.findMany({
    where: {
      studentInstitutionId: { in: targetIds },
      createdAt: { gte: ninetyDaysAgo },
      mockTest: {
        examId: currentExamId
      }
    },
    orderBy: { createdAt: "asc" }
  });
  const weeklyDataMap = {};
  const orderedWeeks = [];
  for (let w = 0; w < 13; w++) {
    const label = getWeekRangeLabel$1(w, ninetyDaysAgo);
    weeklyDataMap[label] = [];
    orderedWeeks.push(label);
  }
  results.forEach((r) => {
    const weekLabel = getWeekLabelForDate$1(new Date(r.createdAt), ninetyDaysAgo);
    const totalMarks = r.totalMarks || 100;
    const scorePercentage = r.obtainedMarks / totalMarks * 100;
    if (weeklyDataMap[weekLabel]) {
      weeklyDataMap[weekLabel].push(scorePercentage);
    }
  });
  const graphData = orderedWeeks.map((week) => {
    const scores = weeklyDataMap[week];
    if (scores.length === 0) {
      return { week, percentage: null };
    }
    const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
    return { week, percentage: Math.round(avg) };
  });
  res.status(200).json({
    message: "Mock test report fetched successfully",
    period: {
      from: ninetyDaysAgo.toISOString().split("T")[0],
      to: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
      totalDays: 90
    },
    data: {
      examId: activeEnrollment?.examsId || "",
      examName: activeEnrollment?.exam?.examName || "—",
      graphData
    }
  });
});
const getStudentPyqWeeklyTrend = asyncHandler(async (req, res) => {
  const user = req.user;
  let studentId = req.params.studentId;
  if (!studentId) {
    const viewStudentId = req.query.viewStudentId;
    if (viewStudentId && ["ADMIN", "INSTITUTION", "STAFF"].includes(user.role.toUpperCase())) {
      studentId = viewStudentId;
    } else {
      studentId = user.studentInstitutionId;
    }
  }
  if (!studentId) {
    res.status(400).json({ message: "Student institution ID missing or invalid student" });
    return;
  }
  const ninetyDaysAgo = /* @__PURE__ */ new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
  ninetyDaysAgo.setHours(0, 0, 0, 0);
  const examId = req.query.examId;
  const enrollments = await prisma$1.studentInstitution.findMany({
    where: {
      OR: [
        { id: studentId },
        { studentId }
      ]
    },
    include: { exam: true },
    orderBy: { createdAt: "desc" }
  });
  if (enrollments.length === 0) {
    res.status(404).json({ message: "Student profile records not found" });
    return;
  }
  let activeEnrollment = enrollments[0];
  const targetIds = enrollments.map((e) => e.id);
  const currentExamId = examId || activeEnrollment.examsId;
  const results = await prisma$1.examResult.findMany({
    where: {
      studentInstitutionId: { in: targetIds },
      oldQuestionPaperId: { not: null },
      createdAt: { gte: ninetyDaysAgo },
      oldQuestionPaper: {
        examId: currentExamId
      }
    },
    orderBy: { createdAt: "asc" }
  });
  const weeklyDataMap = {};
  const orderedWeeks = [];
  for (let w = 0; w < 13; w++) {
    const label = getWeekRangeLabel$1(w, ninetyDaysAgo);
    weeklyDataMap[label] = [];
    orderedWeeks.push(label);
  }
  results.forEach((r) => {
    const weekLabel = getWeekLabelForDate$1(new Date(r.createdAt), ninetyDaysAgo);
    const totalMarks = r.totalMarks || 100;
    const scorePercentage = r.obtainedMarks / totalMarks * 100;
    if (weeklyDataMap[weekLabel]) {
      weeklyDataMap[weekLabel].push(scorePercentage);
    }
  });
  const graphData = orderedWeeks.map((week) => {
    const scores = weeklyDataMap[week];
    if (scores.length === 0) {
      return { week, percentage: null };
    }
    const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
    return { week, percentage: Math.round(avg) };
  });
  res.status(200).json({
    message: "PYQ report fetched successfully",
    period: {
      from: ninetyDaysAgo.toISOString().split("T")[0],
      to: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
      totalDays: 90
    },
    data: {
      examId: activeEnrollment?.examsId || "",
      examName: activeEnrollment?.exam?.examName || "—",
      graphData
    }
  });
});
const staffsReportPDF = asyncHandler(async (req, res) => {
  const user = req.user;
  const isAdmin = user.role === "ADMIN";
  const institutionId = isAdmin ? req.query.institutionId : user.institutionId;
  const search = req.query.search?.trim() || "";
  const isVerified = req.query.isVerified?.trim();
  const regStartDate = req.query.regStartDate?.trim();
  const regEndDate = req.query.regEndDate?.trim();
  let institutionNameFromRequest = null;
  if (institutionId) {
    const institutionRecord = await prisma$1.institution.findUnique({
      where: { id: institutionId },
      select: {
        user: {
          select: {
            institutionName: true,
            firstName: true,
            email: true
          }
        }
      }
    });
    institutionNameFromRequest = institutionRecord?.user?.institutionName || institutionRecord?.user?.firstName || institutionRecord?.user?.email || null;
  }
  const where = {
    user: { isDeleted: false }
  };
  if (institutionId) {
    where.institutionId = institutionId;
  }
  if (search) {
    const searchWords = search.split(/\s+/).filter(Boolean);
    where.OR = [
      { user: { firstName: { contains: search } } },
      { user: { lastName: { contains: search } } },
      { user: { email: { contains: search } } },
      { user: { phone: { contains: search } } },
      { role: { roleName: { contains: search } } },
      ...searchWords.length > 1 ? [{
        user: {
          AND: searchWords.map((word) => ({
            OR: [
              { firstName: { contains: word } },
              { lastName: { contains: word } }
            ]
          }))
        }
      }] : []
    ];
  }
  if (isVerified) {
    where.user = where.user || {};
    where.user.isVerified = isVerified === "true";
  }
  if (regStartDate || regEndDate) {
    where.user = where.user || {};
    where.user.createdAt = {};
    const convertToISOFormat = (dateStr) => {
      const parts = dateStr.split("/");
      if (parts.length === 3) {
        const [day, month, year] = parts;
        return `${year}-${month}-${day}`;
      }
      return dateStr;
    };
    if (regStartDate) {
      const formattedStartDate = convertToISOFormat(regStartDate);
      where.user.createdAt.gte = /* @__PURE__ */ new Date(`${formattedStartDate}T00:00:00.000Z`);
    }
    if (regEndDate) {
      const formattedEndDate = convertToISOFormat(regEndDate);
      where.user.createdAt.lte = /* @__PURE__ */ new Date(`${formattedEndDate}T23:59:59.999Z`);
    }
  }
  const staffs = await prisma$1.staff.findMany({
    where,
    include: {
      user: true,
      role: true,
      institution: {
        select: {
          user: {
            select: {
              institutionName: true,
              firstName: true,
              email: true
            }
          }
        }
      }
    },
    orderBy: { createdAt: "desc" }
  });
  const rows = staffs.map((staff, index) => {
    const staffName = `${staff.user?.firstName || ""} ${staff.user?.lastName || ""}`.trim() || "—";
    const staffEmail = staff.user?.email || "—";
    const staffInfo = `${staffName}
${staffEmail}`;
    const joinedValue = staff.createdAt;
    return [
      `${index + 1}`,
      staffInfo,
      staff.user?.phone || "—",
      staff.role?.roleName || "Staff",
      staff.user?.isVerified ? "Verified" : "Pending",
      joinedValue ? dayjs(joinedValue).format("DD/MM/YYYY hh:mm A") : "—"
    ];
  });
  const doc = new PDFDocument({ size: "A4", layout: "landscape", margin: 24 });
  res.setHeader("Content-Type", "application/pdf");
  res.setHeader("Content-Disposition", `attachment; filename=Exam_Staff_Report_${dayjs().format("YYYY-MM-DD")}.pdf`);
  doc.pipe(res);
  const white = "#ffffff";
  const primary = "#1976d2";
  const textColor = "#1f2937";
  const altRow = "#f5f7fa";
  const mutedText = "#cbd5e1";
  const margin = 24;
  const headerH = 66;
  const tableTop = margin + headerH + 14;
  const tableWidth = doc.page.width - margin * 2;
  const baseWidths = [40, 200, 100, 100, 80, 120];
  const totalBaseWidth = baseWidths.reduce((sum, value) => sum + value, 0);
  const columnWidths = baseWidths.map((value) => Math.round(value / totalBaseWidth * tableWidth));
  columnWidths[columnWidths.length - 1] = tableWidth - columnWidths.slice(0, -1).reduce((sum, value) => sum + value, 0);
  const headers = [
    "S.No",
    "Staff Info",
    "Phone",
    "Role",
    "Status",
    "Joined"
  ];
  const logoPath = path.resolve(process.cwd(), "public", "logo.png");
  const hasLogo = fs.existsSync(logoPath);
  const drawPageBackground = () => {
    doc.save();
    doc.rect(0, 0, doc.page.width, doc.page.height).fill("#eef1f5");
    doc.roundedRect(margin - 6, margin - 6, doc.page.width - (margin - 6) * 2, doc.page.height - (margin - 6) * 2, 8).fill(white);
    doc.restore();
  };
  const drawHeader = () => {
    doc.roundedRect(margin, margin, tableWidth, headerH, 10).fill(primary);
    if (hasLogo) {
      doc.roundedRect(margin + 14, margin + 13, 40, 40, 6).fill(white);
      try {
        doc.image(logoPath, margin + 18, margin + 17, { fit: [32, 32], align: "center", valign: "center" });
      } catch (imgError) {
        console.error("Failed to render logo image:", imgError);
      }
    }
    const textStartX = hasLogo ? margin + 64 : margin + 20;
    doc.font("Helvetica-Bold").fontSize(20).fillColor(white).text("Exam Infra", textStartX, margin + 14);
    doc.font("Helvetica").fontSize(9).fillColor(mutedText).text("Staff Report", textStartX, margin + 38);
    if (institutionNameFromRequest) {
      doc.font("Helvetica").fontSize(9).fillColor(white).text(`Institution: ${institutionNameFromRequest}`, textStartX, margin + 52);
    }
    doc.font("Helvetica-Bold").fontSize(9).fillColor(white).text(`Generated: ${dayjs().format("DD MMM YYYY, hh:mm A")}`, doc.page.width - margin - 240, margin + 14, { width: 220, align: "right" });
    doc.font("Helvetica").fontSize(9).fillColor(mutedText).text(`Total Records: ${rows.length}`, doc.page.width - margin - 240, margin + 36, { width: 220, align: "right" });
  };
  const drawTableHeader = (y) => {
    doc.rect(margin, y, tableWidth, 28).fill(primary);
    doc.font("Helvetica-Bold").fontSize(9).fillColor(white);
    let x = margin;
    headers.forEach((header, idx) => {
      doc.text(header, x + 8, y + 8, { width: columnWidths[idx] - 16, align: "left" });
      x += columnWidths[idx];
    });
    return y + 28;
  };
  drawPageBackground();
  drawHeader();
  let rowY = drawTableHeader(tableTop);
  const rowFontSize = 8.5;
  const rowPadding = 6;
  const maxY = doc.page.height - margin - 20;
  doc.on("pageAdded", () => {
    drawPageBackground();
    rowY = drawTableHeader(tableTop);
  });
  rows.forEach((row, index) => {
    doc.font("Helvetica").fontSize(rowFontSize);
    const rowHeight = Math.max(
      28,
      ...row.map((cell, idx) => {
        return doc.heightOfString(String(cell), { width: columnWidths[idx] - 16 }) + rowPadding * 2;
      })
    );
    if (rowY + rowHeight > maxY) {
      doc.addPage();
    }
    doc.rect(margin, rowY, tableWidth, rowHeight).fill(index % 2 === 0 ? altRow : white);
    doc.moveTo(margin, rowY + rowHeight).lineTo(margin + tableWidth, rowY + rowHeight);
    let x = margin;
    row.forEach((cell, idx) => {
      doc.fillColor(textColor).font("Helvetica").fontSize(rowFontSize);
      doc.text(String(cell), x + 8, rowY + rowPadding, {
        width: columnWidths[idx] - 16,
        align: "left"
      });
      x += columnWidths[idx];
    });
    rowY += rowHeight;
  });
  doc.end();
});
const router$m = express.Router();
router$m.get("/student", protect, permit("Student"), studentPracticeTestList);
router$m.get("/student/subject", protect, permit("Student"), getStudentSubjects);
router$m.get("/student/practice-test-report", protect, permit("Student", "Admin", "Institution", "Staff"), getStudentPracticeWeeklyTrend);
router$m.get("/student/pyq-report", protect, permit("Student", "Admin", "Institution", "Staff"), getStudentPyqWeeklyTrend);
router$m.get("/student/last-test", protect, permit("Student"), getLastTest);
router$m.get("/student/:id", protect, permit("Student"), getTestById$1);
router$m.post(
  "/student/submit",
  protect,
  permit("Student"),
  (req, res, next) => {
    req.body.testType = "PracticeTest";
    next();
  },
  submitExam
);
router$m.route("/").post(protect, permit("Admin", "Institution", "Staff"), createPracticeTest).get(protect, permit("Admin", "Institution", "Staff"), listPracticeTest);
router$m.post("/map-test-to-exam", protect, permit("Admin", "Institution", "Staff"), mapTestToExam);
router$m.route("/:id").get(protect, permit("Admin", "Institution", "Staff"), practiceTestById).put(protect, permit("Admin", "Institution", "Staff"), updatePracticeTest).delete(protect, permit("Admin", "Institution", "Staff"), deletePracticeTest);
router$m.route("/questions/:id").get(protect, permit("Admin", "Institution", "Staff"), getPracticeTestQuestions);
const createTest = asyncHandler(async (req, res) => {
  const user = req.user;
  const { title, subjectId, topic, questionIds, language, duration, marks } = req.body;
  if (!questionIds || !Array.isArray(questionIds) || questionIds.length === 0) {
    res.status(400).json({ message: "At least one Question ID is required" });
    return;
  }
  const testTitle = title || "New Test";
  const existingTest = await prisma$1.test.findFirst({
    where: {
      title: testTitle,
      institutionId: user.institutionId
    }
  });
  if (existingTest) {
    res.status(400).json({ message: "Test with this title already exists in the institution" });
    return;
  }
  const test = await prisma$1.test.create({
    data: {
      title: title || "New Test",
      subjectId,
      topic,
      language,
      duration: duration ? Number(duration) : void 0,
      marks: marks ? Number(marks) : void 0,
      institutionId: user.institutionId,
      createdById: user.id,
      testQuestions: {
        create: questionIds.map((qId, index) => ({
          questionId: qId
        }))
      }
    },
    include: {
      testQuestions: true
    }
  });
  res.status(201).json({
    message: "Test created successfully",
    data: {
      ...test,
      createdAt: toIST(test.createdAt),
      updatedAt: toIST(test.updatedAt)
    }
  });
});
const getTestsBySubject = asyncHandler(async (req, res) => {
  const user = req.user;
  const { subjectId } = req.params;
  const tests = await prisma$1.test.findMany({
    where: {
      subjectId,
      institutionId: user.institutionId
    },
    include: {
      _count: {
        select: { testQuestions: true, practiceTests: true }
      },
      practiceTests: {
        include: {
          _count: { select: { examResults: true } }
        }
      }
    },
    orderBy: {
      createdAt: "desc"
    }
  });
  res.status(200).json({
    data: tests.map((test) => {
      const hasResults = test.practiceTests.some((pt) => pt._count.examResults > 0);
      const { practiceTests, ...restTest } = test;
      return {
        ...restTest,
        hasResults,
        createdAt: toIST(test.createdAt),
        updatedAt: toIST(test.updatedAt)
      };
    })
  });
});
const getTestById = asyncHandler(async (req, res) => {
  const user = req.user;
  const { id } = req.params;
  const test = await prisma$1.test.findFirst({
    where: { id, institutionId: user.institutionId },
    include: {
      subject: { select: { id: true, subjectName: true, language: true } },
      testQuestions: {
        include: { question: true }
      }
    }
  });
  if (!test) {
    res.status(404).json({ message: "Test not found" });
    return;
  }
  res.status(200).json({
    data: {
      ...test,
      createdAt: toIST(test.createdAt),
      updatedAt: toIST(test.updatedAt)
    }
  });
});
const getTestQuestions = asyncHandler(async (req, res) => {
  const { testId } = req.params;
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const search = req.query.search || "";
  const skip = (page - 1) * limit;
  const testQuestions = await prisma$1.testQuestion.findMany({
    where: {
      testId,
      question: {
        questionText: { contains: search }
      }
    },
    include: {
      question: true
    },
    skip,
    take: limit
  });
  const total = await prisma$1.testQuestion.count({
    where: {
      testId,
      question: {
        questionText: { contains: search }
      }
    }
  });
  res.status(200).json({
    questions: testQuestions.map((tq) => tq.question),
    meta: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit)
    }
  });
});
const getAllTests = asyncHandler(async (req, res) => {
  const user = req.user;
  const { search, subjectId, excludeExamId, page = "1", limit = "10", examId, mapped, language } = req.query;
  const pageNum = parseInt(page) || 1;
  const limitNum = parseInt(limit) || 10;
  const skip = (pageNum - 1) * limitNum;
  const where = {
    institutionId: user.institutionId
  };
  if (search) {
    where.title = { contains: search };
  }
  if (language) {
    where.language = language;
  }
  if (subjectId) {
    if (Array.isArray(subjectId)) {
      where.subjectId = { in: subjectId };
    } else {
      where.subjectId = subjectId;
    }
  }
  if (excludeExamId) {
    where.practiceTests = {
      none: {
        examId: excludeExamId
      }
    };
  }
  let mappedTestIds = [];
  if (examId) {
    const existingMappedTests = await prisma$1.practiceTest.findMany({
      where: {
        examId,
        institutionId: user.institutionId
      },
      select: { testId: true }
    });
    mappedTestIds = existingMappedTests.map((item) => item.testId).filter(Boolean);
    if (mapped === "used") {
      if (mappedTestIds.length > 0) {
        where.id = { in: mappedTestIds };
      } else {
        where.id = { in: [] };
      }
    } else if (mapped === "available") {
      if (mappedTestIds.length > 0) {
        where.id = { notIn: mappedTestIds };
      }
    }
  }
  const [tests, totalCount] = await prisma$1.$transaction([
    prisma$1.test.findMany({
      where,
      include: {
        subject: { select: { subjectName: true, language: true } },
        _count: { select: { testQuestions: true, practiceTests: true } },
        practiceTests: {
          include: {
            _count: { select: { examResults: true } }
          }
        },
        referenceInstitution: {
          select: { user: { select: { institutionName: true, firstName: true, lastName: true } } }
        }
      },
      orderBy: { createdAt: "desc" },
      skip,
      take: limitNum
    }),
    prisma$1.test.count({ where })
  ]);
  const mappedIdSet = new Set(mappedTestIds);
  let stats = void 0;
  if (examId) {
    const subIdStr = typeof subjectId === "string" ? subjectId : void 0;
    const mappedCount = await prisma$1.practiceTest.count({
      where: {
        examId,
        institutionId: user.institutionId,
        test: subIdStr ? { subjectId: subIdStr } : void 0
      }
    });
    const totalTestsCount = await prisma$1.test.count({
      where: {
        institutionId: user.institutionId,
        subjectId: subIdStr ? subIdStr : void 0
      }
    });
    stats = {
      totalCount: totalTestsCount,
      mappedCount,
      availableCount: Math.max(0, totalTestsCount - mappedCount)
    };
  }
  res.status(200).json({
    message: "Successfully fetched tests",
    data: tests.map((test) => {
      const hasResults = test.practiceTests?.some((pt) => pt._count?.examResults > 0) ?? false;
      const { practiceTests, ...restTest } = test;
      return {
        ...restTest,
        isAlreadyMapped: examId ? mappedIdSet.has(test.id) : false,
        hasResults,
        createdAt: toIST(test.createdAt),
        updatedAt: toIST(test.updatedAt)
      };
    }),
    meta: {
      total: totalCount,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(totalCount / limitNum)
    },
    stats
  });
});
const updateTest = asyncHandler(async (req, res) => {
  const user = req.user;
  const { id } = req.params;
  const { title, topic, questionIds, language, duration, marks } = req.body;
  const existing = await prisma$1.test.findFirst({
    where: { id, institutionId: user.institutionId },
    include: { sharedCopies: { select: { id: true } } }
  });
  if (!existing) {
    res.status(404).json({ message: "Test not found" });
    return;
  }
  if (existing.referenceSourceId) {
    res.status(403).json({ message: "Cannot edit a shared test" });
    return;
  }
  if (title && title !== existing.title) {
    const titleExists = await prisma$1.test.findFirst({
      where: {
        title,
        institutionId: user.institutionId,
        id: { not: id }
      }
    });
    if (titleExists) {
      res.status(400).json({ message: "Test with this title already exists in the institution" });
      return;
    }
  }
  const testIdsToUpdate = [id, ...existing.sharedCopies?.map((c) => c.id) || []];
  await prisma$1.testQuestion.deleteMany({ where: { testId: { in: testIdsToUpdate } } });
  const actualMarks = questionIds.length;
  const testTitle = title || existing.title;
  const testTopic = topic !== void 0 ? topic : existing.topic;
  const testLanguage = language ?? existing.language;
  const testDuration = duration ? Number(duration) : existing.duration;
  await prisma$1.test.updateMany({
    where: { id: { in: testIdsToUpdate } },
    data: {
      title: testTitle,
      topic: testTopic,
      language: testLanguage,
      duration: testDuration,
      marks: actualMarks
    }
  });
  const testQuestionData = testIdsToUpdate.flatMap(
    (tId) => questionIds.map((qId) => ({
      testId: tId,
      questionId: qId
    }))
  );
  await prisma$1.testQuestion.createMany({ data: testQuestionData });
  const test = await prisma$1.test.findFirst({
    where: { id },
    include: { testQuestions: true }
  });
  await prisma$1.practiceTest.updateMany({
    where: { testId: { in: testIdsToUpdate } },
    data: {
      title: testTitle,
      language: testLanguage,
      duration: testDuration,
      marks: actualMarks
    }
  });
  res.status(200).json({
    message: "Test updated successfully",
    data: {
      ...test,
      createdAt: toIST(test.createdAt),
      updatedAt: toIST(test.updatedAt)
    }
  });
});
const deleteTest = asyncHandler(async (req, res) => {
  const user = req.user;
  const { id } = req.params;
  const test = await prisma$1.test.findFirst({
    where: { id, institutionId: user.institutionId }
  });
  if (!test) {
    res.status(404).json({ message: "Test not found" });
    return;
  }
  if (test.referenceSourceId) {
    res.status(403).json({ message: "Cannot delete a shared test" });
    return;
  }
  const mappedPracticeTests = await prisma$1.practiceTest.findMany({ where: { testId: id } });
  const ptIds = mappedPracticeTests.map((pt) => pt.id);
  const sharedCopies = await prisma$1.test.findMany({ where: { referenceSourceId: id }, select: { id: true } });
  const sharedTestIds = sharedCopies.map((s) => s.id);
  let allPtIds = [...ptIds];
  if (sharedTestIds.length > 0) {
    const sharedPracticeTests = await prisma$1.practiceTest.findMany({ where: { testId: { in: sharedTestIds } } });
    allPtIds = [...allPtIds, ...sharedPracticeTests.map((pt) => pt.id)];
  }
  if (allPtIds.length > 0) {
    const hasResults = await prisma$1.examResult.findFirst({
      where: { practiceTestId: { in: allPtIds } }
    });
    if (hasResults) {
      res.status(400).json({ message: "Cannot delete this test because it (or its shared copies) is already in use by students." });
      return;
    }
    await prisma$1.practiceTest.deleteMany({ where: { testId: { in: [id, ...sharedTestIds] } } });
  }
  if (sharedTestIds.length > 0) {
    await prisma$1.testQuestion.deleteMany({ where: { testId: { in: sharedTestIds } } });
    await prisma$1.test.deleteMany({ where: { id: { in: sharedTestIds } } });
  }
  await prisma$1.testQuestion.deleteMany({
    where: { testId: id }
  });
  await prisma$1.test.delete({
    where: { id }
  });
  res.status(200).json({
    message: "Test deleted successfully"
  });
});
const router$l = Router();
router$l.post("/", protect, permit("Admin", "Institution", "Staff"), createTest);
router$l.get("/", protect, permit("Admin", "Institution", "Staff"), getAllTests);
router$l.get("/questions/:testId", protect, permit("Admin", "Institution", "Staff"), getTestQuestions);
router$l.get("/subject/:subjectId", protect, permit("Admin", "Institution", "Staff"), getTestsBySubject);
router$l.get("/:id", protect, permit("Admin", "Institution", "Staff"), getTestById);
router$l.put("/:id", protect, permit("Admin", "Institution", "Staff"), updateTest);
router$l.delete("/:id", protect, permit("Admin", "Institution", "Staff"), deleteTest);
const createSupportSchema = yup.object({
  message: yup.string().required("Message is required").min(3, "Message must be at least 3 characters"),
  questionId: yup.string().nullable().required("questionId is required"),
  questionType: yup.string().nullable().required("questionType is required")
});
const createQuestionSupport = asyncHandler(async (req, res) => {
  try {
    await createSupportSchema.validate(req.body, { abortEarly: false });
    const user = req.user;
    const { message, questionId, questionType } = req.body;
    const userId = user.id;
    let question = null;
    if (questionType === "OldQuestion") {
      question = await prisma$1.oldQuestion.findUnique({ where: { id: questionId, institutionId: user.institutionId } });
    } else if (questionType === "QuestionBank") {
      question = await prisma$1.questionBank.findUnique({ where: { id: questionId, institutionId: user.institutionId } });
    } else if (questionType === "Mocktest") {
      question = await prisma$1.questionBank.findUnique({ where: { id: questionId, institutionId: user.institutionId } });
    } else {
      res.status(400).json({ message: "Invalid question type" });
      return;
    }
    if (!question) {
      res.status(404).json({ message: "Question not found" });
      return;
    }
    const support = await prisma$1.support.create({
      data: {
        message,
        questionId,
        userId,
        questionType,
        institutionId: user.institutionId
      }
    });
    res.status(201).json({
      message: "Submitted successfully",
      data: {
        ...support,
        createdAt: toIST(support.createdAt),
        updatedAt: toIST(support.updatedAt)
      }
    });
  } catch (err) {
    if (err instanceof yup.ValidationError) {
      res.status(400).json({
        message: "Validation failed",
        errors: err.errors
      });
      return;
    }
    res.status(500).json({ message: "Server error" });
  }
});
const getAllSupports = asyncHandler(async (req, res) => {
  const user = req.user;
  const page = Math.max(Number(req.query.page) || 1, 1);
  const limit = Math.max(Number(req.query.limit) || 10, 1);
  const skip = (page - 1) * limit;
  const search = req.query.search?.trim();
  const isAdmin = user.role === "ADMIN";
  const where = {};
  if (!isAdmin) {
    where.institutionId = user.institutionId;
  }
  if (search) {
    const searchWords = search.split(/\s+/).filter(Boolean);
    const cleanTicketId = search.replace(/ticket/i, "").replace("#", "").trim();
    where.OR = [
      { id: { contains: cleanTicketId ? cleanTicketId : search } },
      { message: { contains: search } },
      { status: { contains: search } },
      {
        user: {
          OR: [
            { firstName: { contains: search } },
            { lastName: { contains: search } },
            { email: { contains: search } },
            ...searchWords.length > 1 ? [{
              AND: searchWords.map((word) => ({
                OR: [
                  { firstName: { contains: word } },
                  { lastName: { contains: word } }
                ]
              }))
            }] : []
          ]
        }
      }
    ];
  }
  const [supports, total] = await Promise.all([
    prisma$1.support.findMany({
      where,
      include: {
        user: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            email: true
          }
        }
      },
      orderBy: { createdAt: "desc" },
      skip,
      take: limit
    }),
    prisma$1.support.count({ where })
  ]);
  const qbIds = [];
  const oldIds = [];
  const practiceIds = [];
  supports.forEach((s) => {
    if (!s.questionId) return;
    if (s.questionType === "QuestionBank") qbIds.push(s.questionId);
    else if (s.questionType === "OldQuestion") oldIds.push(s.questionId);
    else if (s.questionType === "PracticeTest") practiceIds.push(s.questionId);
  });
  const [qbQuestions, oldQuestions, practiceTests] = await Promise.all([
    prisma$1.questionBank.findMany({
      where: { id: { in: qbIds } },
      select: { id: true, questionText: true }
    }),
    prisma$1.oldQuestion.findMany({
      where: { id: { in: oldIds } },
      select: { id: true, questionText: true }
    }),
    prisma$1.practiceTest.findMany({
      where: { id: { in: practiceIds } },
      select: { id: true, title: true }
    })
  ]);
  const qbMap = new Map(qbQuestions.map((q) => [q.id, q]));
  const oldMap = new Map(oldQuestions.map((q) => [q.id, q]));
  const practiceMap = new Map(practiceTests.map((q) => [q.id, q]));
  const data = supports.map((support) => {
    let question = null;
    if (support.questionType === "QuestionBank") {
      question = qbMap.get(support.questionId);
    } else if (support.questionType === "OldQuestion") {
      question = oldMap.get(support.questionId);
    } else if (support.questionType === "PracticeTest") {
      question = practiceMap.get(support.questionId);
    }
    return {
      id: support.id,
      message: support.message,
      status: support.status,
      questionId: support.questionId,
      questionType: support.questionType,
      question: question ? {
        id: question.id,
        questionText: question.questionText || question.title
      } : null,
      user: support.user ? {
        id: support.user.id,
        name: `${support.user.firstName} ${support.user.lastName}`.trim(),
        email: support.user.email
      } : null,
      createdAt: toIST(support.createdAt),
      updatedAt: toIST(support.updatedAt)
    };
  });
  res.status(200).json({
    success: true,
    data,
    meta: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit)
    }
  });
});
const updateSupportStatus = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const { status } = req.body;
  const user = req.user;
  if (!["pending", "resolved", "closed"].includes(status)) {
    res.status(400).json({ success: false, message: "Invalid status" });
    return;
  }
  const isAdmin = user.role === "ADMIN";
  const whereClause = { id };
  if (!isAdmin) {
    whereClause.institutionId = user.institutionId;
  }
  try {
    const support = await prisma$1.support.update({
      where: whereClause,
      data: { status }
    });
    res.status(200).json({
      success: true,
      message: "Status updated successfully",
      data: {
        ...support,
        createdAt: toIST(support.createdAt),
        updatedAt: toIST(support.updatedAt)
      }
    });
  } catch (error) {
    res.status(404).json({ success: false, message: "Support ticket not found" });
  }
});
const router$k = express.Router();
router$k.post("/", protect, createQuestionSupport);
router$k.get("/", protect, getAllSupports);
router$k.patch("/status/:id", protect, updateSupportStatus);
const createFeedbackSchema = yup.object({
  feedbackMessage: yup.string().required("Message is required").min(3, "Message must be at least 3 characters"),
  rating: yup.number().default(0),
  target: yup.string().oneOf(["INSTITUTION", "PLATFORM"]).required("Target is required"),
  studentInstitutionId: yup.string().nullable().optional()
});
const replyFeedbackSchema = yup.object({
  replyMessage: yup.string().optional().nullable(),
  status: yup.string().oneOf(["PENDING", "REVIEWED", "RESOLVED", "CLOSED"]).optional()
});
const createFeedback = asyncHandler(async (req, res) => {
  try {
    await createFeedbackSchema.validate(req.body, { abortEarly: false });
    const { feedbackMessage, rating, target, studentInstitutionId } = req.body;
    const user = req.user;
    const insertData = {
      feedbackMessage,
      rating: rating || 0,
      userId: user.id,
      status: "PENDING",
      target
      // INSTITUTION or PLATFORM
    };
    if (target === "INSTITUTION") {
      if (user.role !== "STUDENT") {
        res.status(403).json({ message: "Only students can submit feedback to institutions" });
        return;
      }
      if (!studentInstitutionId) {
        res.status(400).json({ message: "studentInstitutionId is required for institution feedback" });
        return;
      }
      insertData.studentInstitutionId = studentInstitutionId;
      const mapping = await prisma$1.studentInstitution.findUnique({
        where: { id: studentInstitutionId },
        select: { institutionId: true }
      });
      if (!mapping) {
        res.status(404).json({ message: "Invalid StudentInstitution mapping context" });
        return;
      }
      insertData.institutionId = mapping.institutionId;
    } else if (target === "PLATFORM") {
      if (user.role === "STUDENT") {
        res.status(403).json({ message: "Students cannot submit feedback to the platform" });
        return;
      }
      if (user.institutionId) {
        insertData.institutionId = user.institutionId;
      }
      insertData.studentInstitutionId = null;
    }
    const feedback = await prisma$1.feedback.create({ data: insertData });
    res.status(201).json({
      message: "Submitted successfully",
      data: {
        ...feedback,
        submittedAt: toIST(feedback.submittedAt),
        repliedAt: feedback.repliedAt ? toIST(feedback.repliedAt) : null,
        updatedAt: toIST(feedback.updatedAt)
      }
    });
  } catch (err) {
    if (err instanceof yup.ValidationError) {
      res.status(400).json({ message: "Validation failed", errors: err.errors });
      return;
    }
    res.status(500).json({ message: "Server error" });
  }
});
const getAllFeedbacks = asyncHandler(async (req, res) => {
  const user = req.user;
  const page = Math.max(Number(req.query.page) || 1, 1);
  const limit = Math.max(Number(req.query.limit) || 10, 1);
  const skip = (page - 1) * limit;
  const search = req.query.search?.trim() || "";
  const targetFilter = req.query.target?.trim();
  const where = {
    AND: []
  };
  if (search) {
    const searchWords = search.split(/\s+/).filter(Boolean);
    const cleanTicketId = search.replace(/ticket/i, "").replace("#", "").trim();
    const upperSearch = search.toUpperCase();
    const validStatuses = ["PENDING", "RESOLVED", "OPEN", "CLOSED"];
    const isStatusEnum = validStatuses.includes(upperSearch);
    where.OR = [
      { id: cleanTicketId ? cleanTicketId : search },
      { feedbackMessage: { contains: search } },
      { replyMessage: { contains: search } },
      ...isStatusEnum ? [{ status: upperSearch }] : [],
      {
        user: {
          OR: [
            { firstName: { contains: search } },
            { lastName: { contains: search } },
            { email: { contains: search } },
            ...searchWords.length > 1 ? [{
              AND: searchWords.map((word) => ({
                OR: [
                  { firstName: { contains: word } },
                  { lastName: { contains: word } }
                ]
              }))
            }] : []
          ]
        }
      }
    ];
  }
  if (targetFilter) {
    where.AND.push({ target: targetFilter });
  }
  if (user.role === "ADMIN") ;
  else if (user.role === "INSTITUTION" || user.role === "STAFF") {
    where.AND.push({
      OR: [
        { institutionId: user.institutionId, target: "INSTITUTION" },
        { userId: user.id, target: "PLATFORM" }
      ]
    });
  } else {
    where.AND.push({ userId: user.id });
  }
  if (where.AND.length === 0) {
    delete where.AND;
  }
  const [feedbacks, total] = await Promise.all([
    prisma$1.feedback.findMany({
      where,
      include: {
        user: { select: { id: true, firstName: true, lastName: true, email: true } },
        institution: {
          include: {
            user: { select: { institutionName: true } }
          }
        }
      },
      orderBy: { submittedAt: "desc" },
      skip,
      take: limit
    }),
    prisma$1.feedback.count({ where })
  ]);
  const data = feedbacks.map((feedback) => {
    const emailPrefix = feedback.user?.email ? feedback.user.email.split("@")[0] : "";
    const computedName = `${feedback.user?.firstName || ""} ${feedback.user?.lastName || ""}`.trim();
    const finalName = computedName || emailPrefix || "Anonymous User";
    const instName = feedback.institution?.user?.institutionName || null;
    return {
      id: feedback.id,
      feedbackMessage: feedback.feedbackMessage,
      replyMessage: feedback.replyMessage,
      rating: feedback.rating,
      status: feedback.status,
      target: feedback.target,
      studentInstitutionId: feedback.studentInstitutionId,
      institutionId: feedback.institutionId,
      user: feedback.user ? {
        id: feedback.user.id,
        name: finalName,
        email: feedback.user.email,
        institutionName: instName
      } : { id: null, name: "Anonymous User", email: "N/A", institutionName: null },
      submittedAt: toIST(feedback.submittedAt),
      repliedAt: feedback.repliedAt ? toIST(feedback.repliedAt) : null,
      updatedAt: toIST(feedback.updatedAt)
    };
  });
  res.status(200).json({
    success: true,
    data,
    meta: { page, limit, total, totalPages: Math.ceil(total / limit) }
  });
});
const replyToFeedback = asyncHandler(async (req, res) => {
  try {
    await replyFeedbackSchema.validate(req.body, { abortEarly: false });
    const { id } = req.params;
    const { replyMessage, status } = req.body;
    const user = req.user;
    const existingFeedback = await prisma$1.feedback.findUnique({
      where: { id }
    });
    if (!existingFeedback) {
      res.status(404).json({ message: "Feedback log record not found" });
      return;
    }
    if (user.role === "ADMIN") {
      if (existingFeedback.target !== "PLATFORM") {
        res.status(403).json({ message: "Admin can only reply to platform feedback" });
        return;
      }
    } else if (user.role === "INSTITUTION" || user.role === "STAFF") {
      if (existingFeedback.target !== "INSTITUTION" || existingFeedback.institutionId !== user.institutionId) {
        res.status(403).json({ message: "Not authorized to modify this feedback context thread" });
        return;
      }
    } else {
      res.status(403).json({ message: "Students are blocked from initiating structural replies" });
      return;
    }
    const updateData = {};
    if (status) updateData.status = status;
    if (replyMessage !== void 0) {
      updateData.replyMessage = replyMessage || null;
      updateData.repliedAt = replyMessage ? /* @__PURE__ */ new Date() : null;
    }
    const feedback = await prisma$1.feedback.update({
      where: { id },
      data: updateData
    });
    res.status(200).json({
      message: "Feedback updated successfully",
      data: {
        ...feedback,
        submittedAt: toIST(feedback.submittedAt),
        repliedAt: feedback.repliedAt ? toIST(feedback.repliedAt) : null,
        updatedAt: toIST(feedback.updatedAt)
      }
    });
  } catch (err) {
    if (err instanceof yup.ValidationError) {
      res.status(400).json({ message: "Validation failed", errors: err.errors });
      return;
    }
    res.status(500).json({ message: "Server error" });
  }
});
const router$j = express.Router();
router$j.post("/ins", protect, createFeedback);
router$j.post("/", protect, createFeedback);
router$j.get("/", protect, getAllFeedbacks);
router$j.patch("/:id/reply", protect, replyToFeedback);
const formatDateShort = (date) => {
  return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
};
const getWeekRangeLabel = (weekIndex, fromDate) => {
  const startOfWeek = new Date(fromDate);
  startOfWeek.setDate(fromDate.getDate() + weekIndex * 7);
  const endOfWeek = new Date(startOfWeek);
  endOfWeek.setDate(startOfWeek.getDate() + 6);
  return `From ${formatDateShort(startOfWeek)} to ${formatDateShort(endOfWeek)}`;
};
const getCompactWeekRangeLabel = (start, end) => {
  const startMonth = start.toLocaleDateString("en-US", { month: "short" });
  const endMonth = end.toLocaleDateString("en-US", { month: "short" });
  const endMonthLong = end.toLocaleDateString("en-US", { month: "long" });
  const startDay = start.getDate();
  const endDay = end.getDate();
  if (startMonth === endMonth) {
    return `${startMonth} ${startDay} - ${endDay}`;
  }
  return `${startMonth} ${startDay} - ${endMonthLong} ${endDay}`;
};
const getWeekLabelForDate = (createdDate, fromDate) => {
  const diffTime = Math.abs(createdDate.getTime() - fromDate.getTime());
  const diffDays = Math.ceil(diffTime / (1e3 * 60 * 60 * 24));
  const weekIndex = Math.min(Math.ceil(diffDays / 7), 13) - 1;
  return getWeekRangeLabel(Math.max(0, weekIndex), fromDate);
};
const getOverallPracticeWeeklyTrend = asyncHandler(async (req, res) => {
  const user = req.user;
  const isAdmin = user.role === "ADMIN";
  const institutionId = isAdmin ? req.query.institutionId : user.institutionId;
  const examId = req.query.examId;
  let examName = "—";
  if (examId) {
    const exam = await prisma$1.exam.findUnique({
      where: { id: examId },
      select: { examName: true }
    });
    if (exam) {
      examName = exam.examName;
    }
  }
  const ninetyDaysAgo = /* @__PURE__ */ new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
  ninetyDaysAgo.setHours(0, 0, 0, 0);
  const whereClause = {
    practiceTestId: { not: null },
    createdAt: { gte: ninetyDaysAgo }
  };
  if (examId) {
    whereClause.practiceTest = { examId };
  }
  if (institutionId) {
    whereClause.studentInstitution = {
      institutionId
    };
  }
  const results = await prisma$1.examResult.findMany({
    where: whereClause,
    orderBy: { createdAt: "asc" }
  });
  const weeklyDataMap = {};
  const orderedWeeks = [];
  for (let w = 0; w < 13; w++) {
    const label = getWeekRangeLabel(w, ninetyDaysAgo);
    weeklyDataMap[label] = [];
    orderedWeeks.push(label);
  }
  results.forEach((r) => {
    const weekLabel = getWeekLabelForDate(new Date(r.createdAt), ninetyDaysAgo);
    const totalMarks = r.totalMarks || 100;
    const scorePercentage = totalMarks > 0 ? r.obtainedMarks / totalMarks * 100 : 0;
    if (weeklyDataMap[weekLabel]) {
      weeklyDataMap[weekLabel].push(scorePercentage);
    }
  });
  const graphData = orderedWeeks.map((week) => {
    const scores = weeklyDataMap[week];
    if (scores.length === 0) {
      return { week, percentage: null };
    }
    const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
    return { week, percentage: Math.round(avg) };
  });
  res.status(200).json({
    message: "Overall practice test weekly trend fetched successfully",
    period: {
      from: ninetyDaysAgo.toISOString().split("T")[0],
      to: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
      totalDays: 90
    },
    data: {
      examId: examId || "",
      examName,
      graphData
    }
  });
});
const getSubjectWisePracticePerformance = asyncHandler(async (req, res) => {
  const user = req.user;
  const isAdmin = user.role === "ADMIN";
  const institutionId = isAdmin ? req.query.institutionId : user.institutionId;
  const examId = req.query.examId;
  if (!examId) {
    res.status(400).json({ message: "examId is required" });
    return;
  }
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const skip = (page - 1) * limit;
  const sortOrder = req.query.sortOrder === "asc" ? "asc" : "desc";
  const availableSubjects = await prisma$1.subject.findMany({
    where: {
      OR: [
        {
          questions: {
            some: {
              practiceTests: {
                some: {
                  examId,
                  ...institutionId ? { institutionId } : {}
                }
              }
            }
          }
        },
        {
          questions: {
            some: {
              testQuestions: {
                some: {
                  test: {
                    practiceTests: {
                      some: {
                        examId,
                        ...institutionId ? { institutionId } : {}
                      }
                    }
                  }
                }
              }
            }
          }
        }
      ]
    },
    select: {
      id: true,
      subjectName: true,
      institutionId: true,
      institution: {
        select: {
          user: {
            select: { institutionName: true }
          }
        }
      }
    }
  });
  const publishedSubjects = await prisma$1.subject.findMany({
    where: {
      OR: [
        {
          questions: {
            some: {
              practiceTests: {
                some: {
                  examId,
                  publish: true,
                  ...institutionId ? { institutionId } : {}
                }
              }
            }
          }
        },
        {
          questions: {
            some: {
              testQuestions: {
                some: {
                  test: {
                    practiceTests: {
                      some: {
                        examId,
                        publish: true,
                        ...institutionId ? { institutionId } : {}
                      }
                    }
                  }
                }
              }
            }
          }
        }
      ]
    },
    select: { id: true }
  });
  const publishedSubjectIds = new Set(publishedSubjects.map((s) => s.id));
  const subjectScoresMap = {};
  availableSubjects.forEach((s) => {
    const isShared = !isAdmin && s.institutionId !== institutionId;
    const sharedInstitutionName = isShared ? s.institution?.user?.institutionName : null;
    subjectScoresMap[s.id] = {
      name: s.subjectName,
      isShared,
      sharedInstitutionName,
      scores: []
    };
  });
  const ninetyDaysAgo = /* @__PURE__ */ new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
  ninetyDaysAgo.setHours(0, 0, 0, 0);
  const resultWhereClause = {
    practiceTestId: { not: null },
    createdAt: { gte: ninetyDaysAgo },
    practiceTest: {
      examId
    }
  };
  if (institutionId) {
    resultWhereClause.OR = [
      { institutionId },
      { studentInstitution: { institutionId } }
    ];
  }
  const results = await prisma$1.examResult.findMany({
    where: resultWhereClause,
    include: {
      practiceTest: {
        include: {
          questions: {
            select: {
              subjectId: true
            }
          },
          test: {
            include: {
              testQuestions: {
                include: {
                  question: {
                    select: {
                      subjectId: true
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  });
  results.forEach((result) => {
    if (!result.practiceTest) return;
    const totalMarks = result.totalMarks || 100;
    const scorePercentage = totalMarks > 0 ? result.obtainedMarks / totalMarks * 100 : 0;
    const uniqueSubjectIdsInResult = /* @__PURE__ */ new Set();
    if (result.practiceTest.questions) {
      result.practiceTest.questions.forEach((q) => {
        if (q.subjectId) uniqueSubjectIdsInResult.add(q.subjectId);
      });
    }
    if (result.practiceTest.test?.testQuestions) {
      result.practiceTest.test.testQuestions.forEach((tq) => {
        if (tq.question?.subjectId) uniqueSubjectIdsInResult.add(tq.question.subjectId);
      });
    }
    uniqueSubjectIdsInResult.forEach((subId) => {
      if (subjectScoresMap[subId]) {
        subjectScoresMap[subId].scores.push(scorePercentage);
      }
    });
  });
  const allSubjectData = Object.keys(subjectScoresMap).map((subjectId) => {
    const item = subjectScoresMap[subjectId];
    const average = item.scores.length > 0 ? Math.round(item.scores.reduce((a, b) => a + b, 0) / item.scores.length) : null;
    return {
      subjectId,
      subjectName: item.name,
      percentage: average,
      isShared: item.isShared,
      sharedInstitutionName: item.sharedInstitutionName
    };
  });
  const filteredSubjectData = allSubjectData.filter(
    (item) => subjectScoresMap[item.subjectId]?.scores && subjectScoresMap[item.subjectId].scores.length > 0 || publishedSubjectIds.has(item.subjectId)
  );
  filteredSubjectData.sort((a, b) => {
    const aMissing = a.percentage === null;
    const bMissing = b.percentage === null;
    if (aMissing && bMissing) return 0;
    if (sortOrder === "asc") {
      if (aMissing) return -1;
      if (bMissing) return 1;
      return a.percentage - b.percentage;
    } else {
      if (aMissing) return 1;
      if (bMissing) return -1;
      return b.percentage - a.percentage;
    }
  });
  const totalSubjects = filteredSubjectData.length;
  const paginatedData = filteredSubjectData.slice(skip, skip + limit);
  const totalPages = Math.ceil(totalSubjects / limit);
  res.status(200).json({
    message: "Subject wise performance trends fetched successfully",
    pagination: {
      totalSubjects,
      currentPage: page,
      limit,
      totalPages,
      hasNextPage: page < totalPages,
      hasPrevPage: page > 1
    },
    data: paginatedData
  });
});
const getParticularSubjectPracticeTrend = asyncHandler(async (req, res) => {
  const user = req.user;
  const isAdmin = user.role === "ADMIN";
  const institutionId = isAdmin ? req.query.institutionId : user.institutionId;
  const examId = req.query.examId;
  const subjectId = req.query.subjectId;
  if (!examId || !subjectId) {
    res.status(400).json({ message: "Both examId and subjectId are required" });
    return;
  }
  const ninetyDaysAgo = /* @__PURE__ */ new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
  ninetyDaysAgo.setHours(0, 0, 0, 0);
  const resultWhereClause = {
    practiceTestId: { not: null },
    createdAt: { gte: ninetyDaysAgo },
    practiceTest: {
      examId,
      OR: [
        {
          questions: {
            some: {
              subjectId
            }
          }
        },
        {
          test: {
            testQuestions: {
              some: {
                question: {
                  subjectId
                }
              }
            }
          }
        }
      ]
    }
  };
  if (institutionId) {
    resultWhereClause.OR = [
      { institutionId },
      { studentInstitution: { institutionId } }
    ];
  }
  const results = await prisma$1.examResult.findMany({
    where: resultWhereClause,
    select: {
      createdAt: true,
      obtainedMarks: true,
      totalMarks: true
    },
    orderBy: {
      createdAt: "asc"
    }
  });
  const weeks = [];
  for (let i = 12; i >= 0; i--) {
    const start = /* @__PURE__ */ new Date();
    start.setDate(start.getDate() - (i * 7 + 6));
    start.setHours(0, 0, 0, 0);
    const end = /* @__PURE__ */ new Date();
    end.setDate(end.getDate() - i * 7);
    end.setHours(23, 59, 59, 999);
    const label = getCompactWeekRangeLabel(start, end);
    weeks.push({
      start,
      end,
      label,
      scores: []
    });
  }
  results.forEach((result) => {
    const totalMarks = result.totalMarks || 100;
    const scorePercentage = totalMarks > 0 ? result.obtainedMarks / totalMarks * 100 : 0;
    const resultDate = new Date(result.createdAt);
    const matchingWeek = weeks.find((w) => resultDate >= w.start && resultDate <= w.end);
    if (matchingWeek) {
      matchingWeek.scores.push(scorePercentage);
    }
  });
  const trendData = weeks.map((w) => {
    const average = w.scores.length > 0 ? Math.round(w.scores.reduce((a, b) => a + b, 0) / w.scores.length) : null;
    return {
      week: w.label,
      percentage: average
    };
  });
  res.status(200).json({
    message: "Particular subject practice weekly trend fetched successfully",
    data: trendData
  });
});
const getParticularSubjectStudentTable = asyncHandler(async (req, res) => {
  const user = req.user;
  const isAdmin = user.role === "ADMIN";
  const institutionId = isAdmin ? req.query.institutionId : user.institutionId;
  const examId = req.query.examId;
  const subjectId = req.query.subjectId;
  if (!examId) {
    res.status(400).json({ message: "examId is required" });
    return;
  }
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const skip = (page - 1) * limit;
  const search = req.query.search?.toLowerCase();
  const filterParam = (req.query.filter || req.query.statusFilter)?.toUpperCase() || "ALL";
  const filter = filterParam === "NOTATTENDED" ? "NOT_ATTENDED" : filterParam;
  const sortByOverall = req.query.sortByOverall || req.query.sortOrder || "desc";
  const weeks = [];
  for (let i = 12; i >= 0; i--) {
    const start = /* @__PURE__ */ new Date();
    start.setDate(start.getDate() - (i * 7 + 6));
    start.setHours(0, 0, 0, 0);
    const end = /* @__PURE__ */ new Date();
    end.setDate(end.getDate() - i * 7);
    end.setHours(23, 59, 59, 999);
    const label = getCompactWeekRangeLabel(start, end);
    weeks.push({ start, end, label });
  }
  const weekHeaders = weeks.map((w) => w.label);
  const ninetyDaysAgo = weeks[0].start;
  const studentWhereClause = {};
  if (institutionId) {
    studentWhereClause.institutions = {
      some: {
        institutionId,
        deletedAt: null,
        isVerified: true
      }
    };
  }
  if (search) {
    studentWhereClause.user = {
      OR: [
        { firstName: { contains: search } },
        { lastName: { contains: search } },
        { email: { contains: search } }
      ]
    };
  }
  const students = await prisma$1.student.findMany({
    where: studentWhereClause,
    select: {
      id: true,
      user: {
        select: {
          firstName: true,
          lastName: true,
          email: true
        }
      },
      institutions: {
        orderBy: { createdAt: "desc" },
        select: {
          exam: {
            select: {
              examName: true
            }
          }
        }
      }
    }
  });
  const results = await prisma$1.examResult.findMany({
    where: {
      practiceTestId: { not: null },
      createdAt: { gte: ninetyDaysAgo },
      studentId: { in: students.map((s) => s.id) },
      ...institutionId ? { studentInstitution: { institutionId } } : {},
      practiceTest: {
        examId,
        ...subjectId ? {
          OR: [
            {
              questions: {
                some: {
                  subjectId
                }
              }
            },
            {
              test: {
                testQuestions: {
                  some: {
                    question: {
                      subjectId
                    }
                  }
                }
              }
            }
          ]
        } : {}
      }
    },
    select: {
      studentId: true,
      createdAt: true,
      obtainedMarks: true,
      totalMarks: true
    }
  });
  const resultsByStudent = {};
  results.forEach((res2) => {
    if (!resultsByStudent[res2.studentId]) {
      resultsByStudent[res2.studentId] = [];
    }
    resultsByStudent[res2.studentId].push(res2);
  });
  const attendedExamWhere = {
    studentId: { in: students.map((s) => s.id) },
    practiceTestId: { not: null }
  };
  if (institutionId) {
    attendedExamWhere.studentInstitution = { institutionId };
  }
  const attendedExamRows = await prisma$1.examResult.findMany({
    where: attendedExamWhere,
    select: {
      studentId: true,
      practiceTest: {
        select: {
          exam: {
            select: {
              examName: true
            }
          }
        }
      }
    }
  });
  const attendedExamsByStudent = {};
  attendedExamRows.forEach((row) => {
    const examName = row.practiceTest?.exam?.examName;
    if (!examName) return;
    if (!attendedExamsByStudent[row.studentId]) {
      attendedExamsByStudent[row.studentId] = /* @__PURE__ */ new Set();
    }
    attendedExamsByStudent[row.studentId].add(examName);
  });
  let tableRows = students.map((student) => {
    const studentResults = resultsByStudent[student.id] || [];
    const attended = studentResults.length > 0;
    const weeklyScores = {};
    weekHeaders.forEach((label) => {
      weeklyScores[label] = { totalScore: 0, count: 0 };
    });
    let allScoresSum = 0;
    let totalAttendedCount = 0;
    studentResults.forEach((res2) => {
      const totalMarks = res2.totalMarks || 100;
      const percentage = totalMarks > 0 ? res2.obtainedMarks / totalMarks * 100 : 0;
      const resDate = new Date(res2.createdAt);
      const matchedWeek = weeks.find((w) => resDate >= w.start && resDate <= w.end);
      if (matchedWeek) {
        weeklyScores[matchedWeek.label].totalScore += percentage;
        weeklyScores[matchedWeek.label].count += 1;
      }
      allScoresSum += percentage;
      totalAttendedCount += 1;
    });
    const weekData = {};
    weekHeaders.forEach((label) => {
      const bucket = weeklyScores[label];
      weekData[label] = bucket.count > 0 ? Math.round(bucket.totalScore / bucket.count) : null;
    });
    const overallPercentage = totalAttendedCount > 0 ? Math.round(allScoresSum / totalAttendedCount) : null;
    const resolvedName = student.user?.firstName || student.user?.lastName ? `${student.user?.firstName || ""} ${student.user?.lastName || ""}`.trim() : student.user?.email || "Anonymous Student";
    const currentExam = student.institutions && student.institutions.length > 0 && student.institutions[0].exam ? student.institutions[0].exam.examName : "No Exam Assigned";
    const attendedExamNames = Array.from(attendedExamsByStudent[student.id] || []);
    const orderedAttendedExamNames = attendedExamNames.includes(currentExam) ? [currentExam, ...attendedExamNames.filter((name) => name !== currentExam)] : attendedExamNames;
    const previousExams = orderedAttendedExamNames.filter((name) => name !== currentExam);
    const assignedExam = orderedAttendedExamNames.length > 0 ? orderedAttendedExamNames.join(", ") : currentExam;
    return {
      studentId: student.id,
      studentName: resolvedName,
      email: student.user?.email || "",
      assignedExam,
      currentExam,
      previousExams,
      weeks: weekData,
      overallAverage: overallPercentage ?? 0,
      hasAttended: attended
    };
  });
  if (filter === "ATTENDED") {
    tableRows = tableRows.filter((row) => row.hasAttended === true);
  } else if (filter === "NOT_ATTENDED") {
    tableRows = tableRows.filter((row) => row.hasAttended === false);
  }
  tableRows.sort((a, b) => {
    const valA = a.hasAttended ? a.overallAverage : -1;
    const valB = b.hasAttended ? b.overallAverage : -1;
    if (sortByOverall === "asc") {
      if (!a.hasAttended) return 1;
      if (!b.hasAttended) return -1;
      return valA - valB;
    } else {
      return valB - valA;
    }
  });
  const totalRecords = tableRows.length;
  const paginatedRows = tableRows.slice(skip, skip + limit);
  const totalPages = Math.ceil(totalRecords / limit);
  res.status(200).json({
    message: "Student subject-wise tabular report fetched successfully",
    columns: ["Student Name", ...weekHeaders, "Overall Average"],
    pagination: {
      total: totalRecords,
      currentPage: page,
      limit,
      totalPages,
      hasNextPage: page < totalPages,
      hasPrevPage: page > 1
    },
    data: paginatedRows
  });
});
const getOverallMockWeeklyTrend = asyncHandler(async (req, res) => {
  const user = req.user;
  const isAdmin = user.role === "ADMIN";
  const institutionId = isAdmin ? req.query.institutionId : user.institutionId;
  const examId = req.query.examId;
  let examName = "—";
  if (examId) {
    const exam = await prisma$1.exam.findUnique({
      where: { id: examId },
      select: { examName: true }
    });
    if (exam) {
      examName = exam.examName;
    }
  }
  const ninetyDaysAgo = /* @__PURE__ */ new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
  ninetyDaysAgo.setHours(0, 0, 0, 0);
  const whereClause = {
    createdAt: { gte: ninetyDaysAgo }
  };
  if (examId) {
    whereClause.mockTest = {
      examId
    };
  }
  if (institutionId) {
    whereClause.studentInstitution = {
      institutionId
    };
  }
  const results = await prisma$1.mockTestResult.findMany({
    where: whereClause,
    orderBy: { createdAt: "asc" }
  });
  const weeklyDataMap = {};
  const orderedWeeks = [];
  for (let w = 0; w < 13; w++) {
    const label = getWeekRangeLabel(w, ninetyDaysAgo);
    weeklyDataMap[label] = [];
    orderedWeeks.push(label);
  }
  results.forEach((r) => {
    const weekLabel = getWeekLabelForDate(new Date(r.createdAt), ninetyDaysAgo);
    const totalMarks = r.totalMarks || 100;
    const scorePercentage = totalMarks > 0 ? r.obtainedMarks / totalMarks * 100 : 0;
    if (weeklyDataMap[weekLabel]) {
      weeklyDataMap[weekLabel].push(scorePercentage);
    }
  });
  const graphData = orderedWeeks.map((week) => {
    const scores = weeklyDataMap[week];
    if (scores.length === 0) {
      return { week, percentage: null };
    }
    const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
    return { week, percentage: Math.round(avg) };
  });
  res.status(200).json({
    message: "Overall Mock test weekly trend fetched successfully",
    period: {
      from: ninetyDaysAgo.toISOString().split("T")[0],
      to: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
      totalDays: 90
    },
    data: {
      examId: examId || "",
      examName,
      graphData
    }
  });
});
const getStudentMockWeeklyReport = asyncHandler(async (req, res) => {
  const user = req.user;
  const isAdmin = user.role === "ADMIN";
  const institutionId = isAdmin ? req.query.institutionId : user.institutionId;
  const examId = req.query.examId;
  const sortOrder = req.query.sortOrder === "asc" ? "asc" : "desc";
  const statusFilter = req.query.statusFilter || "all";
  const search = (req.query.search || "").trim();
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const skip = (page - 1) * limit;
  const ninetyDaysAgo = /* @__PURE__ */ new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
  ninetyDaysAgo.setHours(0, 0, 0, 0);
  let examName = "All Exams";
  if (examId) {
    const exam = await prisma$1.exam.findUnique({
      where: { id: examId },
      select: { examName: true }
    });
    if (exam) examName = exam.examName;
  }
  const studentWhereClause = {};
  if (institutionId) {
    studentWhereClause.institutions = {
      some: { institutionId, deletedAt: null, isVerified: true }
    };
  }
  if (search) {
    studentWhereClause.user = {
      OR: [
        { firstName: { contains: search } },
        { lastName: { contains: search } },
        { email: { contains: search } }
      ]
    };
  }
  const students = await prisma$1.student.findMany({
    where: studentWhereClause,
    select: {
      id: true,
      user: {
        select: {
          firstName: true,
          lastName: true,
          email: true
        }
      },
      institutions: {
        orderBy: { createdAt: "desc" },
        select: {
          exam: {
            select: {
              examName: true
            }
          }
        }
      }
    }
  });
  const whereClause = {
    createdAt: { gte: ninetyDaysAgo },
    studentId: { in: students.map((s) => s.id) }
  };
  if (examId) {
    whereClause.mockTest = { examId };
  }
  if (institutionId) {
    whereClause.studentInstitution = { institutionId };
  }
  const results = await prisma$1.mockTestResult.findMany({
    where: whereClause,
    select: {
      studentId: true,
      obtainedMarks: true,
      totalMarks: true,
      createdAt: true
    },
    orderBy: { createdAt: "asc" }
  });
  const orderedWeeks = [];
  for (let w = 0; w < 13; w++) {
    orderedWeeks.push(getWeekRangeLabel(w, ninetyDaysAgo));
  }
  const studentDataMap = {};
  students.forEach((student) => {
    const firstName = student.user?.firstName || "";
    const lastName = student.user?.lastName || "";
    const studentName = `${firstName} ${lastName}`.trim() || student.user?.email || `Student (ID: ${student.id.substring(0, 5)}...)`;
    const assignedExam = student.institutions && student.institutions.length > 0 && student.institutions[0].exam ? student.institutions[0].exam.examName : "No Exam Assigned";
    studentDataMap[student.id] = {
      studentName,
      email: student.user?.email || "",
      assignedExam,
      weeklyScores: {},
      allScores: []
    };
    orderedWeeks.forEach((w) => {
      studentDataMap[student.id].weeklyScores[w] = [];
    });
  });
  results.forEach((r) => {
    if (!studentDataMap[r.studentId]) return;
    const weekLabel = getWeekLabelForDate(new Date(r.createdAt), ninetyDaysAgo);
    const totalMarks = r.totalMarks || 100;
    const scorePercentage = totalMarks > 0 ? r.obtainedMarks / totalMarks * 100 : 0;
    if (studentDataMap[r.studentId].weeklyScores[weekLabel]) {
      studentDataMap[r.studentId].weeklyScores[weekLabel].push(scorePercentage);
    }
    studentDataMap[r.studentId].allScores.push(scorePercentage);
  });
  const studentsReport = [];
  Object.keys(studentDataMap).forEach((studentId) => {
    const student = studentDataMap[studentId];
    const hasAttended = student.allScores.length > 0;
    if (statusFilter === "attended" && !hasAttended) return;
    if (statusFilter === "notattended" && hasAttended) return;
    if (search && !student.studentName.toLowerCase().includes(search.toLowerCase())) {
      return;
    }
    const weeksBreakdown = {};
    orderedWeeks.forEach((week) => {
      const scores = student.weeklyScores[week];
      weeksBreakdown[week] = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : null;
    });
    const overallAverage = hasAttended ? Math.round(student.allScores.reduce((a, b) => a + b, 0) / student.allScores.length) : 0;
    studentsReport.push({
      studentId,
      studentName: student.studentName,
      email: student.email,
      assignedExam: student.assignedExam,
      weeks: weeksBreakdown,
      overallAverage,
      hasAttended
    });
  });
  studentsReport.sort((a, b) => {
    const valA = a.hasAttended ? a.overallAverage : -1;
    const valB = b.hasAttended ? b.overallAverage : -1;
    if (sortOrder === "asc") {
      if (!a.hasAttended) return 1;
      if (!b.hasAttended) return -1;
      return valA - valB;
    } else {
      return valB - valA;
    }
  });
  const totalItems = studentsReport.length;
  const totalPages = Math.ceil(totalItems / limit);
  const paginatedStudents = studentsReport.slice(skip, skip + limit);
  res.status(200).json({
    message: "Student Mock test weekly performance reports matrix fetched successfully",
    examName,
    columns: ["Student Name", ...orderedWeeks, "Overall Average"],
    data: paginatedStudents,
    pagination: {
      total: totalItems,
      page,
      limit,
      totalPages
    }
  });
});
const getOverallPyqWeeklyTrend = asyncHandler(async (req, res) => {
  const user = req.user;
  const isAdmin = user.role === "ADMIN";
  const institutionId = isAdmin ? req.query.institutionId : user.institutionId;
  const examId = req.query.examId;
  let examName = "—";
  if (examId) {
    const exam = await prisma$1.exam.findUnique({
      where: { id: examId },
      select: { examName: true }
    });
    if (exam) {
      examName = exam.examName;
    }
  }
  const ninetyDaysAgo = /* @__PURE__ */ new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
  ninetyDaysAgo.setHours(0, 0, 0, 0);
  const whereClause = {
    testType: "OldQuestionPaper",
    createdAt: { gte: ninetyDaysAgo }
  };
  if (examId) {
    whereClause.oldQuestionPaper = {
      examId
    };
  }
  if (institutionId) {
    whereClause.studentInstitution = {
      institutionId
    };
  }
  const results = await prisma$1.examResult.findMany({
    where: whereClause,
    orderBy: { createdAt: "asc" }
  });
  const weeklyDataMap = {};
  const orderedWeeks = [];
  for (let w = 0; w < 13; w++) {
    const label = getWeekRangeLabel(w, ninetyDaysAgo);
    weeklyDataMap[label] = [];
    orderedWeeks.push(label);
  }
  results.forEach((r) => {
    const weekLabel = getWeekLabelForDate(new Date(r.createdAt), ninetyDaysAgo);
    const totalMarks = r.totalMarks || 100;
    const scorePercentage = totalMarks > 0 ? r.obtainedMarks / totalMarks * 100 : 0;
    if (weeklyDataMap[weekLabel]) {
      weeklyDataMap[weekLabel].push(scorePercentage);
    }
  });
  const graphData = orderedWeeks.map((week) => {
    const scores = weeklyDataMap[week];
    if (scores.length === 0) {
      return { week, percentage: null };
    }
    const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
    return { week, percentage: Math.round(avg) };
  });
  res.status(200).json({
    message: "Overall PYQ test weekly trend fetched successfully",
    period: {
      from: ninetyDaysAgo.toISOString().split("T")[0],
      to: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
      totalDays: 90
    },
    data: {
      examId: examId || "",
      examName,
      graphData
    }
  });
});
const getStudentPyqWeeklyReport = asyncHandler(async (req, res) => {
  const user = req.user;
  const isAdmin = user.role === "ADMIN";
  const institutionId = isAdmin ? req.query.institutionId : user.institutionId;
  const examId = req.query.examId;
  const sortOrder = req.query.sortOrder === "asc" ? "asc" : "desc";
  const statusFilter = req.query.statusFilter || "all";
  const search = req.query.search?.trim();
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const skip = (page - 1) * limit;
  const ninetyDaysAgo = /* @__PURE__ */ new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
  ninetyDaysAgo.setHours(0, 0, 0, 0);
  let examName = "All Exams";
  if (examId) {
    const exam = await prisma$1.exam.findUnique({
      where: { id: examId },
      select: { examName: true }
    });
    if (exam) examName = exam.examName;
  }
  const studentWhereClause = {};
  if (institutionId) {
    studentWhereClause.institutions = {
      some: { institutionId, deletedAt: null, isVerified: true }
    };
  }
  if (search) {
    studentWhereClause.user = {
      OR: [
        { firstName: { contains: search } },
        { lastName: { contains: search } },
        { email: { contains: search } }
      ]
    };
  }
  const students = await prisma$1.student.findMany({
    where: studentWhereClause,
    select: {
      id: true,
      user: {
        select: {
          firstName: true,
          lastName: true,
          email: true
        }
      },
      institutions: {
        orderBy: { createdAt: "desc" },
        select: {
          exam: {
            select: {
              examName: true
            }
          }
        }
      }
    }
  });
  const whereClause = {
    testType: "OldQuestionPaper",
    createdAt: { gte: ninetyDaysAgo },
    studentId: { in: students.map((s) => s.id) }
  };
  if (examId) {
    whereClause.oldQuestionPaper = { examId };
  }
  if (institutionId) {
    whereClause.studentInstitution = { institutionId };
  }
  const results = await prisma$1.examResult.findMany({
    where: whereClause,
    select: {
      studentId: true,
      obtainedMarks: true,
      totalMarks: true,
      createdAt: true
    },
    orderBy: { createdAt: "asc" }
  });
  const orderedWeeks = [];
  for (let w = 0; w < 13; w++) {
    orderedWeeks.push(getWeekRangeLabel(w, ninetyDaysAgo));
  }
  const studentDataMap = {};
  students.forEach((student) => {
    const firstName = student.user?.firstName || "";
    const lastName = student.user?.lastName || "";
    const studentName = `${firstName} ${lastName}`.trim() || student.user?.email || `Student (ID: ${student.id.substring(0, 5)}...)`;
    const assignedExam = student.institutions && student.institutions.length > 0 && student.institutions[0].exam ? student.institutions[0].exam.examName : "No Exam Assigned";
    studentDataMap[student.id] = {
      studentName,
      email: student.user?.email || "",
      assignedExam,
      weeklyScores: {},
      allScores: []
    };
    orderedWeeks.forEach((w) => {
      studentDataMap[student.id].weeklyScores[w] = [];
    });
  });
  results.forEach((r) => {
    if (!studentDataMap[r.studentId]) return;
    const weekLabel = getWeekLabelForDate(new Date(r.createdAt), ninetyDaysAgo);
    const totalMarks = r.totalMarks || 100;
    const scorePercentage = totalMarks > 0 ? r.obtainedMarks / totalMarks * 100 : 0;
    if (studentDataMap[r.studentId].weeklyScores[weekLabel]) {
      studentDataMap[r.studentId].weeklyScores[weekLabel].push(scorePercentage);
    }
    studentDataMap[r.studentId].allScores.push(scorePercentage);
  });
  const studentsReport = [];
  Object.keys(studentDataMap).forEach((studentId) => {
    const student = studentDataMap[studentId];
    const hasAttended = student.allScores.length > 0;
    if (statusFilter === "attended" && !hasAttended) return;
    if (statusFilter === "notattended" && hasAttended) return;
    if (search && !student.studentName.toLowerCase().includes(search.toLowerCase())) {
      return;
    }
    const weeksBreakdown = {};
    orderedWeeks.forEach((week) => {
      const scores = student.weeklyScores[week];
      weeksBreakdown[week] = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : null;
    });
    const overallAverage = hasAttended ? Math.round(student.allScores.reduce((a, b) => a + b, 0) / student.allScores.length) : 0;
    studentsReport.push({
      studentId,
      studentName: student.studentName,
      email: student.email,
      assignedExam: student.assignedExam,
      weeks: weeksBreakdown,
      overallAverage,
      hasAttended
    });
  });
  studentsReport.sort((a, b) => {
    const valA = a.hasAttended ? a.overallAverage : -1;
    const valB = b.hasAttended ? b.overallAverage : -1;
    if (sortOrder === "asc") {
      if (!a.hasAttended) return 1;
      if (!b.hasAttended) return -1;
      return valA - valB;
    } else {
      return valB - valA;
    }
  });
  const totalItems = studentsReport.length;
  const totalPages = Math.ceil(totalItems / limit);
  const paginatedStudents = studentsReport.slice(skip, skip + limit);
  res.status(200).json({
    message: "Student PYQ weekly performance reports matrix fetched successfully",
    examName,
    columns: ["Student Name", ...orderedWeeks, "Overall Average"],
    pagination: {
      total: totalItems,
      page,
      limit,
      totalPages
    },
    data: paginatedStudents
  });
});
const router$i = express.Router();
router$i.get("/", protect, permit("Admin", "Staff", "Institution"), getExamResults);
router$i.get("/score-report-pdf", protect, permit("Admin", "Staff", "Institution"), scoreReportsPDF);
router$i.get("/overall/practice-test-weekly", protect, permit("Admin", "Staff", "Institution"), getOverallPracticeWeeklyTrend);
router$i.get("/overall/practice-test-subjects", protect, permit("Admin", "Staff", "Institution"), getSubjectWisePracticePerformance);
router$i.get("/overall/practice-test-subject-trend", protect, permit("Admin", "Staff", "Institution"), getParticularSubjectPracticeTrend);
router$i.get("/overall/particular-subject-student-table", protect, permit("Admin", "Staff", "Institution"), getParticularSubjectStudentTable);
router$i.get("/overall/mock-test-weekly", protect, permit("Admin", "Staff", "Institution"), getOverallMockWeeklyTrend);
router$i.get("/overall/mock-test-student-weekly", protect, permit("Admin", "Staff", "Institution"), getStudentMockWeeklyReport);
router$i.get("/overall/pyq-test-weekly", protect, permit("Admin", "Staff", "Institution"), getOverallPyqWeeklyTrend);
router$i.get("/overall/pyq-test-student-weekly", protect, permit("Admin", "Staff", "Institution"), getStudentPyqWeeklyReport);
router$i.get("/institutions-list", protect, permit("Admin"), getInstitutionsList);
router$i.get("/students-stats", protect, permit("Admin", "Staff", "Institution"), getStudentStats);
router$i.get("/students/:studentId/performance", protect, permit("Admin", "Staff", "Institution"), getStudentPerformance);
router$i.get("/students/:studentId/performance-pdf", protect, permit("Admin", "Staff", "Institution"), studentPerformancePDF);
router$i.get("/students-report-pdf", protect, permit("Admin", "Staff", "Institution"), studentReportsPDF);
router$i.get("/staffs-report-pdf", protect, permit("Admin", "Staff", "Institution"), staffsReportPDF);
router$i.get("/:studentId/practice-test-weekly", protect, permit("Admin", "Staff", "Institution"), getStudentPracticeWeeklyTrend);
router$i.get("/:studentId/practice-test-subjects", protect, permit("Admin", "Staff", "Institution"), getStudentPracticeSubjectAvg);
router$i.get("/:studentId/mock-test-weekly", protect, permit("Admin", "Staff", "Institution"), getStudentMockWeeklyTrend);
router$i.get("/:studentId/pyq-weekly", protect, permit("Admin", "Staff", "Institution"), getStudentPyqWeeklyTrend);
const router$h = express.Router();
router$h.get("/statuses", protect, permit("Admin", "Staff", "Institution"), getPaymentStatuses);
router$h.get("/payment-report-pdf", protect, permit("Admin", "Staff", "Institution"), paymentReportsPDF);
router$h.get("/", protect, permit("Admin", "Staff", "Institution"), listPayments);
router$h.get("/:id", protect, permit("Admin", "Staff", "Institution"), getPaymentDetails);
router$h.get("/:id/download-receipt", protect, permit("Admin", "Staff", "Institution"), downloadPaymentReceipt);
router$h.post("/bulk-quote", protect, permit("Admin", "Staff", "Institution", "Student"), getBulkPaymentQuote);
router$h.post("/create-payment-order", protect, permit("Admin", "Staff", "Institution", "Student"), createRazorpayOrder);
router$h.post("/verify-subscription-payment", protect, permit("Admin", "Staff", "Institution", "Student"), verifyAndProcessBulkPayment);
router$h.post("/cancel-payment", protect, permit("Admin", "Staff", "Institution", "Student"), cancelPayment);
router$h.post("/webhook", razorpayWebhook);
const createPlan = asyncHandler(async (req, res) => {
  const { planName, planType, actualPrice, discountPercentage, gstPercentage, duration, features, description, institutionIds } = req.body;
  if (!planName || !planType || !actualPrice || discountPercentage === void 0 || gstPercentage === void 0 || !duration) {
    res.status(400).json({ message: "Missing required fields" });
    return;
  }
  const exists = await prisma$1.subscriptionPlan.findFirst({
    where: {
      planType
    }
  });
  if (exists) {
    res.status(400).json({ message: "Plan with this type already exists" });
    return;
  }
  const priceAfterDiscount = parseFloat(actualPrice) - parseFloat(actualPrice) * parseFloat(discountPercentage) / 100;
  const finalPrice = priceAfterDiscount;
  const plan = await prisma$1.subscriptionPlan.create({
    data: {
      planName,
      planType,
      actualPrice: parseFloat(actualPrice),
      discountPercentage: parseFloat(discountPercentage),
      gstPercentage: parseFloat(gstPercentage),
      priceAfterDiscount,
      duration: parseInt(duration),
      features: Array.isArray(features) ? features : [],
      description,
      ...institutionIds && Array.isArray(institutionIds) ? {
        institutionSubscriptions: {
          create: institutionIds.map((id) => ({
            institutionId: id
          }))
        }
      } : {},
      isActive: true,
      isDeleted: false
    }
  });
  res.status(201).json({
    message: "Subscription plan created successfully",
    data: {
      ...plan,
      finalPrice,
      createdAt: toIST(plan.createdAt),
      updatedAt: toIST(plan.updatedAt)
    }
  });
});
const listPlans = asyncHandler(async (req, res) => {
  const search = req.query.search?.toString() || "";
  const institutionId = req.query.institutionId?.toString();
  const includeHidden = req.query.includeHidden === "true";
  const page = Math.max(1, Number(req.query.page || 1));
  const limit = Math.max(1, Number(req.query.limit || 10));
  const skip = (page - 1) * limit;
  const user = req.user;
  user.role === "ADMIN";
  const andConditions = [
    { isDeleted: false }
  ];
  let targetInstitutionId = void 0;
  if (user.role === "INSTITUTION" || user.role === "STAFF") {
    targetInstitutionId = user.institutionId;
  } else if (institutionId) {
    targetInstitutionId = institutionId;
  }
  let institutionHasStudents = true;
  if (targetInstitutionId) {
    const studentCount = await prisma$1.studentInstitution.count({ where: { institutionId: targetInstitutionId, deletedAt: null } });
    institutionHasStudents = studentCount > 0;
  }
  if (!includeHidden) {
    andConditions.push({ isActive: true });
  }
  if (user.role === "INSTITUTION" || user.role === "STAFF") {
    if (includeHidden) {
      const orArr = [
        { institutionSubscriptions: { some: { institutionId: user.institutionId } } }
      ];
      if (institutionHasStudents) {
        orArr.push({ AND: [{ institutionSubscriptions: { none: {} } }, { planType: "trial" }] });
      }
      andConditions.push({ OR: orArr });
    } else {
      andConditions.push({
        NOT: {
          institutionSubscriptions: {
            some: { institutionId: user.institutionId, isVisible: false }
          }
        }
      });
      const orArr = [
        { institutionSubscriptions: { some: { institutionId: user.institutionId } } }
      ];
      if (institutionHasStudents) {
        orArr.push({ AND: [{ institutionSubscriptions: { none: {} } }, { planType: "trial" }] });
      }
      andConditions.push({ OR: orArr });
    }
  } else if (institutionId) {
    if (includeHidden) {
      const orArr = [
        { institutionSubscriptions: { some: { institutionId } } }
      ];
      if (institutionHasStudents) {
        orArr.push({ AND: [{ institutionSubscriptions: { none: {} } }, { planType: "trial" }] });
      }
      andConditions.push({ OR: orArr });
    } else {
      andConditions.push({
        NOT: {
          institutionSubscriptions: {
            some: { institutionId, isVisible: false }
          }
        }
      });
      const orArr = [
        { institutionSubscriptions: { some: { institutionId } } }
      ];
      if (institutionHasStudents) {
        orArr.push({ AND: [{ institutionSubscriptions: { none: {} } }, { planType: "trial" }] });
      }
      andConditions.push({ OR: orArr });
    }
  }
  if (search) {
    andConditions.push({
      OR: [
        { planName: { contains: search } },
        { planType: { contains: search } },
        { description: { contains: search } }
      ]
    });
  }
  const where = andConditions.length > 1 ? { AND: andConditions } : andConditions[0];
  const orderBy = { priceAfterDiscount: "asc" };
  const [plans, totalPlans] = await Promise.all([
    prisma$1.subscriptionPlan.findMany({
      where,
      include: {
        institutionSubscriptions: {
          include: {
            institution: {
              select: {
                id: true,
                user: {
                  select: { institutionName: true }
                }
              }
            }
          }
        },
        _count: {
          select: {
            subscriptions: true
          }
        }
      },
      orderBy,
      skip,
      take: limit
    }),
    prisma$1.subscriptionPlan.count({ where })
  ]);
  const planTypes = [...new Set(plans.map((p) => p.planType))];
  const activeSubscriptions = await prisma$1.subscription.findMany({
    where: { plan: { planType: { in: planTypes } } },
    select: {
      plan: { select: { planType: true } },
      institutionId: true,
      studentInstitution: { select: { institutionId: true } },
      student: { select: { institutionId: true } }
    }
  });
  const activeMap = /* @__PURE__ */ new Map();
  for (const sub of activeSubscriptions) {
    if (!sub.plan?.planType) continue;
    const pType = sub.plan.planType.toLowerCase();
    if (!activeMap.has(pType)) activeMap.set(pType, /* @__PURE__ */ new Set());
    const set = activeMap.get(pType);
    if (sub.institutionId) set.add(sub.institutionId);
    if (sub.studentInstitution?.institutionId) set.add(sub.studentInstitution.institutionId);
    if (sub.student?.institutionId) set.add(sub.student.institutionId);
  }
  const verifiedInstitutionsList = await prisma$1.institution.findMany({
    where: { user: { isVerified: true, isDeleted: false } },
    select: { id: true }
  });
  if (!activeMap.has("trial")) activeMap.set("trial", /* @__PURE__ */ new Set());
  const trialSet = activeMap.get("trial");
  for (const inst of verifiedInstitutionsList) {
    trialSet.add(inst.id);
  }
  const finalPlans = plans.map((plan) => {
    const mappedInstIds = new Set(plan.institutionSubscriptions.map((s) => s.institutionId));
    const activeInsts = activeMap.get(plan.planType) || /* @__PURE__ */ new Set();
    for (const instId of activeInsts) {
      if (!mappedInstIds.has(instId)) {
        plan.institutionSubscriptions.push({
          id: `dynamic_${instId}`,
          institutionId: instId,
          subscriptionPlanId: plan.id,
          isVisible: true,
          isUsed: true
        });
        mappedInstIds.add(instId);
      }
    }
    return {
      ...plan,
      usedCount: plan._count?.subscriptions ?? 0,
      finalPrice: plan.priceAfterDiscount + plan.priceAfterDiscount * plan.gstPercentage / 100,
      createdAt: toIST(plan.createdAt),
      updatedAt: toIST(plan.updatedAt)
    };
  });
  res.status(200).json({
    message: "Plans fetched successfully",
    data: finalPlans,
    meta: {
      total: totalPlans,
      page,
      limit,
      totalPages: Math.ceil(totalPlans / limit)
    }
  });
});
const mapPlanToInstitutions = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const { institutionIds } = req.body;
  if (!Array.isArray(institutionIds)) {
    res.status(400).json({ message: "institutionIds must be an array" });
    return;
  }
  const plan = await prisma$1.subscriptionPlan.findUnique({
    where: { id, isDeleted: false }
  });
  if (!plan) {
    res.status(404).json({ message: "Plan not found" });
    return;
  }
  if (plan.planType.toLowerCase() === "trial") {
    res.status(400).json({ message: "Institutions are automatically assigned to the Trial plan and cannot be manually mapped or removed." });
    return;
  }
  const uniqueInstitutionIds = Array.from(new Set(institutionIds.filter(Boolean)));
  await prisma$1.$transaction(async (tx) => {
    if (uniqueInstitutionIds.length === 0) {
      await tx.institutionSubscription.deleteMany({
        where: { subscriptionPlanId: id }
      });
    } else {
      await tx.institutionSubscription.deleteMany({
        where: {
          subscriptionPlanId: id,
          institutionId: { notIn: uniqueInstitutionIds }
        }
      });
      const existing = await tx.institutionSubscription.findMany({
        where: {
          subscriptionPlanId: id,
          institutionId: { in: uniqueInstitutionIds }
        }
      });
      const existingInstitutionIds = new Set(existing.map((item) => item.institutionId));
      const newInstitutionIds = uniqueInstitutionIds.filter((institutionId) => !existingInstitutionIds.has(institutionId));
      if (newInstitutionIds.length > 0) {
        await tx.institutionSubscription.createMany({
          data: newInstitutionIds.map((institutionId) => ({
            subscriptionPlanId: id,
            institutionId,
            isVisible: true
          }))
        });
      }
    }
  });
  const updatedPlan = await prisma$1.subscriptionPlan.findUnique({
    where: { id },
    include: {
      institutionSubscriptions: {
        include: {
          institution: {
            select: {
              id: true,
              user: {
                select: { institutionName: true }
              }
            }
          }
        }
      }
    }
  });
  res.status(200).json({
    message: "Plan mapped to institutions successfully",
    data: updatedPlan
  });
});
const getPlanInstitutionMappings = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const plan = await prisma$1.subscriptionPlan.findUnique({
    where: { id, isDeleted: false },
    include: {
      institutionSubscriptions: {
        include: {
          institution: {
            select: {
              id: true,
              user: {
                select: { institutionName: true }
              }
            }
          }
        }
      }
    }
  });
  if (!plan) {
    res.status(404).json({ message: "Plan not found" });
    return;
  }
  const subscriptions = await prisma$1.subscription.findMany({
    where: {
      planId: id
    },
    select: {
      planId: true,
      institutionId: true,
      studentInstitution: { select: { institutionId: true } },
      student: { select: { institutionId: true } }
    }
  });
  const usedSet = /* @__PURE__ */ new Set();
  for (const sub of subscriptions) {
    if (sub.institutionId) usedSet.add(sub.institutionId);
    if (sub.studentInstitution?.institutionId) usedSet.add(sub.studentInstitution.institutionId);
    if (sub.student?.institutionId) usedSet.add(sub.student.institutionId);
  }
  if (plan.planType.toLowerCase() === "trial") {
    const verifiedInstitutionsList = await prisma$1.institution.findMany({
      where: { user: { isVerified: true, isDeleted: false } },
      select: { id: true }
    });
    for (const inst of verifiedInstitutionsList) {
      usedSet.add(inst.id);
    }
  }
  const mappedInstitutionIds = /* @__PURE__ */ new Set();
  const data = plan.institutionSubscriptions.map((sub) => {
    mappedInstitutionIds.add(sub.institutionId);
    return {
      ...sub,
      isUsed: usedSet.has(sub.institutionId)
    };
  });
  const unmappedInstitutionIds = Array.from(usedSet).filter((instId) => !mappedInstitutionIds.has(instId));
  if (unmappedInstitutionIds.length > 0) {
    const unmappedInstitutions = await prisma$1.institution.findMany({
      where: { id: { in: unmappedInstitutionIds } },
      select: {
        id: true,
        user: { select: { institutionName: true } }
      }
    });
    for (const inst of unmappedInstitutions) {
      data.push({
        id: `dynamic_${inst.id}`,
        institutionId: inst.id,
        subscriptionPlanId: id,
        isVisible: true,
        isUsed: true,
        institution: inst
      });
    }
  }
  res.status(200).json({
    message: "Institution mappings fetched successfully",
    data
  });
});
const updateInstitutionSubscriptionVisibility = asyncHandler(async (req, res) => {
  const { id, institutionId } = req.params;
  const { isVisible } = req.body;
  if (typeof isVisible !== "boolean") {
    res.status(400).json({ message: "isVisible must be a boolean" });
    return;
  }
  const plan = await prisma$1.subscriptionPlan.findUnique({
    where: { id, isDeleted: false }
  });
  if (!plan) {
    res.status(404).json({ message: "Plan not found" });
    return;
  }
  const institutionSubscription = await prisma$1.institutionSubscription.findFirst({
    where: {
      subscriptionPlanId: id,
      institutionId
    }
  });
  let updatedSubscription;
  if (!institutionSubscription) {
    updatedSubscription = await prisma$1.institutionSubscription.create({
      data: {
        subscriptionPlanId: id,
        institutionId,
        isVisible
      }
    });
  } else {
    updatedSubscription = await prisma$1.institutionSubscription.update({
      where: { id: institutionSubscription.id },
      data: { isVisible }
    });
  }
  res.status(200).json({
    message: "Institution visibility updated successfully",
    data: updatedSubscription
  });
});
const updatePlan = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const {
    planName,
    actualPrice: bodyActualPrice,
    discountPercentage: bodyDiscountPercentage,
    gstPercentage,
    duration,
    features,
    description,
    isVisible,
    institutionIds
  } = req.body;
  const oldPlan = await prisma$1.subscriptionPlan.findUnique({
    where: { id, isDeleted: false },
    include: { institutionSubscriptions: true }
  });
  if (!oldPlan) {
    res.status(404).json({ message: "Plan not found" });
    return;
  }
  const actualPrice = Number(bodyActualPrice !== void 0 ? bodyActualPrice : oldPlan.actualPrice);
  const discountPercentage = Number(bodyDiscountPercentage !== void 0 ? bodyDiscountPercentage : oldPlan.discountPercentage);
  const priceAfterDiscount = bodyActualPrice !== void 0 || bodyDiscountPercentage !== void 0 ? Number((actualPrice - actualPrice * discountPercentage / 100).toFixed(2)) : oldPlan.priceAfterDiscount;
  const newPlanData = {
    planName: planName ?? oldPlan.planName,
    planType: oldPlan.planType,
    actualPrice,
    discountPercentage,
    gstPercentage: gstPercentage !== void 0 ? Number(gstPercentage) : oldPlan.gstPercentage,
    priceAfterDiscount,
    duration: duration !== void 0 ? Number(duration) : oldPlan.duration,
    features: features ?? oldPlan.features,
    description: description ?? oldPlan.description,
    isActive: true,
    isDeleted: false
  };
  const newPlan = await prisma$1.$transaction(async (tx) => {
    await tx.subscriptionPlan.updateMany({
      where: { planType: oldPlan.planType },
      data: { isActive: false }
    });
    const instIdsToMap = institutionIds && Array.isArray(institutionIds) ? institutionIds : oldPlan.institutionSubscriptions.map((sub) => sub.institutionId);
    const oldVisibilityMap = /* @__PURE__ */ new Map();
    oldPlan.institutionSubscriptions.forEach((sub) => {
      oldVisibilityMap.set(sub.institutionId, sub.isVisible);
    });
    const createdPlan = await tx.subscriptionPlan.create({
      data: {
        ...newPlanData,
        institutionSubscriptions: {
          create: instIdsToMap.map((instId) => ({
            institutionId: instId,
            isVisible: oldVisibilityMap.has(instId) ? oldVisibilityMap.get(instId) : isVisible !== void 0 ? isVisible : true
          }))
        }
      }
    });
    return createdPlan;
  });
  res.status(200).json({
    message: "Plan updated successfully",
    data: {
      ...newPlan,
      createdAt: toIST(newPlan.createdAt),
      updatedAt: toIST(newPlan.updatedAt)
    }
  });
});
const deletePlan = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const deleteAll = req.query.deleteAll === "true";
  const plan = await prisma$1.subscriptionPlan.findUnique({
    where: { id, isDeleted: false }
  });
  if (!plan) {
    res.status(404).json({ message: "Plan not found" });
    return;
  }
  const assignedCount = await prisma$1.subscription.count({
    where: { planId: id }
  });
  if (assignedCount > 0) {
    res.status(400).json({ message: "Plan cannot be deleted as it is assigned to some students." });
    return;
  }
  if (deleteAll) {
    await prisma$1.subscriptionPlan.updateMany({
      where: { planType: plan.planType },
      data: { isDeleted: true }
    });
    res.status(200).json({
      message: "Plan pack and its entire history deleted successfully",
      deletedId: id
    });
  } else {
    await prisma$1.subscriptionPlan.update({
      where: { id },
      data: { isDeleted: true }
    });
    res.status(200).json({
      message: "Plan version deleted successfully",
      deletedId: id
    });
  }
});
const getPlanHistory = asyncHandler(async (req, res) => {
  const planType = req.query.planType?.toString().toLowerCase().trim() || "trial";
  const page = Number(req.query.page || 1);
  const limit = Number(req.query.limit || 10);
  const skip = (page - 1) * limit;
  const where = {
    planType,
    isDeleted: false
  };
  const plans = await prisma$1.subscriptionPlan.findMany({
    where,
    orderBy: { updatedAt: "desc" },
    skip,
    take: limit
  });
  const finalPlans = plans.map((plan) => ({
    ...plan,
    finalPrice: plan.priceAfterDiscount,
    basicPrice: Number(plan.priceAfterDiscount * 100 / (100 + plan.gstPercentage)).toFixed(2),
    gstAmount: Number(plan.priceAfterDiscount * (plan.gstPercentage / (100 + plan.gstPercentage))).toFixed(2),
    createdAt: toIST(plan.createdAt),
    updatedAt: toIST(plan.updatedAt)
  }));
  const totalPlans = await prisma$1.subscriptionPlan.count({
    where
  });
  res.status(200).json({
    message: "Plans fetched successfully",
    data: finalPlans,
    meta: {
      total: totalPlans,
      page,
      limit,
      totalPages: Math.ceil(totalPlans / limit)
    }
  });
});
const router$g = express.Router();
router$g.route("/").post(protect, permit("Admin"), createPlan).get(protect, listPlans);
router$g.get("/history", protect, getPlanHistory);
router$g.route("/:id/institutions").get(protect, permit("Admin"), getPlanInstitutionMappings).put(protect, permit("Admin"), mapPlanToInstitutions);
router$g.patch("/:id/institutions/:institutionId", protect, permit("Admin"), updateInstitutionSubscriptionVisibility);
router$g.route("/:id").put(protect, permit("Admin"), updatePlan).delete(protect, permit("Admin"), deletePlan);
const quotes = [
  {
    "quote": "Life isn’t about getting and having, it’s about giving and being.",
    "author": "Kevin Kruse"
  },
  {
    "quote": "Whatever the mind of man can conceive and believe, it can achieve.",
    "author": "Napoleon Hill"
  },
  {
    "quote": "Strive not to be a success, but rather to be of value.",
    "author": "Albert Einstein"
  },
  {
    "quote": "Two roads diverged in a wood, and I—I took the one less traveled by, And that has made all the difference.",
    "author": "Robert Frost"
  },
  {
    "quote": "I attribute my success to this: I never gave or took any excuse.",
    "author": "Florence Nightingale"
  },
  {
    "quote": "You miss 100% of the shots you don’t take.",
    "author": "Wayne Gretzky"
  },
  {
    "quote": "I’ve missed more than 9000 shots in my career. I’ve lost almost 300 games. 26 times I’ve been trusted to take the game winning shot and missed. I’ve failed over and over and over again in my life. And that is why I succeed.",
    "author": "Michael Jordan"
  },
  {
    "quote": "The most difficult thing is the decision to act, the rest is merely tenacity.",
    "author": "Amelia Earhart"
  },
  {
    "quote": "Every strike brings me closer to the next home run.",
    "author": "Babe Ruth"
  },
  {
    "quote": "Definiteness of purpose is the starting point of all achievement.",
    "author": "W. Clement Stone"
  },
  {
    "quote": "We must balance conspicuous consumption with conscious capitalism.",
    "author": "Kevin Kruse"
  },
  {
    "quote": "Life is what happens to you while you’re busy making other plans.",
    "author": "John Lennon"
  },
  {
    "quote": "We become what we think about.",
    "author": "Earl Nightingale"
  },
  {
    "quote": "Twenty years from now you will be more disappointed by the things that you didn’t do than by the ones you did do, so throw off the bowlines, sail away from safe harbor, catch the trade winds in your sails.  Explore, Dream, Discover.",
    "author": "Mark Twain"
  },
  {
    "quote": "Life is 10% what happens to me and 90% of how I react to it.",
    "author": "Charles Swindoll"
  },
  {
    "quote": "The most common way people give up their power is by thinking they don’t have any.",
    "author": "Alice Walker"
  },
  {
    "quote": "The mind is everything. What you think you become.",
    "author": "Buddha"
  },
  {
    "quote": "The best time to plant a tree was 20 years ago. The second best time is now.",
    "author": "Chinese Proverb"
  },
  {
    "quote": "An unexamined life is not worth living.",
    "author": "Socrates"
  },
  {
    "quote": "Eighty percent of success is showing up.",
    "author": "Woody Allen"
  },
  {
    "quote": "Your time is limited, so don’t waste it living someone else’s life.",
    "author": "Steve Jobs"
  },
  {
    "quote": "Winning isn’t everything, but wanting to win is.",
    "author": "Vince Lombardi"
  },
  {
    "quote": "I am not a product of my circumstances. I am a product of my decisions.",
    "author": "Stephen Covey"
  },
  {
    "quote": "Every child is an artist.  The problem is how to remain an artist once he grows up.",
    "author": "Pablo Picasso"
  },
  {
    "quote": "You can never cross the ocean until you have the courage to lose sight of the shore.",
    "author": "Christopher Columbus"
  },
  {
    "quote": "I’ve learned that people will forget what you said, people will forget what you did, but people will never forget how you made them feel.",
    "author": "Maya Angelou"
  },
  {
    "quote": "Either you run the day, or the day runs you.",
    "author": "Jim Rohn"
  },
  {
    "quote": "Whether you think you can or you think you can’t, you’re right.",
    "author": "Henry Ford"
  },
  {
    "quote": "The two most important days in your life are the day you are born and the day you find out why.",
    "author": "Mark Twain"
  },
  {
    "quote": "Whatever you can do, or dream you can, begin it.  Boldness has genius, power and magic in it.",
    "author": "Johann Wolfgang von Goethe"
  },
  {
    "quote": "The best revenge is massive success.",
    "author": "Frank Sinatra"
  },
  {
    "quote": "People often say that motivation doesn’t last. Well, neither does bathing.  That’s why we recommend it daily.",
    "author": "Zig Ziglar"
  },
  {
    "quote": "Life shrinks or expands in proportion to one’s courage.",
    "author": "Anais Nin"
  },
  {
    "quote": "If you hear a voice within you say “you cannot paint,” then by all means paint and that voice will be silenced.",
    "author": "Vincent Van Gogh"
  },
  {
    "quote": "There is only one way to avoid criticism: do nothing, say nothing, and be nothing.",
    "author": "Aristotle"
  },
  {
    "quote": "Ask and it will be given to you; search, and you will find; knock and the door will be opened for you.",
    "author": "Jesus"
  },
  {
    "quote": "The only person you are destined to become is the person you decide to be.",
    "author": "Ralph Waldo Emerson"
  },
  {
    "quote": "Go confidently in the direction of your dreams.  Live the life you have imagined.",
    "author": "Henry David Thoreau"
  },
  {
    "quote": "When I stand before God at the end of my life, I would hope that I would not have a single bit of talent left and could say, I used everything you gave me.",
    "author": "Erma Bombeck"
  },
  {
    "quote": "Few things can help an individual more than to place responsibility on him, and to let him know that you trust him.",
    "author": "Booker T. Washington"
  },
  {
    "quote": "Certain things catch your eye, but pursue only those that capture the heart.",
    "author": " Ancient Indian Proverb"
  },
  {
    "quote": "Believe you can and you’re halfway there.",
    "author": "Theodore Roosevelt"
  },
  {
    "quote": "Everything you’ve ever wanted is on the other side of fear.",
    "author": "George Addair"
  },
  {
    "quote": "We can easily forgive a child who is afraid of the dark; the real tragedy of life is when men are afraid of the light.",
    "author": "Plato"
  },
  {
    "quote": "Teach thy tongue to say, “I do not know,” and thous shalt progress.",
    "author": "Maimonides"
  },
  {
    "quote": "Start where you are. Use what you have.  Do what you can.",
    "author": "Arthur Ashe"
  },
  {
    "quote": "When I was 5 years old, my mother always told me that happiness was the key to life.  When I went to school, they asked me what I wanted to be when I grew up.  I wrote down ‘happy’.  They told me I didn’t understand the assignment, and I told them they didn’t understand life.",
    "author": "John Lennon"
  },
  {
    "quote": "Fall seven times and stand up eight.",
    "author": "Japanese Proverb"
  },
  {
    "quote": "When one door of happiness closes, another opens, but often we look so long at the closed door that we do not see the one that has been opened for us.",
    "author": "Helen Keller"
  },
  {
    "quote": "Everything has beauty, but not everyone can see.",
    "author": "Confucius"
  },
  {
    "quote": "How wonderful it is that nobody need wait a single moment before starting to improve the world.",
    "author": "Anne Frank"
  },
  {
    "quote": "When I let go of what I am, I become what I might be.",
    "author": "Lao Tzu"
  },
  {
    "quote": "Life is not measured by the number of breaths we take, but by the moments that take our breath away.",
    "author": "Maya Angelou"
  },
  {
    "quote": "Happiness is not something readymade.  It comes from your own actions.",
    "author": "Dalai Lama"
  },
  {
    "quote": "If you’re offered a seat on a rocket ship, don’t ask what seat! Just get on.",
    "author": "Sheryl Sandberg"
  },
  {
    "quote": "First, have a definite, clear practical ideal; a goal, an objective. Second, have the necessary means to achieve your ends; wisdom, money, materials, and methods. Third, adjust all your means to that end.",
    "author": "Aristotle"
  },
  {
    "quote": "If the wind will not serve, take to the oars.",
    "author": "Latin Proverb"
  },
  {
    "quote": "You can’t fall if you don’t climb.  But there’s no joy in living your whole life on the ground.",
    "author": "Unknown"
  },
  {
    "quote": "We must believe that we are gifted for something, and that this thing, at whatever cost, must be attained.",
    "author": "Marie Curie"
  },
  {
    "quote": "Too many of us are not living our dreams because we are living our fears.",
    "author": "Les Brown"
  },
  {
    "quote": "Challenges are what make life interesting and overcoming them is what makes life meaningful.",
    "author": "Joshua J. Marine"
  },
  {
    "quote": "If you want to lift yourself up, lift up someone else.",
    "author": "Booker T. Washington"
  },
  {
    "quote": "I have been impressed with the urgency of doing. Knowing is not enough; we must apply. Being willing is not enough; we must do.",
    "author": "Leonardo da Vinci"
  },
  {
    "quote": "Limitations live only in our minds.  But if we use our imaginations, our possibilities become limitless.",
    "author": "Jamie Paolinetti"
  },
  {
    "quote": "You take your life in your own hands, and what happens? A terrible thing, no one to blame.",
    "author": "Erica Jong"
  },
  {
    "quote": "What’s money? A man is a success if he gets up in the morning and goes to bed at night and in between does what he wants to do.",
    "author": "Bob Dylan"
  },
  {
    "quote": "I didn’t fail the test. I just found 100 ways to do it wrong.",
    "author": "Benjamin Franklin"
  },
  {
    "quote": "In order to succeed, your desire for success should be greater than your fear of failure.",
    "author": "Bill Cosby"
  },
  {
    "quote": "A person who never made a mistake never tried anything new.",
    "author": " Albert Einstein"
  },
  {
    "quote": "The person who says it cannot be done should not interrupt the person who is doing it.",
    "author": "Chinese Proverb"
  },
  {
    "quote": "There are no traffic jams along the extra mile.",
    "author": "Roger Staubach"
  },
  {
    "quote": "It is never too late to be what you might have been.",
    "author": "George Eliot"
  },
  {
    "quote": "You become what you believe.",
    "author": "Oprah Winfrey"
  },
  {
    "quote": "I would rather die of passion than of boredom.",
    "author": "Vincent van Gogh"
  },
  {
    "quote": "A truly rich man is one whose children run into his arms when his hands are empty.",
    "author": "Unknown"
  },
  {
    "quote": "It is not what you do for your children, but what you have taught them to do for themselves, that will make them successful human beings.",
    "author": "Ann Landers"
  },
  {
    "quote": "If you want your children to turn out well, spend twice as much time with them, and half as much money.",
    "author": "Abigail Van Buren"
  },
  {
    "quote": "Build your own dreams, or someone else will hire you to build theirs.",
    "author": "Farrah Gray"
  },
  {
    "quote": "The battles that count aren’t the ones for gold medals. The struggles within yourself–the invisible battles inside all of us–that’s where it’s at.",
    "author": "Jesse Owens"
  },
  {
    "quote": "Education costs money.  But then so does ignorance.",
    "author": "Sir Claus Moser"
  },
  {
    "quote": "I have learned over the years that when one’s mind is made up, this diminishes fear.",
    "author": "Rosa Parks"
  },
  {
    "quote": "It does not matter how slowly you go as long as you do not stop.",
    "author": "Confucius"
  },
  {
    "quote": "If you look at what you have in life, you’ll always have more. If you look at what you don’t have in life, you’ll never have enough.",
    "author": "Oprah Winfrey"
  },
  {
    "quote": "Remember that not getting what you want is sometimes a wonderful stroke of luck.",
    "author": "Dalai Lama"
  },
  {
    "quote": "You can’t use up creativity.  The more you use, the more you have.",
    "author": "Maya Angelou"
  },
  {
    "quote": "Dream big and dare to fail.",
    "author": "Norman Vaughan"
  },
  {
    "quote": "Our lives begin to end the day we become silent about things that matter.",
    "author": "Martin Luther King Jr."
  },
  {
    "quote": "Do what you can, where you are, with what you have.",
    "author": "Teddy Roosevelt"
  },
  {
    "quote": "If you do what you’ve always done, you’ll get what you’ve always gotten.",
    "author": "Tony Robbins"
  },
  {
    "quote": "Dreaming, after all, is a form of planning.",
    "author": "Gloria Steinem"
  },
  {
    "quote": "It’s your place in the world; it’s your life. Go on and do all you can with it, and make it the life you want to live.",
    "author": "Mae Jemison"
  },
  {
    "quote": "You may be disappointed if you fail, but you are doomed if you don’t try.",
    "author": "Beverly Sills"
  },
  {
    "quote": "Remember no one can make you feel inferior without your consent.",
    "author": "Eleanor Roosevelt"
  },
  {
    "quote": "Life is what we make it, always has been, always will be.",
    "author": "Grandma Moses"
  },
  {
    "quote": "The question isn’t who is going to let me; it’s who is going to stop me.",
    "author": "Ayn Rand"
  },
  {
    "quote": "When everything seems to be going against you, remember that the airplane takes off against the wind, not with it.",
    "author": "Henry Ford"
  },
  {
    "quote": "It’s not the years in your life that count. It’s the life in your years.",
    "author": "Abraham Lincoln"
  },
  {
    "quote": "Change your thoughts and you change your world.",
    "author": "Norman Vincent Peale"
  },
  {
    "quote": "Either write something worth reading or do something worth writing.",
    "author": "Benjamin Franklin"
  },
  {
    "quote": "Nothing is impossible, the word itself says, “I’m possible!”",
    "author": "–Audrey Hepburn"
  },
  {
    "quote": "The only way to do great work is to love what you do.",
    "author": "Steve Jobs"
  },
  {
    "quote": "If you can dream it, you can achieve it.",
    "author": "Zig Ziglar"
  },
  {
    "quote": "The only person you are destined to become is the person you decide to be.",
    "author": "Ralph Waldo Emerson"
  },
  {
    "quote": "We can’t help everyone, but everyone can help someone.",
    "author": "Ronald Reagan"
  },
  {
    "quote": "Success is not final, failure is not fatal: It is the courage to continue that counts.",
    "author": "Winston Churchill"
  },
  {
    "quote": "The future belongs to those who believe in the beauty of their dreams.",
    "author": "Eleanor Roosevelt"
  },
  {
    "quote": "The only limit to our realization of tomorrow will be our doubts of today.",
    "author": "Franklin D. Roosevelt"
  },
  {
    "quote": "In the middle of every difficulty lies opportunity.",
    "author": "Albert Einstein"
  },
  {
    "quote": "The greatest glory in living lies not in never falling, but in rising every time we fall.",
    "author": "Nelson Mandela"
  },
  {
    "quote": "Success is not the key to happiness. Happiness is the key to success. If you love what you are doing, you will be successful.",
    "author": "Albert Schweitzer"
  },
  {
    "quote": "The only way to do great work is to love what you do.",
    "author": "Steve Jobs"
  },
  {
    "quote": "Believe you can and you're halfway there.",
    "author": "Theodore Roosevelt"
  },
  {
    "quote": "The greatest pleasure in life is doing what people say you cannot do.",
    "author": "Walter Bagehot"
  },
  {
    "quote": "Success usually comes to those who are too busy to be looking for it.",
    "author": "Henry David Thoreau"
  },
  {
    "quote": "People’s lives don’t end when they die, it ends when they lose faith.",
    "author": "Itachi Uchiha"
  },
  {
    "quote": "If you don’t take risks, you can’t create a future!",
    "author": "Monkey D Luffy"
  },
  {
    "quote": "If you don’t like your destiny, don’t accept it.",
    "author": "Naruto Uzumaki"
  },
  {
    "quote": "When you give up, that’s when the game ends.",
    "author": "Mitsuyoshi Anzai"
  },
  {
    "quote": "All we can do is live until the day we die. Control what we can…and fly free.",
    "author": "Deneil Young"
  },
  {
    "quote": "Forgetting is like a wound. The wound may heal, but it has already left a scar.",
    "author": "Monkey D Luffy"
  },
  {
    "quote": "It’s just pathetic to give up on something before you even give it a shot.",
    "author": "Reiko Mikami"
  },
  {
    "quote": "If you don’t share someone’s pain, you can never understand them.",
    "author": "Nagato"
  },
  {
    "quote": "Whatever you lose, you’ll find it again. But what you throw away you’ll never get back.",
    "author": "Himura Kenshin"
  },
  {
    "quote": "We don’t have to know what tomorrow holds! That’s why we can live for everything we’re worth today!",
    "author": "Natsu Dragneel"
  },
  {
    "quote": "Why should I apologize for being a monster? Has anyone ever apologized for turning me into one?",
    "author": "Juuzou Suzuya"
  },
  {
    "quote": "People become stronger because they have memories they can’t forget.",
    "author": "Tsunade"
  },
  {
    "quote": "I’ll leave tomorrow’s problems to tomorrow’s me.",
    "author": "Saitama"
  },
  {
    "quote": "If you wanna make people dream, you’ve gotta start by believing in that dream yourself!",
    "author": "Seiya Kanie"
  },
  {
    "quote": "Being lonely is more painful then getting hurt.",
    "author": "Monkey D Luffy"
  },
  {
    "quote": "There’s no shame in falling down! True shame is to not stand up again!",
    "author": "Shintar Midorima"
  },
  {
    "quote": "Simplicity is the easiest path to true beauty.",
    "author": "Seishuu Handa"
  },
  {
    "quote": "If you can’t do something, then don’t. Focus on what you can.",
    "author": "Shiroe"
  },
  {
    "quote": "Giving up kills people. When people reject giving up… they finally win the right to transcend humanity.",
    "author": "Alucard"
  },
  {
    "quote": "You can die anytime, but living takes true courage.",
    "author": "Kenshin Himura"
  },
  {
    "quote": "Every journey begins with a single step. We just have to have patience.",
    "author": "Milly Thompson"
  },
  {
    "quote": "It doesn’t do any good to pretend you can’t see what’s going on.",
    "author": "Yuuya Mochizuki"
  },
  {
    "quote": "Being weak is nothing to be ashamed of… Staying weak is !!",
    "author": "Fuegoleon Vermillion"
  },
  {
    "quote": "To act is not necessarily compassion. True compassion sometimes comes from inaction.",
    "author": "Hinata Miyake"
  },
  {
    "quote": "A dropout will beat a genius through hard work.",
    "author": "Rock Lee"
  },
  {
    "quote": "Reject common sense to make the impossible possible.",
    "author": "Simon"
  },
  {
    "quote": "Whatever you lose, you’ll find it again. But what you throw away you’ll never get back.",
    "author": "Kenshin Himura"
  },
  {
    "quote": "If you really want to be strong… Stop caring about what your surrounding thinks of you!",
    "author": "Saitama"
  },
  {
    "quote": "Vision is not what your eyes see, but an image that your brain comprehends.",
    "author": "Touko Aozaki"
  },
  {
    "quote": "Sometimes, people are just mean. Don’t fight mean with mean. Hold your head high.",
    "author": "Hinata Miyake"
  },
  {
    "quote": "The ticket to the future is always open.",
    "author": "Vash The Stampede"
  },
  {
    "quote": "Hard work is worthless for those that don’t believe in themselves.",
    "author": "Naruto Uzumaki"
  },
  {
    "quote": "A place where someone still thinks about you is a place you can call home.",
    "author": "Jiraiya"
  },
  {
    "quote": "Life comes at a cost. Wouldn’t it be arrogant to die before you’ve repaid that debt?",
    "author": "Yuuji Kazami"
  },
  {
    "quote": "You can die anytime, but living takes true courage.",
    "author": "Himura Kenshin"
  },
  {
    "quote": "Every journey begins with a single step. We just have to have patience.",
    "author": "Milly Thompson"
  },
  {
    "quote": "If you just submit yourself to fate, then that’s the end of it.",
    "author": "Keiichi Maebara"
  },
  {
    "quote": "It is at the moment of death that humanity has value.",
    "author": "Archer"
  },
  {
    "quote": "People, who can’t throw something important away, can never hope to change anything.",
    "author": "Armin Arlert"
  },
  {
    "quote": "Whatever you do, enjoy it to the fullest. That is the secret of life.",
    "author": "Rider"
  },
  {
    "quote": "Power comes in response to a need, not a desire. You have to create that need.",
    "author": "Goku"
  },
  {
    "quote": "There are no regrets. If one can be proud of one’s life, one should not wish for another chance.",
    "author": "Saber"
  },
  {
    "quote": "You can’t always hold on to the things that are important. By letting them go we gain something else.",
    "author": "Kunio Yaobi"
  },
  {
    "quote": "If you don’t like your destiny, don’t accept it. Instead, have the courage to change it the way you want it to be.",
    "author": "Naruto Uzumaki"
  },
  {
    "quote": "Don’t beg for things. Do it yourself, or else you won’t get anything.",
    "author": "Renton Thurston"
  },
  {
    "quote": "I refuse to let my fear control me anymore.",
    "author": "Maka Albarn"
  },
  {
    "quote": "If you can’t find a reason to fight, then you shouldn’t be fighting.",
    "author": "Akame"
  },
  {
    "quote": "You should never give up on life, no matter how you feel. No matter how badly you want to give up.",
    "author": "Canaan"
  },
  {
    "quote": "People who can’t throw something important away, can never hope to change anything.",
    "author": "Armin Arlelt"
  },
  {
    "quote": "We can’t waste time worrying about the what if’s.",
    "author": "Ichigo Kurosaki"
  },
  {
    "quote": "Fools who don’t respect the past are likely to repeat it.",
    "author": "Nico Robin"
  },
  {
    "quote": "That’s why I can’t make a change. Everything I do is so… Half-assed.",
    "author": "Hiroshi Kido"
  },
  {
    "quote": "Sometimes it’s necessary to do unnecessary things.",
    "author": "Kanade Jinguuji"
  },
  {
    "quote": "An excellent leader must be passionate because it’s their duty to keep everyone moving forward.",
    "author": "Nico Yazawa"
  },
  {
    "quote": "Protecting someone means giving them a place to belong. Giving them a place where they can be happy.",
    "author": "Princess Lenessia"
  },
  {
    "quote": "Thinking you’re no-good and worthless is the worst thing you can do",
    "author": "Nobito"
  },
  {
    "quote": "Sometimes I do feel like I’m a failure. Like there’s no hope for me. But even so, I’m not gonna give up. Ever!",
    "author": "Izuku Midoriya"
  },
  {
    "quote": "If you can’t do something, then don’t. Focus on what you can do.",
    "author": "Shiroe"
  },
  {
    "quote": "When you lose sight of your path, listen for the destination in your heart.",
    "author": "Allen Walker"
  },
  {
    "quote": "The moment you think of giving up, think of the reason why you held on so long.",
    "author": "Natsu Dragneel"
  },
  {
    "quote": "Don’t give up, there’s no shame in falling down! True shame is to not stand up again!",
    "author": "Shintaro Midorima"
  },
  {
    "quote": "No matter how hard or impossible it is, never lose sight of your goal.",
    "author": "Monkey D Luffy"
  },
  {
    "quote": "Life is not a game of luck. If you wanna win, work hard.",
    "author": "Sora"
  },
  {
    "quote": "The world isn’t perfect. But it’s there for us, doing the best it can….that’s what makes it so damn beautiful.",
    "author": "Roy Mustang"
  },
  {
    "quote": "Fear is not evil. It tells you what your weakness is. And once you know your weakness, you can become stronger as well as kinder.",
    "author": "Gildarts Clive"
  },
  {
    "quote": "To know sorrow is not terrifying. What is terrifying is to know you can’t go back to happiness you could have.",
    "author": "Matsumoto Rangiku"
  },
  {
    "quote": "Knowing you’re different is only the beginning. If you accept these differences you’ll be able to get past them and grow even closer.",
    "author": "Miss Kobayashi"
  },
  {
    "quote": "Don’t be so quick to throw away your life. No matter how disgraceful or embarrassing it may be, you need to keep struggling to find your way out until the very end.",
    "author": "Clare"
  },
  {
    "quote": "The world’s not perfect, but it’s there for us trying the best it can. That’s what makes it so damn beautiful.",
    "author": "Roy Mustang"
  },
  {
    "quote": "We are all like fireworks: we climb, we shine and always go our separate ways and become further apart. But even when that time comes, let’s not disappear like a firework and continue to shine.. forever.",
    "author": "Hitsugaya Toshiro"
  },
  {
    "quote": "If nobody cares to accept you and wants you in this world, accept yourself and you will see that you don’t need them and their selfish ideas.",
    "author": "Alibaba Saluja"
  },
  {
    "quote": "When you hit the point of no return, that’s the moment it truly becomes a journey. If you can still turn back, it’s not really a journey.",
    "author": "Hinata Miyake"
  },
  {
    "quote": "Those who stand at the top determine what’s wrong and what’s right! This very place is neutral ground! Justice will prevail, you say? But of course it will! Whoever wins this war becomes justice!",
    "author": "Don Quixote Doflamingo"
  },
  {
    "quote": "A person grows up when he’s able to overcome hardships. Protection is important, but there are some things that a person must learn on his own.",
    "author": "Jiraiya"
  },
  {
    "quote": "Who decides limits? And based on what? You said you worked hard? Well, maybe you need to work a little harder. Is that really the limit of your strength? Could you of tomorrow beat you today? Instead of giving in, move forward.",
    "author": "Saitama"
  },
  {
    "quote": "Mistakes are not shackles that halt one from stepping forward. Rather, they are that which sustain and grow one’s heart.",
    "author": "Mavis Vermillion"
  },
  {
    "quote": "Fear is freedom! Subjugation is liberation! Contradiction is the truth! Those are the facts of this world! And you will all surrender to them, you pigs in human clothing!",
    "author": "Satsuki Kiryuuin"
  },
  {
    "quote": "Hatred and Sorrow are power. They are yours to control. All you have to do is to turn them into strength and use that strength to move forward.",
    "author": "Sebastian Michaelis"
  },
  {
    "quote": "It’s not always possible to do what we want to do, but it’s important to believe in something before you actually do it.",
    "author": "Might Guy"
  },
  {
    "quote": "Life and death are like light and shadow. They’re both always there. But people don’t like thinking about death, so subconsciously, they always look away from it.",
    "author": "Yato"
  },
  {
    "quote": "It’s more important to master the cards you’re holding than to complain about the ones your opponent was dealt.",
    "author": "Grimsley"
  },
  {
    "quote": "I am the hope of the universe. I am the answer to all living things that cry out for peace. I am the protector of the innocent. I am the light in the darkness. I am the truth. Ally to good! Nightmare to you!",
    "author": "Son Goku"
  },
  {
    "quote": "If you’re gonna insist on gambling and then complain when you lose, you had better work on your game.",
    "author": "Hatsu Kominato"
  },
  {
    "quote": "Moving on doesn’t mean you forget about things. It just means you have to accept what’s happened and continue living.",
    "author": "Erza Scarlet"
  },
  {
    "quote": "If nobody cares to accept you and wants you in this world, accept yourself and you will see that you don’t need them and their selfish ideas.",
    "author": "Alibaba Saluja"
  },
  {
    "quote": "If you keep on hiding your true feelings, who is going to be happy? If you are sad, you should say it out loud!",
    "author": "Haruhi Fujioka"
  },
  {
    "quote": "Religion, ideology, resources, land, spite, love or just because… No matter how pathetic the reason, it’s enough to start a war. War will never cease to exist… reasons can be thought up after the fact… Human nature pursues strife.",
    "author": "Paine"
  },
  {
    "quote": "Don’t be upset because of what you can’t do. Do what you do best, live as carefree and optimistically as you can, because some people aren’t able to do that.",
    "author": "Keima Katsuragi"
  },
  {
    "quote": "If you begin to regret, you’ll dull your future decisions and let others make your choices for you. All that’s left for you then is to die. Nobody can foretell the outcome. Each decision you make holds meaning only by affecting your next decision.",
    "author": "Erwin Smith"
  },
  {
    "quote": "Everything has a beginning and an end. Life is just a cycle of starts and stops. There are ends we don’t desire, but they’re inevitable, we have to face them. It’s what being human is all about.",
    "author": "Jet Black"
  },
  {
    "quote": "Anything can happen. No one ever thinks it will until it does. What will happen, happens. That’s how the world is. The most important thing is to not let the tragedy defeat you. To believe that you can get through it.",
    "author": "Kyousuke Natsume"
  },
  {
    "quote": "You’ll only realize that you truly love someone if they already caused you enormous pain. Your enemies can never hurt you the way your loved ones can. It’s the people close to your heart that can give you the most piercing wound. Love is a double-edged sword, it can heal the wound faster or it can sink the blade even deeper.",
    "author": "Himura Kenshin"
  },
  {
    "quote": "I want you to be happy. I want you to laugh a lot. I don’t know what exactly I’ll be able to do for you, but I’ll always be by your side.",
    "author": "Kagome"
  },
  {
    "quote": "“A lesson without pain is meaningless. That’s because no one can gain without sacrificing something. But by enduring that pain and overcoming it, he shall obtain a powerful, unmatched heart.",
    "author": "Edward Elric "
  },
  {
    "quote": "You need to accept the fact that you’re not the best and have all the will to strive to be better than anyone you face.",
    "author": "Roronoa Zoro"
  },
  {
    "quote": "I too will obtain everything that I desire. Not because someone asked me to do it, but because I know in my heart that I have something worth fighting for.",
    "author": "Julis Alexia Van Riessfeld"
  },
  {
    "quote": "You can’t win a game by doing nothing. And if someone else wins it for you then you haven’t accomplished anything. Life is the same way.",
    "author": "Junichirou Kagami"
  },
  {
    "quote": "Do not think about other things, there is only one thing you can do. So master that one thing. Do not forget. What you must imagine is always that you, yourself, are the strongest. You do not need outside enemies. For you, the one you have to fight is none other than your own image.",
    "author": "Archer"
  },
  {
    "quote": "Life is like a tube of toothpaste. When you’ve used all the toothpaste down to the last squeeze, that’s when you’ve really lived. Live with all your might, and struggle as long as you have life.",
    "author": "Mion Sonozaki"
  },
  {
    "quote": "Just like games, no matter how well you have things lined up in your life, there’s always something to keep you on your toes.",
    "author": "Junichirou Kagami"
  },
  {
    "quote": "Do exactly as you like. That is the true meaning of pleasure. Pleasure leads to joy and joy leads to happiness.",
    "author": "Gilgamesh"
  },
  {
    "quote": "It Doesn t Matter How Strong The Opposition Is It Doesn t Matter How Fearsome The World Is It Doesn t Matter How Cruel The World Is Fight",
    "author": "Eren"
  },
  {
    "quote": "If There Are Humans Who Can Bring About Change They re Those Who Are Capable Of Abandoning Everything People Who When Required To Surpass Even Monsters Are Capable Of Tossing Aside Their Very Humanity",
    "author": "Armin Arlert"
  },
  {
    "quote": "I Don t Like The Terms Good Person or Bad Person It Is Impossible To Be Entirely Good To Everyone To Some You Are A Good Person While To Others You Are A Bad Person",
    "author": "Armin Arlert"
  },
  {
    "quote": "As Long As We Continue To Fight We Are Not Defeated",
    "author": "Miche Zacharius"
  },
  {
    "quote": "If You Win You Live If You Lose You Die If You Don t Fight You Can t Win",
    "author": "Eren"
  },
  {
    "quote": "You Understand Don t You One Day Or Another Everyone You Care About Eventually Dies It s Something We Simply Can t Accept It s A Realization That Could Drive You Insane",
    "author": "Hange Zoë"
  },
  {
    "quote": "You re Gonna Care What Other People Think And Be Someone You re Not Your Whole Life You re Fine As You Are So Talk In Your Own Words",
    "author": "Ymir"
  },
  {
    "quote": "Everyone Had To Be Drunk On Somethin To Keep Pushing On Everyone Was A Slave To Somethin",
    "author": "Kenny Ackerman"
  },
  {
    "quote": "This World Is Cruel And It s Also Very Beautiful",
    "author": "Mikasa"
  },
  {
    "quote": "No One Knows What The Outcome Will Be So Choose Whatever You ll Regret The Least",
    "author": "Levi Ackerman"
  },
  {
    "quote": "Do you need a reason to not want to lose",
    "author": "Shoyo Hinata"
  },
  {
    "quote": "Being the best decoy ever is as cool as being the ace",
    "author": "Tobio Kageyama"
  },
  {
    "quote": "You can fly even higher",
    "author": "Tobio Kageyama"
  },
  {
    "quote": "If they adjust to me I have to adjust in turn Whoever stops adjusting won t be able to continue forward",
    "author": "Tobio Kageyama"
  },
  {
    "quote": "The last ones standing are the victors Only the strongest If you want to be the last one standing become strong",
    "author": "Tobio Kageyama"
  },
  {
    "quote": "Life s a bore if you don t challenge yourself",
    "author": "Yuu Nishinoya"
  },
  {
    "quote": "There are some flowers you only see when you take detours",
    "author": "Tanaka Saeko"
  },
  {
    "quote": "Being weak means that there is room to grow",
    "author": "Ittetsu Takeda"
  },
  {
    "quote": "Today might be the chance to grasp the chance to let your talent bloom",
    "author": "Tooru Oikawa"
  },
  {
    "quote": "If you re gonna hit it hit it until it breaks",
    "author": "Tooru Oikawa"
  }
];
const getRandomQuote = (req, res) => {
  const quote = quotes[Math.floor(Math.random() * quotes.length)];
  return res.status(200).json({ message: "Quote fetched successfully", quote });
};
const router$f = Router();
router$f.get("/", protect, getRandomQuote);
const createSubject = asyncHandler(async (req, res) => {
  const user = req.user;
  const { subjectName, topics, language } = req.body;
  if (!subjectName) {
    res.status(400).json({ message: "Subject name is required" });
    return;
  }
  const existing = await prisma$1.subject.findFirst({
    where: { institutionId: user.institutionId, subjectName: { equals: String(subjectName).trim() } }
  });
  if (existing) {
    res.status(400).json({ message: "Subject name already exists" });
    return;
  }
  const topicSet = {};
  if (Array.isArray(topics)) {
    topics.forEach((t) => {
      topicSet[t.trim().toLowerCase()] = true;
    });
    if (topics.length !== Number(Object.keys(topicSet).length)) {
      res.status(400).json({ message: "Duplicate topics found" });
      return;
    }
  }
  const subject = await prisma$1.subject.create({
    data: {
      subjectName,
      language: language || null,
      topics: Array.isArray(topics) ? topics.map((t) => t?.trim()) : [],
      institutionId: user.institutionId
    }
  });
  res.status(201).json({
    message: "Subject created successfully",
    data: {
      ...subject,
      createdAt: toIST(subject.createdAt),
      updatedAt: toIST(subject.updatedAt)
    }
  });
});
const getSubjects = asyncHandler(async (req, res) => {
  const user = req.user;
  const { search = "", page = "1", limit = "10", institutionId = "", includeShared = "false" } = req.query;
  const pageNum = Number(page);
  const limitNum = Number(limit);
  const skip = (pageNum - 1) * limitNum;
  const searchString = search?.trim() || "";
  const searchWords = searchString.split(/\s+/).filter(Boolean);
  const where = {};
  if (user.role === "ADMIN") {
    if (institutionId) {
      const instRec = await prisma$1.institution.findFirst({
        where: { userId: String(institutionId) },
        select: { id: true }
      });
      where.institutionId = instRec?.id ?? String(institutionId);
    }
  } else {
    if (includeShared === "true") {
      const baseInstCondition = user.role === "STAFF" ? { AND: [{ institutionId: user.institutionId }, { staffs: { some: { userId: user.id } } }] } : { institutionId: user.institutionId };
      where.OR = [
        baseInstCondition,
        {
          tests: {
            some: {
              institutionId: user.institutionId,
              referenceSourceId: { not: null }
            }
          }
        }
      ];
    } else {
      where.institutionId = user.institutionId;
      if (user.role === "STAFF") {
        where.staffs = { some: { userId: user.id } };
      }
    }
  }
  if (searchWords.length > 0) {
    where.AND = searchWords.map((word) => ({
      subjectName: { contains: word }
    }));
  }
  const [data, total] = await Promise.all([
    prisma$1.subject.findMany({
      where,
      orderBy: {
        subjectName: "asc"
      },
      skip,
      take: limitNum,
      include: {
        notesLinks: {
          select: {
            type: true
          }
        },
        subjectsNotesToExam: {
          select: {
            exam: {
              select: { id: true, examName: true }
            }
          }
        },
        institution: {
          select: {
            user: {
              select: { institutionName: true }
            }
          }
        }
      }
    }),
    prisma$1.subject.count({ where })
  ]);
  res.json({
    data: data.map((subject) => {
      const pdfCount = subject.notesLinks?.filter((item) => item.type === "PDF").length || 0;
      const linkCount = subject.notesLinks?.filter((item) => item.type === "LINK").length || 0;
      const exams = subject.subjectsNotesToExam?.map((se) => se.exam) || [];
      const isShared = user.role !== "ADMIN" && subject.institutionId !== user.institutionId;
      const sharedInstitutionName = isShared ? subject.institution?.user?.institutionName : null;
      return {
        ...subject,
        createdAt: toIST(subject.createdAt),
        updatedAt: toIST(subject.updatedAt),
        topics: subject.topics?.sort(
          (a, b) => a.localeCompare(b, void 0, { sensitivity: "base" })
        ) || [],
        pdfCount,
        linkCount,
        exams,
        isShared,
        sharedInstitutionName
      };
    }),
    meta: {
      total,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(total / limitNum)
    }
  });
});
const getSubjectById = asyncHandler(async (req, res) => {
  const user = req.user;
  const where = {
    id: req.params.id,
    institutionId: user.institutionId
  };
  if (user.role === "STAFF") {
    where.staffs = { some: { userId: user.id } };
  }
  const subject = await prisma$1.subject.findFirst({
    where,
    include: {
      subjectsNotesToExam: {
        select: {
          exam: {
            select: { id: true, examName: true }
          }
        }
      }
    }
  });
  if (!subject) {
    res.status(404).json({ message: "Subject not found" });
    return;
  }
  const exams = subject.subjectsNotesToExam?.map((se) => se.exam) || [];
  res.json({
    ...subject,
    exams,
    createdAt: toIST(subject.createdAt),
    updatedAt: toIST(subject.updatedAt)
  });
});
const updateSubject = asyncHandler(async (req, res) => {
  const user = req.user;
  try {
    const existing = await prisma$1.subject.findFirst({
      where: { id: req.params.id, institutionId: user.institutionId },
      include: {
        subjectsNotesToExam: true,
        notesLinks: { select: { type: true } }
      }
    });
    if (!existing) {
      res.status(404).json({ message: "Subject not found" });
      return;
    }
    const existingTopics = Array.isArray(existing.topics) ? existing.topics.map((t) => t.trim().toLowerCase()) : [];
    const newTopics = Array.isArray(req.body.topics) ? req.body.topics.map((t) => String(t).trim().toLowerCase()) : existingTopics;
    if (Array.isArray(req.body.topics)) {
      const topicSet = /* @__PURE__ */ new Set();
      req.body.topics.forEach((t) => {
        const normalized = String(t).trim().toLowerCase();
        if (topicSet.has(normalized)) {
          res.status(400).json({ message: "Duplicate topics found" });
          return;
        }
        topicSet.add(normalized);
      });
      if (topicSet.size !== newTopics.length) {
        res.status(400).json({ message: "Duplicate topics found" });
        return;
      }
      const deletedTopics = existingTopics.filter((topic) => !newTopics.includes(topic));
      if (deletedTopics.length > 0) {
        const questionsExist = await prisma$1.questionBank.findFirst({
          where: {
            subjectId: req.params.id,
            institutionId: user.institutionId,
            topic: { in: deletedTopics }
          }
        });
        if (questionsExist) {
          res.status(400).json({
            message: questionsExist.topic + "- Topic cannot be deleted as it has questions"
          });
          return;
        }
      }
    }
    const examIds = Array.isArray(req.body.examIds) ? Array.from(new Set(req.body.examIds.filter(Boolean))) : [];
    const subject = await prisma$1.subject.update({
      where: { id: req.params.id },
      data: {
        subjectName: req.body.subjectName ?? existing.subjectName,
        language: req.body.language !== void 0 ? req.body.language || null : existing.language,
        topics: newTopics,
        subjectsNotesToExam: Array.isArray(req.body.examIds) ? {
          deleteMany: {},
          create: examIds.map((id) => ({ examId: id }))
        } : void 0
      }
    });
    if (examIds.length > 0) {
      const existingExamIds = existing.subjectsNotesToExam?.map((se) => se.examId) || [];
      const newExamIds = examIds.filter((id) => !existingExamIds.includes(id));
      if (newExamIds.length > 0) {
        try {
          const students = await prisma$1.studentInstitution.findMany({
            where: { examsId: { in: newExamIds }, institutionId: user.institutionId },
            select: { student: { select: { fcmToken: true } } }
          });
          const tokens = students.map((s) => s.student?.fcmToken).filter((token) => !!token);
          if (tokens.length > 0) {
            const hasPdf = existing.notesLinks?.some((n) => n.type === "PDF");
            const notifyType = hasPdf ? "notes" : "links";
            await sendNotification(
              tokens,
              `Notes & Links: ${subject.subjectName}`,
              "New materials have been published for your exam.",
              { type: notifyType }
            );
          }
        } catch (error) {
          console.error("Failed to send push notifications for mapped subject:", error);
        }
      }
    }
    res.json({
      message: "Subject updated successfully",
      data: {
        ...subject,
        createdAt: toIST(subject.createdAt),
        updatedAt: toIST(subject.updatedAt)
      }
    });
  } catch (error) {
    res.status(500).json({ message: "Error updating subject" });
  }
});
const deleteSubject = asyncHandler(async (req, res) => {
  const user = req.user;
  const where = {
    id: req.params.id,
    institutionId: user.institutionId
  };
  if (user.role === "STAFF") {
    where.staffs = { some: { userId: user.id } };
  }
  const subject = await prisma$1.subject.findFirst({
    where
  });
  if (!subject) {
    res.status(404).json({ message: "Subject not found" });
    return;
  }
  const questions = await prisma$1.questionBank.findMany({
    where: {
      subjectId: req.params.id,
      institutionId: user.institutionId
    }
  });
  if (questions.length > 0) {
    res.status(400).json({
      message: "Subject cannot be deleted as it has questions"
    });
    return;
  }
  await prisma$1.subject.delete({
    where: { id: req.params.id }
  });
  res.status(200).json({ message: "Subject deleted successfully" });
});
const getSubjectsQuestions = asyncHandler(async (req, res) => {
  const user = req.user;
  const { search = "", page = "1", limit = "10" } = req.query;
  const pageNum = Number(page);
  const limitNum = Number(limit);
  const skip = (pageNum - 1) * limitNum;
  const where = {
    institutionId: user.institutionId,
    subjectName: { contains: search }
  };
  if (user.role === "STAFF") {
    where.staffs = { some: { userId: user.id } };
  }
  const [subjects, totalSubjects, totalQuestions] = await Promise.all([
    prisma$1.subject.findMany({
      where,
      include: {
        _count: {
          select: { questions: true }
        }
      },
      orderBy: { subjectName: "asc" },
      skip,
      take: limitNum
    }),
    prisma$1.subject.count({ where }),
    prisma$1.questionBank.count({
      where: {
        institutionId: user.institutionId,
        subject: {
          subjectName: { contains: search }
        }
      }
    })
  ]);
  const data = subjects.map((s) => ({
    id: s.id,
    subjectName: s.subjectName,
    language: s.language,
    topicsCount: Array.isArray(s.topics) ? s.topics.length : 0,
    questionsCount: s._count.questions
  }));
  res.status(200).json({
    data,
    totalQuestions,
    meta: {
      total: totalSubjects,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(totalSubjects / limitNum)
    }
  });
});
const getSubjectTopics = asyncHandler(async (req, res) => {
  const { subjectId } = req.params;
  const { search = "", page = "1", limit = "5" } = req.query;
  const pageNum = Number(page);
  const limitNum = Number(limit);
  const skip = (pageNum - 1) * limitNum;
  const subject = await prisma$1.subject.findUnique({
    where: { id: subjectId }
  });
  if (!subject) {
    res.status(404).json({ message: "Subject not found" });
    return;
  }
  const topics = Array.isArray(subject.topics) ? subject.topics : [];
  let filteredTopics = topics.sort(
    (a, b) => a.localeCompare(b, void 0, { sensitivity: "base" })
  );
  if (search) {
    const lowerSearch = search.toLowerCase();
    filteredTopics = filteredTopics.filter(
      (topic) => topic.toLowerCase().includes(lowerSearch)
    );
  }
  const questionCounts = await prisma$1.questionBank.groupBy({
    by: ["topic"],
    where: { subjectId },
    _count: {
      id: true
    }
  });
  const countMap = {};
  questionCounts.forEach((q) => {
    if (q.topic) {
      countMap[String(q.topic).trim().toLowerCase()] = q._count.id;
    }
  });
  const topicsWithCount = filteredTopics.map((topic) => ({
    topic,
    questionsCount: countMap[String(topic).trim().toLowerCase()] || 0
  }));
  const paginatedTopics = topicsWithCount.slice(skip, skip + limitNum);
  res.status(200).json({
    subjectId,
    topics: paginatedTopics,
    meta: {
      total: topicsWithCount.length,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(topicsWithCount.length / limitNum)
    }
  });
});
const router$e = Router();
const subjectPermission = permit("Admin", "Staff", "Institution");
router$e.post("/", protect, subjectPermission, createSubject);
router$e.get("/", protect, subjectPermission, getSubjects);
router$e.get("/questions", protect, subjectPermission, getSubjectsQuestions);
router$e.get("/:subjectId/topics", protect, subjectPermission, getSubjectTopics);
router$e.get("/:id", protect, subjectPermission, getSubjectById);
router$e.put("/:id", protect, subjectPermission, updateSubject);
router$e.delete("/:id", protect, subjectPermission, deleteSubject);
const deleteFile$1 = (filePath) => {
  if (!filePath) return;
  try {
    const fileName = path.basename(filePath);
    const absolutePath = path.join(process.cwd(), "uploads", fileName);
    if (fs.existsSync(absolutePath)) fs.unlinkSync(absolutePath);
  } catch (error) {
    console.error("Error deleting file:", error);
  }
};
const getStudentCurrentExamId$1 = async (req) => {
  const user = req.user;
  if (user?.role !== "STUDENT") return null;
  if (!user?.studentInstitutionId) return null;
  const studentInstitution = await prisma$1.studentInstitution.findFirst({
    where: { id: user.studentInstitutionId, deletedAt: null },
    select: { examsId: true }
  });
  return studentInstitution?.examsId ?? null;
};
const createSyllabus = asyncHandler(async (req, res) => {
  const user = req.user;
  const { exam, description } = req.body;
  const files = req.files;
  const syllabusFiles = files ? files.map((file) => toPublicPath(file.path)) : [];
  if (!exam) {
    syllabusFiles.forEach((file) => deleteFile$1(file));
    res.status(400).json({ message: "Exam is required" });
    return;
  }
  const newSyllabus = await prisma$1.syllabus.create({
    data: {
      examId: exam,
      syllabusFiles,
      description,
      institutionId: user.institutionId
    },
    include: { exam: { select: { examName: true } } }
  });
  try {
    const studentInstitutions = await prisma$1.studentInstitution.findMany({ where: { institutionId: user.institutionId, examsId: exam }, include: { student: { select: { fcmToken: true } } } });
    if (studentInstitutions.length > 0) {
      const tokens = studentInstitutions.map((s) => s?.student?.fcmToken).filter((token) => !!token);
      if (tokens.length > 0) {
        await sendNotification(
          tokens,
          newSyllabus.exam.examName,
          description || "New Syllabus Added",
          { type: "syllabus" }
        );
      }
    }
  } catch (notifyError) {
    console.error("Failed to send push notifications:", notifyError);
  }
  res.status(201).json({
    message: "Syllabus created successfully",
    data: {
      ...newSyllabus,
      createdAt: toIST(newSyllabus.createdAt),
      updatedAt: toIST(newSyllabus.updatedAt),
      syllabusFiles: newSyllabus.syllabusFiles.map((file) => `${getHost()}${file}`)
    }
  });
});
const getAllSyllabus = asyncHandler(async (req, res) => {
  const user = req.user;
  const { search = "", page = "1", limit = "10", exam } = req.query;
  const pageNum = Number(page);
  const limitNum = Number(limit);
  const skip = (pageNum - 1) * limitNum;
  const where = {};
  if (search) {
    where.OR = [
      { description: { contains: search } },
      { exam: { examName: { contains: search } } }
    ];
  }
  const currentExamId = exam ? null : await getStudentCurrentExamId$1(req);
  if (exam) {
    where.examId = exam;
  } else if (currentExamId && user.role === "STUDENT") {
    where.examId = currentExamId;
  }
  const whereClause = {
    institutionId: user.institutionId,
    ...where
  };
  const [total, syllabuses] = await Promise.all([
    prisma$1.syllabus.count({ where: whereClause }),
    prisma$1.syllabus.findMany({
      where: whereClause,
      include: { exam: { select: { id: true, examName: true } } },
      orderBy: { createdAt: "desc" },
      skip,
      take: limitNum
    })
  ]);
  const processedData = syllabuses.map((item) => ({
    ...item,
    createdAt: toIST(item.createdAt),
    updatedAt: toIST(item.updatedAt),
    exam: item.examId,
    examName: item.exam.examName,
    syllabusFiles: (item.syllabusFiles || []).map((f) => `${getHost()}${f}`)
  }));
  res.status(200).json({
    data: processedData,
    meta: {
      total,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(total / limitNum)
    }
  });
});
const getSyllabusById = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const currentExamId = await getStudentCurrentExamId$1(req);
  const syllabus = await prisma$1.syllabus.findUnique({
    where: {
      id,
      institutionId: user.institutionId,
      ...user.role === "STUDENT" && currentExamId ? { examId: currentExamId } : {}
    },
    include: { exam: { select: { examName: true } } }
  });
  if (!syllabus) {
    res.status(404).json({ message: "Syllabus not found" });
    return;
  }
  res.status(200).json({
    data: {
      ...syllabus,
      createdAt: toIST(syllabus.createdAt),
      updatedAt: toIST(syllabus.updatedAt),
      syllabusFiles: (syllabus.syllabusFiles || []).map((file) => `${getHost()}${file}`)
    }
  });
});
const updateSyllabus = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const { exam, description, existingFiles } = req.body;
  const user = req.user;
  const currentSyllabus = await prisma$1.syllabus.findUnique({ where: { id, institutionId: user.institutionId } });
  if (!currentSyllabus) {
    res.status(404).json({ message: "Syllabus not found" });
    return;
  }
  const updates = {};
  if (description) updates.description = description;
  if (exam) updates.examId = exam;
  let finalFiles = [];
  if (existingFiles) {
    const paths = Array.isArray(existingFiles) ? existingFiles : [existingFiles];
    finalFiles = paths.map((p) => p.includes(getHost()) ? p.replace(getHost(), "") : p);
  }
  if (req.files && req.files.length > 0) {
    const newFiles = req.files.map((file) => toPublicPath(file.path));
    finalFiles = [...finalFiles, ...newFiles];
  }
  const filesToDelete = currentSyllabus.syllabusFiles.filter((f) => !finalFiles.includes(f));
  filesToDelete.forEach((file) => deleteFile$1(file));
  updates.syllabusFiles = finalFiles;
  const updatedSyllabus = await prisma$1.syllabus.update({
    where: { id },
    data: updates,
    include: { exam: { select: { examName: true } } }
  });
  res.status(200).json({
    message: "Syllabus updated successfully",
    data: {
      ...updatedSyllabus,
      createdAt: toIST(updatedSyllabus.createdAt),
      updatedAt: toIST(updatedSyllabus.updatedAt),
      syllabusFiles: updatedSyllabus.syllabusFiles.map((file) => `${getHost()}${file}`)
    }
  });
});
const deleteSyllabus = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const syllabus = await prisma$1.syllabus.delete({ where: { id, institutionId: user.institutionId } });
  if (syllabus.syllabusFiles && syllabus.syllabusFiles.length > 0) {
    syllabus.syllabusFiles.forEach((file) => deleteFile$1(file));
  }
  res.status(200).json({ message: "Syllabus deleted successfully" });
});
const router$d = express.Router();
const permissions$1 = permit("Admin", "Staff", "Institution");
router$d.post("/", protect, permissions$1, syllabusUpload.array("syllabusFiles", 5), createSyllabus);
router$d.get("/", protect, getAllSyllabus);
router$d.get("/:id", protect, getSyllabusById);
router$d.put("/:id", protect, permissions$1, syllabusUpload.array("syllabusFiles", 5), updateSyllabus);
router$d.delete("/:id", protect, permissions$1, deleteSyllabus);
const deleteFile = (filePath) => {
  if (!filePath) return;
  try {
    const fileName = path.basename(filePath);
    const absolutePath = path.join(process.cwd(), "uploads", fileName);
    if (fs.existsSync(absolutePath)) {
      fs.unlinkSync(absolutePath);
    }
  } catch (error) {
    console.error("Error deleting file:", error);
  }
};
const getStudentCurrentExamId = async (req) => {
  const user = req.user;
  if (user?.role !== "STUDENT") return null;
  if (!user?.studentInstitutionId) return null;
  const studentInstitution = await prisma$1.studentInstitution.findFirst({
    where: { id: user.studentInstitutionId, deletedAt: null },
    select: { examsId: true }
  });
  return studentInstitution?.examsId ?? null;
};
const createNotification = asyncHandler(async (req, res) => {
  const user = req.user;
  const { exam, title, description, date } = req.body;
  const reqFiles = req.files;
  const files = reqFiles ? reqFiles.map((file) => toPublicPath(file.path)) : [];
  if (!exam || !title || !description) {
    files.forEach((f) => deleteFile(f));
    res.status(400).json({ message: "All fields are required" });
    return;
  }
  const newNotification = await prisma$1.notification.create({
    data: {
      institutionId: user.institutionId,
      examId: exam,
      title,
      description,
      date: date ? new Date(date) : /* @__PURE__ */ new Date(),
      files
    },
    include: { exam: { select: { id: true, examName: true } } }
  });
  try {
    const students = await prisma$1.studentInstitution.findMany({
      where: { examsId: exam, institutionId: user.institutionId, deletedAt: null },
      select: {
        student: {
          select: {
            fcmToken: true
          }
        }
      }
    });
    if (students.length > 0) {
      const tokens = students.map((s) => s?.student?.fcmToken).filter((token) => !!token);
      if (tokens.length > 0) {
        await sendNotification(
          tokens,
          newNotification.exam.examName,
          description || "New Notification Added",
          { type: "notification" }
        );
      }
    }
  } catch (notifyError) {
    console.error("Failed to send push notifications:", notifyError);
  }
  res.status(201).json({
    message: "Notification created successfully",
    data: {
      ...newNotification,
      date: toIST(newNotification.date),
      createdAt: toIST(newNotification.createdAt),
      updatedAt: toIST(newNotification.updatedAt),
      files: newNotification.files?.map((f) => `${getHost()}${f}`) || []
    }
  });
});
const getAllNotifications = asyncHandler(async (req, res) => {
  const user = req.user;
  const { search = "", page = "1", limit = "10", exam } = req.query;
  const pageNum = Number(page);
  const limitNum = Number(limit);
  const skip = (pageNum - 1) * limitNum;
  const where = {};
  if (search) {
    where.OR = [
      { title: { contains: search } },
      { description: { contains: search } },
      { exam: { examName: { contains: search } } }
    ];
  }
  const currentExamId = exam ? null : await getStudentCurrentExamId(req);
  if (exam) {
    where.examId = exam;
  } else if (currentExamId && user.role === "STUDENT") {
    where.examId = currentExamId;
  }
  const whereClause = {
    institutionId: user.institutionId,
    ...where
  };
  const [total, notifications] = await Promise.all([
    prisma$1.notification.count({ where: whereClause }),
    prisma$1.notification.findMany({
      where: whereClause,
      include: { exam: { select: { id: true, examName: true } } },
      orderBy: { createdAt: "desc" },
      skip,
      take: limitNum
    })
  ]);
  const processedData = notifications.map((item) => ({
    ...item,
    date: toIST(item.date),
    createdAt: toIST(item.createdAt),
    updatedAt: toIST(item.updatedAt),
    exam: item.examId,
    examName: item.exam.examName,
    files: (item.files || []).map((f) => `${getHost()}${f}`)
  }));
  res.status(200).json({
    data: processedData,
    meta: {
      total,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(total / limitNum)
    }
  });
});
const getNotificationById = asyncHandler(async (req, res) => {
  const user = req.user;
  const { id } = req.params;
  const currentExamId = await getStudentCurrentExamId(req);
  const notification = await prisma$1.notification.findUnique({
    where: {
      id,
      institutionId: user.institutionId,
      ...user.role === "STUDENT" && currentExamId ? { examId: currentExamId } : {}
    },
    include: { exam: { select: { examName: true } } }
  });
  if (!notification) {
    res.status(404).json({ message: "Notification not found" });
    return;
  }
  res.status(200).json({
    data: {
      ...notification,
      date: toIST(notification.date),
      createdAt: toIST(notification.createdAt),
      updatedAt: toIST(notification.updatedAt),
      files: (notification.files || []).map((f) => `${getHost()}${f}`)
    }
  });
});
const updateNotification = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const { exam, title, description, date, existingFiles } = req.body;
  const user = req.user;
  const currentNotification = await prisma$1.notification.findUnique({ where: { id, institutionId: user.institutionId } });
  if (!currentNotification) {
    res.status(404).json({ message: "Notification not found" });
    return;
  }
  const updates = {};
  if (title) updates.title = title;
  if (description) updates.description = description;
  if (date) updates.date = new Date(date);
  if (exam) updates.examId = exam;
  let finalFiles = [];
  if (existingFiles) {
    const paths = Array.isArray(existingFiles) ? existingFiles : [existingFiles];
    finalFiles = paths.map((p) => p.includes(getHost()) ? p.replace(getHost(), "") : p);
  }
  if (req.files && req.files.length > 0) {
    const newFiles = req.files.map((file) => toPublicPath(file.path));
    finalFiles = [...finalFiles, ...newFiles];
  }
  const filesToDelete = (currentNotification.files || []).filter((f) => !finalFiles.includes(f));
  filesToDelete.forEach((f) => deleteFile(f));
  updates.files = finalFiles;
  const updatedNotification = await prisma$1.notification.update({
    where: { id },
    data: updates,
    include: { exam: { select: { examName: true } } }
  });
  res.status(200).json({
    message: "Notification updated successfully",
    data: {
      ...updatedNotification,
      date: toIST(updatedNotification.date),
      createdAt: toIST(updatedNotification.createdAt),
      updatedAt: toIST(updatedNotification.updatedAt),
      files: updatedNotification.files?.map((f) => `${getHost()}${f}`) || []
    }
  });
});
const deleteNotification = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const notification = await prisma$1.notification.delete({
    where: { id, institutionId: user.institutionId }
  });
  if (notification.files && notification.files.length > 0) {
    notification.files.forEach((f) => deleteFile(f));
  }
  res.status(200).json({ message: "Notification deleted successfully" });
});
const router$c = express.Router();
const permissions = permit("Admin", "Staff", "Institution");
router$c.post("/", protect, permissions, notificationUpload.array("files", 5), createNotification);
router$c.get("/", protect, getAllNotifications);
router$c.get("/:id", protect, getNotificationById);
router$c.put("/:id", protect, permissions, notificationUpload.array("files", 5), updateNotification);
router$c.delete("/:id", protect, permissions, deleteNotification);
function detectLanguage(text) {
  if (/[\u0B80-\u0BFF]/.test(text)) {
    return "Tamil";
  }
  if (/[\u0900-\u097F]/.test(text)) {
    return "Hindi";
  }
  if (/[A-Za-z]/.test(text)) {
    return "English";
  }
  return "Unknown";
}
const createOldQuestion = asyncHandler(async (req, res) => {
  const user = req.user;
  const {
    oldQuestionId,
    questionText,
    correctAnswer,
    marks,
    difficulty,
    publish,
    explanation,
    options: optionsReq
  } = req.body;
  if (questionText && questionText.trim() !== "") {
    const existingQuestions = await prisma$1.oldQuestion.findMany({
      where: {
        questionText: questionText.trim(),
        institutionId: user.institutionId,
        oldQuestionPapers: {
          some: {
            id: oldQuestionId
          }
        }
      }
    });
    const duplicate = existingQuestions.find((q) => {
      const sameQuestion = q.questionText.toLowerCase().trim() === questionText.toLowerCase().trim();
      if (!sameQuestion || !Array.isArray(q.options)) {
        return false;
      }
      const dbOptions = q.options.map((opt) => opt.option?.toLowerCase().trim()).sort();
      const uploadedOptions = optionsReq.map((opt) => opt.toLowerCase().trim()).sort();
      return JSON.stringify(dbOptions) === JSON.stringify(uploadedOptions);
    });
    if (duplicate) {
      res.status(400).json({
        success: false,
        message: "A Question with same options is already exists in the selected paper."
      });
      return;
    }
  }
  const filesMap = req.files;
  const questionImageFile = filesMap?.questionImage?.[0] || null;
  const optionImageFiles = filesMap?.optionImage || [];
  const explanationImageFile = filesMap?.explanationImage?.[0] || null;
  const questionImage = questionImageFile ? toPublicPath(questionImageFile.path) : null;
  const explanationImage = explanationImageFile ? toPublicPath(explanationImageFile.path) : null;
  const rawOptions = Array.isArray(req.body.options) ? req.body.options : [req.body.options];
  let optionImageIndexes = [];
  if (req.body.optionImageIndexes) {
    if (Array.isArray(req.body.optionImageIndexes)) {
      optionImageIndexes = req.body.optionImageIndexes.map((i) => parseInt(i, 10));
    } else {
      optionImageIndexes = [parseInt(req.body.optionImageIndexes, 10)];
    }
  }
  const imageMap = {};
  if (optionImageIndexes.length === optionImageFiles.length) {
    optionImageFiles.forEach((file, i) => {
      const optionIndex = optionImageIndexes[i];
      imageMap[optionIndex] = file;
    });
  } else if (optionImageFiles.length > 0) {
    optionImageFiles.forEach((file, i) => {
      imageMap[i] = file;
    });
  }
  const options = rawOptions.map((text, index) => {
    const img = imageMap[index];
    return {
      option: text || "",
      optionImage: img ? toPublicPath(img.path) : null
    };
  });
  if (!questionText && !questionImage || correctAnswer < 0 || correctAnswer >= options.length) {
    res.status(400).json({ success: false, message: "Invalid question data" });
    return;
  }
  const createdById = user.id;
  const paper = await prisma$1.oldQuestionPaper.findFirst({
    where: {
      id: oldQuestionId,
      institutionId: user.institutionId
    },
    include: {
      sharedCopies: { select: { id: true } }
    }
  });
  if (!paper) {
    res.status(404).json({ success: false, message: "Paper not found" });
    return;
  }
  const newQuestion = await prisma$1.oldQuestion.create({
    data: {
      createdById,
      language: paper.language || "English",
      year: paper.year,
      exams: {
        connect: { id: paper.examId }
      },
      questionText,
      questionImage,
      options,
      correctAnswer: parseInt(correctAnswer),
      marks: marks ? parseFloat(marks) : 1,
      difficulty: difficulty || "Medium",
      publish: publish !== void 0 ? publish === "true" || publish === true : true,
      explanation,
      explanationImage,
      institutionId: user.institutionId,
      oldQuestionPapers: {
        connect: [
          { id: oldQuestionId },
          ...paper.sharedCopies?.map((copy) => ({ id: copy.id })) || []
        ]
      }
    },
    include: {
      exams: { select: { id: true, examName: true } }
    }
  });
  res.status(201).json({
    success: true,
    message: "Question created successfully",
    data: {
      ...newQuestion,
      createdAt: toIST(newQuestion.createdAt),
      updatedAt: toIST(newQuestion.updatedAt),
      questionImage: newQuestion.questionImage ? `${getHost()}${newQuestion.questionImage}` : "",
      options: newQuestion.options.map((option) => ({
        ...option,
        optionImage: option.optionImage ? `${getHost()}${option.optionImage}` : ""
      })),
      explanationImage: newQuestion.explanationImage ? `${getHost()}${newQuestion.explanationImage}` : ""
    }
  });
});
const listOldQuestions = asyncHandler(async (req, res) => {
  const user = req.user;
  const { search, exam, year, difficulty, language, page = "1", limit = "10" } = req.query;
  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);
  const skip = (pageNum - 1) * limitNum;
  const where = {
    institutionId: user.institutionId
  };
  if (search) {
    where.OR = [
      { questionText: { contains: search } },
      { year: { contains: search } },
      { language: { contains: search } },
      { explanation: { contains: search } },
      { exams: { some: { examName: { contains: search } } } }
    ];
  }
  if (difficulty) where.difficulty = difficulty;
  if (language) where.language = { contains: language };
  if (year) where.year = year;
  if (exam) where.exams = { some: { id: exam } };
  const requestExamIds = Array.isArray(exam) ? exam : exam ? [exam] : [];
  const [total, questions] = await Promise.all([
    prisma$1.oldQuestion.count({ where }),
    prisma$1.oldQuestion.findMany({
      where,
      include: {
        exams: { select: { id: true, examName: true } },
        createdBy: { select: { firstName: true, lastName: true } },
        oldQuestionPapers: {
          select: { id: true, examId: true },
          where: requestExamIds.length > 0 ? { examId: { in: requestExamIds } } : void 0
        },
        _count: {
          select: { oldQuestionPapers: true }
        }
      },
      orderBy: { createdAt: "desc" },
      skip,
      take: limitNum
    })
  ]);
  const updatedQuestions = questions.map((question) => {
    const testsInSpecificExam = question.oldQuestionPapers.map((t) => t.id);
    const totalTestsCount = question._count.oldQuestionPapers;
    let isAlreadyUsed = 0;
    if (testsInSpecificExam.length > 0) {
      isAlreadyUsed = 1;
    } else if (totalTestsCount === 0) {
      isAlreadyUsed = 2;
    }
    return {
      id: question.id,
      questionText: question.questionText,
      year: question.year,
      language: question.language,
      marks: question.marks,
      difficulty: question.difficulty,
      explanation: question.explanation,
      exams: question.exams,
      correctAnswer: question.correctAnswer,
      questionImage: question.questionImage ? `${getHost()}${question.questionImage}` : "",
      options: question.options.map((option) => ({
        ...option,
        optionImage: option.optionImage ? `${getHost()}${option.optionImage}` : ""
      })),
      explanationImage: question.explanationImage ? `${getHost()}${question.explanationImage}` : "",
      isAlreadyUsed,
      createdBy: question.createdBy ? `${question.createdBy.firstName} ${question.createdBy.lastName}` : "Unknown",
      updatedAt: toIST(question.updatedAt),
      createdAt: toIST(question.createdAt)
    };
  });
  res.status(200).json({
    message: "Successfully fetched questions",
    data: updatedQuestions,
    totalQuestions: total,
    meta: {
      total,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(total / limitNum)
    }
  });
});
const updateOldQuestion = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const updates = req.body;
  const filesMap = req.files;
  const user = req.user;
  const existingQuestion = await prisma$1.oldQuestion.findFirst({
    where: { id, institutionId: user.institutionId }
  });
  if (!existingQuestion) {
    res.status(404).json({ message: "Question not found" });
    return;
  }
  const { questionText, options: optionsReq, oldQuestionId } = updates;
  if (questionText && questionText.trim() !== "") {
    if (questionText && questionText.trim() !== "") {
      const existingQuestions = await prisma$1.oldQuestion.findMany({
        where: {
          questionText: questionText.trim(),
          institutionId: user.institutionId,
          oldQuestionPapers: {
            some: {
              id: oldQuestionId
            }
          }
        }
      });
      const duplicate = existingQuestions.find((q) => {
        const sameQuestion = q.questionText.toLowerCase().trim() === questionText.toLowerCase().trim();
        if (!sameQuestion || !Array.isArray(q.options)) {
          return false;
        }
        const dbOptions = q.options.map((opt) => opt.option?.toLowerCase().trim()).sort();
        const uploadedOptions = optionsReq.map((opt) => opt.toLowerCase().trim()).sort();
        return JSON.stringify(dbOptions) === JSON.stringify(uploadedOptions);
      });
      if (duplicate) {
        res.status(400).json({
          success: false,
          message: "A Question with same options is already exists in the selected paper."
        });
        return;
      }
    }
  }
  const fileDelete = (filePathUrl) => {
    if (!filePathUrl) return;
    const oldFileName = path.basename(filePathUrl || "");
    if (!oldFileName) return;
    const oldFilePath = path.join(process.cwd(), "uploads", oldFileName);
    if (fs.existsSync(oldFilePath)) {
      try {
        fs.unlinkSync(oldFilePath);
      } catch (e) {
        console.error("Error deleting file:", e);
      }
    }
  };
  if (filesMap?.questionImage?.[0]) {
    if (existingQuestion.questionImage) fileDelete(existingQuestion.questionImage);
    updates.questionImage = toPublicPath(filesMap.questionImage[0].path);
  } else if (updates.questionImage === "" || updates.questionImage === "null" || updates.questionImage === null) {
    if (existingQuestion.questionImage) fileDelete(existingQuestion.questionImage);
    updates.questionImage = null;
  }
  if (filesMap?.explanationImage?.[0]) {
    if (existingQuestion.explanationImage) fileDelete(existingQuestion.explanationImage);
    updates.explanationImage = toPublicPath(filesMap.explanationImage[0].path);
  } else if (updates.explanationImage === "" || updates.explanationImage === "null" || updates.explanationImage === null) {
    if (existingQuestion.explanationImage) fileDelete(existingQuestion.explanationImage);
    updates.explanationImage = null;
  }
  if (updates.options) {
    const rawOptions = Array.isArray(updates.options) ? updates.options : [updates.options];
    const optionImageFiles = filesMap?.optionImage || [];
    let optionImageIndexes = [];
    if (req.body.optionImageIndexes) {
      optionImageIndexes = Array.isArray(req.body.optionImageIndexes) ? req.body.optionImageIndexes.map((i) => parseInt(i, 10)) : [parseInt(req.body.optionImageIndexes, 10)];
    }
    const uploadedImageMap = {};
    if (optionImageIndexes.length === optionImageFiles.length) {
      optionImageFiles.forEach((file, i) => {
        uploadedImageMap[optionImageIndexes[i]] = file;
      });
    }
    let existingOptionImages = {};
    if (req.body.existingOptionImages) {
      try {
        existingOptionImages = JSON.parse(req.body.existingOptionImages);
      } catch (e) {
      }
    }
    const newOptions = rawOptions.map((text, index) => {
      const uploadedFile = uploadedImageMap[index];
      const existingUrl = existingOptionImages[index];
      return {
        option: text || "",
        optionImage: uploadedFile ? toPublicPath(uploadedFile.path) : existingUrl || null
      };
    });
    const oldOptions = existingQuestion.options;
    if (oldOptions) {
      const newImageUrls = new Set(newOptions.map((o) => o.optionImage).filter(Boolean));
      oldOptions.forEach((oldOpt) => {
        if (oldOpt.optionImage && !newImageUrls.has(oldOpt.optionImage)) fileDelete(oldOpt.optionImage);
      });
    }
    updates.options = newOptions;
  }
  if (updates.exams) {
    const examArray = Array.isArray(updates.exams) ? updates.exams : [updates.exams];
    updates.exams = {
      set: examArray.map((eid) => ({ id: eid }))
    };
  }
  const allowedFields = [
    "questionText",
    "questionImage",
    "explanation",
    "explanationImage",
    "options",
    "exams",
    "year",
    "difficulty",
    "language",
    "correctAnswer",
    "marks",
    "publish"
  ];
  const validUpdates = {};
  for (const field of allowedFields) {
    if (updates[field] !== void 0) {
      validUpdates[field] = updates[field];
    }
  }
  if (updates.correctAnswer !== void 0) validUpdates.correctAnswer = parseInt(updates.correctAnswer);
  if (updates.marks !== void 0) validUpdates.marks = parseFloat(updates.marks);
  const updatedQuestion = await prisma$1.oldQuestion.update({
    where: { id },
    data: validUpdates,
    include: { exams: true }
  });
  res.status(200).json({
    message: "Question updated successfully",
    data: {
      ...updatedQuestion,
      createdAt: toIST(updatedQuestion.createdAt),
      updatedAt: toIST(updatedQuestion.updatedAt),
      questionImage: updatedQuestion.questionImage ? `${getHost()}${updatedQuestion.questionImage}` : "",
      options: updatedQuestion.options.map((option) => ({
        ...option,
        optionImage: option.optionImage ? `${getHost()}${option.optionImage}` : ""
      })),
      explanationImage: updatedQuestion.explanationImage ? `${getHost()}${updatedQuestion.explanationImage}` : ""
    }
  });
});
const deleteOldQuestion = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const question = await prisma$1.oldQuestion.findFirst({
    where: { id, institutionId: user.institutionId },
    include: {
      oldQuestionPapers: {
        select: { id: true }
      }
    }
  });
  if (!question) {
    res.status(404).json({ message: "Question not found" });
    return;
  }
  await prisma$1.oldQuestion.delete({ where: { id } });
  res.status(201).json({
    message: "Question deleted successfully",
    deletedId: id
  });
});
const normalize = (value) => value === void 0 || value === null ? "" : String(value).trim().toLowerCase();
const previewBulkOldQuestionUploadByExcel = asyncHandler(async (req, res) => {
  try {
    const user = req.user;
    if (!req.file) {
      res.status(400).json({ message: "No file uploaded" });
      return;
    }
    const filePath = req.file.path;
    const workbook = XLSX.readFile(filePath);
    const data = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]);
    if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
    if (!data || data.length === 0) {
      res.status(400).json({ message: "Excel file is empty" });
      return;
    }
    const exams = await prisma$1.exam.findMany({
      where: { institutionId: user.institutionId },
      select: { id: true, examName: true }
    });
    const validQuestionTexts = data.filter((item) => item.questionText).map((item) => String(item.questionText).trim());
    const existingQuestions = (await prisma$1.oldQuestion.findMany({
      where: {
        institutionId: user.institutionId,
        questionText: { in: validQuestionTexts }
      },
      select: {
        questionText: true,
        options: true,
        year: true,
        oldQuestionPapers: { select: { id: true, title: true, year: true, exam: { select: { examName: true, id: true } } } }
      }
    })).map((question) => {
      return {
        ...question,
        questionText: question.questionText.toLowerCase().trim(),
        year: question.year ? question.year.toLowerCase().trim() : "",
        oldQuestionPaperIds: question.oldQuestionPapers.map((qp) => qp.id),
        oldQuestionPaperNames: question.oldQuestionPapers.map((qp) => qp.title),
        oldQuestionPaperYears: question.oldQuestionPapers.map((qp) => qp.year)
      };
    });
    const questionDataSet = {};
    for (const question of data) {
      const option = [normalize(question.option1), normalize(question.option2), normalize(question.option3), normalize(question.option4)].sort();
      const key = [
        normalize(question.exam),
        normalize(question.title),
        normalize(question.year),
        normalize(question.questionText),
        option.join("-")
      ].join("-");
      if (questionDataSet[key]) {
        questionDataSet[key].errors.push(`Duplicate question found in the uploaded file`);
        continue;
      }
      questionDataSet[key] = {
        ...question,
        errors: []
      };
    }
    const previewData = await Promise.all(data.map(async (row, i) => {
      const errors = [];
      if (!row.questionText) errors.push("Missing questionText.");
      if (row.correctAnswer === void 0) errors.push("Missing correctAnswer.");
      let finalExamIds = [];
      const rowExams = row.exams || row.exam || "";
      const rowExamIds = row.examId || "";
      if (rowExamIds) {
        const ids = Array.isArray(rowExamIds) ? rowExamIds : String(rowExamIds).split(",").map((id) => id.trim());
        finalExamIds = exams.filter((e) => ids.includes(e.id)).map((e) => e.id);
      } else if (rowExams) {
        const normalizedName = normalize(rowExams);
        const matched = exams.find((e) => normalize(e.examName) === normalizedName);
        if (matched) {
          finalExamIds = [matched.id];
        }
      }
      if (!rowExams && !rowExamIds) {
        errors.push("Missing exam name column in excel header.");
      } else if (finalExamIds.length === 0) {
        errors.push(`Exam "${String(rowExams).trim()}" not found in your institution.`);
      }
      if (!row.title) {
        errors.push("Title is required.");
      }
      if (!row.year) {
        errors.push("Year is required.");
      }
      let matchedPaper = null;
      const options = [];
      for (let j = 1; j <= 4; j++) {
        const opt = row[`option${j}`];
        if (opt !== void 0 && opt !== null) options.push(String(opt));
      }
      if (options.length < 2) errors.push("At least 2 options required.");
      const key = [
        normalize(row.exam),
        normalize(row.title),
        normalize(row.year),
        normalize(row.questionText),
        options.map(normalize).sort().join("-")
      ].join("-");
      if (questionDataSet[key]?.errors?.length > 0) {
        errors.push(`Duplicate question in the same file.`);
      }
      const correctAnswerIndex = parseInt(row.correctAnswer) - 1;
      if (isNaN(correctAnswerIndex) || correctAnswerIndex < 0 || correctAnswerIndex >= options.length) {
        errors.push("Invalid correctAnswer index.");
      }
      const foundExamNames = exams.filter((e) => finalExamIds.includes(e.id)).map((e) => e.examName);
      const uploadedOptions = options.map((opt) => normalize(opt)).sort();
      const duplicate = existingQuestions.some((eq) => {
        const sameQuestion = normalize(eq.questionText) === normalize(row.questionText || "");
        if (!sameQuestion) return false;
        const dbOptions = eq.options.map((opt) => normalize(opt.option)).sort();
        if (JSON.stringify(dbOptions) !== JSON.stringify(uploadedOptions)) return false;
        return eq.oldQuestionPapers.some(
          (paper) => normalize(paper.title) === normalize(row.title || "") && normalize(paper.year) === normalize(row.year || "") && finalExamIds.includes(paper.exam?.id)
        );
      });
      if (duplicate) {
        errors.push("A Question with same options is already exists in the selected paper.");
      }
      return {
        rowNumber: i + 1,
        questionText: row.questionText || "",
        title: row.title || row.paperName || null,
        year: row.year ? String(row.year) : null,
        examName: String(rowExams),
        examNames: foundExamNames,
        examId: finalExamIds,
        language: row.language || detectLanguage(row.questionText) || "English",
        options,
        correctAnswer: row.correctAnswer,
        explanation: row.explanation || "",
        isValid: errors.length === 0,
        errors
      };
    }));
    res.status(200).json({
      message: "Preview data generated",
      data: {
        totalRows: data.length,
        validCount: previewData.filter((d) => d.isValid).length,
        invalidCount: previewData.filter((d) => !d.isValid).length,
        questions: previewData
      }
    });
  } catch (error) {
    if (req.file?.path && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
    res.status(500).json({ message: error.message || "Preview failed" });
  }
});
const confirmBulkOldQuestionUploadByExcel = asyncHandler(async (req, res) => {
  const { questions } = req.body;
  if (!questions?.length) {
    res.status(400).json({ message: "No questions provided" });
    return;
  }
  const user = req.user;
  const createdById = user.id;
  const results = await prisma$1.$transaction(async (tx) => {
    let createdCount = 0;
    const paperCache = /* @__PURE__ */ new Map();
    for (const q of questions) {
      if (!q.questionText?.trim() || !q.title) continue;
      const finalExamIds = Array.isArray(q.examId) ? q.examId : [];
      if (finalExamIds.length === 0) continue;
      const primaryExamId = finalExamIds[0];
      const year = q.year ? String(q.year) : "0000";
      const language = q.language || "English";
      const paperKey = `${q.title}|${year}|${language}|${primaryExamId}`;
      let paperIds = paperCache.get(paperKey);
      if (!paperIds) {
        const existingPaper = await tx.oldQuestionPaper.findFirst({
          where: {
            title: q.title,
            year,
            language,
            examId: primaryExamId,
            institutionId: user.institutionId
          },
          include: { sharedCopies: { select: { id: true } } }
        });
        if (existingPaper) {
          paperIds = [existingPaper.id, ...existingPaper.sharedCopies.map((c) => c.id)];
        } else {
          const newPaper = await tx.oldQuestionPaper.create({
            data: {
              title: q.title,
              year,
              language,
              examId: primaryExamId,
              institutionId: user.institutionId,
              createdById,
              duration: "60",
              marks: 1,
              difficulty: "Medium",
              publish: false
            }
          });
          paperIds = [newPaper.id];
        }
        paperCache.set(paperKey, paperIds);
      }
      const newOpts = q.options.map((o) => String(o).toLowerCase().trim()).sort();
      const existingInPaper = await tx.oldQuestion.findFirst({
        where: {
          questionText: q.questionText.trim(),
          institutionId: user.institutionId,
          oldQuestionPapers: { some: { id: { in: paperIds } } }
        },
        select: { id: true, options: true }
      });
      if (existingInPaper) {
        const dbOpts = existingInPaper.options.map((o) => o.option?.toLowerCase().trim()).sort();
        if (JSON.stringify(dbOpts) === JSON.stringify(newOpts)) {
          continue;
        }
      }
      let targetQuestionId = null;
      let existingPaperIds = [];
      const candidates = await tx.oldQuestion.findMany({
        where: {
          questionText: q.questionText.trim(),
          institutionId: user.institutionId
        },
        include: { oldQuestionPapers: { select: { id: true } } }
      });
      const match = candidates.find((c) => {
        const dbOpts = c.options.map((o) => o.option?.toLowerCase().trim()).sort();
        return JSON.stringify(dbOpts) === JSON.stringify(newOpts);
      });
      if (match) {
        targetQuestionId = match.id;
        existingPaperIds = match.oldQuestionPapers.map((p) => p.id);
      } else {
        const options = q.options.map((opt) => ({ option: opt, optionImage: null }));
        const newQuestion = await tx.oldQuestion.create({
          data: {
            createdById,
            year: q.year ? String(q.year) : null,
            language: q.language || detectLanguage(q.questionText) || "English",
            questionText: String(q.questionText).trim(),
            options,
            correctAnswer: parseInt(q.correctAnswer) - 1,
            marks: 1,
            difficulty: "Medium",
            explanation: q.explanation,
            exams: { connect: finalExamIds.map((id) => ({ id })) },
            institutionId: user.institutionId
          }
        });
        targetQuestionId = newQuestion.id;
        createdCount++;
      }
      const newPaperLinks = paperIds.filter((id) => !existingPaperIds.includes(id));
      if (newPaperLinks.length > 0) {
        await tx.oldQuestion.update({
          where: { id: targetQuestionId },
          data: {
            updatedAt: /* @__PURE__ */ new Date(),
            oldQuestionPapers: {
              connect: newPaperLinks.map((id) => ({ id }))
            }
          }
        });
      }
    }
    return createdCount;
  });
  res.status(201).json({ message: `${results} Questions created successfully`, count: results });
});
const downloadOldQuestionTemplateByExcel = asyncHandler(async (req, res) => {
  const templateData = [{
    questionText: "Which is the capital of India?",
    option1: "Mumbai",
    option2: "New Delhi",
    option3: "Chennai",
    option4: "Kolkata",
    correctAnswer: 2,
    exam: "TNPSC",
    title: "TNPSC Group 4",
    year: "2023",
    language: "English",
    explanation: "New Delhi is the capital of India."
  }];
  const worksheet = XLSX.utils.json_to_sheet(templateData);
  const workbook = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(workbook, worksheet, "Questions");
  const buffer = XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
  res.setHeader("Content-Disposition", "attachment; filename=old_question_bank_template.xlsx");
  res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  res.send(buffer);
});
const getOldQuestionById = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const question = await prisma$1.oldQuestion.findFirst({
    where: {
      id,
      OR: [
        { institutionId: user.institutionId },
        { institutionId: null }
      ]
    },
    include: {
      exams: true,
      oldQuestionPapers: true
    }
  });
  if (!question) {
    res.status(404).json({ success: false, message: "Question not found" });
    return;
  }
  res.status(200).json({ success: true, data: question });
});
const router$b = express.Router();
router$b.get("/template", protect, downloadOldQuestionTemplateByExcel);
router$b.post("/preview-bulk", protect, excelUpload.single("file"), previewBulkOldQuestionUploadByExcel);
router$b.post("/confirm-bulk", protect, confirmBulkOldQuestionUploadByExcel);
router$b.route("/").post(
  protect,
  imageUpload("old_question").fields([
    { name: "questionImage", maxCount: 1 },
    { name: "optionImage", maxCount: 4 },
    { name: "explanationImage", maxCount: 1 }
  ]),
  optimizeImage,
  createOldQuestion
).get(protect, listOldQuestions);
router$b.route("/:id").get(protect, getOldQuestionById).put(
  protect,
  imageUpload("old_question").fields([
    { name: "questionImage", maxCount: 1 },
    { name: "optionImage", maxCount: 4 },
    { name: "explanationImage", maxCount: 1 }
  ]),
  optimizeImage,
  updateOldQuestion
).delete(protect, deleteOldQuestion);
const createOldQuestionPaper = asyncHandler(
  async (req, res) => {
    const user = req.user;
    const {
      title,
      exam,
      year,
      duration,
      language,
      marks,
      publish
    } = req.body;
    if (!title) {
      res.status(400).json({ success: false, message: "title is required" });
      return;
    }
    if (!exam) {
      res.status(400).json({ success: false, message: "exam is required" });
      return;
    }
    if (!year) {
      res.status(400).json({ success: false, message: "year is required" });
      return;
    }
    if (!duration) {
      res.status(400).json({ success: false, message: "duration is required" });
      return;
    }
    if (!marks) {
      res.status(400).json({ success: false, message: "marks is required" });
      return;
    }
    const yearStr = String(year);
    const examData = await prisma$1.exam.findFirst({
      where: {
        id: exam,
        institutionId: user.institutionId
      }
    });
    if (!examData) {
      res.status(404).json({
        success: false,
        message: "Exam not found"
      });
      return;
    }
    const existOldQuestionPaper = await prisma$1.oldQuestionPaper.findFirst({ where: { title: { equals: title }, examId: exam, year: { equals: String(year) }, institutionId: user.institutionId } });
    if (existOldQuestionPaper) {
      res.status(400).json({ success: false, message: "Old Question Paper title with year already exist" });
      return;
    }
    const oldQuestionPaper = await prisma$1.oldQuestionPaper.create({
      data: {
        title,
        examId: exam,
        year: yearStr,
        language: language || "English",
        duration,
        marks: Number(marks),
        publish: publish === true || publish === "true",
        createdById: user.id,
        institutionId: user.institutionId
        // questions: {
        //     connect: questions.map((q) => ({ id: q.id })),
        // },
      }
    });
    res.status(201).json({
      success: true,
      message: "Old Question Paper created successfully",
      data: {
        id: oldQuestionPaper.id,
        title: oldQuestionPaper.title,
        year: oldQuestionPaper.year,
        totalQuestions: 0,
        publish: oldQuestionPaper.publish,
        createdAt: toIST(oldQuestionPaper.createdAt),
        updatedAt: toIST(oldQuestionPaper.updatedAt)
      }
    });
  }
);
const listOldQuestionYearsByExam = asyncHandler(
  async (req, res) => {
    const user = req.user;
    const { exam, page = "1", limit = "10", search = "" } = req.query;
    if (!exam) {
      res.status(400).json({ message: "exam is required" });
      return;
    }
    const pageNum = Number(page);
    const limitNum = Number(limit);
    const skip = (pageNum - 1) * limitNum;
    const exams = await prisma$1.exam.findFirst({
      where: {
        id: exam,
        institutionId: user.institutionId
      }
    });
    if (!exams) {
      res.status(404).json({ message: "exam not found" });
      return;
    }
    const whereCondition = {
      examId: exam,
      institutionId: user.institutionId
    };
    if (search) {
      whereCondition.OR = [
        {
          title: {
            contains: String(search)
          }
        },
        {
          year: {
            contains: String(search)
          }
        }
      ];
    }
    const totalCount = await prisma$1.oldQuestionPaper.count({
      where: whereCondition
    });
    const oldQuestionPapers = await prisma$1.oldQuestionPaper.findMany({
      where: whereCondition,
      include: {
        _count: {
          select: {
            questions: true
          }
        },
        referenceInstitution: {
          select: {
            user: {
              select: {
                institutionName: true
              }
            }
          }
        }
      },
      orderBy: [
        { year: "desc" },
        { title: "asc" }
      ],
      skip,
      take: limitNum
    });
    res.status(200).json({
      success: true,
      data: oldQuestionPapers.map((p) => ({
        id: p.id,
        title: p.title,
        year: p.year,
        duration: p.duration,
        marks: p.marks,
        language: p.language,
        publish: p.publish,
        referenceSourceId: p.referenceSourceId,
        referenceInstitutionId: p.referenceInstitutionId,
        referenceInstitutionName: p.referenceInstitution?.user?.institutionName,
        totalQuestions: p._count.questions
      })),
      meta: {
        page: pageNum,
        limit: limitNum,
        total: totalCount,
        totalPages: Math.ceil(totalCount / limitNum)
      }
    });
  }
);
const updateOldQuestionPaper = asyncHandler(
  async (req, res) => {
    const user = req.user;
    const { id } = req.params;
    const data = { ...req.body };
    if (data.year) data.year = data.year.toString();
    if (data.marks) data.marks = parseFloat(data.marks);
    if (data.publish !== void 0) data.publish = data.publish === "true" || data.publish === true;
    const existingPaper = await prisma$1.oldQuestionPaper.findUnique({
      where: { id }
    });
    if (!existingPaper) {
      res.status(404).json({ success: false, message: "Old Question Paper not found" });
      return;
    }
    if (existingPaper.referenceSourceId) {
      if (data.publish === void 0) {
        res.status(403).json({ success: false, message: "Cannot edit a shared resource" });
        return;
      }
      const updatedTest2 = await prisma$1.oldQuestionPaper.update({
        where: { id, institutionId: user.institutionId },
        data: { publish: data.publish }
      });
      res.status(200).json({
        message: "Old Question Paper publish status updated successfully",
        data: {
          ...updatedTest2,
          createdAt: toIST(updatedTest2.createdAt),
          updatedAt: toIST(updatedTest2.updatedAt)
        }
      });
      return;
    }
    const updateData = {
      title: data.title,
      year: data.year,
      marks: data.marks,
      publish: data.publish,
      duration: data.duration,
      language: data.language,
      difficulty: data.difficulty
    };
    if (data.exam) {
      updateData.exam = { connect: { id: data.exam } };
    }
    const updatedTest = await prisma$1.oldQuestionPaper.update({
      where: { id, institutionId: user.institutionId },
      data: updateData
    });
    res.status(200).json({
      message: "Old Question Paper updated successfully",
      data: {
        ...updatedTest,
        createdAt: toIST(updatedTest.createdAt),
        updatedAt: toIST(updatedTest.updatedAt)
      }
    });
  }
);
const getExamsOldQuestions = asyncHandler(async (req, res) => {
  const user = req.user;
  const { search = "", page = "1", limit = "10" } = req.query;
  const pageNum = Number(page);
  const limitNum = Number(limit);
  const skip = (pageNum - 1) * limitNum;
  const where = {
    examName: { contains: search },
    institutionId: user.institutionId
  };
  const [exams, totalExams, totalQuestions, totalPapers] = await Promise.all([
    prisma$1.exam.findMany({
      where,
      include: {
        _count: {
          select: {
            oldQuestions: true,
            oldQuestionPapers: true
          }
        }
      },
      orderBy: { createdAt: "desc" },
      skip,
      take: limitNum
    }),
    prisma$1.exam.count({ where }),
    prisma$1.oldQuestion.count({
      where: {
        institutionId: user.institutionId
      }
    }),
    prisma$1.oldQuestionPaper.count({
      where: {
        institutionId: user.institutionId,
        referenceSourceId: null
      }
    })
  ]);
  const data = exams.map((e) => {
    return {
      id: e.id,
      examName: e.examName,
      baseExamName: e.examName,
      sharedFromName: null,
      questionsCount: e._count.oldQuestions,
      papersCount: e._count.oldQuestionPapers
    };
  });
  res.status(200).json({
    data,
    totalQuestions,
    totalPapers,
    meta: {
      total: totalExams,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(totalExams / limitNum)
    }
  });
});
const getOldQuestionsById = asyncHandler(
  async (req, res) => {
    const user = req.user;
    const { id } = req.params;
    const { search = "", page = "1", limit = "10" } = req.query;
    const pageNum = Number(page);
    const limitNum = Number(limit);
    const skip = (pageNum - 1) * limitNum;
    const paper = await prisma$1.oldQuestionPaper.findFirst({
      where: {
        id,
        institutionId: user.institutionId
      },
      include: {
        questions: { select: { id: true } },
        exam: true
      }
    });
    if (!paper) {
      res.status(404).json({ message: "Old Question Paper not found" });
      return;
    }
    const questionIds = paper.questions.map((q) => q.id);
    const where = {
      // institutionId: user.institutionId,
      id: { in: questionIds }
    };
    if (search) {
      where.OR = [
        { questionText: { contains: search } },
        { year: { contains: search } },
        { language: { contains: search } },
        { explanation: { contains: search } },
        {
          exams: {
            some: {
              examName: { contains: search }
            }
          }
        }
      ];
    }
    const [total, questions] = await Promise.all([
      prisma$1.oldQuestion.count({ where }),
      prisma$1.oldQuestion.findMany({
        where,
        include: {
          exams: { select: { id: true, examName: true } },
          createdBy: { select: { firstName: true, lastName: true } }
        },
        orderBy: { createdAt: "desc" },
        skip,
        take: limitNum
      })
    ]);
    const formattedQuestions = questions.map((question) => ({
      id: question.id,
      questionText: question.questionText,
      year: question.year,
      language: question.language,
      marks: question.marks,
      difficulty: question.difficulty,
      explanation: question.explanation,
      correctAnswer: question.correctAnswer,
      exams: question.exams,
      questionImage: question.questionImage ? `${getHost()}${question.questionImage}` : "",
      explanationImage: question.explanationImage ? `${getHost()}${question.explanationImage}` : "",
      options: question.options.map((option) => ({
        ...option,
        optionImage: option.optionImage ? `${getHost()}${option.optionImage}` : ""
      })),
      createdBy: question.createdBy ? `${question.createdBy.firstName} ${question.createdBy.lastName}` : "Unknown",
      createdAt: toIST(question.createdAt),
      updatedAt: toIST(question.updatedAt)
    }));
    res.status(200).json({
      success: true,
      data: {
        id: paper.id,
        examId: paper.examId,
        examName: paper.exam.examName,
        title: paper.title,
        year: paper.year,
        language: paper.language,
        duration: paper.duration,
        marks: paper.marks,
        difficulty: paper.difficulty,
        publish: paper.publish,
        createdAt: toIST(paper.createdAt),
        updatedAt: toIST(paper.updatedAt)
      },
      questions: formattedQuestions,
      meta: {
        total,
        page: pageNum,
        limit: limitNum,
        totalPages: Math.ceil(total / limitNum)
      }
    });
  }
);
const deleteOldQuestionPaper = asyncHandler(async (req, res) => {
  const user = req.user;
  const { id } = req.params;
  const paper = await prisma$1.oldQuestionPaper.findUnique({
    where: { id, institutionId: user.institutionId }
  });
  if (!paper) {
    res.status(404).json({ message: "Old Question Paper not found" });
    return;
  }
  if (paper.referenceSourceId) {
    res.status(403).json({ success: false, message: "Cannot delete a shared resource." });
    return;
  }
  await prisma$1.$transaction(async (tx) => {
    await tx.oldQuestionPaper.delete({
      where: { id }
    });
    if (paper.examId) {
      const examUsage = await tx.exam.count({
        where: {
          id: paper.examId,
          OR: [
            { students: { some: {} } },
            { oldQuestions: { some: {} } },
            { practiceTests: { some: {} } },
            { oldQuestionPapers: { some: {} } },
            { mockTests: { some: {} } },
            { syllabuses: { some: {} } },
            { notesLinks: { some: {} } },
            { subjectsNotesToExam: { some: {} } }
          ]
        }
      });
      if (examUsage === 0) {
        await tx.exam.delete({ where: { id: paper.examId } });
      }
    }
  });
  res.status(200).json({ success: true, message: "Old Question Paper deleted successfully" });
});
const studentOldQuestionPaperList = asyncHandler(async (req, res) => {
  const user = req.user;
  user.id;
  const studentInstitutionId = user.studentInstitutionId;
  const institutionId = user.institutionId;
  const { search, attempted, exam, year, language, page = "1", limit = "10" } = req.query;
  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);
  const skip = (pageNum - 1) * limitNum;
  const studentInstitution = await prisma$1.studentInstitution.findUnique({
    where: { id: studentInstitutionId },
    include: {
      examResults: {
        select: {
          oldQuestionPaperId: true
        }
      },
      subscriptions: { where: { isCurrent: true } },
      exam: true
    }
  });
  if (!studentInstitution) {
    res.status(404).json({ message: "Student record not found" });
    return;
  }
  const attemptedTestIds = new Set(studentInstitution.examResults.map((r) => r.oldQuestionPaperId).filter(Boolean));
  const where = {
    publish: true,
    institutionId
  };
  if (search) {
    where.title = { contains: search };
  }
  if (year) {
    where.year = { equals: year };
  }
  if (exam) {
    where.examId = exam;
  } else if (studentInstitution?.examsId) {
    where.examId = studentInstitution.examsId;
  }
  if (attempted === "true") {
    where.id = { in: Array.from(attemptedTestIds) };
  } else if (attempted === "false") {
    where.id = { notIn: Array.from(attemptedTestIds) };
  }
  const [total, tests] = await Promise.all([
    prisma$1.oldQuestionPaper.count({ where }),
    prisma$1.oldQuestionPaper.findMany({
      where,
      include: {
        exam: { select: { examName: true } },
        _count: { select: { questions: true } }
      },
      orderBy: { title: "asc" },
      skip,
      take: limitNum
    })
  ]);
  res.status(200).json({
    message: tests.length > 0 ? "Successfully fetched old question papers" : "We will update this year's question paper shortly. Please check another year.",
    data: tests.map((test) => ({
      id: test.id,
      exam: test.exam?.examName,
      language: test.language,
      title: test.title,
      duration: test.duration,
      year: test.year,
      marks: test.marks,
      totalQuestions: test._count.questions,
      attempted: attemptedTestIds.has(test.id)
    })),
    meta: {
      total,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(total / limitNum)
    }
  });
});
const getOldQuestionPaperById = asyncHandler(async (req, res) => {
  const user = req.user;
  const studentInstitutionId = user.studentInstitutionId;
  const examId = req.params.id;
  const { page = "1", limit = "10" } = req.query;
  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);
  const skip = (pageNum - 1) * limitNum;
  await prisma$1.studentInstitution.findUnique({
    where: { id: studentInstitutionId },
    include: { subscriptions: { where: { isCurrent: true } } }
  });
  const oldQuestionPaper = await prisma$1.oldQuestionPaper.findUnique({
    where: { id: examId, institutionId: user.institutionId },
    include: {
      questions: {
        select: {
          id: true,
          questionText: true,
          questionImage: true,
          options: true
        },
        orderBy: {
          createdAt: "asc"
        }
      }
    }
  });
  if (!oldQuestionPaper) {
    res.status(404).json({ message: "Old Question Paper not found" });
    return;
  }
  const totalQuestions = oldQuestionPaper.questions.length;
  const paginatedQuestions = oldQuestionPaper.questions.slice(skip, skip + limitNum);
  const formattedExam = {
    examId: oldQuestionPaper.id,
    title: oldQuestionPaper.title,
    duration: oldQuestionPaper.duration,
    marks: oldQuestionPaper.marks,
    year: oldQuestionPaper.year,
    questions: paginatedQuestions.map((q) => ({
      id: q.id,
      questionText: q.questionText,
      questionImage: q.questionImage ? `${getHost()}${q.questionImage}` : "",
      options: q.options.map((opt) => ({
        option: String(opt?.option) || "",
        optionImage: opt.optionImage ? `${getHost()}${opt.optionImage}` : ""
      }))
    }))
  };
  res.status(200).json({
    message: "Old Question Paper fetched successfully",
    data: formattedExam,
    meta: {
      total: totalQuestions,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(totalQuestions / limitNum)
    }
  });
});
const router$a = express.Router();
const adminProtect = permit("Admin", "Staff", "Institution");
router$a.route("/").post(protect, adminProtect, createOldQuestionPaper).get(protect, adminProtect, listOldQuestionYearsByExam);
router$a.post(
  "/student/submit",
  protect,
  permit("Student"),
  (req, res, next) => {
    req.body.testType = "OldQuestionPaper";
    next();
  },
  submitExam
);
router$a.get("/exams", protect, adminProtect, getExamsOldQuestions);
router$a.get("/student", protect, studentOldQuestionPaperList);
router$a.get("/student/:id", protect, getOldQuestionPaperById);
router$a.route("/:id").get(protect, adminProtect, getOldQuestionsById).put(protect, adminProtect, updateOldQuestionPaper).delete(protect, adminProtect, deleteOldQuestionPaper);
const getAdminStats = asyncHandler(async (req, res) => {
  const [
    totalInstitutions,
    activeInstitutions,
    inactiveInstitutions
  ] = await Promise.all([
    prisma$1.user.count({ where: { role: "INSTITUTION", isDeleted: false } }),
    prisma$1.user.count({ where: { role: "INSTITUTION", isDeleted: false, isVerified: true } }),
    prisma$1.user.count({ where: { role: "INSTITUTION", isDeleted: false, isVerified: false } })
  ]);
  const [totalStudents, totalStaff] = await Promise.all([
    prisma$1.user.count({
      where: {
        role: "STUDENT",
        isDeleted: false,
        createdBy: { role: "INSTITUTION" }
      }
    }),
    prisma$1.user.count({
      where: {
        role: "STAFF",
        isDeleted: false,
        createdBy: { role: "INSTITUTION" }
      }
    })
  ]);
  const recentUsers = await prisma$1.user.findMany({
    where: {
      role: "INSTITUTION",
      isDeleted: false
    },
    orderBy: { createdAt: "desc" },
    take: 10
  });
  const userIds = recentUsers.map((u) => u.id);
  const institutions = await prisma$1.institution.findMany({
    where: {
      userId: { in: userIds }
    },
    include: {
      subscriptions: {
        where: { isCurrent: true },
        take: 1,
        include: {
          plan: {
            select: {
              planName: true,
              actualPrice: true,
              planType: true
            }
          }
        }
      },
      students: {
        select: { id: true }
      },
      staffs: {
        where: { user: { isDeleted: false } },
        select: { id: true }
      }
    }
  });
  const instMap = new Map(institutions.map((inst) => [inst.userId, inst]));
  const recentInstitutions = recentUsers.map((u) => {
    const inst = instMap.get(u.id);
    const currentSub = inst?.subscriptions?.[0] || null;
    return {
      id: inst?.id || u.id,
      userId: u.id,
      institutionName: u.institutionName || `${u.firstName || ""} ${u.lastName || ""}`.trim() || u.email,
      email: u.email,
      phone: u.phone,
      isVerified: u.isVerified ?? false,
      createdAt: toIST(u.createdAt),
      totalStudents: inst?.students?.length ?? 0,
      totalStaff: inst?.staffs?.length ?? 0,
      subscription: currentSub ? {
        id: currentSub.id,
        status: currentSub.status,
        action: currentSub.action,
        planName: currentSub.plan?.planName || "N/A",
        planType: currentSub.plan?.planType || "N/A",
        amount: currentSub.amount,
        chargedAmount: currentSub.chargedAmount,
        startDate: toIST(currentSub.startDate),
        endDate: currentSub.endDate ? toIST(currentSub.endDate) : null
      } : null
    };
  });
  const sevenDaysAgo = /* @__PURE__ */ new Date();
  sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 6);
  sevenDaysAgo.setHours(0, 0, 0, 0);
  const institutionUsers = await prisma$1.user.findMany({
    where: {
      role: "INSTITUTION",
      isDeleted: false,
      createdAt: { gte: sevenDaysAgo }
    },
    select: { createdAt: true }
  });
  const trendMap = {};
  for (let i = 0; i < 7; i++) {
    const d = /* @__PURE__ */ new Date();
    d.setDate(d.getDate() - (6 - i));
    const dateStr = toIST(d, "YYYY-MM-DD");
    trendMap[dateStr] = 0;
  }
  institutionUsers.forEach((u) => {
    const dateStr = toIST(u.createdAt, "YYYY-MM-DD");
    if (trendMap[dateStr] !== void 0) {
      trendMap[dateStr] += 1;
    }
  });
  const platformRegistrationTrend = Object.entries(trendMap).map(([date, count]) => ({ date, count })).sort((a, b) => a.date.localeCompare(b.date));
  res.status(200).json({
    success: true,
    data: {
      role: "ADMIN",
      counts: {
        institutions: {
          total: totalInstitutions,
          verified: activeInstitutions,
          unverified: inactiveInstitutions
        },
        users: {
          totalStudents,
          totalStaff,
          totalPlatformUsers: totalStudents + totalStaff
        }
      },
      recentInstitutions,
      subscriptionDistribution: [],
      platformRegistrationTrend
    }
  });
});
const getInstitutionStats = asyncHandler(async (req, res) => {
  const user = req.user;
  const whereClause = { institutionId: user.institutionId };
  const today = /* @__PURE__ */ new Date();
  today.setHours(0, 0, 0, 0);
  const sevenDaysAgo = new Date(today);
  sevenDaysAgo.setDate(today.getDate() - 6);
  const dateMap = {};
  for (let i = 0; i < 7; i++) {
    const d = new Date(sevenDaysAgo);
    d.setDate(sevenDaysAgo.getDate() + i);
    const dateKey = toIST(d, "YYYY-MM-DD");
    dateMap[dateKey] = { registeredCount: 0, activeSubscriptionCount: 0 };
  }
  const [
    totalStudents,
    verifiedStudents,
    pendingStudents,
    paidStudents,
    expiredStudents,
    totalStaff,
    verifiedStaff,
    pendingStaff,
    totalQuestions,
    totalExams,
    totalSubjects,
    practiceTestsCount,
    mockTestsCount,
    pyqTestsCount,
    registeredStudentsRecent,
    subscriptionsRecent,
    totalRegisteredBefore,
    totalActiveSubscriptionsBefore
  ] = await Promise.all([
    prisma$1.studentInstitution.count({ where: { ...whereClause, deletedAt: null } }),
    prisma$1.studentInstitution.count({ where: { ...whereClause, deletedAt: null, isVerified: true } }),
    prisma$1.studentInstitution.count({ where: { ...whereClause, deletedAt: null, isVerified: false } }),
    prisma$1.studentInstitution.count({
      where: {
        ...whereClause,
        deletedAt: null,
        subscriptions: { some: { status: "ACTIVE", isCurrent: true } }
      }
    }),
    prisma$1.studentInstitution.count({
      where: {
        ...whereClause,
        deletedAt: null,
        subscriptions: { some: { status: "EXPIRED", isCurrent: true } }
      }
    }),
    prisma$1.staff.count({ where: { ...whereClause, user: { isDeleted: false } } }),
    prisma$1.staff.count({ where: { ...whereClause, user: { isDeleted: false, isVerified: true } } }),
    prisma$1.staff.count({ where: { ...whereClause, user: { isDeleted: false, isVerified: false } } }),
    prisma$1.questionBank.count({ where: whereClause }),
    prisma$1.exam.count({ where: whereClause }),
    prisma$1.subject.count({ where: whereClause }),
    prisma$1.practiceTest.count({ where: whereClause }),
    prisma$1.mockTest.count({ where: whereClause }),
    prisma$1.oldQuestionPaper.count({ where: whereClause }),
    // Registered students in last 7 days
    prisma$1.studentInstitution.findMany({
      where: {
        ...whereClause,
        deletedAt: null,
        createdAt: { gte: sevenDaysAgo }
      },
      select: { createdAt: true }
    }),
    // Active subscriptions created in last 7 days
    prisma$1.subscription.findMany({
      where: {
        studentInstitution: {
          institutionId: user.institutionId,
          deletedAt: null
        },
        status: "ACTIVE",
        isCurrent: true,
        createdAt: { gte: sevenDaysAgo }
      },
      select: { createdAt: true }
    }),
    // Baseline counts prior to 7 days
    prisma$1.studentInstitution.count({
      where: {
        ...whereClause,
        deletedAt: null,
        createdAt: { lt: sevenDaysAgo }
      }
    }),
    // Subscriptions baseline prior to 7 days
    prisma$1.subscription.count({
      where: {
        studentInstitution: {
          institutionId: user.institutionId,
          deletedAt: null
        },
        status: "ACTIVE",
        isCurrent: true,
        createdAt: { lt: sevenDaysAgo }
      }
    })
  ]);
  registeredStudentsRecent.forEach((student) => {
    const dateStr = toIST(student.createdAt, "YYYY-MM-DD");
    if (dateMap[dateStr]) {
      dateMap[dateStr].registeredCount += 1;
    }
  });
  subscriptionsRecent.forEach((sub) => {
    const dateStr = toIST(sub.createdAt, "YYYY-MM-DD");
    if (dateMap[dateStr]) {
      dateMap[dateStr].activeSubscriptionCount += 1;
    }
  });
  let cumulativeRegistered = totalRegisteredBefore;
  let cumulativeSubscriptions = totalActiveSubscriptionsBefore;
  const studentSubscriptionTrend = Object.entries(dateMap).sort(([dateA], [dateB]) => dateA.localeCompare(dateB)).map(([date, counts]) => {
    cumulativeRegistered += counts.registeredCount;
    cumulativeSubscriptions += counts.activeSubscriptionCount;
    return {
      date,
      registeredCount: cumulativeRegistered,
      activeSubscriptionCount: cumulativeSubscriptions,
      newRegistrations: counts.registeredCount,
      newSubscriptions: counts.activeSubscriptionCount
    };
  });
  const trendResults = await prisma$1.examResult.findMany({
    where: {
      createdAt: { gte: sevenDaysAgo },
      ...whereClause
    },
    select: { createdAt: true }
  });
  const trendMap = {};
  trendResults.forEach((r) => {
    const dateStr = toIST(r.createdAt, "YYYY-MM-DD");
    trendMap[dateStr] = (trendMap[dateStr] || 0) + 1;
  });
  const trend = Object.entries(trendMap).map(([date, count]) => ({ date, count })).sort((a, b) => a.date.localeCompare(b.date));
  const qBySubjectRaw = await prisma$1.questionBank.groupBy({
    by: ["subjectId"],
    where: whereClause,
    _count: { id: true },
    orderBy: { _count: { id: "desc" } },
    take: 6
  });
  const subjectIds = qBySubjectRaw.map((q) => q.subjectId);
  const subjects = await prisma$1.subject.findMany({
    where: { id: { in: subjectIds } },
    select: { id: true, subjectName: true }
  });
  const questionsBySubject = qBySubjectRaw.map((q) => {
    const s = subjects.find((sub) => sub.id === q.subjectId);
    return {
      id: s?.id || "",
      subjectName: s?.subjectName || "Unknown",
      count: q._count.id
    };
  });
  const [recentExamResultsRaw, recentMockResultsRaw] = await Promise.all([
    prisma$1.examResult.findMany({
      where: whereClause,
      include: {
        practiceTest: { select: { title: true, id: true, exam: { select: { examName: true } } } },
        oldQuestionPaper: { select: { title: true, id: true, exam: { select: { examName: true } } } },
        studentInstitution: {
          select: {
            student: { include: { user: { select: { firstName: true, lastName: true, email: true } } } }
          }
        }
      },
      orderBy: { createdAt: "desc" },
      take: 10
    }),
    prisma$1.mockTestResult.findMany({
      where: { studentInstitution: { institutionId: user.institutionId } },
      include: {
        mockTest: { select: { title: true, id: true, exam: { select: { examName: true } } } },
        studentInstitution: {
          select: {
            student: { include: { user: { select: { firstName: true, lastName: true, email: true } } } }
          }
        }
      },
      orderBy: { createdAt: "desc" },
      take: 10
    })
  ]);
  const mappedExamResults = recentExamResultsRaw.map((r) => {
    const resultStudent = r.studentInstitution?.student;
    return {
      ...r,
      createdAt: toIST(r.createdAt),
      updatedAt: toIST(r.updatedAt),
      testId: r.practiceTest ? { id: r.practiceTest.id, title: r.practiceTest.title } : { id: r.oldQuestionPaper?.id, title: r.oldQuestionPaper?.title || null },
      examName: r.practiceTest ? r.practiceTest.exam.examName : r.oldQuestionPaper?.exam.examName || "Unknown Exam",
      examType: r.practiceTest ? "PRACTICE" : "OQP",
      studentId: resultStudent?.user ? {
        id: resultStudent.userId,
        firstName: resultStudent.user.firstName,
        lastName: resultStudent.user.lastName,
        email: resultStudent.user.email
      } : null
    };
  });
  const mappedMockResults = recentMockResultsRaw.map((r) => {
    const resultStudent = r.studentInstitution?.student;
    return {
      ...r,
      createdAt: toIST(r.createdAt),
      updatedAt: toIST(r.updatedAt),
      testId: r.mockTest ? { id: r.mockTest.id, title: r.mockTest.title } : { id: null, title: null },
      examName: r.mockTest?.exam?.examName || "Unknown Exam",
      examType: "MOCK",
      studentId: resultStudent?.user ? {
        id: resultStudent.userId,
        firstName: resultStudent.user.firstName,
        lastName: resultStudent.user.lastName,
        email: resultStudent.user.email
      } : null
    };
  });
  const recentResults = [...mappedExamResults, ...mappedMockResults].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, 10);
  res.status(200).json({
    success: true,
    data: {
      role: user.role,
      counts: {
        students: {
          total: totalStudents,
          verified: verifiedStudents,
          pending: pendingStudents,
          paid: paidStudents,
          expired: expiredStudents
        },
        staff: {
          total: totalStaff,
          verified: verifiedStaff,
          pending: pendingStaff
        },
        questions: totalQuestions,
        tests: {
          total: practiceTestsCount + mockTestsCount + pyqTestsCount,
          practiceTest: practiceTestsCount,
          mockTest: mockTestsCount,
          pyqTest: pyqTestsCount
        },
        exams: totalExams,
        subjects: totalSubjects
      },
      studentSubscriptionTrend,
      recentResults,
      questionsBySubject,
      trend
    }
  });
});
const getStaffStats = asyncHandler(async (req, res) => {
  const user = req.user;
  const userId = user.id;
  const staffUser = await prisma$1.user.findUnique({
    where: { id: userId },
    select: { createdById: true }
  });
  if (!staffUser || !staffUser.createdById) {
    res.status(404).json({ message: "Staff owner not found" });
    return;
  }
  staffUser.createdById;
  const [totalQuestions, totalTests] = await Promise.all([
    prisma$1.questionBank.count({
      where: {
        institutionId: user.institutionId
      }
    }),
    prisma$1.practiceTest.count({
      where: {
        institutionId: user.institutionId
      }
    })
  ]);
  const [myQuestionsCount, myTestsCount] = await Promise.all([
    prisma$1.questionBank.count({ where: { createdById: userId } }),
    prisma$1.practiceTest.count({ where: { createdById: userId } })
  ]);
  const staffDetails = await prisma$1.staff.findUnique({
    where: { userId },
    include: {
      subjects: { select: { id: true, subjectName: true } }
    }
  });
  const assignedSubjects = staffDetails?.subjects || [];
  const assignedSubjectIds = assignedSubjects.map((s) => s.id);
  const qBySubjectRaw = await prisma$1.questionBank.groupBy({
    by: ["subjectId"],
    where: {
      createdById: userId,
      subjectId: { in: assignedSubjectIds }
    },
    _count: { id: true },
    orderBy: { _count: { id: "desc" } }
  });
  const questionsBySubject = qBySubjectRaw.map((q) => {
    const s = assignedSubjects.find((sub) => sub.id === q.subjectId);
    return {
      id: q.subjectId,
      subjectName: s?.subjectName || "Unknown",
      count: q._count.id
    };
  });
  res.status(200).json({
    success: true,
    data: {
      role: "Staff",
      counts: {
        totalQuestions,
        totalTests,
        myQuestions: myQuestionsCount,
        myTests: myTestsCount,
        assignedSubjects: assignedSubjects.length
      },
      assigned: {
        subjects: assignedSubjects
      },
      questionsBySubject
    }
  });
});
const router$9 = express.Router();
router$9.get("/admin", protect, getAdminStats);
router$9.get("/staff", protect, getStaffStats);
router$9.get("/institution", protect, getInstitutionStats);
const createRole = asyncHandler(async (req, res) => {
  const user = req.user;
  const { roleName, permissions: permissions2 } = req.body;
  if (!roleName || !permissions2) {
    res.status(400).json({ message: "Missing fields" });
    return;
  }
  const where = {
    institutionId: user.institutionId,
    roleName
  };
  const existingRole = await prisma$1.role.findFirst({ where });
  if (existingRole) {
    res.status(400).json({ message: "Role already exists" });
    return;
  }
  const role = await prisma$1.role.create({
    data: {
      roleName,
      permissions: Array.isArray(permissions2) ? permissions2 : [],
      createdById: user.id,
      institutionId: user.institutionId
    }
  });
  res.status(201).json({
    message: "Role created successfully",
    data: {
      ...role,
      createdAt: toIST(role.createdAt),
      updatedAt: toIST(role.updatedAt)
    }
  });
});
const listRoles = asyncHandler(async (req, res) => {
  const user = req.user;
  const search = req.query.search?.trim() || "";
  const page = Math.max(parseInt(req.query.page) || 1, 1);
  const limit = Math.max(parseInt(req.query.limit) || 10, 1);
  const skip = (page - 1) * limit;
  const where = {
    institutionId: user.institutionId
  };
  if (search) {
    where.roleName = { contains: search };
  }
  const [total, roles] = await Promise.all([
    prisma$1.role.count({ where }),
    prisma$1.role.findMany({
      where,
      orderBy: { roleName: "asc" },
      skip,
      take: limit
    })
  ]);
  res.status(200).json({
    message: "Successfully fetched roles list.",
    data: roles.map((role) => ({
      ...role,
      createdAt: toIST(role.createdAt),
      updatedAt: toIST(role.updatedAt)
    })),
    meta: {
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit)
    }
  });
});
const updateRole = asyncHandler(async (req, res) => {
  const roleId = req.params.id;
  const { roleName, permissions: permissions2 } = req.body;
  const user = req.user;
  try {
    const role = await prisma$1.role.update({
      where: { id: roleId, institutionId: user.institutionId },
      data: {
        roleName,
        permissions: Array.isArray(permissions2) ? permissions2 : permissions2 ? [permissions2] : void 0
      }
    });
    res.status(200).json({
      message: "Role updated successfully",
      data: {
        ...role,
        createdAt: toIST(role.createdAt),
        updatedAt: toIST(role.updatedAt)
      }
    });
  } catch (error) {
    res.status(404).json({ message: "Role not found" });
  }
});
const deleteRole = asyncHandler(async (req, res) => {
  const roleId = req.params.id;
  const user = req.user;
  if (!roleId) {
    res.status(400).json({ message: "Role ID is required" });
    return;
  }
  const role = await prisma$1.role.findFirst({
    where: { id: roleId, institutionId: user.institutionId }
  });
  if (!role) {
    res.status(404).json({ message: "Role not found" });
    return;
  }
  const staffCount = await prisma$1.staff.count({
    where: { roleId }
  });
  if (staffCount > 0) {
    res.status(400).json({ message: "Role is in use and cannot be deleted" });
    return;
  }
  await prisma$1.role.delete({
    where: { id: roleId }
  });
  res.status(200).json({
    message: "Role deleted successfully",
    deletedRoleId: roleId
  });
});
const router$8 = express.Router();
router$8.use(protect);
router$8.post("/", createRole);
router$8.get("/", listRoles);
router$8.put("/:id", updateRole);
router$8.delete("/:id", deleteRole);
const formatSequence = (sequence) => ({
  ...sequence,
  endedAt: sequence.endedAt ? toIST(sequence.endedAt) : null,
  createdAt: toIST(sequence.createdAt),
  updatedAt: toIST(sequence.updatedAt)
});
const createReferenceSequence = asyncHandler(async (req, res) => {
  const { prefix, countFrom } = req.body;
  if (!prefix) {
    res.status(400).json({
      message: "Prefix is required"
    });
    return;
  }
  const sequenceType = "RECEIPT";
  const sequencePrefix = prefix.trim().toUpperCase();
  const allowedCountFrom = ["NEW", "CONTINUE"];
  if (!allowedCountFrom.includes(countFrom)) {
    res.status(400).json({
      message: "countFrom must be NEW or CONTINUE"
    });
    return;
  }
  const activeSequence = await prisma$1.referenceSequence.findFirst({
    where: {
      type: sequenceType,
      isActive: true,
      endedAt: null
    },
    select: {
      id: true,
      prefix: true,
      lastCount: true
    },
    orderBy: {
      createdAt: "desc"
    }
  });
  if (activeSequence && activeSequence.prefix === sequencePrefix) {
    res.status(400).json({
      message: "This prefix is already active"
    });
    return;
  }
  const existingPrefixData = await prisma$1.referenceSequence.aggregate({
    where: {
      type: sequenceType,
      prefix: sequencePrefix
    },
    _max: {
      lastCount: true
    }
  });
  const maxLastCount = existingPrefixData._max.lastCount || 0;
  const prefixExists = existingPrefixData._max.lastCount !== null;
  if (prefixExists && countFrom === "NEW") {
    res.status(400).json({
      message: "This prefix already exists. Please use CONTINUE."
    });
    return;
  }
  if (!prefixExists && countFrom === "CONTINUE") {
    res.status(400).json({
      message: "This prefix does not exist before. Please use NEW."
    });
    return;
  }
  let newLastCount = 0;
  if (countFrom === "CONTINUE") {
    newLastCount = maxLastCount;
  }
  if (countFrom === "NEW") {
    newLastCount = 0;
  }
  const newSequence = await prisma$1.$transaction(
    async (tx) => {
      if (activeSequence) {
        await tx.referenceSequence.update({
          where: {
            id: activeSequence.id
          },
          data: {
            isActive: false,
            endedAt: /* @__PURE__ */ new Date()
          }
        });
      }
      return await tx.referenceSequence.create({
        data: {
          type: sequenceType,
          prefix: sequencePrefix,
          lastCount: newLastCount,
          isActive: true
        }
      });
    }
  );
  res.status(201).json({
    success: true,
    message: "Sequence updated successfully",
    data: newSequence
  });
});
const listReferenceSequences = asyncHandler(async (req, res) => {
  const search = req.query.search?.trim() || "";
  const status = req.query.status?.trim() || "";
  const page = Math.max(parseInt(req.query.page) || 1, 1);
  const limit = Math.max(parseInt(req.query.limit) || 10, 1);
  const skip = (page - 1) * limit;
  const where = {};
  if (search) {
    where.OR = [
      { type: { contains: search } },
      { prefix: { contains: search } }
    ];
  }
  if (status === "active") {
    where.isActive = true;
    where.endedAt = null;
  }
  if (status === "inactive") {
    where.isActive = false;
  }
  const [total, sequences] = await Promise.all([
    prisma$1.referenceSequence.count({ where }),
    prisma$1.referenceSequence.findMany({
      where,
      orderBy: [
        { isActive: "desc" },
        { type: "asc" },
        { createdAt: "desc" }
      ],
      skip,
      take: limit
    })
  ]);
  res.status(200).json({
    message: "Successfully fetched reference sequences.",
    data: sequences.map(formatSequence),
    meta: {
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit)
    }
  });
});
const router$7 = express.Router();
router$7.use(protect);
router$7.post("/", onlyAdmin, createReferenceSequence);
router$7.get("/", listReferenceSequences);
const __filename$1 = fileURLToPath(import.meta.url);
const __dirname$2 = path.dirname(__filename$1);
const logFormat = winston.format.combine(
  winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
  winston.format.errors({ stack: true }),
  winston.format.json()
);
const logger = winston.createLogger({
  level: "info",
  format: logFormat,
  transports: [
    new winston.transports.File({
      filename: path.join(__dirname$2, "../logs/error.log"),
      level: "error"
    }),
    new winston.transports.File({
      filename: path.join(__dirname$2, "../logs/app.log")
    })
  ]
});
const errorHandler = (err, req, res, next) => {
  let statusCode = err.status || 500;
  let message = err?.message || "Internal Server Error";
  console.log(err);
  if (err instanceof Prisma.PrismaClientKnownRequestError) {
    statusCode = 400;
    switch (err.code) {
      case "P2002":
        message = `Unique constraint failed on field: ${err.meta?.target} (Code: ${err.code})`;
        break;
      case "P2025":
        message = `Record not found (Code: ${err.code})`;
        break;
      case "P2003":
        message = `Foreign key constraint failed (Code: ${err.code})`;
        break;
      case "P2021":
        message = `Table does not exist (Code: ${err.code})`;
        break;
      default:
        message = `${err.message} (Code: ${err.code})`;
    }
  } else if (err instanceof Prisma.PrismaClientValidationError) {
    statusCode = 400;
    message = "Database validation error: " + err.message;
  }
  logger.error({
    message,
    method: req.method,
    url: req.originalUrl,
    status: statusCode,
    stack: err.stack
  });
  res.status(statusCode).json({
    success: false,
    message,
    code: err.code || void 0
  });
};
const createMockTest = asyncHandler(async (req, res) => {
  const user = req.user;
  const { title, examId, language, duration, totalMarks, publish, generationMode, questions } = req.body;
  if (!title || !examId) {
    res.status(400).json({ message: "Title and Exam are required" });
    return;
  }
  if (questions && questions.length > 200) {
    res.status(400).json({ message: "A mock test cannot exceed 200 questions" });
    return;
  }
  const mockTest = await prisma$1.mockTest.create({
    data: {
      title,
      examId,
      language: language || "English",
      duration: duration ? Number(duration) : 30,
      totalMarks: totalMarks ? Number(totalMarks) : 0,
      questionCount: questions?.length || 0,
      generationMode: generationMode || "manual",
      publish: publish || false,
      createdById: user.id,
      institutionId: user.institutionId,
      questions: {
        create: questions?.map((q) => ({
          questionId: q.questionId,
          subjectId: q.subjectId || null,
          topic: q.topic || null,
          sourceType: q.sourceType || generationMode || "manual"
        })) || []
      }
    },
    include: { questions: true }
  });
  res.status(201).json({
    message: "Mock Test created successfully",
    data: { ...mockTest, createdAt: toIST(mockTest.createdAt), updatedAt: toIST(mockTest.updatedAt) }
  });
});
const generateRandomQuestions = asyncHandler(async (req, res) => {
  const user = req.user;
  const { subjectIds, topics, difficulty, language, count, examId, currentMockTestId } = req.body;
  const countNum = Number(count);
  if (!countNum || countNum <= 0 || countNum > 200) {
    res.status(400).json({ message: "Count must be a number between 1 and 200" });
    return;
  }
  const where = { institutionId: user.institutionId };
  if (subjectIds && Array.isArray(subjectIds) && subjectIds.length > 0) {
    where.subjectId = { in: subjectIds };
  }
  if (topics && Array.isArray(topics) && topics.length > 0) {
    where.topic = { in: topics };
  }
  if (difficulty) where.difficulty = difficulty;
  if (language) where.language = language;
  if (examId) {
    where.mockTestQuestions = {
      none: {
        mockTest: {
          examId,
          ...currentMockTestId ? { id: { not: currentMockTestId } } : {}
        }
      }
    };
  }
  const totalMatching = await prisma$1.questionBank.count({ where });
  if (totalMatching === 0) {
    res.status(404).json({ message: "No questions found matching the criteria" });
    return;
  }
  const allIds = await prisma$1.questionBank.findMany({
    where,
    select: { id: true, subjectId: true }
  });
  const groupedBySubject = {};
  for (const q of allIds) {
    const key = q.subjectId || "unknown";
    if (!groupedBySubject[key]) {
      groupedBySubject[key] = [];
    }
    groupedBySubject[key].push(q);
  }
  for (const key in groupedBySubject) {
    groupedBySubject[key].sort(() => 0.5 - Math.random());
  }
  const selectedIds = [];
  const availableSubjectIds = Object.keys(groupedBySubject);
  let i = 0;
  while (selectedIds.length < countNum && availableSubjectIds.length > 0) {
    const currentIndex = i % availableSubjectIds.length;
    const subId = availableSubjectIds[currentIndex];
    const group = groupedBySubject[subId];
    if (group.length > 0) {
      selectedIds.push(group.pop().id);
      i++;
    } else {
      availableSubjectIds.splice(currentIndex, 1);
    }
  }
  const questions = await prisma$1.questionBank.findMany({
    where: { id: { in: selectedIds } },
    include: {
      subject: { select: { subjectName: true } }
    }
  });
  res.status(200).json({
    message: "Questions generated successfully",
    totalAvailable: totalMatching,
    data: questions.map((q) => ({
      id: q.id,
      questionText: q.questionText,
      questionImage: q.questionImage ? `${getHost()}${q.questionImage}` : "",
      options: (q.options || []).map((opt) => ({
        option: String(opt?.option) ?? "",
        optionImage: opt.optionImage ? `${getHost()}${opt.optionImage}` : ""
      })),
      marks: q.marks,
      difficulty: q.difficulty,
      subject: q.subject?.subjectName || "",
      subjectId: q.subjectId,
      topic: q.topic || "",
      language: q.language,
      correctAnswer: q.correctAnswer,
      explanation: q.explanation,
      explanationImage: q.explanationImage ? `${getHost()}${q.explanationImage}` : "",
      sourceType: "auto"
    }))
  });
});
const replaceQuestion = asyncHandler(async (req, res) => {
  const user = req.user;
  const { subjectIds, topics, difficulty, language, examId, currentMockTestId, existingQuestionIds = [] } = req.body;
  const where = { institutionId: user.institutionId };
  if (subjectIds && Array.isArray(subjectIds) && subjectIds.length > 0) {
    where.subjectId = { in: subjectIds };
  }
  if (topics && Array.isArray(topics) && topics.length > 0) {
    where.topic = { in: topics };
  }
  if (difficulty) where.difficulty = difficulty;
  if (language) where.language = language;
  const excludeIds = Array.isArray(existingQuestionIds) ? existingQuestionIds : [];
  where.id = { notIn: excludeIds };
  if (examId) {
    where.mockTestQuestions = {
      none: {
        mockTest: {
          examId,
          ...currentMockTestId ? { id: { not: currentMockTestId } } : {}
        }
      }
    };
  }
  const alternativeIds = await prisma$1.questionBank.findMany({
    where,
    select: { id: true }
  });
  if (alternativeIds.length === 0) {
    res.status(404).json({ message: "No alternative questions found matching your criteria" });
    return;
  }
  const randomPick = alternativeIds[Math.floor(Math.random() * alternativeIds.length)];
  const question = await prisma$1.questionBank.findUnique({
    where: { id: randomPick.id },
    include: {
      subject: { select: { subjectName: true } }
    }
  });
  if (!question) {
    res.status(404).json({ message: "Question retrieval failed" });
    return;
  }
  res.status(200).json({
    message: "Alternative question found successfully",
    data: {
      id: question.id,
      questionText: question.questionText,
      questionImage: question.questionImage ? `${getHost()}${question.questionImage}` : "",
      options: (question.options || []).map((opt) => ({
        option: String(opt?.option) ?? "",
        optionImage: opt.optionImage ? `${getHost()}${opt.optionImage}` : ""
      })),
      marks: question.marks,
      difficulty: question.difficulty,
      subject: question.subject?.subjectName || "",
      subjectId: question.subjectId,
      topic: question.topic || "",
      language: question.language,
      correctAnswer: question.correctAnswer,
      explanation: question.explanation,
      explanationImage: question.explanationImage ? `${getHost()}${question.explanationImage}` : "",
      sourceType: "auto"
    }
  });
});
const listMockTests = asyncHandler(async (req, res) => {
  const user = req.user;
  const { search, examId, page = "1", limit = "10" } = req.query;
  const pageNum = Number(page);
  const limitNum = Number(limit);
  const skip = (pageNum - 1) * limitNum;
  const where = { institutionId: user.institutionId };
  if (examId) where.examId = examId;
  if (search) where.title = { contains: search };
  const [tests, totalCount] = await prisma$1.$transaction([
    prisma$1.mockTest.findMany({
      where,
      include: {
        exam: { select: { examName: true } },
        _count: { select: { questions: true } },
        referenceInstitution: {
          select: {
            user: { select: { institutionName: true } }
          }
        }
      },
      orderBy: { createdAt: "desc" },
      skip,
      take: limitNum
    }),
    prisma$1.mockTest.count({ where })
  ]);
  res.status(200).json({
    data: tests.map((test) => ({
      ...test,
      createdAt: toIST(test.createdAt),
      updatedAt: toIST(test.updatedAt)
    })),
    meta: {
      total: totalCount,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(totalCount / limitNum)
    }
  });
});
const updateMockTest = asyncHandler(async (req, res) => {
  const user = req.user;
  const { id } = req.params;
  const { title, language, duration, totalMarks, publish, generationMode, questions } = req.body;
  const existing = await prisma$1.mockTest.findFirst({
    where: { id, institutionId: user.institutionId }
  });
  if (!existing) {
    res.status(404).json({ message: "Mock Test not found" });
    return;
  }
  const updateData = {
    title: title || existing.title,
    language: language ?? existing.language,
    duration: duration !== void 0 ? Number(duration) : existing.duration,
    totalMarks: totalMarks !== void 0 ? Number(totalMarks) : existing.totalMarks,
    generationMode: generationMode || existing.generationMode,
    publish: publish !== void 0 ? publish : existing.publish
  };
  if (questions !== void 0) {
    await prisma$1.mockTestQuestion.deleteMany({ where: { mockTestId: id } });
    updateData.questionCount = questions?.length || 0;
    updateData.questions = {
      create: questions?.map((q) => ({
        questionId: q.questionId,
        subjectId: q.subjectId || null,
        topic: q.topic || null,
        sourceType: q.sourceType || "manual"
      })) || []
    };
  }
  const mockTest = await prisma$1.mockTest.update({
    where: { id },
    data: updateData,
    include: { questions: true }
  });
  res.status(200).json({
    message: "Mock Test updated successfully",
    data: { ...mockTest, createdAt: toIST(mockTest.createdAt), updatedAt: toIST(mockTest.updatedAt) }
  });
});
const getMockTestById = asyncHandler(async (req, res) => {
  const user = req.user;
  const { id } = req.params;
  const mockTest = await prisma$1.mockTest.findFirst({
    where: { id, institutionId: user.institutionId },
    include: {
      questions: {
        include: {
          question: true,
          subject: { select: { subjectName: true } }
        },
        orderBy: { createdAt: "asc" }
      }
    }
  });
  if (!mockTest) {
    res.status(404).json({ message: "Mock Test not found" });
    return;
  }
  res.status(200).json({
    data: {
      ...mockTest,
      questions: mockTest.questions.map((q) => ({
        id: q.questionId,
        questionText: q.question.questionText,
        questionImage: q.question.questionImage ? `${getHost()}${q.question.questionImage}` : "",
        options: (q.question.options || []).map((opt) => ({
          option: String(opt?.option) ?? "",
          optionImage: opt.optionImage ? `${getHost()}${opt.optionImage}` : ""
        })),
        marks: q.question.marks,
        difficulty: q.question.difficulty,
        subject: q.subject?.subjectName || "",
        topic: q.topic || "",
        language: q.question.language,
        correctAnswer: q.question.correctAnswer,
        explanation: q.question.explanation,
        explanationImage: q.question.explanationImage ? `${getHost()}${q.question.explanationImage}` : "",
        mockTestQuestionId: q.id,
        sourceType: q.sourceType
      })),
      createdAt: toIST(mockTest.createdAt),
      updatedAt: toIST(mockTest.updatedAt)
    }
  });
});
const getAvailableQuestionCount = asyncHandler(async (req, res) => {
  const user = req.user;
  const { subjectIds, topics, difficulty, language, examId, currentMockTestId } = req.body;
  const whereBase = { institutionId: user.institutionId };
  if (subjectIds && Array.isArray(subjectIds) && subjectIds.length > 0) {
    whereBase.subjectId = { in: subjectIds };
  }
  if (topics && Array.isArray(topics) && topics.length > 0) {
    whereBase.topic = { in: topics };
  }
  if (difficulty) whereBase.difficulty = difficulty;
  if (language) whereBase.language = language;
  const whereAvailable = { ...whereBase };
  if (examId) {
    whereAvailable.mockTestQuestions = {
      none: {
        mockTest: {
          examId,
          ...currentMockTestId ? { id: { not: currentMockTestId } } : {}
        }
      }
    };
  }
  const whereUsed = { ...whereBase };
  if (examId) {
    whereUsed.mockTestQuestions = {
      some: {
        mockTest: {
          examId,
          ...currentMockTestId ? { id: { not: currentMockTestId } } : {}
        }
      }
    };
  }
  const availableCount = await prisma$1.questionBank.count({ where: whereAvailable });
  const usedCount = await prisma$1.questionBank.count({ where: whereUsed });
  const totalCount = availableCount + usedCount;
  res.status(200).json({
    message: "Count fetched successfully",
    available: availableCount,
    used: usedCount,
    total: totalCount,
    count: availableCount
  });
});
const deleteMockTest = asyncHandler(async (req, res) => {
  const user = req.user;
  const { id } = req.params;
  const existing = await prisma$1.mockTest.findFirst({
    where: { id, institutionId: user.institutionId }
  });
  if (!existing) {
    res.status(404).json({ message: "Mock Test not found" });
    return;
  }
  await prisma$1.mockTest.delete({ where: { id } });
  res.status(200).json({ message: "Mock Test deleted successfully" });
});
const studentMockTestList = asyncHandler(async (req, res) => {
  const user = req.user;
  const studentInstitutionId = user.studentInstitutionId;
  const institutionId = user.institutionId;
  const { search, attempted, exam, language, page = "1", limit = "10" } = req.query;
  const fetchAll = limit === "all";
  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);
  const safePageNum = isNaN(pageNum) ? 1 : pageNum;
  const safeLimitNum = isNaN(limitNum) ? 10 : limitNum;
  const skip = (safePageNum - 1) * safeLimitNum;
  const studentInstitution = await prisma$1.studentInstitution.findUnique({
    where: { id: studentInstitutionId, deletedAt: null },
    include: {
      subscriptions: { where: { isCurrent: true } },
      exam: true,
      student: { include: { user: { select: { details: true } } } }
    }
  });
  if (!studentInstitution || !studentInstitution.institutionId) {
    res.status(404).json({ message: "Student record not found" });
    return;
  }
  const results = await prisma$1.mockTestResult.findMany({
    where: { studentInstitutionId },
    select: { mockTestId: true }
  });
  const attemptedTestIds = new Set(results.map((r) => r.mockTestId));
  const where = {
    publish: true,
    institutionId
  };
  if (search) {
    where.title = { contains: search };
  }
  if (exam) {
    where.examId = exam;
  } else if (studentInstitution.examsId) {
    where.examId = studentInstitution.examsId;
  }
  const motherTongue = studentInstitution.student?.language || studentInstitution.language;
  const mediumOfExam = studentInstitution.student?.language ? studentInstitution.language : studentInstitution.student?.user?.details?.mediumOfExam || "English";
  const filterLanguage = language || mediumOfExam;
  if (filterLanguage || motherTongue) {
    const validLanguages = [];
    if (filterLanguage) validLanguages.push(filterLanguage);
    if (motherTongue) validLanguages.push(motherTongue);
    const languageConditions = [
      { language: { in: validLanguages } },
      { questions: { some: { subject: { language: { in: validLanguages } } } } }
    ];
    where.AND = [
      ...where.AND || [],
      { OR: languageConditions }
    ];
  }
  if (attempted === "true") {
    where.id = { in: Array.from(attemptedTestIds) };
  } else if (attempted === "false") {
    where.id = { notIn: Array.from(attemptedTestIds) };
  }
  const [total, tests] = await Promise.all([
    prisma$1.mockTest.count({ where }),
    prisma$1.mockTest.findMany({
      where,
      include: {
        exam: { select: { examName: true } }
      },
      orderBy: { title: "asc" },
      ...!fetchAll && {
        skip,
        take: safeLimitNum
      }
    })
  ]);
  res.status(200).json({
    message: tests.length > 0 ? "Successfully fetched Mock tests" : "No mock test available",
    data: tests.map((test) => ({
      id: test.id,
      exam: test.exam?.examName,
      language: test.language,
      title: test.title,
      duration: test.duration,
      marks: test.totalMarks,
      totalQuestions: test.questionCount,
      attempted: attemptedTestIds.has(test.id)
    })),
    meta: {
      total,
      page: fetchAll ? 1 : safePageNum,
      limit: fetchAll ? total : safeLimitNum,
      totalPages: fetchAll ? 1 : Math.ceil(total / safeLimitNum)
    }
  });
});
const getMockTestByIdForStudent = asyncHandler(async (req, res) => {
  const user = req.user;
  const studentInstitutionId = user.studentInstitutionId;
  const testId = req.params.id;
  const { page = "1", limit = "10" } = req.query;
  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);
  const skip = (pageNum - 1) * limitNum;
  const studentInstitution = await prisma$1.studentInstitution.findUnique({
    where: { id: studentInstitutionId },
    include: { subscriptions: { where: { isCurrent: true } } }
  });
  if (!studentInstitution) {
    res.status(404).json({ message: "Student record not found" });
    return;
  }
  const mockTest = await prisma$1.mockTest.findFirst({
    where: { id: testId, institutionId: user.institutionId },
    include: {
      questions: {
        where: { isActive: true },
        include: {
          question: {
            select: {
              id: true,
              questionText: true,
              questionImage: true,
              options: true
            }
          }
        },
        orderBy: { createdAt: "asc" }
      }
    }
  });
  if (!mockTest) {
    res.status(404).json({ message: "Mock Test not found" });
    return;
  }
  const mappedQuestions = mockTest.questions.map((mtq) => mtq.question);
  const paginatedQuestions = mappedQuestions.slice(skip, skip + limitNum);
  const formattedTest = {
    testId: mockTest.id,
    title: mockTest.title,
    duration: mockTest.duration,
    marks: mockTest.totalMarks,
    questions: paginatedQuestions.map((q) => ({
      id: q.id,
      questionText: q.questionText,
      questionImage: q.questionImage ? `${getHost()}${q.questionImage}` : "",
      options: (q.options || []).map((opt) => ({
        option: String(opt?.option) || "",
        optionImage: opt.optionImage ? `${getHost()}${opt.optionImage}` : ""
      }))
    })),
    totalQuestions: mappedQuestions.length,
    meta: {
      total: mappedQuestions.length,
      page: pageNum,
      limit: limitNum,
      totalPages: Math.ceil(mappedQuestions.length / limitNum)
    }
  };
  res.status(200).json({
    message: "Mock Test fetched successfully",
    data: formattedTest
  });
});
const submitMockTest = asyncHandler(async (req, res) => {
  const user = req.user;
  const userId = user.id;
  const institutionId = user.institutionId;
  const studentInstitutionId = user.studentInstitutionId;
  const { testId, answers } = req.body;
  if (!testId || !answers) {
    res.status(400).json({ message: "testId & answers are required" });
    return;
  }
  const studentProfile = await prisma$1.student.findUnique({
    where: { userId }
  });
  if (!studentProfile) {
    res.status(404).json({ message: "Student profile matching this account was not found" });
    return;
  }
  const studentId = studentProfile.id;
  const test = await prisma$1.mockTest.findUnique({
    where: { id: testId, institutionId },
    include: {
      questions: {
        where: { isActive: true },
        include: {
          question: {
            select: {
              id: true,
              questionText: true,
              questionImage: true,
              options: true,
              correctAnswer: true,
              marks: true,
              difficulty: true,
              explanation: true,
              explanationImage: true
            }
          }
        }
      }
    }
  });
  if (!test) {
    res.status(404).json({ message: "Mock Test not found" });
    return;
  }
  let totalMarks = 0;
  let obtainedMarks = 0;
  let correctCount = 0;
  let wrongCount = 0;
  const validationDetails = [];
  const detailsDataForDb = [];
  test.questions.forEach((mtq) => {
    const q = mtq.question;
    const questionId = q.id;
    const selectedAnswerIndex = answers[questionId];
    const correctIndex = q.correctAnswer;
    const mark = q.marks || 1;
    totalMarks += mark;
    const selectedExist = selectedAnswerIndex != null;
    const isCorrect = selectedExist && selectedAnswerIndex == correctIndex;
    if (isCorrect) {
      correctCount++;
      obtainedMarks += mark;
    } else {
      wrongCount++;
    }
    const optionsArray = Array.isArray(q.options) ? q.options : [];
    validationDetails.push({
      questionId,
      questionText: q.questionText,
      questionImage: q.questionImage ? `${getHost()}${q.questionImage}` : "",
      options: optionsArray.map((option) => ({
        option: String(option.option) ?? "",
        optionImage: option.optionImage ? `${getHost()}${option.optionImage}` : ""
      })),
      correctAnswerIndex: correctIndex,
      difficulty: q.difficulty,
      selectedAnswerIndex: selectedExist ? selectedAnswerIndex : null,
      isCorrect: selectedExist ? isCorrect : null,
      explanation: q.explanation ?? "",
      explanationImage: q.explanationImage ? `${getHost()}${q.explanationImage}` : ""
    });
    detailsDataForDb.push({
      questionId,
      selectedOption: selectedExist ? selectedAnswerIndex : null,
      isCorrect: selectedExist ? isCorrect : null,
      obtainedMarks: isCorrect ? mark : 0
    });
  });
  const computedPercentage = totalMarks > 0 ? obtainedMarks / totalMarks * 100 : 0;
  const result = await prisma$1.mockTestResult.create({
    data: {
      mockTestId: testId,
      studentId,
      studentInstitutionId,
      totalMarks,
      obtainedMarks,
      details: {
        create: detailsDataForDb
      }
    }
  });
  res.status(200).json({
    message: "Mock Test submitted successfully",
    result: {
      resultId: result.id,
      totalQuestions: test.questions.length,
      totalMarks,
      obtainedMarks,
      correctCount,
      wrongCount,
      percentage: computedPercentage.toFixed(2),
      validationDetails
    }
  });
});
const getLastMockTest = asyncHandler(async (req, res) => {
  const user = req.user;
  const userId = user.id;
  const studentInstitutionId = user.studentInstitutionId;
  const responseData = {
    testTitle: "",
    createdAt: "",
    obtainedMarks: 0,
    totalMarks: 0,
    correctCount: 0,
    wrongCount: 0,
    percentage: "0.00",
    validationDetails: []
  };
  const studentProfile = await prisma$1.student.findUnique({
    where: { userId }
  });
  if (!studentProfile) {
    res.status(404).json({
      message: "Student not found"
    });
    return;
  }
  const studentId = studentProfile.id;
  const studentInstitution = studentInstitutionId ? await prisma$1.studentInstitution.findUnique({
    where: { id: studentInstitutionId },
    select: { examsId: true, language: true, student: { select: { language: true } } }
  }) : null;
  const motherTongue = studentInstitution?.student?.language || studentInstitution?.language || "";
  const filterLanguage = studentInstitution?.student?.language ? studentInstitution?.language || "English" : "English";
  const enrolledExamId = studentInstitution?.examsId ?? null;
  const mockTestFilter = {};
  if (enrolledExamId) mockTestFilter.examId = enrolledExamId;
  {
    mockTestFilter.OR = [];
    {
      mockTestFilter.OR.push(
        { language: filterLanguage, questions: { some: { question: { subject: { language: null } } } } }
      );
    }
    if (motherTongue) {
      mockTestFilter.OR.push(
        { questions: { some: { question: { subject: { language: { not: null, equals: motherTongue } } } } } }
      );
    }
  }
  const lastMockResult = await prisma$1.mockTestResult.findFirst({
    where: {
      studentId,
      studentInstitutionId,
      mockTest: mockTestFilter
    },
    include: {
      mockTest: {
        include: {
          questions: {
            where: { isActive: true },
            include: {
              question: true
            }
          }
        }
      },
      details: true
    },
    orderBy: {
      createdAt: "desc"
    }
  });
  if (!lastMockResult) {
    res.status(404).json({
      message: "No mock test attempts found"
    });
    return;
  }
  responseData.testTitle = lastMockResult.mockTest?.title || "Mock Test";
  responseData.createdAt = lastMockResult.createdAt;
  const testQuestions = lastMockResult.mockTest?.questions || [];
  const testAnswers = lastMockResult.details || [];
  testQuestions.forEach((mtq) => {
    const q = mtq.question;
    if (!q) return;
    const questionId = q.id;
    const studentSubmission = testAnswers.find(
      (t) => t.questionId === questionId
    );
    const selectedAnswerIndex = studentSubmission?.selectedOption ?? null;
    const correctIndex = q.correctAnswer;
    const mark = q.marks || 1;
    responseData.totalMarks += mark;
    const selectedExist = selectedAnswerIndex !== null;
    const isCorrect = selectedExist && selectedAnswerIndex == correctIndex;
    if (isCorrect) {
      responseData.correctCount++;
      responseData.obtainedMarks += mark;
    } else {
      responseData.wrongCount++;
    }
    const optionsArray = Array.isArray(q.options) ? q.options : [];
    responseData.validationDetails.push({
      questionId,
      questionText: q.questionText,
      questionImage: q.questionImage ? `${getHost()}${q.questionImage}` : "",
      options: optionsArray.map((option) => ({
        option: option?.option || "",
        optionImage: option?.optionImage ? `${getHost()}${option.optionImage}` : ""
      })),
      correctAnswerIndex: correctIndex,
      difficulty: q.difficulty,
      selectedAnswerIndex,
      isCorrect: selectedExist ? isCorrect : null,
      explanation: q.explanation || "",
      explanationImage: q.explanationImage ? `${getHost()}${q.explanationImage}` : ""
    });
  });
  responseData.percentage = responseData.totalMarks > 0 ? (responseData.obtainedMarks / responseData.totalMarks * 100).toFixed(2) : "0.00";
  res.status(200).json({
    message: "Last mock test details fetched successfully",
    result: responseData
  });
});
const router$6 = Router();
router$6.get("/student", protect, permit("Student"), studentMockTestList);
router$6.post("/student/submit", protect, permit("Student"), submitMockTest);
router$6.get("/student/last-mock-test", protect, permit("Student"), getLastMockTest);
router$6.get("/student/mock-test-report", protect, permit("Student", "Admin", "Institution", "Staff"), getStudentMockWeeklyTrend);
router$6.get("/student/:id", protect, permit("Student"), getMockTestByIdForStudent);
router$6.post("/replace-question", protect, permit("Admin", "Institution", "Staff"), replaceQuestion);
router$6.post("/generate-questions", protect, permit("Admin", "Institution", "Staff"), generateRandomQuestions);
router$6.post("/available-questions-count", protect, permit("Admin", "Institution", "Staff"), getAvailableQuestionCount);
router$6.post("/", protect, permit("Admin", "Institution", "Staff"), createMockTest);
router$6.get("/", protect, permit("Admin", "Institution", "Staff"), listMockTests);
router$6.get("/:id", protect, permit("Admin", "Institution", "Staff"), getMockTestById);
router$6.put("/:id", protect, permit("Admin", "Institution", "Staff"), updateMockTest);
router$6.delete("/:id", protect, permit("Admin", "Institution", "Staff"), deleteMockTest);
const getLeaderboard = asyncHandler(async (req, res) => {
  const user = req.user;
  const studentInstitutionId = user.studentInstitutionId;
  const institutionId = user.institutionId;
  if (!user || user.role !== "STUDENT") {
    res.status(403).json({ message: "Only students can access leaderboard" });
    return;
  }
  const studentInstitution = await prisma$1.studentInstitution.findFirst({
    where: { id: studentInstitutionId },
    include: {
      student: {
        select: {
          language: true,
          user: { select: { firstName: true, lastName: true, details: true } }
        }
      },
      exam: { select: { id: true } }
    }
  });
  if (!studentInstitution || !studentInstitution.examsId) {
    res.status(404).json({ message: "Student exam not found" });
    return;
  }
  const examId = studentInstitution.examsId;
  const myStudentId = studentInstitution.studentId;
  const motherTongue = studentInstitution.student?.language || studentInstitution.language;
  const mediumOfExam = studentInstitution.student?.language ? studentInstitution.language : studentInstitution.student?.user?.details?.mediumOfExam || "English";
  const publishedPracticeTests = await prisma$1.practiceTest.findMany({
    where: {
      institutionId,
      examId,
      publish: true,
      OR: [
        { language: motherTongue },
        { language: mediumOfExam }
      ]
    },
    select: { id: true }
  });
  if (publishedPracticeTests.length === 0) {
    res.json({ leaderboard: [], myRank: null });
    return;
  }
  const practiceTestIds = publishedPracticeTests.map((t) => t.id);
  const activeStudentInstitutions = await prisma$1.studentInstitution.findMany({
    where: { institutionId, deletedAt: null },
    select: { studentId: true }
  });
  const activeStudentIds = activeStudentInstitutions.map((s) => s.studentId);
  const results = await prisma$1.examResult.findMany({
    where: {
      institutionId,
      testType: "PracticeTest",
      practiceTestId: { in: practiceTestIds },
      studentId: { in: activeStudentIds }
    },
    select: {
      obtainedMarks: true,
      totalMarks: true,
      createdAt: true,
      studentId: true,
      student: {
        select: {
          user: { select: { firstName: true, lastName: true } }
        }
      }
    },
    orderBy: { createdAt: "desc" }
  });
  const studentAggregateMap = {};
  for (const r of results) {
    const sId = r.studentId;
    const testUser = r.student?.user;
    const name = testUser ? `${testUser.firstName} ${testUser.lastName}` : "Unknown Student";
    if (!studentAggregateMap[sId]) {
      studentAggregateMap[sId] = {
        studentId: sId,
        name,
        totalObtainedMarks: 0,
        totalMarks: 0,
        latestAttemptAt: r.createdAt
      };
    }
    studentAggregateMap[sId].totalObtainedMarks += r.obtainedMarks;
    studentAggregateMap[sId].totalMarks += r.totalMarks;
    if (r.createdAt > studentAggregateMap[sId].latestAttemptAt) {
      studentAggregateMap[sId].latestAttemptAt = r.createdAt;
    }
  }
  const leaderboard = Object.values(studentAggregateMap).map((item) => {
    const percentage = item.totalMarks > 0 ? parseFloat((item.totalObtainedMarks / item.totalMarks * 100).toFixed(2)) : 0;
    return {
      studentId: item.studentId,
      name: item.name,
      obtainedMarks: item.totalObtainedMarks,
      totalMarks: item.totalMarks,
      percentage,
      createdAtRaw: item.latestAttemptAt,
      createdAt: toIST(item.latestAttemptAt)
    };
  });
  leaderboard.sort((a, b) => {
    if (b.percentage !== a.percentage) return b.percentage - a.percentage;
    if (b.obtainedMarks !== a.obtainedMarks) return b.obtainedMarks - a.obtainedMarks;
    return a.createdAtRaw.getTime() - b.createdAtRaw.getTime();
  });
  let myRank = null;
  const ranked = leaderboard.map((item, index) => {
    const rank = index + 1;
    if (item.studentId === myStudentId) {
      myRank = rank;
    }
    return { rank, ...item, createdAtRaw: void 0 };
  });
  const top10 = ranked.slice(0, 10);
  res.json({
    leaderboard: top10,
    myRank
  });
});
const router$5 = express.Router();
router$5.get("/", protect, permit("Student"), getLeaderboard);
const contactSchema = yup.object({
  name: yup.string().required("Name is required"),
  email: yup.string().email("Invalid email").required("Email is required"),
  subject: yup.string().required("Subject is required"),
  message: yup.string().required("Message is required").min(10, "Message must be at least 10 characters")
});
const sendContactMessage = asyncHandler(async (req, res) => {
  try {
    await contactSchema.validate(req.body, { abortEarly: false });
    const { name, email, subject, message } = req.body;
    const emailHtml = `
            <div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; border: 1px solid #eee; border-radius: 10px; overflow: hidden;">
                <div style="background-color: #0c1b4d; color: #fff; padding: 20px; text-align: center;">
                    <h1 style="margin: 0;">New Contact Message</h1>
                    <p style="margin: 5px 0 0;">ExamInfra Platform</p>
                </div>
                <div style="padding: 20px;">
                    <p><strong>Name:</strong> ${name}</p>
                    <p><strong>Email:</strong> ${email}</p>
                    <p><strong>Subject:</strong> ${subject}</p>
                    <hr style="border: 0; border-top: 1px solid #eee; margin: 20px 0;">
                    <p><strong>Message:</strong></p>
                    <div style="background-color: #f9f9f9; padding: 15px; border-radius: 5px; border-left: 4px solid #0c1b4d;">
                        ${message}
                    </div>
                </div>
                <div style="background-color: #f4f4f4; padding: 15px; text-align: center; font-size: 12px; color: #777;">
                    This message was sent from the ExamInfra Contact Us form.
                </div>
            </div>
        `;
    const emailRes = await sendEmail(
      process.env.FROM_EMAIL || "info@examinfra.com",
      `ExamInfra Contact: ${subject}`,
      emailHtml
    );
    if (!emailRes.success) {
      res.status(500).json({ message: "Failed to send message. Please try again later." });
      return;
    }
    res.status(200).json({
      success: true,
      message: "Your message has been sent successfully!"
    });
  } catch (err) {
    if (err instanceof yup.ValidationError) {
      res.status(400).json({
        success: false,
        message: "Validation failed",
        errors: err.errors
      });
      return;
    }
    res.status(500).json({
      success: false,
      message: "Server error while sending message",
      error: err?.message
    });
  }
});
const router$4 = express.Router();
router$4.post("/", sendContactMessage);
asyncHandler(async (req, res) => {
  const { planId, studentId, studentInstitutionId } = req.body;
  const user = req.user;
  if (!planId || !studentId) {
    res.status(400).json({ message: "planId and studentId are required" });
    return;
  }
  const plan = await prisma$1.subscriptionPlan.findUnique({
    where: { id: planId, isActive: true, isDeleted: false }
  });
  if (!plan) {
    res.status(400).json({ message: "Plan not found or not available" });
    return;
  }
  const studentInstitution = await prisma$1.studentInstitution.findFirst({
    where: {
      id: studentInstitutionId || void 0,
      studentId,
      institutionId: user.institutionId
    },
    include: { student: true }
  });
  if (!studentInstitution) {
    res.status(400).json({ message: "Student not found in your institution" });
    return;
  }
  const existingProcessing = await prisma$1.subscriptionRequest.findFirst({
    where: { studentId, status: "PROCESSING" }
  });
  if (existingProcessing) {
    res.status(400).json({ message: "Student has a payment in progress" });
    return;
  }
  const existingPending = await prisma$1.subscriptionRequest.findFirst({
    where: { studentId, status: "PENDING" }
  });
  if (existingPending) {
    await prisma$1.subscriptionRequest.update({
      where: { id: existingPending.id },
      data: { status: "CANCELLED" }
    });
  }
  const finalAmount = Number(plan.priceAfterDiscount) + Number(plan.priceAfterDiscount) * Number(plan.gstPercentage) / 100;
  const expiryDate = /* @__PURE__ */ new Date();
  expiryDate.setDate(expiryDate.getDate() + 7);
  const request = await prisma$1.subscriptionRequest.create({
    data: {
      subscriptionPlanId: planId,
      studentId,
      studentInstitutionId: studentInstitution.id,
      institutionId: user.institutionId,
      createdById: user.id,
      amount: finalAmount,
      actualPrice: Number(plan.actualPrice),
      discountPercentage: Number(plan.discountPercentage),
      gstPercentage: Number(plan.gstPercentage),
      priceAfterDiscount: Number(plan.priceAfterDiscount),
      duration: plan.duration,
      planSnapshot: JSON.parse(JSON.stringify(plan)),
      expiryDate,
      status: "PENDING"
    },
    include: {
      student: { include: { user: true } },
      subscriptionPlan: true
    }
  });
  res.status(201).json({
    message: "Subscription request created",
    data: {
      ...request,
      createdAt: toIST(request.createdAt),
      updatedAt: toIST(request.updatedAt)
    }
  });
});
const listRequests = asyncHandler(async (req, res) => {
  const user = req.user;
  const search = req.query.search?.trim() || "";
  const status = req.query.status;
  const page = Number(req.query.page || 1);
  const limit = Number(req.query.limit || 10);
  const skip = (page - 1) * limit;
  const where = { institutionId: user.institutionId };
  if (status) where.status = status;
  if (search) {
    const searchWords = search.split(/\s+/).filter(Boolean);
    where.student = {
      user: {
        OR: [
          { firstName: { contains: search } },
          { lastName: { contains: search } },
          { email: { contains: search } },
          ...searchWords.length > 1 ? [{
            AND: searchWords.map((word) => ({
              OR: [
                { firstName: { contains: word } },
                { lastName: { contains: word } }
              ]
            }))
          }] : []
        ]
      }
    };
  }
  const [total, requests] = await Promise.all([
    prisma$1.subscriptionRequest.count({ where }),
    prisma$1.subscriptionRequest.findMany({
      where,
      include: {
        student: {
          include: {
            user: { select: { id: true, firstName: true, lastName: true, email: true, phone: true } }
          }
        },
        subscriptionPlan: { select: { id: true, planName: true, planType: true, duration: true } },
        createdBy: { select: { id: true, firstName: true, lastName: true } },
        subscription: { select: { id: true, startDate: true, endDate: true } }
      },
      orderBy: { createdAt: "desc" },
      skip,
      take: limit
    })
  ]);
  const data = requests.map((r) => ({
    ...r,
    createdAt: toIST(r.createdAt),
    updatedAt: toIST(r.updatedAt),
    expiryDate: r.expiryDate ? toIST(r.expiryDate) : null,
    paidAt: r.paidAt ? toIST(r.paidAt) : null
  }));
  res.status(200).json({
    data,
    meta: { total, page, limit, totalPages: Math.ceil(total / limit) }
  });
});
const getRequest = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const request = await prisma$1.subscriptionRequest.findFirst({
    where: { id, institutionId: user.institutionId },
    include: {
      student: {
        include: {
          user: { select: { id: true, firstName: true, lastName: true, email: true, phone: true } }
        }
      },
      subscriptionPlan: true,
      createdBy: { select: { id: true, firstName: true, lastName: true } },
      subscription: true
    }
  });
  if (!request) {
    res.status(404).json({ message: "Request not found" });
    return;
  }
  res.status(200).json({
    data: {
      ...request,
      createdAt: toIST(request.createdAt),
      updatedAt: toIST(request.updatedAt),
      expiryDate: request.expiryDate ? toIST(request.expiryDate) : null,
      paidAt: request.paidAt ? toIST(request.paidAt) : null
    }
  });
});
const cancelRequest = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = req.user;
  const result = await prisma$1.subscriptionRequest.updateMany({
    where: { id, institutionId: user.institutionId, status: { in: ["PENDING"] } },
    data: { status: "CANCELLED" }
  });
  if (result.count === 0) {
    res.status(400).json({ message: "Request cannot be cancelled (already processed)" });
    return;
  }
  res.status(200).json({ message: "Request cancelled successfully" });
});
const bulkValidate = asyncHandler(async (req, res) => {
  const { studentInstitutionIds, planId } = req.body;
  const validations = [
    {
      validate: () => !Array.isArray(studentInstitutionIds),
      message: "students must be an array"
    },
    {
      validate: () => studentInstitutionIds.length < 1,
      message: "students is required"
    },
    {
      validate: () => !planId,
      message: "planId is required"
    }
  ];
  for (const validation of validations) {
    if (validation.validate()) {
      res.status(400).json({ message: validation.message });
      return;
    }
  }
  const plan = await prisma$1.subscriptionPlan.findUnique({
    where: { id: planId, isActive: true, isDeleted: false }
  });
  if (!plan) {
    res.status(404).json({ message: "Subscription plan not found" });
    return;
  }
  const studentInstitutions = await prisma$1.studentInstitution.findMany({
    where: {
      id: { in: studentInstitutionIds },
      deletedAt: null,
      isVerified: true
    },
    include: {
      student: {
        include: {
          user: { select: { firstName: true, lastName: true, email: true } }
        }
      },
      subscriptions: {
        where: { isCurrent: true },
        include: {
          plan: { select: { planName: true, duration: true, planType: true, priceAfterDiscount: true, gstPercentage: true } }
        }
      }
    }
  });
  const foundIds = studentInstitutions.map((si) => si.id);
  const missingIds = studentInstitutionIds.filter((id) => !foundIds.includes(id));
  const processingRequests = await prisma$1.subscriptionRequest.findMany({
    where: {
      studentInstitutionId: { in: foundIds },
      status: "PROCESSING"
    }
  });
  const processingStudentIds = processingRequests.map((r) => r.studentInstitutionId);
  const students = studentInstitutions.filter((si) => !processingStudentIds.includes(si.id)).map((si) => {
    const currentSub = si.subscriptions[0];
    return {
      studentId: si.studentId,
      studentInstitutionId: si.id,
      name: `${si.student.user.firstName} ${si.student.user.lastName}`,
      email: si.student.user.email,
      currentPlan: currentSub?.planId || "TRIAL",
      currentPlanName: currentSub?.plan?.planName || "TRIAL",
      currentPlanType: currentSub?.plan?.planType || "TRIAL",
      currentPlanStatus: currentSub?.status || "TRIAL"
    };
  });
  res.status(200).json({
    message: "Bulk validation complete",
    data: {
      planName: plan.planName,
      duration: plan.duration,
      priceAfterDiscount: Number(plan.priceAfterDiscount),
      gstPercentage: Number(plan.gstPercentage),
      totalEligible: students.length,
      totalSelected: studentInstitutionIds.length,
      students,
      skipped: {
        missingFromInstitution: missingIds,
        hasProcessingPayment: processingStudentIds
      }
    }
  });
});
const bulkSend = asyncHandler(async (req, res) => {
  const { studentInstitutionIds, planId } = req.body;
  const user = req.user;
  const validations = [
    {
      validate: () => !Array.isArray(studentInstitutionIds),
      message: "students must be an array"
    },
    {
      validate: () => studentInstitutionIds.length < 1,
      message: "students is required"
    },
    {
      validate: () => !planId,
      message: "planId is required"
    }
  ];
  for (const validation of validations) {
    if (validation.validate()) {
      res.status(400).json({ message: validation.message });
      return;
    }
  }
  const plan = await prisma$1.subscriptionPlan.findUnique({
    where: { id: planId, isActive: true, isDeleted: false }
  });
  if (!plan) {
    res.status(404).json({ message: "Plan not found or not available" });
    return;
  }
  const studentInstitutions = await prisma$1.studentInstitution.findMany({
    where: {
      id: { in: studentInstitutionIds }
    }
  });
  const eligibleStudentInstitutionsIds = studentInstitutions.map((si) => si.id);
  const processingRequests = await prisma$1.subscriptionRequest.findMany({
    where: {
      studentInstitutionId: { in: eligibleStudentInstitutionsIds },
      status: "PROCESSING"
    }
  });
  const processingStudentInstitutionIds = processingRequests.map((r) => r.studentInstitutionId);
  const validStudentInstitutionIds = eligibleStudentInstitutionsIds.filter((id) => !processingStudentInstitutionIds.includes(id));
  if (validStudentInstitutionIds.length === 0) {
    res.status(400).json({ message: "No eligible students to send requests" });
    return;
  }
  const existingPending = await prisma$1.subscriptionRequest.findMany({
    where: {
      studentInstitutionId: { in: validStudentInstitutionIds },
      status: "PENDING"
    }
  });
  if (existingPending.length > 0) {
    await prisma$1.subscriptionRequest.updateMany({
      where: { id: { in: existingPending.map((r) => r.id) } },
      data: { status: "CANCELLED" }
    });
  }
  const finalAmount = Number(plan.priceAfterDiscount);
  const expiryDate = /* @__PURE__ */ new Date();
  expiryDate.setDate(expiryDate.getDate() + 3);
  const requests = await prisma$1.subscriptionRequest.createMany({
    data: validStudentInstitutionIds.map((studentInstitutionId) => ({
      subscriptionPlanId: plan.id,
      studentId: studentInstitutions.find((s) => s.id == studentInstitutionId)?.studentId,
      studentInstitutionId,
      institutionId: user.institutionId,
      createdById: user.id,
      amount: finalAmount,
      actualPrice: Number(plan.actualPrice),
      discountPercentage: Number(plan.discountPercentage),
      gstPercentage: Number(plan.gstPercentage),
      priceAfterDiscount: Number(plan.priceAfterDiscount),
      duration: plan.duration,
      planSnapshot: JSON.parse(JSON.stringify(plan)),
      expiryDate,
      status: "PENDING"
    }))
  });
  try {
    const students = await prisma$1.studentInstitution.findMany({
      where: { id: { in: validStudentInstitutionIds } },
      select: {
        student: {
          select: {
            fcmToken: true
          }
        }
      }
    });
    if (students.length > 0) {
      console.log(students);
      const tokens = students.map((s) => s?.student?.fcmToken).filter((token) => !!token);
      if (tokens.length > 0) {
        await sendNotification(
          tokens,
          "New Subscription Request Added",
          "For Plan: " + plan.planName,
          { type: "subscription_request" }
        );
      }
    }
  } catch (notifyError) {
    console.error("Failed to send push notifications:", notifyError);
  }
  res.status(201).json({
    message: "Bulk requests sent successfully",
    data: {
      totalSent: requests.count,
      planName: plan.planName
    }
  });
});
const getMyRequests = asyncHandler(async (req, res) => {
  const user = req.user;
  const studentInstitution = await prisma$1.studentInstitution.findUnique({ where: { id: user.studentInstitutionId } });
  if (!studentInstitution) {
    res.status(404).json({ message: "Student not found" });
    return;
  }
  const request = await prisma$1.subscriptionRequest.findFirst({
    where: { studentInstitutionId: studentInstitution.id, status: "PENDING" },
    include: {
      subscriptionPlan: { select: { id: true, planName: true, planType: true, duration: true, features: true, description: true } },
      institution: {
        select: {
          id: true,
          user: { select: { institutionName: true } }
        }
      },
      subscription: { select: { id: true, startDate: true, endDate: true, status: true } }
    },
    orderBy: { createdAt: "desc" }
  });
  const data = request ? {
    ...request,
    subscriptionRequestId: request.id,
    id: void 0,
    createdAt: toIST(request.createdAt),
    expiryDate: request.expiryDate ? toIST(request.expiryDate) : null,
    paidAt: request.paidAt ? toIST(request.paidAt) : null,
    institutionName: request.institution?.user?.institutionName || null,
    planSnapshot: null
  } : {};
  res.status(200).json({
    message: "Requests fetched successfully",
    data
  });
  return;
});
const router$3 = Router();
router$3.get("/mine", protect, permit("Student"), getMyRequests);
router$3.post("/bulk-validate", protect, permit("Institution", "Staff"), bulkValidate);
router$3.post("/bulk-send", protect, permit("Institution", "Staff"), bulkSend);
router$3.get("/", protect, permit("Institution", "Staff"), listRequests);
router$3.get("/:id", protect, permit("Institution", "Staff"), getRequest);
router$3.delete("/:id", protect, permit("Institution", "Staff"), cancelRequest);
const prisma = new PrismaClient();
const formatFileUrl = (filePath) => {
  if (!filePath) return null;
  if (filePath.startsWith("http")) return filePath;
  const publicPath = toPublicPath(filePath);
  return publicPath ? `${getHost()}${publicPath}` : null;
};
const notifyStudentsAboutNotes = async (institutionId, subjectId, subjectName, title, itemType) => {
  try {
    const mappedExams = await prisma.subjectsNotesToExam.findMany({
      where: { subjectId },
      select: { examId: true }
    });
    const examIds = mappedExams.map((m) => m.examId);
    if (examIds.length > 0) {
      const students = await prisma.studentInstitution.findMany({
        where: { examsId: { in: examIds }, institutionId, deletedAt: null },
        select: { student: { select: { fcmToken: true } } }
      });
      const tokens = students.map((s) => s.student?.fcmToken).filter((token) => !!token);
      if (tokens.length > 0) {
        const payload = {
          title: `Notes & Links: ${subjectName}`,
          body: "New materials have been published for your exam.",
          data: { type: itemType === "PDF" ? "notes" : "links" }
        };
        const fcmResponse = await sendNotification(
          tokens,
          payload.title,
          payload.body,
          payload.data
        );
        return { ...fcmResponse, sentPayload: payload };
      }
    }
    return { successCount: 0, failureCount: 0, invalidTokens: [], message: "No students to notify" };
  } catch (error) {
    console.error("Failed to send push notifications for notes:", error);
    return { successCount: 0, failureCount: 0, invalidTokens: [], error: String(error) };
  }
};
const createNotesLink = async (req, res) => {
  try {
    let { title, type, linkUrl, subjectId, institutionId } = req.body;
    const userId = req.user?.id;
    if (!institutionId && req.user?.institutionId) {
      institutionId = req.user.institutionId;
    }
    if (!title || !type || !subjectId || !institutionId) {
      return res.status(400).json({
        success: false,
        message: "Title, type, subjectId, and institutionId are required."
      });
    }
    if (!userId) {
      return res.status(401).json({ success: false, message: "Unauthorized." });
    }
    let fileUrl = void 0;
    if (type === NotesType.PDF) {
      if (req.file) {
        fileUrl = toPublicPath(req.file.path) || void 0;
      } else if (!req.body.fileUrl) {
        return res.status(400).json({
          success: false,
          message: "PDF file or file URL is required for PDF type notes."
        });
      } else {
        fileUrl = req.body.fileUrl;
      }
    } else if (type === NotesType.LINK) {
      if (!linkUrl) {
        return res.status(400).json({
          success: false,
          message: "Link URL is required for LINK type notes."
        });
      }
    }
    const autoVerification = type === NotesType.PDF ? NotesLinksStatus.VERIFIED : NotesLinksStatus.PENDING;
    const notesLink = await prisma.notesLinks.create({
      data: {
        title,
        type,
        fileUrl: type === NotesType.PDF ? fileUrl : null,
        linkUrl: type === NotesType.LINK ? linkUrl : null,
        subjectId,
        institutionId,
        createdById: userId,
        verification: autoVerification
      },
      include: {
        subject: { select: { id: true, subjectName: true } }
      }
    });
    const notificationStatus = await notifyStudentsAboutNotes(
      notesLink.institutionId,
      notesLink.subjectId,
      notesLink.subject.subjectName,
      notesLink.title,
      notesLink.type
    );
    return res.status(201).json({
      success: true,
      message: type === NotesType.PDF ? "Notes uploaded successfully and is now available to students." : "Link submitted successfully and is pending admin approval.",
      notificationStatus,
      data: {
        id: notesLink.id,
        title: notesLink.title,
        type: notesLink.type,
        fileUrl: formatFileUrl(notesLink.fileUrl),
        linkUrl: notesLink.linkUrl,
        verification: notesLink.verification,
        subjectId: notesLink.subjectId,
        subject: notesLink.subject,
        institutionId: notesLink.institutionId,
        createdAt: notesLink.createdAt,
        updatedAt: notesLink.updatedAt
      }
    });
  } catch (error) {
    console.error("Error creating notes/link:", error);
    return res.status(500).json({
      success: false,
      message: "Internal server error.",
      error: error.message
    });
  }
};
const getAllNotesLinks = async (req, res) => {
  try {
    const user = req.user;
    if (!user) {
      return res.status(401).json({ success: false, message: "Unauthorized." });
    }
    const {
      subjectId,
      examId,
      institutionId,
      type,
      verification,
      search,
      title,
      page = 1,
      limit = 10
    } = req.query;
    const pageNum = Number(page);
    const limitNum = Number(limit);
    const skip = (pageNum - 1) * limitNum;
    const whereCondition = {};
    if (user.role === "STUDENT") {
      whereCondition.verification = { not: NotesLinksStatus.REJECTED };
      const studentInstitutionId = user.studentInstitutionId;
      if (studentInstitutionId) {
        const studentInstitution = await prisma.studentInstitution.findUnique({
          where: { id: studentInstitutionId },
          select: { examsId: true, institutionId: true }
        });
        if (studentInstitution?.examsId) {
          const studentExamId = studentInstitution.examsId;
          whereCondition.institutionId = studentInstitution.institutionId;
          whereCondition.OR = [
            { subject: { subjectsNotesToExam: { some: { examId: studentExamId } } } }
          ];
        } else {
          whereCondition.institutionId = studentInstitution?.institutionId || "NONE";
        }
      }
    } else if (user.role === "INSTITUTION" || user.role === "STAFF") {
      whereCondition.institutionId = user.institutionId;
      if (verification && Object.values(NotesLinksStatus).includes(
        verification
      )) {
        whereCondition.verification = verification;
      }
    } else if (user.role === "ADMIN") {
      if (institutionId)
        whereCondition.institutionId = String(institutionId);
      if (verification && Object.values(NotesLinksStatus).includes(
        verification
      )) {
        whereCondition.verification = verification;
      }
      whereCondition.subject = {
        subjectsNotesToExam: { some: {} }
      };
    }
    if (examId) {
      whereCondition.OR = [
        { subject: { subjectsNotesToExam: { some: { examId: String(examId) } } } }
      ];
    }
    if (subjectId) {
      whereCondition.subjectId = String(subjectId).trim();
    }
    if (type && Object.values(NotesType).includes(type)) {
      whereCondition.type = type;
    }
    const searchQuery = title || search;
    if (searchQuery) {
      whereCondition.title = {
        contains: String(searchQuery).trim()
      };
    }
    const [total, notesLinks] = await Promise.all([
      prisma.notesLinks.count({ where: whereCondition }),
      prisma.notesLinks.findMany({
        where: whereCondition,
        skip,
        take: limitNum,
        orderBy: { createdAt: "desc" },
        include: {
          subject: { select: { id: true, subjectName: true } },
          institution: { include: { user: { select: { institutionName: true } } } },
          _count: { select: { reports: true } }
        }
      })
    ]);
    const formattedNotesLinks = notesLinks.map((note) => ({
      id: note.id,
      title: note.title,
      type: note.type,
      fileUrl: formatFileUrl(note.fileUrl),
      linkUrl: note.linkUrl,
      verification: note.verification,
      rejectReason: note.rejectReason,
      subjectId: note.subjectId,
      subject: note.subject,
      institutionId: note.institutionId,
      institutionName: note.institution?.user?.institutionName,
      createdAt: note.createdAt,
      updatedAt: note.updatedAt,
      _count: note._count
    }));
    return res.status(200).json({
      success: true,
      data: formattedNotesLinks,
      pagination: {
        total,
        page: pageNum,
        limit: limitNum,
        totalPages: Math.ceil(total / limitNum)
      }
    });
  } catch (error) {
    console.error("Error fetching notes/links:", error);
    return res.status(500).json({
      success: false,
      message: "Internal server error.",
      error: error.message
    });
  }
};
const getNotesLinkById = async (req, res) => {
  try {
    const { id } = req.params;
    const notesLink = await prisma.notesLinks.findUnique({
      where: { id },
      include: {
        subject: { select: { id: true, subjectName: true } },
        reports: {
          include: {
            student: {
              select: {
                id: true,
                userId: true,
                user: {
                  select: {
                    id: true,
                    firstName: true,
                    lastName: true,
                    email: true
                  }
                }
              }
            }
          }
        }
      }
    });
    if (!notesLink) {
      return res.status(404).json({ success: false, message: "Notes/Link not found." });
    }
    if (req.user?.role === "STUDENT" && notesLink.verification === NotesLinksStatus.REJECTED) {
      return res.status(403).json({ success: false, message: "Access denied. This note/link has been rejected." });
    }
    const formattedNote = {
      id: notesLink.id,
      title: notesLink.title,
      type: notesLink.type,
      fileUrl: formatFileUrl(notesLink.fileUrl),
      linkUrl: notesLink.linkUrl,
      verification: notesLink.verification,
      rejectReason: notesLink.rejectReason,
      subjectId: notesLink.subjectId,
      subject: notesLink.subject,
      institutionId: notesLink.institutionId,
      createdAt: notesLink.createdAt,
      updatedAt: notesLink.updatedAt,
      reports: notesLink.reports
    };
    return res.status(200).json({ success: true, data: formattedNote });
  } catch (error) {
    console.error("Error fetching notes/link detail:", error);
    return res.status(500).json({
      success: false,
      message: "Internal server error.",
      error: error.message
    });
  }
};
const updateVerificationStatus = async (req, res) => {
  try {
    const { id } = req.params;
    const { verification, rejectReason } = req.body;
    if (!verification || !Object.values(NotesLinksStatus).includes(verification)) {
      return res.status(400).json({
        success: false,
        message: "Invalid verification status. Must be VERIFIED or REJECTED."
      });
    }
    if (verification === NotesLinksStatus.REJECTED && !rejectReason) {
      return res.status(400).json({
        success: false,
        message: "Reason is required when rejecting a note/link."
      });
    }
    const existingNote = await prisma.notesLinks.findUnique({ where: { id } });
    if (!existingNote) {
      return res.status(404).json({ success: false, message: "Notes/Link not found." });
    }
    if (existingNote.type === NotesType.PDF) {
      return res.status(400).json({
        success: false,
        message: "PDF notes are automatically verified and cannot have their status changed."
      });
    }
    const updatedNotesLink = await prisma.notesLinks.update({
      where: { id },
      data: {
        verification,
        rejectReason: verification === NotesLinksStatus.REJECTED ? rejectReason : null
      }
    });
    return res.status(200).json({
      success: true,
      message: `Notes/Link status updated to ${verification}.`,
      data: {
        ...updatedNotesLink,
        fileUrl: formatFileUrl(updatedNotesLink.fileUrl)
      }
    });
  } catch (error) {
    console.error("Error updating verification status:", error);
    return res.status(500).json({
      success: false,
      message: "Internal server error.",
      error: error.message
    });
  }
};
const updateNotesLink = async (req, res) => {
  try {
    const { id } = req.params;
    const user = req.user;
    const { title, type, linkUrl, subjectId, examId } = req.body;
    const existingNote = await prisma.notesLinks.findUnique({ where: { id } });
    if (!existingNote) {
      return res.status(404).json({ success: false, message: "Notes/Link not found." });
    }
    if (user?.role !== "ADMIN" && existingNote.institutionId !== user?.institutionId) {
      return res.status(403).json({ success: false, message: "Forbidden: You can only edit your own institution's notes." });
    }
    let fileUrl = existingNote.fileUrl;
    if (req.file) {
      fileUrl = toPublicPath(req.file.path) || void 0;
    }
    const resolvedType = type ?? existingNote.type;
    const newVerification = resolvedType === NotesType.PDF ? NotesLinksStatus.VERIFIED : NotesLinksStatus.PENDING;
    const updatedNotesLink = await prisma.notesLinks.update({
      where: { id },
      data: {
        title: title ?? existingNote.title,
        type: resolvedType,
        fileUrl: resolvedType === NotesType.PDF ? fileUrl : null,
        linkUrl: resolvedType === NotesType.LINK ? linkUrl ?? existingNote.linkUrl : null,
        subjectId: subjectId ?? existingNote.subjectId,
        examId: examId ?? existingNote.examId,
        verification: newVerification,
        rejectReason: null
      }
    });
    return res.status(200).json({
      success: true,
      message: resolvedType === NotesType.PDF ? "Notes updated successfully." : "Link updated and sent for admin approval.",
      data: {
        ...updatedNotesLink,
        fileUrl: formatFileUrl(updatedNotesLink.fileUrl)
      }
    });
  } catch (error) {
    console.error("Error updating notes/link:", error);
    return res.status(500).json({
      success: false,
      message: "Internal server error.",
      error: error.message
    });
  }
};
const deleteNotesLink = async (req, res) => {
  try {
    const { id } = req.params;
    const user = req.user;
    const existingNote = await prisma.notesLinks.findUnique({ where: { id } });
    if (!existingNote) {
      return res.status(404).json({ success: false, message: "Notes/Link not found." });
    }
    if (user?.role !== "ADMIN" && existingNote.institutionId !== user?.institutionId) {
      return res.status(403).json({ success: false, message: "Forbidden: You can only delete your own institution's notes." });
    }
    await prisma.notesLinks.delete({ where: { id } });
    const remainingNotes = await prisma.notesLinks.count({
      where: { subjectId: existingNote.subjectId }
    });
    if (remainingNotes === 0) {
      await prisma.subjectsNotesToExam.deleteMany({
        where: { subjectId: existingNote.subjectId }
      });
    }
    return res.status(200).json({
      success: true,
      message: "Notes/Link deleted successfully."
    });
  } catch (error) {
    console.error("Error deleting notes/link:", error);
    return res.status(500).json({
      success: false,
      message: "Internal server error.",
      error: error.message
    });
  }
};
const reportNotesLink = async (req, res) => {
  try {
    const { id: notesLinksId } = req.params;
    const user = req.user;
    const { message, studentInstitutionId } = req.body;
    if (!message) {
      return res.status(400).json({
        success: false,
        message: "Message is required."
      });
    }
    const studentRecord = await prisma.student.findFirst({
      where: { userId: user?.id }
    });
    if (!studentRecord) {
      return res.status(404).json({ success: false, message: "Student record not found." });
    }
    const notesLink = await prisma.notesLinks.findUnique({ where: { id: notesLinksId } });
    if (!notesLink || notesLink.verification === NotesLinksStatus.REJECTED) {
      return res.status(404).json({ success: false, message: "Notes/Link not found or not accessible." });
    }
    const report = await prisma.notesLinksReport.create({
      data: {
        message,
        notesLinksId,
        studentId: studentRecord.id,
        studentInstitutionId: studentInstitutionId || null
      }
    });
    return res.status(201).json({
      success: true,
      message: "Report submitted successfully.",
      data: {
        ...notesLink,
        fileUrl: formatFileUrl(notesLink.fileUrl)
      }
    });
  } catch (error) {
    console.error("Error submitting report:", error);
    return res.status(500).json({
      success: false,
      message: "Internal server error.",
      error: error.message
    });
  }
};
const router$2 = Router();
router$2.post("/", protect, permit("Admin", "Institution", "Staff"), notesUpload.single("file"), createNotesLink);
router$2.get("/", protect, getAllNotesLinks);
router$2.get("/:id", protect, getNotesLinkById);
router$2.patch("/:id/verify", protect, permit("Admin"), updateVerificationStatus);
router$2.put("/:id", protect, permit("Admin", "Institution", "Staff"), notesUpload.single("file"), updateNotesLink);
router$2.delete("/:id", protect, permit("Admin", "Institution", "Staff"), deleteNotesLink);
router$2.post("/:id/report", protect, permit("Student"), reportNotesLink);
const router$1 = express.Router();
router$1.get("/", protect, getLanguages);
async function getOrCreateTargetExam(tx, sourceExamId, targetInstId) {
  if (!sourceExamId) return null;
  const sourceExam = await tx.exam.findUnique({
    where: { id: sourceExamId }
  });
  if (!sourceExam) return null;
  const existingTargetExam = await tx.exam.findFirst({
    where: {
      examName: { equals: sourceExam.examName.trim() },
      institutionId: targetInstId
    }
  });
  if (existingTargetExam) {
    return existingTargetExam.id;
  }
  const newExam = await tx.exam.create({
    data: {
      examName: sourceExam.examName,
      institutionId: targetInstId
    }
  });
  return newExam.id;
}
const getAvailableResources = asyncHandler(async (req, res) => {
  const { institutionId, resourceType, search, page = "1", limit = "10", targetInstitutionId, sharedStatus, subjectId, examId } = req.query;
  if (!institutionId || !resourceType) {
    res.status(400).json({ success: false, message: "Missing required fields" });
    return;
  }
  const pageNum = parseInt(page) || 1;
  const limitNum = parseInt(limit) || 10;
  const skip = (pageNum - 1) * limitNum;
  const institutionRecord = await prisma$1.institution.findFirst({
    where: { userId: institutionId },
    select: { id: true }
  });
  const resolvedInstitutionId = institutionRecord?.id ?? institutionId;
  let data = [];
  let total = 0;
  let stats = void 0;
  const searchQuery = search ? {
    title: { contains: search }
  } : {};
  let resolvedTargetInstId = null;
  if (targetInstitutionId) {
    const targetRec = await prisma$1.institution.findFirst({ where: { userId: targetInstitutionId }, select: { id: true } });
    resolvedTargetInstId = targetRec?.id ?? targetInstitutionId;
  }
  if (resourceType === "TEST") {
    const whereClause = { institutionId: resolvedInstitutionId, referenceSourceId: null, ...searchQuery };
    if (subjectId) {
      whereClause.subjectId = subjectId;
    }
    if (resolvedTargetInstId && sharedStatus) {
      if (sharedStatus === "SHARED") {
        whereClause.sharedCopies = { some: { institutionId: resolvedTargetInstId } };
      } else if (sharedStatus === "NOT_SHARED") {
        whereClause.sharedCopies = { none: { institutionId: resolvedTargetInstId } };
      }
    }
    const [tests, count] = await Promise.all([
      prisma$1.test.findMany({
        where: whereClause,
        skip,
        take: limitNum,
        include: { subject: true, _count: { select: { testQuestions: true } } },
        orderBy: { createdAt: "desc" }
      }),
      prisma$1.test.count({ where: whereClause })
    ]);
    if (resolvedTargetInstId) {
      const processedResources = [];
      for (const t of tests) {
        const shared = await prisma$1.test.findFirst({ where: { referenceSourceId: t.id, institutionId: resolvedTargetInstId } });
        let isRemovable = false;
        if (shared) {
          const mappedPracticeTests = await prisma$1.practiceTest.findMany({ where: { testId: shared.id, institutionId: resolvedTargetInstId }, select: { id: true } });
          if (mappedPracticeTests.length === 0) {
            isRemovable = true;
          } else {
            const practiceIds = mappedPracticeTests.map((p) => p.id);
            const resultsCount = await prisma$1.examResult.count({ where: { practiceTestId: { in: practiceIds } } });
            if (resultsCount === 0) isRemovable = true;
          }
        }
        processedResources.push({
          ...t,
          isShared: !!shared,
          sharedId: shared?.id || null,
          isRemovable
        });
      }
      data = processedResources;
    } else {
      data = tests;
    }
    total = count;
    if (resolvedTargetInstId) {
      const [totalCount, sharedCount, availableCount] = await Promise.all([
        prisma$1.test.count({
          where: {
            institutionId: resolvedInstitutionId,
            referenceSourceId: null,
            subjectId: subjectId ? subjectId : void 0
          }
        }),
        prisma$1.test.count({
          where: {
            institutionId: resolvedInstitutionId,
            referenceSourceId: null,
            subjectId: subjectId ? subjectId : void 0,
            sharedCopies: {
              some: {
                institutionId: resolvedTargetInstId
              }
            }
          }
        }),
        prisma$1.test.count({
          where: {
            institutionId: resolvedInstitutionId,
            referenceSourceId: null,
            subjectId: subjectId ? subjectId : void 0,
            sharedCopies: {
              none: {
                institutionId: resolvedTargetInstId
              }
            }
          }
        })
      ]);
      stats = {
        totalCount,
        sharedCount,
        availableCount
      };
    } else {
      const totalCount = await prisma$1.test.count({
        where: {
          institutionId: resolvedInstitutionId,
          referenceSourceId: null,
          subjectId: subjectId ? subjectId : void 0
        }
      });
      stats = {
        totalCount,
        sharedCount: 0,
        availableCount: totalCount
      };
    }
  } else if (resourceType === "PYQ") {
    const whereClause = { institutionId: resolvedInstitutionId, referenceSourceId: null, ...searchQuery };
    if (examId) {
      whereClause.examId = examId;
    }
    if (resolvedTargetInstId && sharedStatus) {
      if (sharedStatus === "SHARED") {
        whereClause.sharedCopies = { some: { institutionId: resolvedTargetInstId } };
      } else if (sharedStatus === "NOT_SHARED") {
        whereClause.sharedCopies = { none: { institutionId: resolvedTargetInstId } };
      }
    }
    const [pyqs, count] = await Promise.all([
      prisma$1.oldQuestionPaper.findMany({
        where: whereClause,
        skip,
        take: limitNum,
        include: { _count: { select: { questions: true } }, exam: true },
        orderBy: { createdAt: "desc" }
      }),
      prisma$1.oldQuestionPaper.count({ where: whereClause })
    ]);
    if (resolvedTargetInstId) {
      const processedResources = [];
      for (const p of pyqs) {
        const shared = await prisma$1.oldQuestionPaper.findFirst({ where: { referenceSourceId: p.id, institutionId: resolvedTargetInstId } });
        let isRemovable = false;
        if (shared) {
          const resultsCount = await prisma$1.examResult.count({ where: { oldQuestionPaperId: shared.id, institutionId: resolvedTargetInstId } });
          isRemovable = resultsCount === 0;
        }
        processedResources.push({ ...p, isShared: !!shared, sharedId: shared?.id || null, isRemovable });
      }
      data = processedResources;
    } else {
      data = pyqs;
    }
    total = count;
    if (resolvedTargetInstId) {
      const [totalCount, sharedCount, availableCount] = await Promise.all([
        prisma$1.oldQuestionPaper.count({
          where: {
            institutionId: resolvedInstitutionId,
            referenceSourceId: null,
            examId: examId ? examId : void 0
          }
        }),
        prisma$1.oldQuestionPaper.count({
          where: {
            institutionId: resolvedInstitutionId,
            referenceSourceId: null,
            examId: examId ? examId : void 0,
            sharedCopies: {
              some: {
                institutionId: resolvedTargetInstId
              }
            }
          }
        }),
        prisma$1.oldQuestionPaper.count({
          where: {
            institutionId: resolvedInstitutionId,
            referenceSourceId: null,
            examId: examId ? examId : void 0,
            sharedCopies: {
              none: {
                institutionId: resolvedTargetInstId
              }
            }
          }
        })
      ]);
      stats = {
        totalCount,
        sharedCount,
        availableCount
      };
    } else {
      const totalCount = await prisma$1.oldQuestionPaper.count({
        where: {
          institutionId: resolvedInstitutionId,
          referenceSourceId: null,
          examId: examId ? examId : void 0
        }
      });
      stats = {
        totalCount,
        sharedCount: 0,
        availableCount: totalCount
      };
    }
  } else if (resourceType === "MOCK_TEST") {
    const whereClause = { institutionId: resolvedInstitutionId, referenceSourceId: null, ...searchQuery };
    if (examId) {
      whereClause.examId = examId;
    }
    if (resolvedTargetInstId && sharedStatus) {
      if (sharedStatus === "SHARED") {
        whereClause.sharedCopies = { some: { institutionId: resolvedTargetInstId } };
      } else if (sharedStatus === "NOT_SHARED") {
        whereClause.sharedCopies = { none: { institutionId: resolvedTargetInstId } };
      }
    }
    const [mocks, count] = await Promise.all([
      prisma$1.mockTest.findMany({
        where: whereClause,
        skip,
        take: limitNum,
        include: { exam: true },
        orderBy: { createdAt: "desc" }
      }),
      prisma$1.mockTest.count({ where: whereClause })
    ]);
    if (resolvedTargetInstId) {
      const processedResources = [];
      for (const m of mocks) {
        const shared = await prisma$1.mockTest.findFirst({ where: { referenceSourceId: m.id, institutionId: resolvedTargetInstId } });
        let isRemovable = false;
        if (shared) {
          const resultsCount = await prisma$1.mockTestResult.count({ where: { mockTestId: shared.id } });
          isRemovable = resultsCount === 0;
        }
        console.log(`Mock Test ${m.id} shared status:`, !!shared, shared?.id);
        processedResources.push({ ...m, isShared: !!shared, sharedId: shared?.id || null, isRemovable });
      }
      data = processedResources;
    } else {
      data = mocks;
    }
    total = count;
    if (resolvedTargetInstId) {
      const [totalCount, sharedCount, availableCount] = await Promise.all([
        prisma$1.mockTest.count({
          where: {
            institutionId: resolvedInstitutionId,
            referenceSourceId: null,
            examId: examId ? examId : void 0
          }
        }),
        prisma$1.mockTest.count({
          where: {
            institutionId: resolvedInstitutionId,
            referenceSourceId: null,
            examId: examId ? examId : void 0,
            sharedCopies: {
              some: {
                institutionId: resolvedTargetInstId
              }
            }
          }
        }),
        prisma$1.mockTest.count({
          where: {
            institutionId: resolvedInstitutionId,
            referenceSourceId: null,
            examId: examId ? examId : void 0,
            sharedCopies: {
              none: {
                institutionId: resolvedTargetInstId
              }
            }
          }
        })
      ]);
      stats = {
        totalCount,
        sharedCount,
        availableCount
      };
    } else {
      const totalCount = await prisma$1.mockTest.count({
        where: {
          institutionId: resolvedInstitutionId,
          referenceSourceId: null,
          examId: examId ? examId : void 0
        }
      });
      stats = {
        totalCount,
        sharedCount: 0,
        availableCount: totalCount
      };
    }
  } else {
    res.status(400).json({ success: false, message: "Invalid resource type" });
    return;
  }
  res.status(200).json({
    success: true,
    data,
    meta: {
      page: pageNum,
      limit: limitNum,
      total,
      totalPages: Math.ceil(total / limitNum)
    },
    stats
  });
});
async function shareTestToInstitution(tx, resourceId, resolvedSourceInstId, targetInstId, user) {
  const sourceTest = await tx.test.findFirst({
    where: { id: resourceId, institutionId: resolvedSourceInstId },
    include: {
      testQuestions: { include: { question: true } }
    }
  });
  if (!sourceTest) throw new Error("Test not found");
  if (sourceTest.referenceSourceId) throw new Error("Cannot share a resource that was shared with you");
  const alreadyShared = await tx.test.findFirst({
    where: {
      referenceSourceId: sourceTest.id,
      institutionId: targetInstId
    }
  });
  if (alreadyShared) throw new Error("Resource already shared");
  const targetSubjectId = sourceTest.subjectId;
  const testPayload = {
    title: sourceTest.title,
    subjectId: targetSubjectId,
    duration: sourceTest.duration,
    institutionId: targetInstId,
    createdById: user.id,
    referenceSourceId: sourceTest.id,
    referenceInstitutionId: resolvedSourceInstId,
    topic: sourceTest.topic,
    language: sourceTest.language,
    publish: false
  };
  if (sourceTest.marks !== void 0) testPayload.marks = sourceTest.marks;
  if (sourceTest.difficulty !== void 0) testPayload.difficulty = sourceTest.difficulty;
  const newTest = await tx.test.create({
    data: testPayload
  });
  const filteredQuestions = (sourceTest.testQuestions || []).filter((tq) => {
    const q = tq.question;
    if (!q) return false;
    if (q.subjectId && sourceTest.subjectId && q.subjectId !== sourceTest.subjectId) return false;
    if (sourceTest.topic && Array.isArray(sourceTest.topic) && sourceTest.topic.length > 0 && q.topic && !sourceTest.topic.includes(q.topic)) return false;
    return true;
  });
  for (const tq of filteredQuestions) {
    if (!tq.question) continue;
    await tx.testQuestion.create({
      data: {
        testId: newTest.id,
        questionId: tq.questionId
      }
    });
  }
  return newTest;
}
async function sharePYQToInstitution(tx, resourceId, resolvedSourceInstId, targetInstId, user) {
  const sourcePYQ = await tx.oldQuestionPaper.findFirst({
    where: { id: resourceId, institutionId: resolvedSourceInstId },
    include: {
      questions: true,
      exam: true
    }
  });
  if (!sourcePYQ) throw new Error("PYQ / Old Question Paper not found");
  if (sourcePYQ.referenceSourceId) throw new Error("Cannot share a resource that was shared with you");
  const alreadyShared = await tx.oldQuestionPaper.findFirst({
    where: {
      referenceSourceId: sourcePYQ.id,
      institutionId: targetInstId
    }
  });
  if (alreadyShared) throw new Error("Resource already shared");
  const targetExamId = await getOrCreateTargetExam(tx, sourcePYQ.examId, targetInstId) || sourcePYQ.examId;
  const pyqPayload = {
    title: sourcePYQ.title,
    examId: targetExamId,
    year: sourcePYQ.year,
    language: sourcePYQ.language,
    duration: sourcePYQ.duration,
    marks: sourcePYQ.marks,
    difficulty: sourcePYQ.difficulty,
    publish: false,
    createdById: user.id,
    institutionId: targetInstId,
    referenceSourceId: sourcePYQ.id,
    referenceInstitutionId: resolvedSourceInstId
  };
  const newPYQ = await tx.oldQuestionPaper.create({
    data: pyqPayload
  });
  if (sourcePYQ.questions && sourcePYQ.questions.length > 0) {
    await tx.oldQuestionPaper.update({
      where: { id: newPYQ.id },
      data: {
        questions: {
          connect: sourcePYQ.questions.map((q) => ({ id: q.id }))
        }
      }
    });
  }
  return newPYQ;
}
async function shareMockTestToInstitution(tx, resourceId, resolvedSourceInstId, targetInstId, user) {
  const sourceMock = await tx.mockTest.findFirst({
    where: { id: resourceId, institutionId: resolvedSourceInstId },
    include: {
      questions: { include: { question: true } },
      exam: true
    }
  });
  if (!sourceMock) throw new Error("Mock Test not found");
  if (sourceMock.referenceSourceId) throw new Error("Cannot share a resource that was shared with you");
  const alreadyShared = await tx.mockTest.findFirst({
    where: {
      referenceSourceId: sourceMock.id,
      institutionId: targetInstId
    }
  });
  if (alreadyShared) throw new Error("Resource already shared");
  const targetExamId = await getOrCreateTargetExam(tx, sourceMock.examId, targetInstId) || sourceMock.examId;
  const newMock = await tx.mockTest.create({
    data: {
      title: sourceMock.title,
      examId: targetExamId,
      language: sourceMock.language,
      duration: sourceMock.duration,
      totalMarks: sourceMock.totalMarks,
      questionCount: sourceMock.questionCount,
      generationMode: sourceMock.generationMode,
      publish: false,
      createdById: user.id,
      institutionId: targetInstId,
      referenceSourceId: sourceMock.id,
      referenceInstitutionId: resolvedSourceInstId
    }
  });
  if (sourceMock.questions && sourceMock.questions.length > 0) {
    for (const mq of sourceMock.questions) {
      await tx.mockTestQuestion.create({
        data: {
          mockTestId: newMock.id,
          questionId: mq.questionId,
          subjectId: mq.subjectId,
          topic: mq.topic,
          sourceType: mq.sourceType,
          isActive: mq.isActive
        }
      });
    }
  }
  return newMock;
}
const shareMockTest = asyncHandler(async (req, res) => {
  const user = req.user;
  const { resourceIds, sourceInstitutionId, targetInstitutionIds } = req.body;
  if (!resourceIds || !Array.isArray(resourceIds) || resourceIds.length === 0) {
    res.status(400).json({ success: false, message: "resourceIds is required" });
    return;
  }
  if (!sourceInstitutionId) {
    res.status(400).json({ success: false, message: "sourceInstitutionId is required" });
    return;
  }
  if (!targetInstitutionIds || !Array.isArray(targetInstitutionIds) || targetInstitutionIds.length === 0) {
    res.status(400).json({ success: false, message: "targetInstitutionIds array is required" });
    return;
  }
  if (targetInstitutionIds.length > 1) {
    res.status(400).json({ success: false, message: "Only one institution can be selected per sharing request" });
    return;
  }
  const shareResults = [];
  const sourceInstRecord = await prisma$1.institution.findFirst({
    where: { userId: sourceInstitutionId },
    select: { id: true }
  });
  const resolvedSourceInstId = sourceInstRecord?.id ?? sourceInstitutionId;
  const resolvedTargetInstIds = [];
  for (const userId of targetInstitutionIds) {
    const rec = await prisma$1.institution.findFirst({
      where: { userId },
      select: { id: true }
    });
    if (rec?.id) resolvedTargetInstIds.push(rec.id);
  }
  for (const resourceId of resourceIds) {
    for (const targetInstId of resolvedTargetInstIds) {
      if (targetInstId === resolvedSourceInstId) continue;
      try {
        const sharedResult = await prisma$1.$transaction(async (tx) => {
          return await shareMockTestToInstitution(tx, resourceId, resolvedSourceInstId, targetInstId, user);
        });
        if (sharedResult) {
          shareResults.push({
            resourceId,
            institutionId: targetInstId,
            sharedId: sharedResult.id
          });
        }
      } catch (err) {
        console.error(`Error sharing MockTest ${resourceId} for institution ${targetInstId}:`, err.message);
        res.status(400).json({ success: false, message: err.message });
        return;
      }
    }
  }
  res.status(201).json({
    success: true,
    message: "Mock Tests shared successfully",
    data: shareResults
  });
});
const shareTest = asyncHandler(async (req, res) => {
  const user = req.user;
  const { resourceIds, sourceInstitutionId, targetInstitutionIds } = req.body;
  if (!resourceIds || !Array.isArray(resourceIds) || resourceIds.length === 0) {
    res.status(400).json({ success: false, message: "resourceIds is required" });
    return;
  }
  if (!sourceInstitutionId) {
    res.status(400).json({ success: false, message: "sourceInstitutionId is required" });
    return;
  }
  if (!targetInstitutionIds || !Array.isArray(targetInstitutionIds) || targetInstitutionIds.length === 0) {
    res.status(400).json({ success: false, message: "targetInstitutionIds array is required" });
    return;
  }
  if (targetInstitutionIds.length > 1) {
    res.status(400).json({ success: false, message: "Only one institution can be selected per sharing request" });
    return;
  }
  const shareResults = [];
  const sourceInstRecord = await prisma$1.institution.findFirst({
    where: { userId: sourceInstitutionId },
    select: { id: true }
  });
  const resolvedSourceInstId = sourceInstRecord?.id ?? sourceInstitutionId;
  const resolvedTargetInstIds = [];
  for (const userId of targetInstitutionIds) {
    const rec = await prisma$1.institution.findFirst({
      where: { userId },
      select: { id: true }
    });
    if (rec?.id) resolvedTargetInstIds.push(rec.id);
  }
  for (const resourceId of resourceIds) {
    for (const targetInstId of resolvedTargetInstIds) {
      if (targetInstId === resolvedSourceInstId) continue;
      try {
        const sharedResult = await prisma$1.$transaction(async (tx) => {
          return await shareTestToInstitution(tx, resourceId, resolvedSourceInstId, targetInstId, user);
        });
        if (sharedResult) {
          shareResults.push({
            resourceId,
            institutionId: targetInstId,
            sharedId: sharedResult.id
          });
        }
      } catch (err) {
        console.error(`Error sharing TEST ${resourceId} for institution ${targetInstId}:`, err.message);
        res.status(400).json({ success: false, message: err.message });
        return;
      }
    }
  }
  res.status(201).json({
    success: true,
    message: "Tests shared successfully",
    data: shareResults
  });
});
const sharePYQ = asyncHandler(async (req, res) => {
  const user = req.user;
  const { resourceIds, sourceInstitutionId, targetInstitutionIds } = req.body;
  if (!resourceIds || !Array.isArray(resourceIds) || resourceIds.length === 0) {
    res.status(400).json({ success: false, message: "resourceIds is required" });
    return;
  }
  if (!sourceInstitutionId) {
    res.status(400).json({ success: false, message: "sourceInstitutionId is required" });
    return;
  }
  if (!targetInstitutionIds || !Array.isArray(targetInstitutionIds) || targetInstitutionIds.length === 0) {
    res.status(400).json({ success: false, message: "targetInstitutionIds array is required" });
    return;
  }
  if (targetInstitutionIds.length > 1) {
    res.status(400).json({ success: false, message: "Only one institution can be selected per sharing request" });
    return;
  }
  const shareResults = [];
  const sourceInstRecord = await prisma$1.institution.findFirst({
    where: { userId: sourceInstitutionId },
    select: { id: true }
  });
  const resolvedSourceInstId = sourceInstRecord?.id ?? sourceInstitutionId;
  const resolvedTargetInstIds = [];
  for (const userId of targetInstitutionIds) {
    const rec = await prisma$1.institution.findFirst({
      where: { userId },
      select: { id: true }
    });
    if (rec?.id) resolvedTargetInstIds.push(rec.id);
  }
  for (const resourceId of resourceIds) {
    for (const targetInstId of resolvedTargetInstIds) {
      if (targetInstId === resolvedSourceInstId) continue;
      try {
        const sharedResult = await prisma$1.$transaction(async (tx) => {
          return await sharePYQToInstitution(tx, resourceId, resolvedSourceInstId, targetInstId, user);
        });
        if (sharedResult) {
          shareResults.push({
            resourceId,
            institutionId: targetInstId,
            sharedId: sharedResult.id
          });
        }
      } catch (err) {
        console.error(`Error sharing PYQ ${resourceId} for institution ${targetInstId}:`, err.message);
        res.status(400).json({ success: false, message: err.message });
        return;
      }
    }
  }
  res.status(201).json({
    success: true,
    message: "PYQs shared successfully",
    data: shareResults
  });
});
const removeSharedResource = asyncHandler(async (req, res) => {
  const { resourceType, sharedId } = req.body;
  if (!resourceType || !["TEST", "PYQ", "MOCK_TEST"].includes(resourceType)) {
    res.status(400).json({ success: false, message: "Valid resourceType ('TEST', 'PYQ', or 'MOCK_TEST') is required" });
    return;
  }
  if (!sharedId) {
    res.status(400).json({ success: false, message: "Missing required fields" });
    return;
  }
  let record;
  if (resourceType === "TEST") {
    record = await prisma$1.test.findUnique({ where: { id: sharedId } });
  } else if (resourceType === "PYQ") {
    record = await prisma$1.oldQuestionPaper.findUnique({ where: { id: sharedId } });
  } else if (resourceType === "MOCK_TEST") {
    record = await prisma$1.mockTest.findUnique({ where: { id: sharedId } });
  } else {
    record = await prisma$1.questionBank.findUnique({ where: { id: sharedId } });
  }
  if (!record || !record.referenceSourceId) {
    res.status(404).json({ success: false, message: "Resource not found" });
    return;
  }
  if (resourceType === "TEST") {
    const sharedTest = await prisma$1.test.findUnique({
      where: { id: sharedId },
      select: { subjectId: true, testQuestions: { select: { questionId: true } } }
    });
    if (!sharedTest) {
      res.status(404).json({ success: false, message: "Resource not found" });
      return;
    }
    const practiceTests = await prisma$1.practiceTest.findMany({ where: { testId: sharedId }, select: { id: true } });
    const practiceTestIds = practiceTests.map((pt) => pt.id);
    await prisma$1.$transaction(async (tx) => {
      if (practiceTestIds.length > 0) {
        await tx.examResult.deleteMany({ where: { practiceTestId: { in: practiceTestIds } } });
        await tx.practiceTest.deleteMany({ where: { id: { in: practiceTestIds } } });
      }
      await tx.testQuestion.deleteMany({ where: { testId: sharedId } });
      await tx.test.delete({ where: { id: sharedId } });
      const subjectUsage = await tx.subject.count({
        where: {
          id: sharedTest.subjectId,
          OR: [
            { tests: { some: {} } },
            { questions: { some: {} } },
            { mockTestQuestions: { some: {} } }
          ]
        }
      });
      if (subjectUsage === 0) {
        await tx.subject.delete({ where: { id: sharedTest.subjectId } });
      }
    });
  } else if (resourceType === "PYQ") {
    const sharedPYQ = await prisma$1.oldQuestionPaper.findUnique({
      where: { id: sharedId },
      select: { examId: true }
    });
    await prisma$1.$transaction(async (tx) => {
      await tx.oldQuestionPaper.update({ where: { id: sharedId }, data: { questions: { set: [] } } });
      await tx.examResult.deleteMany({ where: { oldQuestionPaperId: sharedId } });
      await tx.oldQuestionPaper.delete({ where: { id: sharedId } });
      if (sharedPYQ && sharedPYQ.examId) {
        const examUsage = await tx.exam.count({
          where: {
            id: sharedPYQ.examId,
            OR: [
              { students: { some: {} } },
              { oldQuestions: { some: {} } },
              { practiceTests: { some: {} } },
              { oldQuestionPapers: { some: {} } },
              { mockTests: { some: {} } },
              { syllabuses: { some: {} } },
              { notesLinks: { some: {} } },
              { subjectsNotesToExam: { some: {} } }
            ]
          }
        });
        if (examUsage === 0) {
          await tx.exam.delete({ where: { id: sharedPYQ.examId } });
        }
      }
    });
  } else if (resourceType === "MOCK_TEST") {
    const sharedMock = await prisma$1.mockTest.findUnique({
      where: { id: sharedId },
      select: { examId: true, institutionId: true }
    });
    await prisma$1.$transaction(async (tx) => {
      const results = await tx.mockTestResult.findMany({ where: { mockTestId: sharedId }, select: { id: true } });
      const resultIds = results.map((r) => r.id);
      if (resultIds.length > 0) {
        await tx.mockTestResultDetail.deleteMany({ where: { mockTestResultId: { in: resultIds } } });
        await tx.mockTestResult.deleteMany({ where: { id: { in: resultIds } } });
      }
      await tx.mockTestQuestion.deleteMany({ where: { mockTestId: sharedId } });
      await tx.mockTest.delete({ where: { id: sharedId } });
      if (sharedMock && sharedMock.examId && sharedMock.institutionId) {
        const examUsage = await tx.exam.count({
          where: {
            id: sharedMock.examId,
            OR: [
              { students: { some: {} } },
              { oldQuestions: { some: {} } },
              { practiceTests: { some: {} } },
              { oldQuestionPapers: { some: {} } },
              { mockTests: { some: {} } },
              { syllabuses: { some: {} } },
              { notesLinks: { some: {} } },
              { subjectsNotesToExam: { some: {} } }
            ]
          }
        });
        if (examUsage === 0) {
          await tx.exam.delete({ where: { id: sharedMock.examId } });
        }
      }
    });
  } else {
    await prisma$1.$transaction(async (tx) => {
      await tx.questionBank.update({ where: { id: sharedId }, data: { practiceTests: { set: [] } } });
      await tx.mockTestResultDetail.deleteMany({ where: { questionId: sharedId } });
      await tx.mockTestQuestion.deleteMany({ where: { questionId: sharedId } });
      await tx.testQuestion.deleteMany({ where: { questionId: sharedId } });
      await tx.questionBank.delete({ where: { id: sharedId } });
    });
  }
  res.status(200).json({ success: true, message: "Resource removed successfully" });
});
const getSharedHistory = asyncHandler(async (req, res) => {
  const { fromInstitutionId, toInstitutionId, resourceType, subjectId, examId, page = "1", limit = "10" } = req.query;
  const pageNum = parseInt(page) || 1;
  const limitNum = parseInt(limit) || 10;
  const skip = (pageNum - 1) * limitNum;
  let resolvedFromId;
  let resolvedToId;
  if (fromInstitutionId) {
    const fromRec = await prisma$1.institution.findFirst({
      where: { userId: fromInstitutionId },
      select: { id: true }
    });
    resolvedFromId = fromRec?.id ?? fromInstitutionId;
  }
  if (toInstitutionId) {
    const toRec = await prisma$1.institution.findFirst({
      where: { userId: toInstitutionId },
      select: { id: true }
    });
    resolvedToId = toRec?.id ?? toInstitutionId;
  }
  let combinedHistory = [];
  const institutionInclude = {
    select: {
      user: {
        select: {
          institutionName: true
        }
      }
    }
  };
  const fetchTests = async () => {
    const tests = await prisma$1.test.findMany({
      where: {
        referenceSourceId: { not: null },
        ...resolvedToId ? { institutionId: resolvedToId } : {},
        ...resolvedFromId ? { referenceSource: { institutionId: resolvedFromId } } : {},
        ...subjectId ? { subjectId } : {}
      },
      include: {
        institution: institutionInclude,
        subject: { select: { subjectName: true } },
        referenceSource: {
          include: {
            institution: institutionInclude,
            subject: { select: { subjectName: true } }
          }
        },
        referenceInstitution: institutionInclude
      },
      orderBy: { createdAt: "desc" }
    });
    return tests.map((t) => ({
      id: t.id,
      resourceName: t.referenceSource?.title || t.title,
      subjectName: (t.referenceSource?.subject?.subjectName || t.subject?.subjectName) ?? null,
      type: "TEST",
      fromInstitution: t.referenceSource?.institution?.user?.institutionName || t.referenceInstitution?.user?.institutionName || "Unknown",
      toInstitution: t.institution?.user?.institutionName || "Unknown",
      targetInstitutionId: t.institutionId,
      sharedDate: t.createdAt
    }));
  };
  const fetchPYQs = async () => {
    const pyqs = await prisma$1.oldQuestionPaper.findMany({
      where: {
        referenceSourceId: { not: null },
        ...resolvedToId ? { institutionId: resolvedToId } : {},
        ...resolvedFromId ? { referenceSource: { institutionId: resolvedFromId } } : {},
        ...examId ? { examId } : {}
      },
      include: {
        institution: institutionInclude,
        exam: { select: { examName: true } },
        referenceSource: {
          include: {
            institution: institutionInclude,
            exam: { select: { examName: true } }
          }
        },
        referenceInstitution: institutionInclude
      },
      orderBy: { createdAt: "desc" }
    });
    return pyqs.map((p) => ({
      id: p.id,
      resourceName: p.referenceSource?.title || p.title,
      examName: (p.referenceSource?.exam?.examName || p.exam?.examName) ?? null,
      type: "PYQ",
      fromInstitution: p.referenceSource?.institution?.user?.institutionName || p.referenceInstitution?.user?.institutionName || "Unknown",
      toInstitution: p.institution?.user?.institutionName || "Unknown",
      targetInstitutionId: p.institutionId,
      sharedDate: p.createdAt
    }));
  };
  const fetchMockTests = async () => {
    const mocks = await prisma$1.mockTest.findMany({
      where: {
        referenceSourceId: { not: null },
        ...resolvedToId ? { institutionId: resolvedToId } : {},
        ...resolvedFromId ? { referenceSource: { institutionId: resolvedFromId } } : {},
        ...examId ? { examId } : {}
      },
      include: {
        institution: institutionInclude,
        exam: { select: { examName: true } },
        referenceSource: {
          include: {
            institution: institutionInclude,
            exam: { select: { examName: true } }
          }
        },
        referenceInstitution: institutionInclude
      },
      orderBy: { createdAt: "desc" }
    });
    return mocks.map((m) => ({
      id: m.id,
      resourceName: m.referenceSource?.title || m.title,
      examName: (m.referenceSource?.exam?.examName || m.exam?.examName) ?? null,
      type: "MOCK_TEST",
      fromInstitution: m.referenceSource?.institution?.user?.institutionName || m.referenceInstitution?.user?.institutionName || "Unknown",
      toInstitution: m.institution?.user?.institutionName || "Unknown",
      targetInstitutionId: m.institutionId,
      sharedDate: m.createdAt
    }));
  };
  if (!resourceType || resourceType === "TEST") {
    combinedHistory = [...combinedHistory, ...await fetchTests()];
  }
  if (!resourceType || resourceType === "PYQ") {
    combinedHistory = [...combinedHistory, ...await fetchPYQs()];
  }
  if (!resourceType || resourceType === "MOCK_TEST") {
    combinedHistory = [...combinedHistory, ...await fetchMockTests()];
  }
  combinedHistory.sort((a, b) => new Date(b.sharedDate).getTime() - new Date(a.sharedDate).getTime());
  const total = combinedHistory.length;
  const paginatedData = combinedHistory.slice(skip, skip + limitNum);
  const processedHistory = [];
  for (const item of paginatedData) {
    let isRemovable = false;
    if (item.type === "TEST") {
      const mappedPracticeTests = await prisma$1.practiceTest.findMany({ where: { testId: item.id, institutionId: item.targetInstitutionId }, select: { id: true } });
      if (mappedPracticeTests.length === 0) {
        isRemovable = true;
      } else {
        const practiceIds = mappedPracticeTests.map((p) => p.id);
        const resultsCount = await prisma$1.examResult.count({ where: { practiceTestId: { in: practiceIds } } });
        if (resultsCount === 0) isRemovable = true;
      }
    } else if (item.type === "PYQ") {
      const resultsCount = await prisma$1.examResult.count({ where: { oldQuestionPaperId: item.id, institutionId: item.targetInstitutionId } });
      isRemovable = resultsCount === 0;
    } else if (item.type === "MOCK_TEST") {
      const resultsCount = await prisma$1.mockTestResult.count({ where: { mockTestId: item.id } });
      isRemovable = resultsCount === 0;
    }
    processedHistory.push({ ...item, isRemovable });
  }
  res.status(200).json({
    success: true,
    data: processedHistory,
    meta: {
      page: pageNum,
      limit: limitNum,
      total,
      totalPages: Math.ceil(total / limitNum)
    }
  });
});
const router = express.Router();
router.use(protect);
router.use(permit("Admin"));
router.get("/available", getAvailableResources);
router.post("/share/test", shareTest);
router.post("/share/pyq", sharePYQ);
router.post("/share/mock-test", shareMockTest);
router.post("/remove", removeSharedResource);
router.get("/history", getSharedHistory);
dotenv.config({
  path: path.resolve(process.cwd(), ".env")
});
async function createServer() {
  await checkDbConnection();
  const app = express();
  app.use(cors());
  app.use(express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf;
    }
  }));
  app.use(express.urlencoded({ extended: true }));
  app.use("/api/health", (req, res) => {
    res.status(200).json({ message: "API working fine...!" });
  });
  app.use("/api/auth", router$s);
  app.use("/api/users", router$r);
  app.use("/api/exams", router$q);
  app.use("/api/staffs", router$p);
  app.use("/api/students", router$o);
  app.use("/api/question-bank", router$n);
  app.use("/api/notes-links", router$2);
  app.use("/api/languages", router$1);
  app.use("/api/practice-test", router$m);
  app.use("/api/tests", router$l);
  app.use("/api/support", router$k);
  app.use("/api/feedback", router$j);
  app.use("/api/reports", router$i);
  app.use("/api/payments", router$h);
  app.use("/api/subscription-plans", router$g);
  app.use("/api/subscription-requests", router$3);
  app.use("/api/quotes", router$f);
  app.use("/api/subjects", router$e);
  app.use("/api/syllabus", router$d);
  app.use("/api/notifications", router$c);
  app.use("/api/old-questions", router$b);
  app.use("/api/old-question-papers", router$a);
  app.use("/api/dashboard", router$9);
  app.use("/api/roles", router$8);
  app.use("/api/reference-sequences", router$7);
  app.use("/api/mock-test", router$6);
  app.use("/api/leaderboard", router$5);
  app.use("/api/public-contact", router$4);
  app.use("/api/shared-resources", router);
  app.use(
    "/uploads",
    express.static(path.join(process.cwd(), "uploads"), {
      setHeaders: (res, filePath) => {
        res.setHeader("Access-Control-Allow-Origin", "*");
        res.setHeader("Content-Disposition", "inline");
        res.setHeader("Content-Disposition", "attachment");
      }
    })
  );
  app.use(errorHandler);
  return app;
}
const port = process.env.PORT || 5e3;
const __dirname$1 = import.meta.dirname;
const distPath = path.join(__dirname$1, "../client");
(async () => {
  const app = await createServer();
  app.use(express.static(distPath));
  app.get("*", (req, res) => {
    if (req.path.startsWith("/api/") || req.path.startsWith("/health")) {
      return res.status(404).json({ error: "API endpoint not found" });
    }
    res.sendFile(path.join(distPath, "index.html"));
  });
  app.listen(port, () => {
    console.log(`🚀 Server running on port ${port}`);
  });
  ["SIGTERM", "SIGINT"].forEach((signal) => {
    process.on(signal, async () => {
      console.log(`🛑 Received ${signal}, shutting down gracefully`);
      await prisma$1.$disconnect();
      process.exit(0);
    });
  });
})();
//# sourceMappingURL=server.mjs.map
