import {
    BadRequestException,
    Injectable,
    NotFoundException,
} from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, isValidObjectId } from 'mongoose';
import * as crypto from 'crypto';
import Razorpay from 'razorpay';
import { timingSafeEqual } from '../common/utils/timing-safe-equal';

import { TransportRoute, TransportRouteDocument } from './schemas/transport-route.schema';
import { TransportAssignment, TransportAssignmentDocument } from './schemas/transport-assignment.schema';
import { Student, StudentDocument } from '../students/schemas/student.schema';
import { AcademicYear, AcademicYearDocument } from '../academic-year/academic-year.schema';
import { AuditService } from '../audit/audit.service';
import { RequestContext } from '../common/context/request-context';

import { CreateRouteDto } from './dto/create-route.dto';
import { UpdateRouteDto } from './dto/update-route.dto';
import { AssignTransportDto } from './dto/assign-transport.dto';
import { RecordTransportPaymentDto } from './dto/record-transport-payment.dto';
import { FilterTransportAssignmentsDto } from './dto/filter-transport-assignments.dto';
import { ToggleTransportDto } from './dto/toggle-transport.dto';
import { clampDayToMonth, yearForMonthInAY } from '../fees/fees.installment-dates';

const MONTHS = [
    '', 'January', 'February', 'March', 'April', 'May', 'June',
    'July', 'August', 'September', 'October', 'November', 'December',
];

@Injectable()
export class TransportService {
    private razor: Razorpay | null = null;
    private razorpayKeyId: string;
    private razorpayKeySecret: string;
    private razorpayWebhookSecret: string;

    constructor(
        @InjectModel(TransportRoute.name) private routeModel: Model<TransportRouteDocument>,
        @InjectModel(TransportAssignment.name) private assignmentModel: Model<TransportAssignmentDocument>,
        @InjectModel(Student.name) private studentModel: Model<StudentDocument>,
        @InjectModel(AcademicYear.name) private academicYearModel: Model<AcademicYearDocument>,
        private readonly auditService: AuditService,
    ) {
        this.razorpayKeyId = process.env.RAZORPAY_KEY_ID || '';
        this.razorpayKeySecret = process.env.RAZORPAY_KEY_SECRET || '';
        this.razorpayWebhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET || '';
    }

    private getRazorpay(): Razorpay {
        if (this.razor) return this.razor;
        if (!this.razorpayKeyId || !this.razorpayKeySecret) {
            throw new BadRequestException(
                'Online payments are not configured. Set RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET.',
            );
        }
        this.razor = new Razorpay({ key_id: this.razorpayKeyId, key_secret: this.razorpayKeySecret });
        return this.razor;
    }

    /* =====================
       Routes CRUD (Admin)
    ===================== */

    async createRoute(dto: CreateRouteDto, ctx?: RequestContext) {
        const ay = await this.academicYearModel.findById(dto.academicYearId).lean();
        if (!ay) throw new BadRequestException('Academic year not found.');

        const route = await this.routeModel.create({
            name: dto.name.trim(),
            description: dto.description,
            academicYearId: dto.academicYearId,
            monthlyFee: dto.monthlyFee,
            dueDay: dto.dueDay,
            stops: dto.stops ?? [],
            vehicleNumber: dto.vehicleNumber,
            driverName: dto.driverName,
            driverPhone: dto.driverPhone,
            lateFine: dto.lateFine ?? { enabled: false, type: 'per_day', amountPerDay: 0, percentage: 0, slabs: [] },
            isActive: dto.isActive ?? true,
        });

        await this.auditService.log({
            entity: 'TransportRoute', entityId: String(route._id),
            action: 'create', after: route.toObject(), ctx,
        });

        return route.toObject();
    }

    async getRoutes(academicYearId?: string, activeOnly?: boolean) {
        const q: any = {};
        if (academicYearId) {
            if (!isValidObjectId(academicYearId)) throw new BadRequestException('Invalid academic year ID');
            q.academicYearId = academicYearId;
        }
        if (activeOnly) q.isActive = true;
        return this.routeModel.find(q).sort({ createdAt: -1 }).lean();
    }

    async getRoute(id: string) {
        const route = await this.routeModel.findById(id).lean();
        if (!route) throw new NotFoundException('Transport route not found');
        return route;
    }

    async updateRoute(id: string, dto: UpdateRouteDto, ctx?: RequestContext) {
        const before = await this.routeModel.findById(id).lean();
        if (!before) throw new NotFoundException('Transport route not found');

        if (dto.academicYearId && dto.academicYearId !== before.academicYearId) {
            throw new BadRequestException('Cannot change academic year. Delete and recreate the route instead.');
        }

        const { academicYearId, ...updateFields } = dto;
        const updated = await this.routeModel
            .findByIdAndUpdate(id, { $set: updateFields }, { new: true, runValidators: true })
            .lean();

        if (updateFields.name && updateFields.name !== before.name) {
            await this.assignmentModel.updateMany(
                { routeId: id, status: { $ne: 'paid' } },
                { $set: { routeName: updateFields.name } },
            );
        }

        await this.auditService.log({
            entity: 'TransportRoute', entityId: id,
            action: 'update', before, after: updated, ctx,
        });

        return updated;
    }

    async deleteRoute(id: string, ctx?: RequestContext) {
        const route = await this.routeModel.findById(id).lean();
        if (!route) throw new NotFoundException('Transport route not found');

        const paidCount = await this.assignmentModel.countDocuments({ routeId: id, status: 'paid' });
        if (paidCount > 0) {
            throw new BadRequestException('Cannot delete route — some students have already paid.');
        }

        // Deactivate route first to prevent new payments, then delete
        await this.routeModel.findByIdAndUpdate(id, { $set: { isActive: false } });

        // Re-check for payments that may have sneaked in after our initial count
        const finalPaidCount = await this.assignmentModel.countDocuments({ routeId: id, status: 'paid' });
        if (finalPaidCount > 0) {
            // Re-activate the route since we can't delete it
            await this.routeModel.findByIdAndUpdate(id, { $set: { isActive: true } });
            throw new BadRequestException('A payment was recorded while deleting. Route has been preserved and re-activated.');
        }

        await this.assignmentModel.deleteMany({ routeId: id, status: { $ne: 'paid' } });
        await this.routeModel.findByIdAndDelete(id);

        await this.auditService.log({
            entity: 'TransportRoute', entityId: id,
            action: 'delete', before: route, ctx,
        });

        return { deleted: true };
    }

    /* =====================
       Assignments
    ===================== */

    async assignStudents(dto: AssignTransportDto, ctx?: RequestContext) {
        const route = await this.routeModel.findById(dto.routeId).lean();
        if (!route) throw new NotFoundException('Transport route not found');

        const students = await this.studentModel
            .find({ _id: { $in: dto.studentIds }, isActive: true }, { _id: 1 })
            .limit(5000)
            .lean();
        if (students.length === 0) {
            throw new BadRequestException('No active students found for the given IDs.');
        }

        const ay = await this.academicYearModel.findById(route.academicYearId).lean();
        if (!ay) throw new NotFoundException('Academic year not found for this transport route');
        const ayStart = new Date(ay.startDate);

        const amount = dto.customAmount ?? route.monthlyFee;
        const months = dto.months && dto.months.length > 0
            ? dto.months.filter((m) => m >= 1 && m <= 12)
            : Array.from({ length: 12 }, (_, i) => i + 1);

        // UTC-safe dueDate computation: resolve year from AY start month, clamp
        // day per month. Replaces the old hardcoded `m >= 6` heuristic.
        const makeDueDate = (month: number) => {
            const year = yearForMonthInAY(ayStart, month);
            const day = clampDayToMonth(year, month, route.dueDay);
            return new Date(Date.UTC(year, month - 1, day));
        };

        const docs: any[] = [];
        for (const stu of students) {
            for (const month of months) {
                docs.push({
                    studentId: String(stu._id),
                    routeId: String(route._id),
                    academicYearId: route.academicYearId,
                    routeName: route.name,
                    installment: MONTHS[month],
                    amount,
                    fineAmount: 0,
                    dueDate: makeDueDate(month),
                    status: 'pending',
                    pickupPoint: dto.pickupPoint,
                });
            }
        }

        let created = 0;
        for (let i = 0; i < docs.length; i += 5000) {
            const batch = docs.slice(i, i + 5000);
            const result = await this.assignmentModel.insertMany(batch, { ordered: false }).catch((err) => {
                if (err?.code === 11000 || err?.writeErrors) {
                    return { insertedCount: batch.length - (err.writeErrors?.length ?? 0) };
                }
                throw err;
            });
            created += (result as any).insertedCount ?? (result as any).length ?? batch.length;
        }

        await this.auditService.log({
            entity: 'TransportAssignment', entityId: String(route._id),
            action: 'create', after: { routeId: dto.routeId, studentCount: students.length, months, amount, created }, ctx,
        });

        return { created };
    }

    async getAssignments(filter: FilterTransportAssignmentsDto) {
        const q: any = {};
        if (filter.academicYearId) q.academicYearId = filter.academicYearId;
        if (filter.routeId) q.routeId = filter.routeId;
        if (filter.studentId) q.studentId = filter.studentId;
        if (filter.month) q.installment = filter.month;
        if (filter.isActive === 'true') q.isActive = true;
        else if (filter.isActive === 'false') q.isActive = false;
        if (filter.status) {
            if (filter.status === 'overdue') {
                q.status = 'pending';
                q.dueDate = { $lt: new Date() };
            } else if (filter.status === 'pending') {
                q.status = 'pending';
                q.dueDate = { $gte: new Date() };
            } else {
                q.status = filter.status;
            }
        }

        const assignments = await this.assignmentModel.find(q).sort({ dueDate: 1 }).limit(5000).lean();
        const withFines = await this.recalculateFinesWithRoutes(assignments);

        const studentIds = [...new Set(withFines.map((a) => a.studentId))];
        if (studentIds.length > 0) {
            const students = await this.studentModel
                .find({ _id: { $in: studentIds } }, { name: 1, uniqueId: 1, rollNumber: 1 })
                .lean();
            const studentMap = new Map(students.map((s) => [String(s._id), s]));
            return withFines.map((a) => {
                const stu = studentMap.get(a.studentId);
                return { ...a, studentName: stu?.name, studentUniqueId: (stu as any)?.uniqueId, studentRollNumber: stu?.rollNumber };
            });
        }

        return withFines;
    }

    /**
     * Sync: fills in missing monthly assignments for students already assigned to a route.
     * Unlike fees (class-wide), transport is opt-in — only students with existing assignments get new months.
     */
    async syncAssignments(routeId?: string) {
        const query: any = { isActive: true };
        if (routeId) query._id = routeId;
        const routes = await this.routeModel.find(query).lean();

        let created = 0;
        for (const route of routes) {
            const rid = String(route._id);

            // Only sync for students already assigned to this route (opt-in model)
            const assignedStudentIds: string[] = await this.assignmentModel.distinct('studentId', { routeId: rid });
            if (assignedStudentIds.length === 0) continue;

            // Find which (student, installment) pairs already exist
            const existing = await this.assignmentModel.find(
                { routeId: rid },
                { studentId: 1, installment: 1 },
            ).lean();
            const existingSet = new Set(existing.map((a) => `${a.studentId}_${a.installment}`));

            const ay = await this.academicYearModel.findById(route.academicYearId).lean();
            if (!ay) continue;
            const ayStart = new Date(ay.startDate);

            const makeDueDate = (month: number) => {
                const year = yearForMonthInAY(ayStart, month);
                const day = clampDayToMonth(year, month, route.dueDay);
                return new Date(Date.UTC(year, month - 1, day));
            };

            const docs: any[] = [];
            for (const studentId of assignedStudentIds) {
                for (let month = 1; month <= 12; month++) {
                    const key = `${studentId}_${MONTHS[month]}`;
                    if (existingSet.has(key)) continue;
                    docs.push({
                        studentId,
                        routeId: rid,
                        academicYearId: route.academicYearId,
                        routeName: route.name,
                        installment: MONTHS[month],
                        amount: route.monthlyFee,
                        fineAmount: 0,
                        dueDate: makeDueDate(month),
                        status: 'pending',
                    });
                }
            }

            if (docs.length > 0) {
                for (let i = 0; i < docs.length; i += 5000) {
                    const batch = docs.slice(i, i + 5000);
                    const result = await this.assignmentModel.insertMany(batch, { ordered: false }).catch((err) => {
                        if (err?.code === 11000 || err?.writeErrors) {
                            return { insertedCount: batch.length - (err.writeErrors?.length ?? 0) };
                        }
                        throw err;
                    });
                    created += (result as any).insertedCount ?? (result as any).length ?? 0;
                }
            }
        }

        return { synced: created };
    }

    /* =====================
       Toggle / Remove / Status
    ===================== */

    async toggleTransport(studentId: string, dto: ToggleTransportDto, ctx?: RequestContext) {
        const student = await this.studentModel.findById(studentId, { _id: 1 }).lean();
        if (!student) throw new NotFoundException('Student not found');

        const result = await this.assignmentModel.updateMany(
            { studentId, routeId: dto.routeId, academicYearId: dto.academicYearId, status: { $ne: 'paid' } },
            { $set: { isActive: dto.isActive } },
        );

        // Also update paid records' isActive so the status is consistent per student-route
        const paidResult = await this.assignmentModel.updateMany(
            { studentId, routeId: dto.routeId, academicYearId: dto.academicYearId, status: 'paid' },
            { $set: { isActive: dto.isActive } },
        );

        await this.auditService.log({
            entity: 'TransportAssignment', entityId: `${studentId}_${dto.routeId}`,
            action: 'update', after: { studentId, routeId: dto.routeId, isActive: dto.isActive }, ctx,
        });

        return { updated: result.matchedCount + paidResult.matchedCount, isActive: dto.isActive };
    }

    async removeTransport(studentId: string, routeId: string, academicYearId: string, ctx?: RequestContext) {
        const student = await this.studentModel.findById(studentId, { _id: 1 }).lean();
        if (!student) throw new NotFoundException('Student not found');

        // Delete all unpaid assignments
        const deleteResult = await this.assignmentModel.deleteMany({
            studentId, routeId, academicYearId, status: { $ne: 'paid' },
        });

        // Count kept (paid) records
        const kept = await this.assignmentModel.countDocuments({
            studentId, routeId, academicYearId, status: 'paid',
        });

        await this.auditService.log({
            entity: 'TransportAssignment', entityId: `${studentId}_${routeId}`,
            action: 'delete', before: { studentId, routeId, deleted: deleteResult.deletedCount, kept }, ctx,
        });

        return { deleted: deleteResult.deletedCount, kept };
    }

    async getStudentTransportStatus(studentId: string, academicYearId?: string) {
        const student = await this.studentModel.findById(studentId, { academicYearId: 1 }).lean();
        if (!student) throw new NotFoundException('Student not found');

        if (academicYearId && !isValidObjectId(academicYearId)) {
            throw new BadRequestException('Invalid academic year ID');
        }

        const ayId = academicYearId || student.academicYearId;
        if (!ayId) return [];

        // Get all distinct routes for this student in this academic year
        const assignments = await this.assignmentModel.find(
            { studentId, academicYearId: ayId },
            { routeId: 1, routeName: 1, isActive: 1, status: 1, pickupPoint: 1, amount: 1 },
        ).lean();

        // Group by routeId
        const routeMap = new Map<string, {
            routeId: string; routeName: string; isActive: boolean;
            pendingCount: number; paidCount: number; pickupPoint?: string; monthlyFee: number;
        }>();

        for (const a of assignments) {
            const rid = String(a.routeId);
            if (!routeMap.has(rid)) {
                routeMap.set(rid, {
                    routeId: rid,
                    routeName: a.routeName,
                    isActive: a.isActive !== false,
                    pendingCount: 0,
                    paidCount: 0,
                    pickupPoint: a.pickupPoint,
                    monthlyFee: a.amount,
                });
            }
            const entry = routeMap.get(rid)!;
            if (a.status === 'paid') entry.paidCount++;
            else {
                entry.pendingCount++;
                // Prefer pending records' isActive — they control fine accumulation and payments
                entry.isActive = a.isActive !== false;
            }
        }

        return Array.from(routeMap.values());
    }

    /* =====================
       Student Dashboard
    ===================== */

    async getStudentDashboard(studentId: string, academicYearId?: string) {
        const student = await this.studentModel.findById(studentId, {
            name: 1, classId: 1, academicYearId: 1, rollNumber: 1,
            sectionId: 1, profileImageUrl: 1, uniqueId: 1,
            guardianName: 1, father: 1, mother: 1, phone: 1, email: 1,
        }).lean();
        if (!student) throw new NotFoundException('Student not found');

        if (academicYearId && !isValidObjectId(academicYearId)) {
            throw new BadRequestException('Invalid academic year ID');
        }
        const ayId = academicYearId || student.academicYearId;

        const raw = await this.assignmentModel
            .find({ studentId, academicYearId: ayId })
            .sort({ dueDate: 1 })
            .lean();

        const assignments = await this.recalculateFinesWithRoutes(raw);

        // Only count active assignments for summary
        const activeAssignments = assignments.filter((a) => a.isActive !== false);
        const totalFee = activeAssignments.reduce((s, a) => s + a.amount + (a.fineAmount || 0), 0);
        const paidAmount = activeAssignments
            .filter((a) => a.status === 'paid')
            .reduce((s, a) => s + a.amount + (a.fineAmount || 0), 0);
        const dueAmount = totalFee - paidAmount;

        const unpaid = activeAssignments.filter((a) => a.status !== 'paid');
        const nextDueDate = unpaid.length > 0
            ? new Date(Math.min(...unpaid.map((a) => new Date(a.dueDate).getTime()))).toISOString()
            : null;

        return {
            student,
            summary: { totalFee, paidAmount, dueAmount, nextDueDate },
            assignments,
        };
    }

    /* =====================
       Razorpay Payments
    ===================== */

    async createRazorpayOrder(assignmentId: string, studentId: string) {
        const assignment = await this.assignmentModel.findOne({ _id: assignmentId, studentId });
        if (!assignment) throw new NotFoundException('Transport assignment not found');
        if (assignment.status === 'paid') {
            throw new BadRequestException('This fee is already paid.');
        }
        if (assignment.isActive === false) {
            throw new BadRequestException('Transport is currently inactive. Contact admin.');
        }

        const fine = await this.calculateFineFromRoute(assignment.toObject());
        const totalAmount = assignment.amount + fine;

        if (assignment.razorpayOrderId) {
            const existingTotal = assignment.amount + (assignment.fineAmount || 0);
            if (existingTotal === totalAmount) {
                return {
                    orderId: assignment.razorpayOrderId,
                    amount: Math.round(totalAmount * 100),
                    currency: 'INR',
                    keyId: this.razorpayKeyId,
                    assignmentId: String(assignment._id),
                };
            }
            assignment.razorpayOrderId = undefined;
        }

        const order = await this.getRazorpay().orders.create({
            amount: Math.round(totalAmount * 100),
            currency: 'INR',
            receipt: `transport_${assignmentId}_${Date.now()}`,
            notes: { assignmentId, studentId, type: 'transport' },
        });

        assignment.razorpayOrderId = order.id;
        assignment.fineAmount = fine;
        await assignment.save();

        return {
            orderId: order.id,
            amount: order.amount,
            currency: order.currency,
            keyId: this.razorpayKeyId,
            assignmentId: String(assignment._id),
        };
    }

    async verifyRazorpayPayment(
        assignmentId: string,
        studentId: string,
        body: { razorpay_order_id: string; razorpay_payment_id: string; razorpay_signature: string },
    ) {
        if (!this.razorpayKeySecret) {
            throw new BadRequestException('Payment verification is not configured.');
        }

        const sign = crypto
            .createHmac('sha256', this.razorpayKeySecret)
            .update(`${body.razorpay_order_id}|${body.razorpay_payment_id}`)
            .digest('hex');

        if (!timingSafeEqual(sign, body.razorpay_signature)) {
            throw new BadRequestException('Payment signature verification failed');
        }

        const assignment = await this.assignmentModel.findOne({
            _id: assignmentId,
            studentId,
            razorpayOrderId: body.razorpay_order_id,
        }).lean();
        if (!assignment) throw new NotFoundException('Transport assignment not found');

        if (assignment.status === 'paid') {
            return { status: 'paid', receiptNumber: assignment.receiptNumber };
        }

        const fine = await this.calculateFineFromRoute(assignment);

        try {
            const payment = await this.getRazorpay().payments.fetch(body.razorpay_payment_id);
            const expectedPaise = Math.round((assignment.amount + fine) * 100);
            if (Number(payment.amount) !== expectedPaise) {
                throw new BadRequestException(
                    `Payment amount mismatch: expected ${expectedPaise} paise, got ${payment.amount} paise`,
                );
            }
        } catch (err) {
            if (err instanceof BadRequestException) throw err;
        }

        // Generate receipt BEFORE atomic claim so it's written in a single operation
        const receiptNumber = await this.generateReceiptNumber();

        const claimed = await this.assignmentModel.findOneAndUpdate(
            { _id: assignmentId, studentId, status: { $ne: 'paid' } },
            {
                $set: {
                    status: 'paid',
                    paidAt: new Date(),
                    paymentMethod: 'razorpay',
                    razorpayPaymentId: body.razorpay_payment_id,
                    razorpaySignature: body.razorpay_signature,
                    fineAmount: fine,
                    receiptNumber,
                },
            },
            { new: true },
        );

        if (!claimed) {
            // Race: another request or webhook already marked it paid
            const existing = await this.assignmentModel.findById(assignmentId).lean();
            return { status: 'paid', receiptNumber: existing?.receiptNumber };
        }

        return { status: 'paid', receiptNumber };
    }

    /* =====================
       Offline Payment (Admin)
    ===================== */

    async recordOfflinePayment(assignmentId: string, dto: RecordTransportPaymentDto, adminId: string) {
        const assignment = await this.assignmentModel.findById(assignmentId).lean();
        if (!assignment) throw new NotFoundException('Transport assignment not found');
        if (assignment.status === 'paid') {
            throw new BadRequestException('This fee is already paid.');
        }

        const fine = await this.calculateFineFromRoute(assignment);
        const receiptNumber = await this.generateReceiptNumber();

        const claimed = await this.assignmentModel.findOneAndUpdate(
            { _id: assignmentId, status: { $ne: 'paid' } },
            {
                $set: {
                    status: 'paid',
                    paidAt: new Date(),
                    paymentMethod: dto.paymentMethod,
                    collectedBy: adminId,
                    notes: dto.notes,
                    fineAmount: fine,
                    receiptNumber,
                },
            },
            { new: true },
        ).lean();

        if (!claimed) {
            throw new BadRequestException('This fee is already paid.');
        }

        return { ...claimed, receiptNumber };
    }

    /* =====================
       Webhook (Razorpay)
    ===================== */

    async handleWebhook(signature: string | undefined, rawBody: Buffer, jsonBody: any) {
        if (!this.razorpayWebhookSecret) throw new BadRequestException('Webhook not configured');

        const expected = crypto.createHmac('sha256', this.razorpayWebhookSecret).update(rawBody).digest('hex');
        if (!signature || !timingSafeEqual(expected, signature)) {
            return { received: true };
        }

        const event = jsonBody?.event;
        const entity = jsonBody?.payload?.payment?.entity || jsonBody?.payload?.order?.entity;

        if (event === 'payment.captured' && entity) {
            const orderId = entity.order_id;

            // Only handle transport orders (receipt starts with transport_)
            const notes = entity.notes || {};
            if (notes.type !== 'transport') return { received: true };

            const assignment = await this.assignmentModel.findOne({ razorpayOrderId: orderId }).lean();
            if (assignment && assignment.status !== 'paid') {
                const fine = await this.calculateFineFromRoute(assignment);
                const expectedPaise = Math.round((assignment.amount + fine) * 100);
                if (Number(entity.amount) !== expectedPaise) {
                    return { received: true };
                }

                const receiptNumber = await this.generateReceiptNumber();
                await this.assignmentModel.findOneAndUpdate(
                    { razorpayOrderId: orderId, status: { $ne: 'paid' } },
                    {
                        $set: {
                            status: 'paid',
                            paidAt: new Date(),
                            razorpayPaymentId: entity.id,
                            paymentMethod: 'razorpay',
                            fineAmount: fine,
                            receiptNumber,
                        },
                    },
                );
            }
        }

        return { received: true };
    }

    /* =====================
       Receipt Verification
    ===================== */

    async verifyReceipt(receiptNumber: string) {
        const assignment = await this.assignmentModel.findOne(
            { receiptNumber },
            { studentId: 1, routeName: 1, installment: 1, amount: 1, fineAmount: 1, status: 1, paidAt: 1, paymentMethod: 1, receiptNumber: 1, pickupPoint: 1 },
        ).lean();
        if (!assignment) return null;

        const student = await this.studentModel.findById(
            assignment.studentId,
            { name: 1, uniqueId: 1, rollNumber: 1 },
        ).lean();

        return {
            receiptNumber: assignment.receiptNumber,
            studentName: student?.name || 'Unknown',
            studentId: (student as any)?.uniqueId || '',
            rollNumber: student?.rollNumber || '',
            feeName: `Transport — ${assignment.routeName}`,
            installment: assignment.installment,
            amount: assignment.amount,
            fineAmount: assignment.fineAmount || 0,
            totalPaid: assignment.amount + (assignment.fineAmount || 0),
            status: assignment.status,
            paidAt: assignment.paidAt,
            paymentMethod: assignment.paymentMethod,
        };
    }

    /* =====================
       Helpers
    ===================== */

    private computeFine(lateFine: any, daysLate: number, baseAmount: number): number {
        if (!lateFine?.enabled || daysLate <= 0) return 0;

        const fineType = lateFine.type || 'per_day';
        switch (fineType) {
            case 'percentage':
                return Math.round(baseAmount * (lateFine.percentage || 0) / 100);
            case 'day_wise_slab': {
                const slabs = lateFine.slabs || [];
                for (const slab of slabs) {
                    if (daysLate >= slab.startDay && daysLate <= slab.endDay) {
                        return Math.round(slab.amount);
                    }
                }
                if (slabs.length > 0) return Math.round(slabs[slabs.length - 1].amount);
                return 0;
            }
            case 'per_day':
            default:
                return Math.round(daysLate * (lateFine.amountPerDay || 0));
        }
    }

    private async calculateFineFromRoute(assignment: any): Promise<number> {
        if (assignment.status === 'paid') return assignment.fineAmount || 0;
        if (assignment.isActive === false) return 0;

        const dueDate = new Date(assignment.dueDate);
        const now = new Date();
        if (now <= dueDate) return 0;
        if (assignment.createdAt && new Date(assignment.createdAt) > dueDate) return 0;

        const route = await this.routeModel.findById(assignment.routeId, { lateFine: 1 }).lean();
        if (!route?.lateFine?.enabled) return 0;

        const diffMs = now.getTime() - dueDate.getTime();
        const daysLate = Math.floor(diffMs / (1000 * 60 * 60 * 24));
        return this.computeFine(route.lateFine, daysLate, assignment.amount);
    }

    private async recalculateFinesWithRoutes(assignments: any[]): Promise<any[]> {
        if (assignments.length === 0) return assignments;

        const routeIds = [...new Set(assignments.map((a) => a.routeId))];
        const routes = await this.routeModel.find({ _id: { $in: routeIds } }, { lateFine: 1 }).lean();
        const routeMap = new Map(routes.map((r) => [String(r._id), r]));

        const now = new Date();
        return assignments.map((a) => {
            if (a.status === 'paid') return a;

            // Inactive assignments: no fine accumulation, keep as pending
            if (a.isActive === false) return { ...a, fineAmount: 0, status: 'pending' as const };

            const dueDate = new Date(a.dueDate);
            if (now <= dueDate) return { ...a, fineAmount: 0, status: 'pending' as const };

            // Past due — mark as overdue
            const createdAt = a.createdAt ? new Date(a.createdAt) : null;
            // If created after due date, skip fine (retroactive assignment) but still mark overdue
            if (createdAt && createdAt > dueDate) return { ...a, fineAmount: 0, status: 'overdue' as const };

            const route = routeMap.get(String(a.routeId));
            const diffMs = now.getTime() - dueDate.getTime();
            const daysLate = Math.floor(diffMs / (1000 * 60 * 60 * 24));
            const fine = route?.lateFine?.enabled
                ? this.computeFine(route.lateFine, daysLate, a.amount)
                : 0;
            return { ...a, fineAmount: fine, status: 'overdue' as const };
        });
    }

    private async generateReceiptNumber(): Promise<string> {
        const year = new Date().getFullYear();
        const prefix = `TRCPT-${year}-`;

        for (let attempt = 0; attempt < 3; attempt++) {
            const count = await this.assignmentModel.countDocuments({
                receiptNumber: { $regex: `^${prefix}\\d+$` },
            });

            const seq = count + 1 + attempt;
            const receiptNumber = `${prefix}${String(seq).padStart(6, '0')}`;
            const exists = await this.assignmentModel.exists({ receiptNumber });
            if (!exists) return receiptNumber;
        }

        return `${prefix}${crypto.randomBytes(4).toString('hex').toUpperCase()}`;
    }
}
