"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var StudentsService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.StudentsService = void 0;
const common_1 = require("@nestjs/common");
const mongoose_1 = require("@nestjs/mongoose");
const student_schema_1 = require("./schemas/student.schema");
const mongoose_2 = require("mongoose");
const bcrypt = __importStar(require("bcryptjs"));
const class_schema_1 = require("../classes/schemas/class.schema");
const section_schema_1 = require("../sections/schemas/section.schema");
const enrollment_schema_1 = require("./schemas/enrollment.schema");
const url_helper_1 = require("../common/helpers/url.helper");
const BCRYPT_ROUNDS = Number(process.env.BCRYPT_ROUNDS || 10);
let StudentsService = StudentsService_1 = class StudentsService {
    studentModel;
    classModel;
    sectionModel;
    enrollmentModel;
    logger = new common_1.Logger(StudentsService_1.name);
    constructor(studentModel, classModel, sectionModel, enrollmentModel) {
        this.studentModel = studentModel;
        this.classModel = classModel;
        this.sectionModel = sectionModel;
        this.enrollmentModel = enrollmentModel;
    }
    async onModuleInit() {
        try {
            await this.studentModel.deleteMany({ $or: [{ uniqueId: null }, { rollNumber: null }, { name: null }] });
            await this.studentModel.syncIndexes();
            await this.enrollmentModel.syncIndexes();
            this.logger.log('Student & Enrollment indexes synced');
        }
        catch (err) {
            this.logger.warn('Index sync failed, will retry on next restart: ' + err.message);
        }
    }
    normEmail(email) {
        return (email || '').trim().toLowerCase();
    }
    normRoll(roll) {
        return (roll || '').trim();
    }
    toDateOrUndefined(iso) {
        return iso ? new Date(iso) : undefined;
    }
    convertDocumentUrls(student) {
        if (!student)
            return student;
        const convertedStudent = { ...student };
        if (convertedStudent.documents) {
            const docs = convertedStudent.documents;
            convertedStudent.documents = {
                ...docs,
                photoUrl: docs.photoUrl ? (0, url_helper_1.toFullUrl)(docs.photoUrl) : docs.photoUrl,
                fatherPhotoUrl: docs.fatherPhotoUrl ? (0, url_helper_1.toFullUrl)(docs.fatherPhotoUrl) : docs.fatherPhotoUrl,
                motherPhotoUrl: docs.motherPhotoUrl ? (0, url_helper_1.toFullUrl)(docs.motherPhotoUrl) : docs.motherPhotoUrl,
                birthCertificateUrl: docs.birthCertificateUrl ? (0, url_helper_1.toFullUrl)(docs.birthCertificateUrl) : docs.birthCertificateUrl,
                scstCertificateUrl: docs.scstCertificateUrl ? (0, url_helper_1.toFullUrl)(docs.scstCertificateUrl) : docs.scstCertificateUrl,
                leavingCertificateUrl: docs.leavingCertificateUrl ? (0, url_helper_1.toFullUrl)(docs.leavingCertificateUrl) : docs.leavingCertificateUrl,
                vaccineCardUrl: docs.vaccineCardUrl ? (0, url_helper_1.toFullUrl)(docs.vaccineCardUrl) : docs.vaccineCardUrl,
            };
        }
        if (convertedStudent.customDocuments && Array.isArray(convertedStudent.customDocuments)) {
            convertedStudent.customDocuments = convertedStudent.customDocuments.map((doc) => ({
                ...doc,
                documentUrl: doc.documentUrl ? (0, url_helper_1.toFullUrl)(doc.documentUrl) : doc.documentUrl,
            }));
        }
        if (convertedStudent.customImages && Array.isArray(convertedStudent.customImages)) {
            convertedStudent.customImages = convertedStudent.customImages.map((img) => ({
                ...img,
                imageUrl: img.imageUrl ? (0, url_helper_1.toFullUrl)(img.imageUrl) : img.imageUrl,
            }));
        }
        return convertedStudent;
    }
    async handleDupKey(err) {
        if (err?.code === 11000) {
            const fields = Object.keys(err.keyPattern || {});
            if (fields.includes('email'))
                throw new common_1.BadRequestException('Email already exists');
            if (fields.includes('rollNumber')) {
                const scope = fields.includes('sectionId')
                    ? 'this class and section'
                    : fields.includes('classId')
                        ? 'this class'
                        : 'this academic year';
                throw new common_1.BadRequestException(`Roll number already exists in ${scope}`);
            }
            throw new common_1.BadRequestException('Duplicate key error');
        }
        throw err;
    }
    get ayModel() {
        return this.studentModel.db.model('AcademicYear');
    }
    parseAYLabel(label) {
        const m = /^(\d{4})\s*-\s*(\d{4})$/.exec(String(label || ''));
        if (!m)
            return null;
        return [parseInt(m[1], 10), parseInt(m[2], 10)];
    }
    async getAcademicYearById(id) {
        const doc = await this.ayModel.findById(id).lean();
        if (!doc)
            throw new common_1.BadRequestException('Academic year not found');
        return doc;
    }
    async assertIsCurrentAcademicYear(yearId) {
        let current = await this.ayModel.findOne({ isCurrent: true }).lean();
        if (!current) {
            const activeYears = await this.ayModel.find({ isActive: true }).lean();
            if (activeYears.length === 0) {
                throw new common_1.BadRequestException('No current or active academic year is set. Please set a current year first.');
            }
            const sorted = activeYears.sort((a, b) => {
                const dateA = new Date(a.startDate).getTime();
                const dateB = new Date(b.startDate).getTime();
                return dateB - dateA;
            });
            current = sorted[0];
        }
        if (String(current._id) !== String(yearId)) {
            throw new common_1.BadRequestException('Promotion is only allowed from the current academic year. Please select the current academic year in the topbar.');
        }
    }
    async assertNextAcademicYear(fromId, toId) {
        if (String(fromId) === String(toId)) {
            throw new common_1.BadRequestException('Target academic year must be the next one (cannot use the same year).');
        }
        const from = await this.getAcademicYearById(fromId);
        const to = await this.getAcademicYearById(toId);
        if (typeof from.order === 'number' && typeof to.order === 'number') {
            if (to.order !== from.order + 1) {
                throw new common_1.BadRequestException('Target academic year must be the immediate next year.');
            }
            return;
        }
        const pf = this.parseAYLabel(from.label);
        const pt = this.parseAYLabel(to.label);
        if (pf && pt) {
            if (pt[0] === pf[0] + 1 && pt[1] === pf[1] + 1)
                return;
            throw new common_1.BadRequestException('Target academic year must be the immediate next year.');
        }
        if (from.endDate && to.startDate) {
            const fromEnd = new Date(from.endDate);
            const toStart = new Date(to.startDate);
            if (toStart > fromEnd)
                return;
        }
        throw new common_1.BadRequestException('Cannot verify academic year sequence; ensure the target is the next year.');
    }
    async create(dto, docs) {
        const email = dto.email ? this.normEmail(dto.email) : undefined;
        if (email) {
            if (await this.studentModel.findOne({ email }).lean())
                throw new common_1.BadRequestException('Email already exists');
        }
        let classDoc, sectionDoc;
        if (dto.sectionId) {
            [classDoc, sectionDoc] = await Promise.all([
                this.classModel.findById(dto.classId).select('name').lean(),
                this.sectionModel.findById(dto.sectionId).select('name').lean(),
            ]);
            if (!classDoc)
                throw new common_1.BadRequestException('Class not found');
            if (!sectionDoc)
                throw new common_1.BadRequestException('Section not found');
        }
        else {
            classDoc = await this.classModel.findById(dto.classId).select('name').lean();
            if (!classDoc)
                throw new common_1.BadRequestException('Class not found');
        }
        const { generateRollNumber } = await Promise.resolve().then(() => __importStar(require('../common/utils/id-generator')));
        const rollNumber = await generateRollNumber(this.enrollmentModel, classDoc.name, sectionDoc?.name, dto.academicYearId);
        const hashedPassword = dto.password
            ? await bcrypt.hash(dto.password, BCRYPT_ROUNDS)
            : undefined;
        const { generateStudentId } = await Promise.resolve().then(() => __importStar(require('../common/utils/id-generator')));
        const uniqueId = await generateStudentId(this.studentModel);
        const session = await this.studentModel.db.startSession();
        let createdStudentId;
        try {
            await session.withTransaction(async () => {
                const student = await this.studentModel.create([{
                        name: (dto.name || '').trim(),
                        uniqueId,
                        rollNumber,
                        ...(email ? { email } : {}),
                        ...(hashedPassword ? { password: hashedPassword } : {}),
                        role: 'student',
                        classId: dto.classId,
                        sectionId: dto.sectionId,
                        academicYearId: dto.academicYearId,
                        gender: dto.gender,
                        dateOfBirth: this.toDateOrUndefined(dto.dateOfBirth),
                        admissionDate: this.toDateOrUndefined(dto.admissionDate),
                        motherTongue: dto.motherTongue,
                        religion: dto.religion,
                        caste: dto.caste,
                        mediumOfInstruction: dto.mediumOfInstruction,
                        nationality: dto.nationality,
                        previousSchoolName: dto.previousSchoolName,
                        tcIssued: dto.tcIssued,
                        clearedAllDues: dto.clearedAllDues,
                        tcNumber: dto.tcNumber,
                        bloodGroup: dto.bloodGroup,
                        heightCm: dto.heightCm,
                        weightKg: dto.weightKg,
                        medicalConditions: dto.medicalConditions,
                        hasAllergies: dto.hasAllergies,
                        allergyDetails: dto.allergyDetails,
                        emergencyMedicalInstructions: dto.emergencyMedicalInstructions,
                        guardianName: dto.guardianName,
                        guardianRelation: dto.guardianRelation,
                        phone: dto.phone,
                        whatsapp: dto.whatsapp,
                        address: dto.address,
                        transportRequired: dto.transportRequired,
                        routePickupPoint: dto.routePickupPoint,
                        isActive: dto.isActive ?? true,
                        leavingDate: this.toDateOrUndefined(dto.leavingDate),
                        leavingReason: dto.leavingReason,
                        father: {
                            name: dto.fatherName, email: dto.fatherEmail,
                            occupation: dto.fatherOccupation, monthlyIncome: dto.fatherMonthlyIncome,
                            phone: dto.fatherPhone, whatsapp: dto.fatherWhatsapp,
                        },
                        mother: {
                            name: dto.motherName, email: dto.motherEmail,
                            occupation: dto.motherOccupation, monthlyIncome: dto.motherMonthlyIncome,
                            phone: dto.motherPhone, whatsapp: dto.motherWhatsapp,
                        },
                        documents: docs,
                        customDocuments: dto.customDocuments || [],
                        customImages: dto.customImages || [],
                        education: dto.education || [],
                    }], { session });
                const s = student[0];
                createdStudentId = String(s._id);
                await this.enrollmentModel.create([{
                        studentId: createdStudentId,
                        academicYearId: dto.academicYearId,
                        classId: dto.classId,
                        sectionId: dto.sectionId,
                        rollNumber,
                    }], { session });
            });
            return this.studentModel.findById(createdStudentId, { password: 0 }).lean();
        }
        catch (err) {
            await this.handleDupKey(err);
        }
        finally {
            await session.endSession();
        }
    }
    async findByEmailForAuth(email) {
        return this.studentModel.findOne({ email: this.normEmail(email) }).lean();
    }
    async findByIdForAuth(id) {
        return this.studentModel.findById(id).lean();
    }
    async findByEmail(email) {
        return this.studentModel.findOne({ email: this.normEmail(email) }, { password: 0 }).lean();
    }
    async findByPhone(phone) {
        return this.studentModel.findOne({ phone }, { password: 0 }).lean();
    }
    async findAll() {
        const students = await this.studentModel.find({}, { password: 0 }).sort({ createdAt: -1 }).lean();
        return students.map(s => this.convertDocumentUrls(s));
    }
    async searchStudents(query, limit = 50) {
        if (!query || query.trim().length === 0) {
            return [];
        }
        const searchRegex = new RegExp(query.trim(), 'i');
        const students = await this.studentModel
            .find({
            $or: [
                { name: searchRegex },
                { email: searchRegex },
                { rollNumber: searchRegex },
            ],
        }, { password: 0 })
            .limit(limit)
            .sort({ name: 1 })
            .lean();
        return students.map(s => this.convertDocumentUrls(s));
    }
    async findOne(id, academicYearId) {
        if (academicYearId) {
            const objectId = new mongoose_2.Types.ObjectId(id);
            const pipeline = [
                { $match: { _id: objectId } },
                {
                    $lookup: {
                        from: 'enrollments',
                        let: { sid: '$_id' },
                        pipeline: [
                            {
                                $match: {
                                    $expr: { $eq: ['$studentId', { $toString: '$$sid' }] },
                                    academicYearId,
                                },
                            },
                        ],
                        as: 'enr',
                    },
                },
                {
                    $project: {
                        _id: 1,
                        name: 1,
                        uniqueId: 1,
                        email: 1,
                        role: 1,
                        gender: 1,
                        dateOfBirth: 1,
                        admissionDate: 1,
                        motherTongue: 1,
                        religion: 1,
                        caste: 1,
                        mediumOfInstruction: 1,
                        nationality: 1,
                        previousSchoolName: 1,
                        tcIssued: 1,
                        clearedAllDues: 1,
                        tcNumber: 1,
                        bloodGroup: 1,
                        heightCm: 1,
                        weightKg: 1,
                        medicalConditions: 1,
                        hasAllergies: 1,
                        allergyDetails: 1,
                        emergencyMedicalInstructions: 1,
                        guardianName: 1,
                        guardianRelation: 1,
                        phone: 1,
                        whatsapp: 1,
                        address: 1,
                        transportRequired: 1,
                        routePickupPoint: 1,
                        isActive: 1,
                        leavingDate: 1,
                        leavingReason: 1,
                        father: 1,
                        mother: 1,
                        documents: 1,
                        customDocuments: 1,
                        customImages: 1,
                        education: 1,
                        createdAt: 1,
                        updatedAt: 1,
                        rollNumber: {
                            $cond: [
                                { $gt: [{ $size: '$enr' }, 0] },
                                { $arrayElemAt: ['$enr.rollNumber', 0] },
                                '$rollNumber',
                            ],
                        },
                        classId: {
                            $cond: [
                                { $gt: [{ $size: '$enr' }, 0] },
                                { $arrayElemAt: ['$enr.classId', 0] },
                                '$classId',
                            ],
                        },
                        sectionId: {
                            $cond: [
                                { $gt: [{ $size: '$enr' }, 0] },
                                { $arrayElemAt: ['$enr.sectionId', 0] },
                                '$sectionId',
                            ],
                        },
                        academicYearId: {
                            $cond: [
                                { $gt: [{ $size: '$enr' }, 0] },
                                { $arrayElemAt: ['$enr.academicYearId', 0] },
                                '$academicYearId',
                            ],
                        },
                        isActiveForSelectedYear: {
                            $and: [
                                { $eq: ['$academicYearId', academicYearId] },
                                { $ne: ['$isActive', false] },
                            ],
                        },
                        statusForSelectedYear: {
                            $cond: [
                                {
                                    $and: [
                                        { $eq: ['$academicYearId', academicYearId] },
                                        { $ne: ['$isActive', false] },
                                    ],
                                },
                                'active',
                                'deactivated',
                            ],
                        },
                    },
                },
            ];
            const result = await this.studentModel.aggregate(pipeline).exec();
            return result.length > 0 ? this.convertDocumentUrls(result[0]) : null;
        }
        const student = await this.studentModel.findById(id, { password: 0 }).lean();
        return this.convertDocumentUrls(student);
    }
    async update(id, dto) {
        if (dto.email !== undefined) {
            const email = this.normEmail(dto.email);
            const emailExists = await this.studentModel.findOne({
                email,
                _id: { $ne: id },
            }).lean();
            if (emailExists) {
                throw new common_1.BadRequestException('Email already exists');
            }
        }
        if (dto.academicYearId !== undefined ||
            dto.rollNumber !== undefined ||
            dto.classId !== undefined ||
            dto.sectionId !== undefined) {
            const current = await this.studentModel.findById(id).lean();
            if (!current)
                throw new common_1.BadRequestException('Student not found');
            const finalAcademicYearId = dto.academicYearId ?? current.academicYearId;
            const finalClassId = dto.classId ?? current.classId;
            const finalSectionId = dto.sectionId !== undefined ? dto.sectionId : current.sectionId;
            const finalRollNumber = dto.rollNumber !== undefined ? this.normRoll(dto.rollNumber) : current.rollNumber;
            const dupScope = {
                academicYearId: finalAcademicYearId,
                classId: finalClassId,
                rollNumber: finalRollNumber,
            };
            if (finalSectionId)
                dupScope.sectionId = finalSectionId;
            else
                dupScope.sectionId = { $in: [null, undefined, ''] };
            const dupMsg = finalSectionId
                ? 'Roll number already exists in this class and section'
                : 'Roll number already exists in this class';
            const duplicateStudent = await this.studentModel.findOne({
                ...dupScope,
                _id: { $ne: id },
            }).lean();
            if (duplicateStudent)
                throw new common_1.BadRequestException(dupMsg);
            const duplicateEnrollment = await this.enrollmentModel.findOne({
                ...dupScope,
                studentId: { $ne: id },
            }).lean();
            if (duplicateEnrollment)
                throw new common_1.BadRequestException(dupMsg);
        }
        const toUpdate = {};
        if (dto.name !== undefined)
            toUpdate.name = (dto.name || '').trim();
        if (dto.email !== undefined)
            toUpdate.email = dto.email ? this.normEmail(dto.email) : undefined;
        if (dto.rollNumber !== undefined)
            toUpdate.rollNumber = this.normRoll(dto.rollNumber);
        if (dto.password !== undefined && dto.password.trim().length > 0) {
            toUpdate.password = await bcrypt.hash(dto.password, BCRYPT_ROUNDS);
        }
        if (dto.dateOfBirth !== undefined)
            toUpdate.dateOfBirth = this.toDateOrUndefined(dto.dateOfBirth);
        if (dto.admissionDate !== undefined)
            toUpdate.admissionDate = this.toDateOrUndefined(dto.admissionDate);
        if (dto.classId !== undefined)
            toUpdate.classId = dto.classId;
        if (dto.sectionId !== undefined)
            toUpdate.sectionId = dto.sectionId;
        if (dto.academicYearId !== undefined)
            toUpdate.academicYearId = dto.academicYearId;
        if (dto.gender !== undefined)
            toUpdate.gender = dto.gender;
        if (dto.motherTongue !== undefined)
            toUpdate.motherTongue = dto.motherTongue;
        if (dto.religion !== undefined)
            toUpdate.religion = dto.religion;
        if (dto.caste !== undefined)
            toUpdate.caste = dto.caste;
        if (dto.mediumOfInstruction !== undefined)
            toUpdate.mediumOfInstruction = dto.mediumOfInstruction;
        if (dto.nationality !== undefined)
            toUpdate.nationality = dto.nationality;
        if (dto.previousSchoolName !== undefined)
            toUpdate.previousSchoolName = dto.previousSchoolName;
        if (dto.tcIssued !== undefined)
            toUpdate.tcIssued = dto.tcIssued;
        if (dto.clearedAllDues !== undefined)
            toUpdate.clearedAllDues = dto.clearedAllDues;
        if (dto.tcNumber !== undefined)
            toUpdate.tcNumber = dto.tcNumber;
        if (dto.bloodGroup !== undefined)
            toUpdate.bloodGroup = dto.bloodGroup;
        if (dto.heightCm !== undefined)
            toUpdate.heightCm = dto.heightCm;
        if (dto.weightKg !== undefined)
            toUpdate.weightKg = dto.weightKg;
        if (dto.medicalConditions !== undefined)
            toUpdate.medicalConditions = dto.medicalConditions;
        if (dto.hasAllergies !== undefined)
            toUpdate.hasAllergies = dto.hasAllergies;
        if (dto.allergyDetails !== undefined)
            toUpdate.allergyDetails = dto.allergyDetails;
        if (dto.emergencyMedicalInstructions !== undefined)
            toUpdate.emergencyMedicalInstructions = dto.emergencyMedicalInstructions;
        if (dto.guardianName !== undefined)
            toUpdate.guardianName = dto.guardianName;
        if (dto.guardianRelation !== undefined)
            toUpdate.guardianRelation = dto.guardianRelation;
        if (dto.phone !== undefined)
            toUpdate.phone = dto.phone;
        if (dto.whatsapp !== undefined)
            toUpdate.whatsapp = dto.whatsapp;
        if (dto.address !== undefined)
            toUpdate.address = dto.address;
        if (dto.transportRequired !== undefined)
            toUpdate.transportRequired = dto.transportRequired;
        if (dto.routePickupPoint !== undefined)
            toUpdate.routePickupPoint = dto.routePickupPoint;
        if (dto.isActive !== undefined)
            toUpdate.isActive = dto.isActive;
        if (dto.leavingDate !== undefined)
            toUpdate.leavingDate = this.toDateOrUndefined(dto.leavingDate);
        if (dto.leavingReason !== undefined)
            toUpdate.leavingReason = dto.leavingReason;
        const unsetFields = {};
        if (dto.isActive === true) {
            if (dto.leavingDate === undefined)
                unsetFields.leavingDate = 1;
            if (dto.leavingReason === undefined)
                unsetFields.leavingReason = 1;
        }
        if (dto.fatherName !== undefined)
            toUpdate['father.name'] = dto.fatherName;
        if (dto.fatherEmail !== undefined)
            toUpdate['father.email'] = dto.fatherEmail;
        if (dto.fatherOccupation !== undefined)
            toUpdate['father.occupation'] = dto.fatherOccupation;
        if (dto.fatherMonthlyIncome !== undefined)
            toUpdate['father.monthlyIncome'] = dto.fatherMonthlyIncome;
        if (dto.fatherPhone !== undefined)
            toUpdate['father.phone'] = dto.fatherPhone;
        if (dto.fatherWhatsapp !== undefined)
            toUpdate['father.whatsapp'] = dto.fatherWhatsapp;
        if (dto.motherName !== undefined)
            toUpdate['mother.name'] = dto.motherName;
        if (dto.motherEmail !== undefined)
            toUpdate['mother.email'] = dto.motherEmail;
        if (dto.motherOccupation !== undefined)
            toUpdate['mother.occupation'] = dto.motherOccupation;
        if (dto.motherMonthlyIncome !== undefined)
            toUpdate['mother.monthlyIncome'] = dto.motherMonthlyIncome;
        if (dto.motherPhone !== undefined)
            toUpdate['mother.phone'] = dto.motherPhone;
        if (dto.motherWhatsapp !== undefined)
            toUpdate['mother.whatsapp'] = dto.motherWhatsapp;
        const anyDto = dto;
        if (anyDto.photoUrl !== undefined)
            toUpdate['documents.photoUrl'] = anyDto.photoUrl;
        if (anyDto.fatherPhotoUrl !== undefined)
            toUpdate['documents.fatherPhotoUrl'] = anyDto.fatherPhotoUrl;
        if (anyDto.motherPhotoUrl !== undefined)
            toUpdate['documents.motherPhotoUrl'] = anyDto.motherPhotoUrl;
        if (anyDto.birthCertificateUrl !== undefined)
            toUpdate['documents.birthCertificateUrl'] = anyDto.birthCertificateUrl;
        if (anyDto.scstCertificateUrl !== undefined)
            toUpdate['documents.scstCertificateUrl'] = anyDto.scstCertificateUrl;
        if (anyDto.leavingCertificateUrl !== undefined)
            toUpdate['documents.leavingCertificateUrl'] = anyDto.leavingCertificateUrl;
        if (anyDto.vaccineCardUrl !== undefined)
            toUpdate['documents.vaccineCardUrl'] = anyDto.vaccineCardUrl;
        if (dto.customDocuments !== undefined)
            toUpdate.customDocuments = dto.customDocuments;
        if (dto.customImages !== undefined)
            toUpdate.customImages = dto.customImages;
        if (dto.education !== undefined)
            toUpdate.education = dto.education;
        try {
            const isEnrollmentFieldUpdated = dto.classId !== undefined || dto.sectionId !== undefined || dto.rollNumber !== undefined;
            const updateOp = { $set: toUpdate };
            if (Object.keys(unsetFields).length > 0)
                updateOp.$unset = unsetFields;
            const session = await this.studentModel.db.startSession();
            let updated = null;
            try {
                await session.withTransaction(async () => {
                    if (isEnrollmentFieldUpdated) {
                        const currentStudent = await this.studentModel.findById(id).session(session).lean();
                        if (!currentStudent)
                            throw new common_1.BadRequestException('Student not found');
                        const finalClassId = dto.classId ?? currentStudent.classId;
                        const finalSectionId = dto.sectionId ?? currentStudent.sectionId;
                        const finalRollNumber = dto.rollNumber !== undefined
                            ? this.normRoll(dto.rollNumber)
                            : currentStudent.rollNumber;
                        const academicYearId = currentStudent.academicYearId;
                        const enrRes = await this.enrollmentModel.updateOne({ studentId: id, academicYearId: academicYearId }, { $set: { classId: finalClassId, sectionId: finalSectionId, rollNumber: finalRollNumber } }, { session });
                        if (enrRes.matchedCount === 0) {
                            common_1.Logger.warn(`No Enrollment row for studentId=${id} academicYearId=${academicYearId}; ` +
                                `snapshot-only update. This usually means the student was created before ` +
                                `Enrollment records were introduced, or the promote flow didn't create one.`, 'StudentsService');
                        }
                    }
                    updated = await this.studentModel.findByIdAndUpdate(id, updateOp, { new: true, projection: { password: 0 }, session }).lean();
                });
            }
            finally {
                await session.endSession();
            }
            return updated;
        }
        catch (err) {
            await this.handleDupKey(err);
        }
    }
    async updateDocuments(id, docs) {
        const set = {};
        if (docs.photoUrl !== undefined)
            set['documents.photoUrl'] = docs.photoUrl;
        if (docs.fatherPhotoUrl !== undefined)
            set['documents.fatherPhotoUrl'] = docs.fatherPhotoUrl;
        if (docs.motherPhotoUrl !== undefined)
            set['documents.motherPhotoUrl'] = docs.motherPhotoUrl;
        const updated = await this.studentModel.findByIdAndUpdate(id, { $set: set }, { new: true, projection: { password: 0 } });
        if (!updated)
            throw new common_1.NotFoundException('Student not found');
        return updated;
    }
    async delete(id) {
        const student = await this.studentModel.findById(id, { clearedAllDues: 1 }).lean();
        if (!student)
            throw new common_1.NotFoundException('Student not found');
        if (student.clearedAllDues === false) {
            throw new common_1.BadRequestException('Cannot delete student: all dues have not been cleared');
        }
        return this.studentModel.findByIdAndDelete(id);
    }
    async promote(studentId, classId, sectionId, academicYearId) {
        const student = await this.studentModel.findById(studentId);
        if (!student)
            throw new common_1.NotFoundException('Student not found');
        await this.assertIsCurrentAcademicYear(String(student.academicYearId));
        await this.assertNextAcademicYear(String(student.academicYearId), academicYearId);
        const rollNumber = student.rollNumber;
        const exists = await this.enrollmentModel.findOne({ studentId, academicYearId }).lean();
        if (exists)
            throw new common_1.BadRequestException('Student already has an enrollment for this academic year');
        const session = await this.studentModel.db.startSession();
        try {
            await session.withTransaction(async () => {
                await this.enrollmentModel.create([{
                        studentId, academicYearId, classId, sectionId, rollNumber,
                    }], { session });
                student.classId = classId;
                student.sectionId = sectionId;
                student.academicYearId = academicYearId;
                await student.save({ session });
            });
            const obj = student.toJSON();
            delete obj.password;
            return obj;
        }
        finally {
            await session.endSession();
        }
    }
    async promoteBulk(dto, headerAY) {
        const fromAY = dto.fromAcademicYearId || headerAY;
        if (!fromAY)
            throw new common_1.BadRequestException('fromAcademicYearId (or x-academic-year-id) is required');
        await this.assertIsCurrentAcademicYear(fromAY);
        const filter = { classId: dto.fromClassId, academicYearId: fromAY };
        if (dto.fromSectionId)
            filter.sectionId = dto.fromSectionId;
        if (dto.studentIds && dto.studentIds.length > 0) {
            filter._id = { $in: dto.studentIds };
        }
        const students = await this.studentModel
            .find(filter)
            .select({ _id: 1, rollNumber: 1, sectionId: 1, academicYearId: 1 })
            .lean();
        if (!students.length)
            return { selected: dto.studentIds?.length || 0, matched: 0, modified: 0 };
        await this.assertNextAcademicYear(fromAY, dto.toAcademicYearId);
        let targetClassId = dto.targetClassId;
        if (!targetClassId) {
            const currentClass = await this.classModel.findById(dto.fromClassId).lean();
            if (!currentClass || currentClass.order === undefined) {
                throw new common_1.BadRequestException('Cannot infer next class: current class not found or has no order');
            }
            const next = await this.classModel
                .findOne({ order: { $gt: currentClass.order } })
                .sort({ order: 1 })
                .lean();
            if (!next)
                throw new common_1.BadRequestException('No next class configured after current class');
            targetClassId = String(next._id);
        }
        let sectionMapping = null;
        if (!dto.targetSectionId) {
            const uniqueSectionIds = [...new Set(students.map(s => s.sectionId).filter(Boolean))];
            if (uniqueSectionIds.length > 0) {
                const sourceSections = await this.sectionModel
                    .find({ _id: { $in: uniqueSectionIds } })
                    .select({ _id: 1, name: 1 })
                    .lean();
                const targetSections = await this.sectionModel
                    .find({ classId: targetClassId })
                    .select({ _id: 1, name: 1 })
                    .lean();
                sectionMapping = new Map();
                for (const srcSection of sourceSections) {
                    const matchingTarget = targetSections.find(t => t.name.toLowerCase() === srcSection.name.toLowerCase());
                    if (matchingTarget) {
                        sectionMapping.set(String(srcSection._id), String(matchingTarget._id));
                    }
                }
            }
        }
        const session = await this.studentModel.db.startSession();
        try {
            let modified = 0;
            await session.withTransaction(async () => {
                const getTargetSectionId = (currentSectionId) => {
                    if (dto.targetSectionId) {
                        return dto.targetSectionId;
                    }
                    if (sectionMapping && currentSectionId) {
                        const mapped = sectionMapping.get(currentSectionId);
                        if (!mapped) {
                            throw new common_1.BadRequestException(`No matching section found in target class for section ID: ${currentSectionId}. Please specify a target section.`);
                        }
                        return mapped;
                    }
                    return currentSectionId;
                };
                const docs = students.map(s => ({
                    studentId: String(s._id),
                    academicYearId: dto.toAcademicYearId,
                    classId: targetClassId,
                    sectionId: getTargetSectionId(s.sectionId),
                    rollNumber: s.rollNumber,
                }));
                try {
                    await this.enrollmentModel.insertMany(docs, { session, ordered: false });
                }
                catch { }
                const bulkOps = students.map(s => ({
                    updateOne: {
                        filter: { _id: s._id },
                        update: {
                            $set: {
                                classId: targetClassId,
                                academicYearId: dto.toAcademicYearId,
                                sectionId: getTargetSectionId(s.sectionId),
                            },
                        },
                    },
                }));
                const result = await this.studentModel.bulkWrite(bulkOps, { session });
                modified = result.modifiedCount ?? 0;
            });
            return {
                selected: students.length,
                matched: students.length,
                modified,
                toAcademicYearId: dto.toAcademicYearId,
                targetClassId,
                targetSectionId: dto.targetSectionId ?? null,
            };
        }
        finally {
            await session.endSession();
        }
    }
    async filterStudents(filters) {
        let students;
        if (filters.academicYearId) {
            const matchEnrollment = { academicYearId: filters.academicYearId };
            if (filters.classId)
                matchEnrollment.classId = filters.classId;
            if (filters.sectionId)
                matchEnrollment.sectionId = filters.sectionId;
            const pipeline = [
                {
                    $lookup: {
                        from: 'enrollments',
                        let: { sid: '$_id' },
                        pipeline: [
                            {
                                $match: {
                                    $expr: { $eq: ['$studentId', { $toString: '$$sid' }] },
                                    ...matchEnrollment,
                                },
                            },
                        ],
                        as: 'enr',
                    },
                },
                { $unwind: { path: '$enr' } },
                {
                    $project: {
                        _id: 1,
                        name: 1,
                        uniqueId: 1,
                        email: 1,
                        role: 1,
                        gender: 1,
                        dateOfBirth: 1,
                        admissionDate: 1,
                        motherTongue: 1,
                        religion: 1,
                        caste: 1,
                        mediumOfInstruction: 1,
                        nationality: 1,
                        previousSchoolName: 1,
                        tcIssued: 1,
                        clearedAllDues: 1,
                        tcNumber: 1,
                        bloodGroup: 1,
                        heightCm: 1,
                        weightKg: 1,
                        medicalConditions: 1,
                        hasAllergies: 1,
                        allergyDetails: 1,
                        emergencyMedicalInstructions: 1,
                        guardianName: 1,
                        guardianRelation: 1,
                        phone: 1,
                        whatsapp: 1,
                        address: 1,
                        transportRequired: 1,
                        routePickupPoint: 1,
                        isActive: 1,
                        leavingDate: 1,
                        leavingReason: 1,
                        father: 1,
                        mother: 1,
                        documents: 1,
                        customDocuments: 1,
                        customImages: 1,
                        education: 1,
                        createdAt: 1,
                        updatedAt: 1,
                        rollNumber: '$enr.rollNumber',
                        classId: '$enr.classId',
                        sectionId: '$enr.sectionId',
                        academicYearId: '$enr.academicYearId',
                        isActiveForSelectedYear: {
                            $and: [
                                { $eq: ['$academicYearId', '$enr.academicYearId'] },
                                { $ne: ['$isActive', false] },
                            ],
                        },
                        statusForSelectedYear: {
                            $cond: [
                                {
                                    $and: [
                                        { $eq: ['$academicYearId', '$enr.academicYearId'] },
                                        { $ne: ['$isActive', false] },
                                    ],
                                },
                                'active',
                                'deactivated',
                            ],
                        },
                    },
                },
                { $sort: { createdAt: -1 } },
            ];
            students = await this.studentModel.aggregate(pipeline).exec();
        }
        else {
            const q = {};
            if (filters.classId)
                q.classId = filters.classId;
            if (filters.sectionId)
                q.sectionId = filters.sectionId;
            students = await this.studentModel.find(q, { password: 0 }).sort({ createdAt: -1 }).lean();
        }
        return students.map(s => this.convertDocumentUrls(s));
    }
    async updatePassword(id, newPasswordHash) {
        const updated = await this.studentModel.findByIdAndUpdate(id, { password: newPasswordHash }, { new: true });
        if (!updated)
            throw new common_1.NotFoundException('Student not found');
        return { message: 'Password updated successfully' };
    }
    async toggleActive(id) {
        const student = await this.studentModel.findById(id, { isActive: 1 });
        if (!student)
            throw new common_1.NotFoundException('Student not found');
        student.isActive = !student.isActive;
        await student.save();
        return { _id: id, isActive: student.isActive };
    }
    async getMonthlyEnrollmentStats(academicYearId) {
        const match = {};
        if (academicYearId) {
            match.academicYearId = academicYearId;
        }
        const pipeline = [];
        if (Object.keys(match).length > 0) {
            pipeline.push({ $match: match });
        }
        pipeline.push({
            $group: {
                _id: { year: { $year: '$createdAt' }, month: { $month: '$createdAt' } },
                count: { $sum: 1 },
            },
        }, { $sort: { '_id.year': 1, '_id.month': 1 } });
        const result = await this.studentModel.aggregate(pipeline);
        return result.map((d) => ({
            month: new Date(d._id.year, d._id.month - 1).toLocaleString('default', {
                month: 'short',
                year: 'numeric',
            }),
            students: d.count,
        }));
    }
    async getRecentEnrollments(limit = 5, academicYearId) {
        const n = Math.max(1, Math.min(Number(limit) || 5, 25));
        const filter = {};
        if (academicYearId) {
            filter.academicYearId = academicYearId;
        }
        const recent = await this.studentModel
            .find(filter, { _id: 1, name: 1, createdAt: 1 })
            .sort({ createdAt: -1 })
            .limit(n)
            .lean();
        return recent.map((s) => ({ id: String(s._id), name: s.name, createdAt: s.createdAt }));
    }
    async findBySection(sectionId) {
        const students = await this.studentModel
            .find({ sectionId }, { password: 0 })
            .sort({ name: 1 })
            .lean();
        return students.map(s => this.convertDocumentUrls(s));
    }
    async findBySectionIds(sectionIds) {
        if (!sectionIds?.length)
            return [];
        const students = await this.studentModel
            .find({ sectionId: { $in: sectionIds } }, { password: 0 })
            .sort({ sectionId: 1, name: 1 })
            .lean();
        return students.map(s => this.convertDocumentUrls(s));
    }
    async reAdmit(studentId, classId, sectionId, academicYearId) {
        const student = await this.studentModel.findById(studentId);
        if (!student)
            throw new common_1.NotFoundException('Student not found');
        const exists = await this.enrollmentModel.findOne({ studentId, academicYearId }).lean();
        if (exists) {
            throw new common_1.BadRequestException('Student already has an enrollment for this academic year');
        }
        const [classDoc, sectionDoc] = await Promise.all([
            this.classModel.findById(classId).select('name').lean(),
            this.sectionModel.findById(sectionId).select('name').lean(),
        ]);
        if (!classDoc)
            throw new common_1.BadRequestException('Class not found');
        if (!sectionDoc)
            throw new common_1.BadRequestException('Section not found');
        const { generateRollNumber } = await Promise.resolve().then(() => __importStar(require('../common/utils/id-generator')));
        const rollNumber = await generateRollNumber(this.enrollmentModel, classDoc.name, sectionDoc.name, academicYearId);
        const session = await this.studentModel.db.startSession();
        try {
            await session.withTransaction(async () => {
                await this.enrollmentModel.create([{
                        studentId,
                        academicYearId,
                        classId,
                        sectionId,
                        rollNumber,
                    }], { session });
                student.classId = classId;
                student.sectionId = sectionId;
                student.academicYearId = academicYearId;
                student.rollNumber = rollNumber;
                await student.save({ session });
            });
            const obj = student.toJSON();
            delete obj.password;
            return obj;
        }
        finally {
            await session.endSession();
        }
    }
    async reAdmitBulk(studentIds, classId, sectionId, academicYearId) {
        if (!studentIds?.length) {
            throw new common_1.BadRequestException('Student IDs are required');
        }
        const [classDoc, sectionDoc] = await Promise.all([
            this.classModel.findById(classId).select('name').lean(),
            this.sectionModel.findById(sectionId).select('name').lean(),
        ]);
        if (!classDoc)
            throw new common_1.BadRequestException('Class not found');
        if (!sectionDoc)
            throw new common_1.BadRequestException('Section not found');
        const students = await this.studentModel
            .find({ _id: { $in: studentIds } })
            .select({ _id: 1, name: 1 })
            .lean();
        if (!students.length) {
            throw new common_1.BadRequestException('No valid students found');
        }
        const session = await this.studentModel.db.startSession();
        try {
            let successCount = 0;
            const errors = [];
            await session.withTransaction(async () => {
                const { generateRollNumber } = await Promise.resolve().then(() => __importStar(require('../common/utils/id-generator')));
                for (const student of students) {
                    const studentId = String(student._id);
                    const exists = await this.enrollmentModel.findOne({ studentId, academicYearId }).lean();
                    if (exists) {
                        errors.push({ studentId, name: student.name, error: 'Already enrolled for this year' });
                        continue;
                    }
                    try {
                        const rollNumber = await generateRollNumber(this.enrollmentModel, classDoc.name, sectionDoc.name, academicYearId);
                        await this.enrollmentModel.create([{
                                studentId,
                                academicYearId,
                                classId,
                                sectionId,
                                rollNumber,
                            }], { session });
                        await this.studentModel.findByIdAndUpdate(studentId, {
                            $set: {
                                classId,
                                sectionId,
                                academicYearId,
                                rollNumber,
                            },
                        }, { session });
                        successCount++;
                    }
                    catch (err) {
                        errors.push({ studentId, name: student.name, error: err.message });
                    }
                }
            });
            return {
                total: studentIds.length,
                successful: successCount,
                failed: errors.length,
                errors: errors.length > 0 ? errors : undefined,
            };
        }
        finally {
            await session.endSession();
        }
    }
    async updateSelfProfile(id, data) {
        const update = {};
        if (data.name !== undefined)
            update.name = (data.name || '').trim() || undefined;
        if (data.phone !== undefined)
            update.phone = data.phone ? data.phone.trim() : undefined;
        if (data.profileImageUrl !== undefined)
            update.profileImageUrl = data.profileImageUrl ? data.profileImageUrl.trim() : '';
        const updated = await this.studentModel.findByIdAndUpdate(id, update, { new: true, projection: { password: 0 } }).lean();
        if (!updated)
            throw new common_1.NotFoundException('Student not found');
        return updated;
    }
    async updateEmail(id, newEmail) {
        const email = this.normEmail(newEmail);
        if (!email)
            throw new common_1.BadRequestException('Email is required');
        const exists = await this.studentModel.findOne({ email, _id: { $ne: id } }).lean();
        if (exists)
            throw new common_1.BadRequestException('Email already in use');
        const updated = await this.studentModel.findByIdAndUpdate(id, { email }, { new: true, projection: { password: 0 } }).lean();
        if (!updated)
            throw new common_1.NotFoundException('Student not found');
        return updated;
    }
    async updateNotificationPrefs(id, prefs) {
        const update = {};
        if (prefs.email !== undefined)
            update['notificationPrefs.email'] = prefs.email;
        if (prefs.whatsapp !== undefined)
            update['notificationPrefs.whatsapp'] = prefs.whatsapp;
        const updated = await this.studentModel.findByIdAndUpdate(id, { $set: update }, { new: true, projection: { password: 0 } }).lean();
        if (!updated)
            throw new common_1.NotFoundException('Student not found');
        return updated;
    }
};
exports.StudentsService = StudentsService;
exports.StudentsService = StudentsService = StudentsService_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, mongoose_1.InjectModel)(student_schema_1.Student.name)),
    __param(1, (0, mongoose_1.InjectModel)(class_schema_1.Class.name)),
    __param(2, (0, mongoose_1.InjectModel)(section_schema_1.Section.name)),
    __param(3, (0, mongoose_1.InjectModel)(enrollment_schema_1.Enrollment.name)),
    __metadata("design:paramtypes", [mongoose_2.Model,
        mongoose_2.Model,
        mongoose_2.Model,
        mongoose_2.Model])
], StudentsService);
//# sourceMappingURL=students.service.js.map