import {
    BadRequestException,
    Injectable,
    Logger,
    NotFoundException,
    OnModuleInit,
} from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Teacher, TeacherDocument } from './schemas/teacher.schema';
import { CreateTeacherDto } from './dto/create-teacher.dto';
import { UpdateTeacherDto } from './dto/update-teacher.dto';
import * as bcrypt from 'bcryptjs';

const BCRYPT_ROUNDS = Number(process.env.BCRYPT_ROUNDS || 10);

@Injectable()
export class TeachersService implements OnModuleInit {
    private readonly logger = new Logger(TeachersService.name);

    constructor(
        @InjectModel(Teacher.name)
        private teacherModel: Model<TeacherDocument>,
    ) { }

    /** One-time cleanup: unset empty-string emails & rebuild indexes so sparse unique works */
    async onModuleInit() {
        await this.teacherModel.updateMany(
            { email: '' },
            { $unset: { email: 1 } },
        );
        // Sync indexes: drops old non-sparse unique index on email, recreates as sparse
        await this.teacherModel.syncIndexes();
    }

    // -------- helpers ----------
    private normEmail(email: string) {
        return (email || '').trim().toLowerCase();
    }
    private normStr(v: string) {
        return (v || '').trim();
    }
    private async handleDupKey(err: any) {
        if (err?.code === 11000) {
            const fields = Object.keys(err.keyPattern || {});
            if (fields.includes('email'))
                throw new BadRequestException('Email already exists');
            if (fields.includes('uniqueId'))
                throw new BadRequestException('Duplicate teacher ID generated. Please try again.');
            throw new BadRequestException('A duplicate value was detected. Please check your input.');
        }
        throw err;
    }

    // -------- CRUD ----------
    async create(dto: CreateTeacherDto) {
        const email = dto.email ? this.normEmail(dto.email) : undefined;

        if (email) {
            const exists = await this.teacherModel.findOne({ email }).lean();
            if (exists) throw new BadRequestException('Email already exists');
        }

        const hashedPassword = dto.password
            ? await bcrypt.hash(dto.password, BCRYPT_ROUNDS)
            : undefined;

        const { generateTeacherId } = await import('../common/utils/id-generator');

        const data = {
            name: this.normStr(dto.name),
            ...(email ? { email } : {}),
            ...(hashedPassword ? { password: hashedPassword } : {}),
            role: 'teacher',
            isActive: dto.isActive !== undefined ? dto.isActive : true,
            designation: dto.designation || undefined,
            subject: dto.subject ? this.normStr(dto.subject) : undefined,
            phone: dto.phone ? this.normStr(dto.phone) : undefined,
            // Personal Details (mandatory)
            dateOfBirth: dto.dateOfBirth,
            joinDate: dto.joinDate,
            mobileNumber: this.normStr(dto.mobileNumber),
            // Personal Details (optional)
            fatherName: dto.fatherName ? this.normStr(dto.fatherName) : undefined,
            motherName: dto.motherName ? this.normStr(dto.motherName) : undefined,
            adhaarNumber: dto.adhaarNumber ? this.normStr(dto.adhaarNumber) : undefined,
            panNumber: dto.panNumber ? this.normStr(dto.panNumber) : undefined,
            address: this.normStr(dto.address),
            highestQualification: this.normStr(dto.highestQualification),
            photoUrl: dto.photoUrl ? this.normStr(dto.photoUrl) : undefined,
            signature: dto.signature ? this.normStr(dto.signature) : undefined,
            // Custom Documents and Images
            customDocuments: dto.customDocuments || [],
            customImages: dto.customImages || [],
            // Education (array)
            education: dto.education || [],
            // Bank Details
            bankName: dto.bankName ? this.normStr(dto.bankName) : undefined,
            accountNumber: dto.accountNumber ? this.normStr(dto.accountNumber) : undefined,
            accountType: dto.accountType ? this.normStr(dto.accountType) : undefined,
            ifscNumber: dto.ifscNumber ? this.normStr(dto.ifscNumber) : undefined,
            // PF Information
            pfMemberId: dto.pfMemberId ? this.normStr(dto.pfMemberId) : undefined,
            pfAccountNumber: dto.pfAccountNumber ? this.normStr(dto.pfAccountNumber) : undefined,
            employeeName: dto.employeeName ? this.normStr(dto.employeeName) : undefined,
            kycStatus: dto.kycStatus ? this.normStr(dto.kycStatus) : undefined,
            // Leaving Details
            schoolLeavingDate: dto.schoolLeavingDate || undefined,
            leavingReason: dto.leavingReason ? this.normStr(dto.leavingReason) : undefined,
        };

        // Retry loop to handle uniqueId race conditions
        for (let attempt = 0; attempt < 3; attempt++) {
            const uniqueId = await generateTeacherId(this.teacherModel);
            try {
                const doc = await this.teacherModel.create({ ...data, uniqueId });
                return doc.toJSON(); // password removed by schema transform
            } catch (err: any) {
                if (err?.code === 11000 && Object.keys(err.keyPattern || {}).includes('uniqueId')) {
                    continue; // Retry with a new ID
                }
                await this.handleDupKey(err);
            }
        }
        throw new BadRequestException('Could not generate unique teacher ID. Please try again.');
    }

    /** Backfill uniqueId for existing teachers that don't have one */
    async backfillTeacherIds() {
        const { generateTeacherId } = await import('../common/utils/id-generator');
        const noIdFilter = { $or: [{ uniqueId: { $exists: false } }, { uniqueId: null }, { uniqueId: '' }] };
        const teachers = await this.teacherModel.find(noIdFilter, { _id: 1 }).lean();

        let updated = 0;
        let failed = 0;
        for (const t of teachers) {
            try {
                for (let attempt = 0; attempt < 3; attempt++) {
                    const uniqueId = await generateTeacherId(this.teacherModel);
                    try {
                        const result = await this.teacherModel.updateOne(
                            { _id: t._id, ...noIdFilter },
                            { $set: { uniqueId } },
                        );
                        if (result.modifiedCount > 0) updated++;
                        break;
                    } catch (err: any) {
                        if (err?.code === 11000 && attempt < 2) continue;
                        throw err;
                    }
                }
            } catch (err) {
                this.logger.warn(`Failed to backfill uniqueId for teacher ${t._id}`, err);
                failed++;
            }
        }
        return { total: teachers.length, updated, failed };
    }

    /** Used by AuthService (needs password hash) */
    async findByEmail(email: string) {
        return this.teacherModel.findOne({ email: this.normEmail(email) }).lean();
    }

    /** Used for password verification (needs password hash) */
    async findByIdForAuth(id: string) {
        return this.teacherModel.findById(id).lean();
    }

    async findByPhone(phone: string) {
        return this.teacherModel.findOne({ phone: this.normStr(phone) }).lean();
    }

    findAll(isActive?: boolean) {
        const filter: any = {};
        if (isActive !== undefined) filter.isActive = isActive;
        return this.teacherModel
            .find(filter, { password: 0 })
            .sort({ createdAt: -1 })
            .limit(5000)
            .lean({ virtuals: true });
    }

    async searchTeachers(query: string, limit: number = 50) {
        if (!query || query.trim().length === 0) {
            return [];
        }

        const escaped = query.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        const searchRegex = new RegExp(escaped, 'i');
        return this.teacherModel
            .find(
                {
                    $or: [
                        { name: searchRegex },
                        { email: searchRegex },
                        { subject: searchRegex },
                        { uniqueId: searchRegex },
                    ],
                },
                { password: 0 }
            )
            .limit(Math.min(limit, 100))
            .sort({ name: 1 })
            .lean({ virtuals: true });
    }

    // Minimal list for selection in UIs (only active teachers)
    async findMinimal() {
        return this.teacherModel
            .find({ isActive: true }, { name: 1, subject: 1 })
            .sort({ name: 1 })
            .limit(1000)
            .lean({ virtuals: true });
    }

    findOne(id: string) {
        return this.teacherModel.findById(id, { password: 0 }).lean({ virtuals: true });
    }

    async update(id: string, dto: UpdateTeacherDto) {
        const $set: any = {};
        const $unset: any = {};

        // Helper: set or unset a string field
        const setOrUnset = (field: string, val: string | undefined, normalize = true) => {
            const v = val && normalize ? this.normStr(val) : val;
            if (v) $set[field] = v; else $unset[field] = 1;
        };

        // Basic fields
        if (dto.isActive !== undefined) $set.isActive = dto.isActive;
        if (dto.email !== undefined) {
            const e = dto.email ? this.normEmail(dto.email) : '';
            if (e) $set.email = e; else $unset.email = 1;
        }
        if (dto.name !== undefined) $set.name = this.normStr(dto.name);
        if (dto.designation !== undefined) setOrUnset('designation', dto.designation);
        if (dto.subject !== undefined) setOrUnset('subject', dto.subject);
        if (dto.phone !== undefined) setOrUnset('phone', dto.phone);

        // Personal Details
        if (dto.dateOfBirth !== undefined) {
            if (dto.dateOfBirth) $set.dateOfBirth = dto.dateOfBirth; else $unset.dateOfBirth = 1;
        }
        if (dto.joinDate !== undefined) {
            if (dto.joinDate) $set.joinDate = dto.joinDate; else $unset.joinDate = 1;
        }
        if (dto.fatherName !== undefined) setOrUnset('fatherName', dto.fatherName);
        if (dto.motherName !== undefined) setOrUnset('motherName', dto.motherName);
        if (dto.mobileNumber !== undefined) setOrUnset('mobileNumber', dto.mobileNumber);
        if (dto.adhaarNumber !== undefined) setOrUnset('adhaarNumber', dto.adhaarNumber);
        if (dto.panNumber !== undefined) setOrUnset('panNumber', dto.panNumber);
        if (dto.address !== undefined) setOrUnset('address', dto.address);
        if (dto.highestQualification !== undefined) setOrUnset('highestQualification', dto.highestQualification);
        if (dto.photoUrl !== undefined) setOrUnset('photoUrl', dto.photoUrl);
        if (dto.signature !== undefined) setOrUnset('signature', dto.signature);

        // Custom Documents and Images
        if (dto.customDocuments !== undefined) $set.customDocuments = dto.customDocuments;
        if (dto.customImages !== undefined) $set.customImages = dto.customImages;

        // Education (array)
        if (dto.education !== undefined) $set.education = dto.education;

        // Bank Details
        if (dto.bankName !== undefined) setOrUnset('bankName', dto.bankName);
        if (dto.accountNumber !== undefined) setOrUnset('accountNumber', dto.accountNumber);
        if (dto.accountType !== undefined) setOrUnset('accountType', dto.accountType);
        if (dto.ifscNumber !== undefined) setOrUnset('ifscNumber', dto.ifscNumber);

        // PF Information
        if (dto.pfMemberId !== undefined) setOrUnset('pfMemberId', dto.pfMemberId);
        if (dto.pfAccountNumber !== undefined) setOrUnset('pfAccountNumber', dto.pfAccountNumber);
        if (dto.employeeName !== undefined) setOrUnset('employeeName', dto.employeeName);
        if (dto.kycStatus !== undefined) setOrUnset('kycStatus', dto.kycStatus);

        // Leaving Details
        if (dto.schoolLeavingDate !== undefined) {
            if (dto.schoolLeavingDate) $set.schoolLeavingDate = dto.schoolLeavingDate; else $unset.schoolLeavingDate = 1;
        }
        if (dto.leavingReason !== undefined) setOrUnset('leavingReason', dto.leavingReason);

        // Only update password if provided and not empty (min 8 chars enforced by DTO validation)
        if (dto.password !== undefined && dto.password.trim().length > 0) {
            $set.password = await bcrypt.hash(dto.password, BCRYPT_ROUNDS);
        }

        const updateOps: any = {};
        if (Object.keys($set).length) updateOps.$set = $set;
        if (Object.keys($unset).length) updateOps.$unset = $unset;

        try {
            const updated = await this.teacherModel.findByIdAndUpdate(id, updateOps, {
                new: true,
                projection: { password: 0 },
            });
            if (!updated) throw new NotFoundException('Teacher not found');
            return updated.toJSON();
        } catch (err) {
            if (err instanceof NotFoundException) throw err;
            await this.handleDupKey(err);
        }
    }

    async toggleActive(id: string) {
        const teacher = await this.teacherModel.findById(id, { isActive: 1 }).lean();
        if (!teacher) throw new NotFoundException('Teacher not found');
        const newState = !teacher.isActive;
        await this.teacherModel.updateOne({ _id: id }, { $set: { isActive: newState } });
        return { _id: id, isActive: newState };
    }

    async delete(id: string) {
        const deleted = await this.teacherModel.findByIdAndDelete(id);
        if (!deleted) throw new NotFoundException('Teacher not found');
        return { message: 'Teacher deleted successfully' };
    }

    /**
     * Updates password for a teacher.
     * IMPORTANT: newPassword is expected to be ALREADY HASHED by the caller (AuthService).
     * Do NOT hash it again here to avoid double hashing.
     */
    async updatePassword(id: string, newPasswordHash: string) {
        const updated = await this.teacherModel.findByIdAndUpdate(
            id,
            { password: newPasswordHash },
            { new: true },
        );
        if (!updated) throw new NotFoundException('Teacher not found');
        return { message: 'Password updated successfully' };
    }

    async updateSelfProfile(id: string, data: { name?: string; phone?: string; photoUrl?: string }) {
        const update: Record<string, any> = {};
        if (data.name !== undefined) {
            const trimmed = (data.name || '').trim();
            if (!trimmed) throw new BadRequestException('Name cannot be empty');
            update.name = trimmed;
        }
        if (data.phone !== undefined) update.phone = data.phone ? data.phone.trim() : undefined;
        if (data.photoUrl !== undefined) update.photoUrl = data.photoUrl ? data.photoUrl.trim() : undefined;

        const updated = await this.teacherModel.findByIdAndUpdate(id, update, { new: true, projection: { password: 0 } }).lean({ virtuals: true });
        if (!updated) throw new NotFoundException('Teacher not found');
        return updated;
    }

    async updateEmail(id: string, newEmail: string) {
        const email = this.normEmail(newEmail);
        if (!email) throw new BadRequestException('Email is required');

        const exists = await this.teacherModel.findOne({ email, _id: { $ne: id } }).lean();
        if (exists) throw new BadRequestException('Email already in use');

        const updated = await this.teacherModel.findByIdAndUpdate(id, { email }, { new: true, projection: { password: 0 } }).lean({ virtuals: true });
        if (!updated) throw new NotFoundException('Teacher not found');
        return updated;
    }

    async updateNotificationPrefs(id: string, prefs: { email?: boolean; whatsapp?: boolean }) {
        const update: Record<string, any> = {};
        if (prefs.email !== undefined) update['notificationPrefs.email'] = prefs.email;
        if (prefs.whatsapp !== undefined) update['notificationPrefs.whatsapp'] = prefs.whatsapp;

        const updated = await this.teacherModel.findByIdAndUpdate(id, { $set: update }, { new: true, projection: { password: 0 } }).lean({ virtuals: true });
        if (!updated) throw new NotFoundException('Teacher not found');
        return updated;
    }
}
