<?php

namespace App\Controllers\View;

require_once APPPATH . 'ThirdParty/fpdf/fpdf.php';

use App\Models\InvoiceModel;
use App\Models\InvoiceStudentListModel;
use App\Models\StudentModel;
use App\Models\EnrollmentModel;
use App\Models\StudentClassModel;
use App\Models\ConfigurationModel;
use App\Models\EventChargesModel;
use App\Models\UserModel;
use App\Models\AdditionalChargeModel;
use App\Models\PaymentModel;
use App\Models\InvoiceEventModel;
use App\Models\ClassSectionModel;
use App\Models\DiscountUsageModel;
use App\Models\RefundModel;
use DateTime;
use DateTimeZone;


use CodeIgniter\RESTful\ResourceController;

class InvoiceController extends ResourceController
{
    protected $invoiceModel;
    protected $additionalChargeModel;
    protected $studentModel;
    protected $enrollmentModel;
    protected $invoicestudentModel;
    protected $schoolYear;  // Declare global variable for the controller
    protected $semester;
    protected $dueDate;
    protected $db;
    protected $configModel;
    protected $userModel;
    protected $studentClassModel;
    protected $firstStudentFee;
    protected $secondStudentFee;
    protected $youthFee;
    protected $refundDeadline;
    protected $invoiceEventModel;
    protected $paymentModel;
    protected $chargesModel;
    protected $discountUsageModel;
    protected $refundModel;
    protected $request;
    protected $gradeFee;
    protected $classSectionModel;

    public function __construct()
    {
        $this->additionalChargeModel = new AdditionalChargeModel();
        $this->classSectionModel = new ClassSectionModel();
        $this->invoiceModel = new InvoiceModel();
        $this->invoicestudentModel = new InvoiceStudentListModel();
        $this->studentModel = new StudentModel();
        $this->enrollmentModel = new EnrollmentModel();
        $this->configModel = new ConfigurationModel();
        $this->userModel = new UserModel();
        $this->studentClassModel = new StudentClassModel();
        $this->invoiceEventModel = new InvoiceEventModel();
        $this->paymentModel = new PaymentModel();
        $this->chargesModel = new EventChargesModel();
        $this->discountUsageModel = new DiscountUsageModel();
        $this->refundModel = new RefundModel();
        $this->db = \Config\Database::connect();
        $this->request = \Config\Services::request();

        $this->gradeFee = $this->configModel->getConfig('grade_fee');
        $this->schoolYear = $this->currentSchoolYearName();
        $this->semester = $this->configModel->getConfig('semester');
        $this->dueDate = $this->configModel->getConfig('due_date');
        $this->firstStudentFee = (float) ($this->configModel->getConfig('first_student_fee') ?? 350);
        $this->secondStudentFee = (float) ($this->configModel->getConfig('second_student_fee') ?? 200);
        $this->youthFee = (float) ($this->configModel->getConfig('youth_fee') ?? 200);
        $this->refundDeadline = date('Y-m-d', strtotime($this->configModel->getConfig('refund_deadline')));
    }

    public function index($schoolYear = null)
    {
        // Get school years from invoices
        $schoolYears = $this->invoiceModel
            ->select('school_year')
            ->distinct()
            ->orderBy('school_year', 'DESC')
            ->findAll();

        // Default school year
        if (!$schoolYear) {
            $schoolYear = $this->schoolYear;
        }

        log_message('info', "Selected school year for invoice retrieval: $schoolYear");

        $invoiceData = [];
        $parents = $this->userModel->getUsersByRoleAndSchoolYear('parent', $schoolYear);

        foreach ($parents as $parent) {
            $students = $this->studentModel->where('parent_id', $parent['id'])->findAll();

            $parentData = [
                'parent_name' => $parent['firstname'] . ' ' . $parent['lastname'],
                'parent_id' => $parent['id'],
                'enrolledKids' => [],
                'withdrawnKids' => [],
                'invoice_amount' => 0,
                'refund_amount' => 0, // default
                'last_updated' => null,
                'invoice_date' => null
            ];

            // Fetch most recent invoice
            $invoices = $this->invoiceModel->getInvoicesByParentId($parent['id'], $schoolYear);
            foreach ($invoices as $invoice) {
                if ($invoice) {
                    $parentData['invoice_amount'] = $invoice['total_amount'];
                    $parentData['last_updated'] = $invoice['updated_at'];
                    // Prefer issue_date (UTC) and render in configured/user local time for display
                    $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
                    $parentData['invoice_date'] = !empty($invoice['issue_date'])
                        ? (new \DateTimeImmutable($invoice['issue_date'], new \DateTimeZone('UTC')))
                            ->setTimezone(new \DateTimeZone($tzName))
                            ->format('Y-m-d H:i:s')
                        : ($invoice['updated_at'] ?? null);
                    $parentData['invoice_id'] = $invoice['id'];

                    // ✅ Fetch refund amount actually PAID this year (Partial/Paid)
                    $refund = $this->db->table('refunds')
                        ->select('COALESCE(SUM(refund_paid_amount),0) AS refund_paid_amount')
                        ->where('parent_id', $parent['id'])
                        ->where('school_year', $schoolYear)
                        ->whereIn('status', ['Partial','Paid'])
                        ->get()
                        ->getRowArray();

                    $parentData['refund_amount'] = (float)($refund['refund_paid_amount'] ?? 0.0);

                    log_message('info', "Latest invoice for parent {$parent['firstname']} {$parent['lastname']} in school year $schoolYear: Amount = {$invoice['total_amount']}, Updated at = {$invoice['updated_at']}");
                } else {
                    log_message('error', "No invoice found for parent {$parent['firstname']} {$parent['lastname']} in school year $schoolYear.");
                }
            }

            foreach ($students as $student) {
                $studentClass = $this->db->table('student_class')
                    ->where('student_id', $student['id'])
                    ->where('school_year', $schoolYear)
                    ->get()->getRowArray();

                $grade = 'N/A';
                if ($studentClass && isset($studentClass['class_section_id'])) {
                    $classSection = $this->db->table('classSection')
                        ->where('class_section_id', $studentClass['class_section_id'])
                        ->get()->getRowArray();

                    if ($classSection && isset($classSection['class_section_name'])) {
                        $grade = $classSection['class_section_name'];
                    }
                }

                $enrollments = $this->enrollmentModel
                    ->where('student_id', $student['id'])
                    ->where('school_year', $schoolYear)
                    ->findAll();

                foreach ($enrollments as $enrollment) {
                    $kidData = [
                        'name' => $student['firstname'] . ' ' . $student['lastname'],
                        'grade' => $grade,
                        'tuition_fee' => $enrollment['tuition_fee'] ?? 0
                    ];

                    switch ($enrollment['enrollment_status']) {
                        case 'payment pending':
                        case 'enrolled':
                            $parentData['enrolledKids'][] = $kidData;
                            break;
                        case 'withdraw under review':
                        case 'withdrawn':
                        case 'refund pending':
                            $parentData['withdrawnKids'][] = $kidData;
                            break;
                        case 'admission under review':
                            log_message('info', "Student ID {$student['id']} is under admission review and not included in the invoice.");
                            break;
                        case 'waitlist':
                            log_message('info', "Student ID {$student['id']} is in waitlist and not included in the invoice.");
                            break;
                        case 'denied':
                            log_message('info', "Student ID {$student['id']} is denied and not included in the invoice.");
                            break;
                        default:
                            log_message('error', "Unexpected enrollment status '{$enrollment['enrollment_status']}' for student ID {$student['id']}.");
                            break;
                    }
                }
            }

            if (!empty($parentData['enrolledKids']) || !empty($parentData['withdrawnKids'])) {
                $invoiceData[] = $parentData;
            }
        }

        return view('invoice_payment/invoice_management', [
            'invoices' => $invoiceData,
            'schoolYears' => $schoolYears,
            'selectedYear' => $schoolYear,
            'parents' => array_column($invoiceData, 'parent_name')
        ]);
    }

    private function currentSchoolYearName(): string
    {
        try {
            return service('schoolYearContext')->resolve($this->request)->yearName();
        } catch (\Throwable $e) {
            return (string) ($this->configModel->getConfig('school_year') ?? '');
        }
    }

    /**
     * API: Invoice management composite data (used by invoice_management view)
     * Returns the same structure previously rendered server-side in index().
     */
    public function managementData()
    {
        $schoolYear = trim((string)($this->request->getGet('schoolYear') ?? $this->request->getGet('year') ?? ''));
        if ($schoolYear === '') {
            $schoolYear = $this->currentSchoolYearName();
        }

        $invoiceData = [];
        try {
            // Distinct school years (for selector)
            $yearsRows = $this->invoiceModel
                ->select('school_year')
                ->distinct()
                ->orderBy('school_year', 'DESC')
                ->findAll();
            $schoolYears = array_values(array_filter(array_map(static function ($r) {
                return isset($r['school_year']) ? (string)$r['school_year'] : null;
            }, $yearsRows)));
            if (empty($schoolYears)) {
                $schoolYears = [$this->schoolYear];
            }

            $parents = $this->userModel->getUsersByRoleAndSchoolYear('parent', $schoolYear);

            foreach ($parents as $parent) {
                $students = $this->studentModel->where('parent_id', $parent['id'])->findAll();

                $parentData = [
                    'parent_name'   => trim(($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? '')),
                    'parent_id'     => (int)$parent['id'],
                    'enrolledKids'  => [],
                    'withdrawnKids' => [],
                    'invoice_amount'=> 0,
                    'refund_amount' => 0,
                    'last_updated'  => null,
                    'invoice_date'  => null,
                    'invoice_id'    => null,
                ];

                // Latest invoice
                $invoices = $this->invoiceModel->getInvoicesByParentId($parent['id'], $schoolYear);
                foreach ($invoices as $invoice) {
                    if ($invoice) {
                        $parentData['invoice_amount'] = (float)($invoice['total_amount'] ?? 0);
                        $parentData['last_updated']   = $invoice['updated_at'] ?? null;
                        // Prefer issue_date (UTC) -> local; fall back to updated_at/created_at
                        if (!empty($invoice['issue_date'])) {
                            $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
                            $parentData['invoice_date'] = (new \DateTimeImmutable($invoice['issue_date'], new \DateTimeZone('UTC')))
                                ->setTimezone(new \DateTimeZone($tzName))
                                ->format('Y-m-d H:i:s');
                        } else {
                            $parentData['invoice_date'] = date('Y-m-d H:i:s', strtotime($invoice['updated_at'] ?? $invoice['created_at'] ?? 'now'));
                        }
                        $parentData['invoice_id']     = $invoice['id'] ?? null;

                        // Refund total paid for parent/year (Partial/Paid)
                        $refund = $this->db->table('refunds')
                            ->select('COALESCE(SUM(refund_paid_amount),0) AS refund_paid_amount')
                            ->where('parent_id', $parent['id'])
                            ->where('school_year', $schoolYear)
                            ->whereIn('status', ['Partial','Paid'])
                            ->get()->getRowArray();
                        $parentData['refund_amount'] = (float)($refund['refund_paid_amount'] ?? 0.0);
                        break; // only most recent as before
                    }
                }

                // Build kids lists based on enrollment statuses
                foreach ($students as $student) {
                    $studentClass = $this->db->table('student_class')
                        ->where('student_id', $student['id'])
                        ->where('school_year', $schoolYear)
                        ->get()->getRowArray();

                    $grade = 'N/A';
                    if ($studentClass && isset($studentClass['class_section_id'])) {
                        $classSection = $this->db->table('classSection')
                            ->where('class_section_id', $studentClass['class_section_id'])
                            ->get()->getRowArray();
                        if ($classSection && isset($classSection['class_section_name'])) {
                            $grade = $classSection['class_section_name'];
                        }
                    }

                    $enrollments = $this->enrollmentModel
                        ->where('student_id', $student['id'])
                        ->where('school_year', $schoolYear)
                        ->findAll();

                    foreach ($enrollments as $enrollment) {
                        $kid = [
                            'name'        => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')),
                            'grade'       => $grade,
                            'tuition_fee' => (float)($enrollment['tuition_fee'] ?? 0),
                        ];
                        switch ($enrollment['enrollment_status']) {
                            case 'payment pending':
                            case 'enrolled':
                                $parentData['enrolledKids'][] = $kid;
                                break;
                            case 'withdraw under review':
                            case 'withdrawn':
                            case 'refund pending':
                                $parentData['withdrawnKids'][] = $kid;
                                break;
                            default:
                                // ignore others for invoice summary
                                break;
                        }
                    }
                }

                if (!empty($parentData['enrolledKids']) || !empty($parentData['withdrawnKids'])) {
                    $invoiceData[] = $parentData;
                }
            }

            return $this->response->setJSON([
                'ok'           => true,
                'schoolYear'   => $schoolYear,
                'schoolYears'  => $schoolYears,
                'invoices'     => $invoiceData,
            ]);
        } catch (\Throwable $e) {
            log_message('error', 'managementData error: ' . $e->getMessage());
            return $this->response->setStatusCode(500)->setJSON(['ok' => false, 'error' => 'Server error']);
        }
    }


    private function hasClassAssignment(string $schoolYear, string $parentId): bool
    {
        //$parentId = session()->get('user_id');

        if (!$parentId) {
            return false;
        }


        // Get all students for the parent
        $students = $this->studentModel->where('parent_id', $parentId)->findAll();

        if (empty($students)) {
            return false;
        }

        // Check if at least one student has a class assignment
        foreach ($students as $student) {
            $studentId = $student['id'];

            $exists = $this->studentClassModel
                ->where('student_id', $studentId)
                ->where('school_year', $schoolYear)
                ->first();

            if ($exists) {
                return true;
            }
        }

        return false;
    }

    public function generateInvoice(
        string $parentId = null,
        ?string $schoolYearOverride = null,
        ?string $semesterOverride = null,
        bool $recalculateDiscounts = true
    )
    {
        $isAjax = $this->request->isAJAX() || str_contains(strtolower($this->request->getHeaderLine('Accept')), 'application/json');
        if ($parentId == null) {
            $parentId = (int)$this->request->getPost('parent_id');
        }
        $schoolYear = (string) ($schoolYearOverride ?: $this->schoolYear);
        $semester = (string) ($semesterOverride ?: $this->semester);

        // Fetch enrolled + withdrawn students
        $enrollments = $this->enrollmentModel
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear)
            ->findAll();

        if (empty($enrollments)) {
            if ($isAjax) {
                return $this->response->setJSON(['ok' => false, 'message' => 'No enrollment records found.']);
            }
            return redirect()->back()->with('error', 'No enrollment records found.');
        }

        $registeredKids = [];
        $withdrawnKids = [];

        foreach ($enrollments as $enrollment) {
            $studentData = [
                'student_id'        => $enrollment['student_id'],
                'parent_id'         => $enrollment['parent_id'],
                'class_section_id'  => $enrollment['class_section_id'],
                'enrollment_status' => $enrollment['enrollment_status'],
                'school_year'       => $enrollment['school_year'],
                'semester'          => $enrollment['semester'],
                'admission_status'  => $enrollment['admission_status'],
                'is_withdrawn'      => $enrollment['is_withdrawn']
            ];

            // Treat 'payment pending' and 'enrolled' as billable regardless of admission_status,
            // since admin mapping should have already normalized this to 'accepted'.
            // This makes the calculation resilient to legacy data.
            if (in_array($enrollment['enrollment_status'], ['enrolled', 'payment pending'], true)) {
                $registeredKids[] = $studentData;
            } elseif (in_array($enrollment['enrollment_status'], ['withdrawn', 'refund pending', 'withdraw under review'])) {
                $withdrawnKids[] = $studentData;
            } else {
                log_message('info', "Enrollment skipped for student_id {$enrollment['student_id']} with status: {$enrollment['enrollment_status']} and admission_status: {$enrollment['admission_status']}");
            }
        }

        $registeredKids = array_values(array_filter($registeredKids, function ($student) use ($schoolYear) {
            $sid = (int)($student['student_id'] ?? 0);
            return $sid > 0 && $this->studentClassModel->hasNonEventAssignment($sid, $schoolYear);
        }));

        $withdrawnKids = array_values(array_filter($withdrawnKids, function ($student) use ($schoolYear) {
            $sid = (int)($student['student_id'] ?? 0);
            return $sid > 0 && $this->studentClassModel->hasNonEventAssignment($sid, $schoolYear);
        }));

        // ✅ Use your helper to calculate tuition fee
        $fees = $this->calculateTuitionFee($registeredKids, $withdrawnKids);
        $tuitionFee = $fees['tuition_fee'];

        // ✅ Fetch event charges
        $eventsList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear);
        $eventchargeTotal = array_sum(array_column($eventsList, 'charged'));

        $totalDiscount = 0.0;
        if ($recalculateDiscounts) {
            $totalDiscount = $this->recalculateAndUpdateDiscount(
                $parentId,
                $schoolYear,
                $tuitionFee,
                $enrollments
            );
        }

        // ✅ Refunds PAID to the parent for this year (Partial/Paid)
        $refundPaid = (float) $this->refundModel->getTotalApprovedRefundByParentIdAndSchoolYear($parentId, $schoolYear);

        $totalPaid = $this->paymentModel->getTotalPaidByParentId($parentId, $schoolYear);

        $discountedTuition = max(0, $tuitionFee);
        $totalAmount = $discountedTuition + $eventchargeTotal;

        // Parent-level balance (informational); we will recalc per invoice below
        $parentBalance = $totalAmount  // original charges (tuition + events)
            - $totalDiscount           // any applied discounts/vouchers
            - $refundPaid              // approved refunds paid to parent
            - $totalPaid;              // payments received

        // Business rule: single invoice per parent per school year.
        // If legacy duplicates exist, prefer the invoice that already has a discount applied,
        // otherwise use the latest invoice for the parent/year.
        $invoice = $this->selectActiveInvoiceForParentYear((int)$parentId, $schoolYear);

        $updated = false;
        $updatedIds = [];
        if (!empty($invoice) && isset($invoice['id'])) {
            $paymentExclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
            $paymentsHasStatus = false;
            $paymentsHasVoid = false;
            try {
                $paymentsHasStatus = $this->db->fieldExists('status', 'payments');
                $paymentsHasVoid = $this->db->fieldExists('is_void', 'payments');
            } catch (\Throwable $e) {
            }

                // Preserve applied additional charges and recalc this invoice only
                $extrasSum = 0.0;
                try {
                    $rows = $this->db->table('additional_charges')
                        ->select('charge_type, amount')
                        ->where('invoice_id', (int)$invoice['id'])
                        ->where('school_year', $schoolYear)
                        ->where('status', 'applied')
                        ->get()->getResultArray();
                    foreach ($rows as $r) {
                        $amt  = (float)($r['amount'] ?? 0);
                        $typ  = strtolower((string)($r['charge_type'] ?? 'add'));
                        if ($typ === 'deduct') $amt = -abs($amt); else $amt = abs($amt);
                        $extrasSum += $amt;
                    }
                } catch (\Throwable $e) {
                    log_message('error', 'additional_charges sum failed for invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
                }

                $newTotal   = round($totalAmount + $extrasSum, 2);

                // Per-invoice discount
                $invDiscount = 0.0;
                try {
                    $d = $this->db->table('discount_usages')
                        ->select('COALESCE(SUM(discount_amount),0) AS tot')
                        ->where('invoice_id', (int)$invoice['id'])
                        ->get()->getRowArray();
                    $invDiscount = (float)($d['tot'] ?? 0.0);
                } catch (\Throwable $e) {
                    log_message('error', 'discount sum failed for invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
                }

                // Per-invoice refunds paid
                $invRefunds = 0.0;
                try {
                    $r = $this->db->table('refunds')
                        ->select('COALESCE(SUM(refund_paid_amount),0) AS tot')
                        ->where('invoice_id', (int)$invoice['id'])
                        ->where('school_year', $schoolYear)
                        ->whereIn('status', ['Partial','Paid'])
                        ->get()->getRowArray();
                    $invRefunds = (float)($r['tot'] ?? 0.0);
                } catch (\Throwable $e) {
                    log_message('error', 'refund sum failed for invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
                }

                // Payments recorded on this invoice (sum payments to avoid stale invoice.paid_amount)
                $paidOnInv = 0.0;
                try {
                    $qb = $this->db->table('payments')
                        ->select('COALESCE(SUM(paid_amount),0) AS tot')
                        ->where('invoice_id', (int)$invoice['id'])
                        ->where('paid_amount >', 0);

                    if ($paymentsHasStatus) {
                        $qb->groupStart()
                            ->whereNotIn('status', $paymentExclude)
                            ->orWhere('status IS NULL', null, false)
                            ->groupEnd();
                    }
                    if ($paymentsHasVoid) {
                        $qb->groupStart()
                            ->where('is_void', 0)
                            ->orWhere('is_void IS NULL', null, false)
                            ->groupEnd();
                    }

                    $row = $qb->get()->getRowArray();
                    $paidOnInv = (float)($row['tot'] ?? 0.0);
                } catch (\Throwable $e) {
                    log_message('error', 'payment sum failed for invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
                    $paidOnInv = (float)($invoice['paid_amount'] ?? 0.0);
                }

                $newBalance = $newTotal - $invDiscount - $invRefunds - $paidOnInv;
                $newStatus  = ($newBalance <= 0.00001)
                    ? 'Paid'
                    : (($paidOnInv > 0) ? 'Partially Paid' : 'Unpaid');

                $this->invoiceModel->update($invoice['id'], [
                    'total_amount' => $newTotal,
                    'paid_amount'  => $paidOnInv,
                    'balance'      => $newBalance,
                'status'       => $newStatus,
                    'updated_at'   => utc_now()
                ]);

                $updatedIds[] = (int)$invoice['id'];
                log_message('info', "Updated invoice ID {$invoice['id']} for parent ID {$parentId}.");
                $updated = true;
        } else {
            // Generate invoice number
            $schoolId = $this->userModel->getSchoolIdByUserId($parentId);
            if (!empty($schoolId)) {
                $invoiceNumber = 'INV-' . $schoolId . '-' . uniqid();
            } else {
                log_message('warning', "No school ID found for parent_id {$parentId}, generating fallback invoice number.");
                $invoiceNumber = uniqid('INV-');
            }

            $issueUtc = (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');

            // Due date: interpret the date in configured/user local TZ,
            // set a default time (noon to avoid DST edge cases), then convert to UTC.
            $dueUtc = null;
            if (!empty($this->dueDate)) {
                $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
                $dueLocal = new DateTime($this->dueDate . ' 19:59:59', new DateTimeZone($tzName));
                $dueLocal->setTimezone(new DateTimeZone('UTC'));
                $dueUtc = $dueLocal->format('Y-m-d H:i:s');
            }

            $insertId = $this->invoiceModel->insert([
                'parent_id'      => $parentId,
                'invoice_number' => $invoiceNumber,
                'total_amount'   => $totalAmount,
                'paid_amount'    => 0,
                // Initial balance equals the created total; discounts/refunds/payments will adjust later
                'balance'        => $totalAmount,
                'status'         => 'Unpaid',
                'school_year'    => $schoolYear,
                'semester'       => $semester,
                'issue_date'     => $issueUtc,
                'due_date'       => $dueUtc,
                'created_at'     => utc_now(),
                'updated_at'     => utc_now()
            ]);

            if (!$insertId) {
                log_message('error', 'Invoice insert failed: ' . json_encode($this->invoiceModel->errors()));
                if ($isAjax) {
                    return $this->response->setJSON(['ok' => false, 'message' => 'Failed to create invoice.']);
                }
                return redirect()->back()->with('error', 'Failed to create invoice. Please check input values.');
            } else {
                log_message('info', "Invoice created successfully. Insert ID: {$insertId}");
            }
            $updated = false;
        }

        // Success response
        if ($isAjax) {
            return $this->response->setJSON([
                'ok'      => true,
                'updated' => $updated,
                'updated_ids' => $updatedIds,
                'insert_id'   => isset($insertId) ? (int)$insertId : null,
                csrf_token()  => csrf_hash(),
                'csrfTokenName' => csrf_token(),
                'csrfHash'      => csrf_hash(),
            ]);
        }

        return redirect()->to(route_to('InvoiceController::index'))
            ->with('success', $updated ? 'Invoice updated.' : 'Invoice created.');
    }

    private function selectActiveInvoiceForParentYear(int $parentId, string $schoolYear): ?array
    {
        if ($parentId <= 0 || $schoolYear === '') {
            return null;
        }

        // Prefer invoices flagged as having discounts.
        $invoice = $this->invoiceModel
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear)
            ->where('has_discount', 1)
            ->orderBy('id', 'DESC')
            ->first();
        if (!empty($invoice)) {
            return $invoice;
        }

        // Fallback: prefer invoices with discount_usages rows.
        try {
            $row = $this->db->table('invoices i')
                ->select('i.*')
                ->join('discount_usages du', 'du.invoice_id = i.id', 'inner')
                ->where('i.parent_id', $parentId)
                ->where('i.school_year', $schoolYear)
                ->orderBy('i.id', 'DESC')
                ->get()
                ->getRowArray();
            if (!empty($row)) {
                return $row;
            }
        } catch (\Throwable $e) {
        }

        // Final fallback: latest invoice for parent/year.
        return $this->invoiceModel
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear)
            ->orderBy('id', 'DESC')
            ->first();
    }

    private function recalculateAndUpdateDiscount(
        int $parentId,
        string $schoolYear,
        float $tuitionFee,
        array $enrollments
    ): float {
        $totalDiscount = 0.00;
        $eventchargeTotal = 0.0;
        try {
            $eventsList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear);
            $eventchargeTotal = array_sum(array_column($eventsList, 'charged'));
        } catch (\Throwable $e) {
            log_message('error', 'Failed to load event charges for discount recalculation: ' . $e->getMessage());
        }

        // Get all invoices for this parent and school year
        $invoices = $this->invoiceModel->getInvoicesByParentId($parentId, $schoolYear);

        if (empty($invoices)) {
            log_message('info', "No invoices found for parent ID $parentId in school year $schoolYear.");
            return 0.00;
        }

        foreach ($invoices as $invoice) {
            if (!isset($invoice['id'])) {
                continue;
            }

            $invoiceId = $invoice['id'];

            // Get discount usage + voucher details
            $discountUsage = $this->db->table('discount_usages du')
                ->select('du.id, dv.id as voucher_id, dv.discount_type, dv.discount_value')
                ->join('discount_vouchers dv', 'du.voucher_id = dv.id')
                ->join('invoices i', 'du.invoice_id = i.id')
                ->where('du.invoice_id', $invoiceId)
                ->where('i.school_year', $schoolYear)
                ->get()
                ->getRowArray();

            if (!$discountUsage) {
                log_message('info', "No discount applied to invoice ID $invoiceId.");
                continue;
            }

            $extrasSum = 0.0;
            try {
                $rows = $this->db->table('additional_charges')
                    ->select('charge_type, amount')
                    ->where('invoice_id', (int)$invoice['id'])
                    ->where('school_year', $schoolYear)
                    ->where('status', 'applied')
                    ->get()->getResultArray();
                foreach ($rows as $r) {
                    $amt  = (float)($r['amount'] ?? 0);
                    $typ  = strtolower((string)($r['charge_type'] ?? 'add'));
                    if ($typ === 'deduct') $amt = -abs($amt); else $amt = abs($amt);
                    $extrasSum += $amt;
                }
            } catch (\Throwable $e) {
                log_message('error', 'additional_charges sum failed for discount recalculation invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
            }

            $baseTotal = round($tuitionFee + $extrasSum, 2);

            // Recalculate discount
            if ($discountUsage['discount_type'] === 'percent') {
                $discountAmount = round(($baseTotal * $discountUsage['discount_value']) / 100, 2);
            } else {
                $discountAmount = min($discountUsage['discount_value'], $baseTotal);
            }

            // Update discount usage
            $this->db->table('discount_usages')
                ->where('id', $discountUsage['id'])
                ->update([
                    'discount_amount' => $discountAmount,
                    'updated_at' => utc_now(),
                    'updated_by' => session()->get('user_id')
                ]);

            $totalDiscount += $discountAmount;

            // Log enrollment summary
            $added = [];
            $withdrawn = [];
            foreach ($enrollments as $e) {
                if (
                    in_array($e['enrollment_status'], ['enrolled', 'payment pending']) &&
                    $e['admission_status'] === 'accepted'
                ) {
                    $added[] = $e['student_id'];
                } elseif (in_array($e['enrollment_status'], ['withdrawn', 'refund pending', 'withdraw under review'])) {
                    $withdrawn[] = $e['student_id'];
                }
            }

            log_message('info', "Recalculated discount for invoice ID $invoiceId: Added students [" . implode(',', $added) . "], Withdrawn students [" . implode(',', $withdrawn) . "]. Discount updated to $discountAmount.");
        }

        return $totalDiscount;
    }


    /**
     * Returns an array:
     * [
     *   'tuition_fee'   => <float>,   // what the parent still owes after any withdrawals
     *   'refund_amount' => <float>,   // what the school should return (0 if none)
     * ]
     */
    private function calculateTuitionFee(array $registeredKids, array $withdrawnKids): array
    {
        // If we are within the refund window, exclude withdrawn kids from billing.
        // After the deadline, treat withdrawn kids as billable (no refunds allowed).
        try {
            $tzName      = (string) (config('School')->attendance['timezone'] ?? user_timezone());
            $tz          = new \DateTimeZone($tzName);
            $today       = new \DateTimeImmutable('today', $tz);
            $deadline    = new \DateTimeImmutable($this->refundDeadline, $tz);
            $refundOk    = $today <= $deadline;
        } catch (\Throwable $e) {
            $refundOk = true; // default conservative
        }

        $tuitionStudents = $refundOk ? $registeredKids : array_merge($registeredKids, $withdrawnKids);

        $tuitionFee = $this->calculateTotalTuitionFee($tuitionStudents);

        log_message('info', 'Total Tuition Fee (refund ' . ($refundOk ? 'allowed' : 'not allowed') . ") = $tuitionFee");

        return [
            'tuition_fee'   => $tuitionFee,
        ];
    }

    private function calculateTotalTuitionFee(array $students): float
    {
        // 1) Normalize each student's grade name (once)
        foreach ($students as &$student) {
            $gradeName       = $this->classSectionModel
                ->getClassSectionNameBySectionId($student['class_section_id']);
            $student['grade'] = strtoupper(trim($gradeName));
        }
        unset($student); // break reference

        // 2) Partition into regular (<= grade 9) vs youth (> grade 9)
        $regularCount = 0;
        $youthCount   = 0;

        foreach ($students as $student) {
            $levelInfo = $this->getGradeLevel($student['grade']);
            $level     = (int) ($levelInfo['level'] ?? 999);

            // Youth if level > $this->gradeFee (e.g., gradeFee = 9)
            if ($level > $this->gradeFee) {
                $youthCount++;
            } else {
                $regularCount++;
            }
        }

        // 3) Calculate totals per your rules
        $total = 0.0;

        // Youth: flat youth fee per student
        $total += $youthCount * $this->youthFee;

        // Regulars: first student full price, others discounted — but only if 2+ regulars
        if ($regularCount >= 2) {
            $total += $this->firstStudentFee;                         // one full
            $total += ($regularCount - 1) * $this->secondStudentFee;  // rest discounted
        } elseif ($regularCount === 1) {
            $total += $this->firstStudentFee; // single regular: no discount even if there are youths
        }
        // if 0 regulars, nothing to add here

        return $total;
    }


    // Method to check and generate an invoice when enrollment status changes
    public function checkAndGenerateInvoice($parentId, $status)
    {
        if ($status == 'payment pending') {
            $this->generateInvoice($parentId);
        } elseif ($status == 'withdrawn' || $status == 'enrolled') {
            $this->generateInvoice($parentId);
        }
    }

    public function generatePdfInvoice($invoiceId)
    {
        $data = $this->prepareInvoiceData($invoiceId);
        if (isset($data['error'])) {
            return $this->generateErrorPdf($data['error']);
        }

        // Fetch discount_amount from discount_usages
        $discountResult = $this->db->table('discount_usages du')
            ->selectSum('du.discount_amount', 'total_discount')
            ->join('invoices i', 'du.invoice_id = i.id')
            ->where('du.invoice_id', $invoiceId)
            ->where('i.school_year', $this->schoolYear)
            ->get()
            ->getRowArray();


        $discountAmount = isset($discountResult['total_discount']) ? (float)$discountResult['total_discount'] : 0.00;

        // Attach discount amount to data
        $data['discount_amount'] = $discountAmount;

        $this->renderPdfInvoice($data);
    }

    // Add this helper method in the same class (once)


    private function prepareInvoiceData($invoiceId)
    {
        $invoice = $this->invoiceModel->find($invoiceId);
        if (!$invoice) {
            return ['error' => "No invoice was generated. Please contact the school administration."];
        }

        $parentId   = $invoice['parent_id'];
        $schoolYear = $invoice['school_year'];

        // --- Payments ---
        $db        = $this->db;
        $table     = $this->paymentModel->table;
        $hasStatus = $db->fieldExists('status', $table);
        $hasVoid   = $db->fieldExists('is_void', $table);
        $exclude   = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];

        $qb = $this->paymentModel
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear)
            ->where('invoice_id', $invoiceId);

        if ($hasStatus) {
            $qb->groupStart()
                ->whereNotIn('status', $exclude)
                ->orWhere('status IS NULL', null, false)
                ->groupEnd();
        }

        if ($hasVoid) {
            $qb->groupStart()
                ->where('is_void', 0)
                ->orWhere('is_void IS NULL', null, false)
                ->groupEnd();
        }

        $payments = $qb->findAll();

        $parent = $this->userModel->find($parentId);
        if (!$parent) {
            return ['error' => "Parent associated with the invoice was not found."];
        }

        $enrollments = $this->enrollmentModel
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear)
            ->findAll();

        if (empty($enrollments)) {
            return ['error' => 'No enrollments found for this invoice.'];
        }

        $registeredKids = [];
        $withdrawnKids  = [];

        foreach ($enrollments as $enrollment) {
            $student = $this->studentModel->find($enrollment['student_id']);
            if (!$student) {
                log_message('error', "Student not found for ID: {$enrollment['student_id']}");
                continue;
            }

            $grade = $this->studentClassModel->getStudentGrade($student['id']);

            $studentData = [
                'student_id'        => $student['id'],
                'student_firstname' => $student['firstname'],
                'student_lastname'  => $student['lastname'],
                'grade'             => $grade,
                'tuition_fee'       => $enrollment['tuition_fee'] ?? 0,
                'enrollment_status' => $enrollment['enrollment_status'],
            ];

            if (in_array($enrollment['enrollment_status'], ['enrolled', 'payment pending'], true)) {
                $registeredKids[] = $studentData;
            } elseif (in_array($enrollment['enrollment_status'], ['withdrawn', 'withdraw under review', 'refund pending'], true)) {
                $withdrawnKids[] = $studentData;
            } else {
                log_message('info', "Skipping student ID {$student['id']} with status {$enrollment['enrollment_status']}");
            }
        }

        $registeredKids = array_values(array_filter($registeredKids, function ($student) use ($schoolYear) {
            $sid = (int)($student['student_id'] ?? 0);
            return $sid > 0 && $this->studentClassModel->hasNonEventAssignment($sid, $schoolYear);
        }));

        $withdrawnKids = array_values(array_filter($withdrawnKids, function ($student) use ($schoolYear) {
            $sid = (int)($student['student_id'] ?? 0);
            return $sid > 0 && $this->studentClassModel->hasNonEventAssignment($sid, $schoolYear);
        }));

        usort($registeredKids, fn($a, $b) => $this->compareGrades($a['grade'], $b['grade']));
        usort($withdrawnKids,  fn($a, $b) => $this->compareGrades($a['grade'], $b['grade']));

        $tzName        = (string) (config('School')->attendance['timezone'] ?? user_timezone());
        $tz            = new \DateTimeZone($tzName);
        $currentDate   = new \DateTimeImmutable('today', $tz);
        $deadline      = new \DateTimeImmutable($this->refundDeadline, $tz);
        $refundAllowed = $currentDate <= $deadline;

        $studentCharges = [];
        $regularCount   = 0;

        /**
         * Computes the fee for a student given numeric level and original grade name.
         * - KG/K/Kindergarten are always treated as "regular".
         * - Otherwise, "regular" means gradeLevel <= $this->gradeFee threshold.
         */
        $computeCharge = function (int $gradeLevel, string $gradeName) use (&$regularCount) {
            $threshold = (int) $this->gradeFee;

            // Force Kindergarten to be regular regardless of numeric mapping
            $isRegular = $this->isKindergarten($gradeName) || ($gradeLevel > 0 && $gradeLevel <= $threshold);

            if ($isRegular) {
                $fee = ($regularCount === 0) ? $this->firstStudentFee : $this->secondStudentFee;
                $regularCount++;
                return $fee;
            }
            return $this->youthFee;
        };

        // Registered kids -> pay unit fee
        foreach ($registeredKids as $student) {
            $gradeName  = (string)$student['grade'];
            $gradeLevel = $this->gradeLevelInt($gradeName);
            $unitFee    = $computeCharge($gradeLevel, $gradeName);
            $studentCharges[$student['student_id']] = ['unit_fee' => $unitFee, 'refund' => 0];
        }

        // Refund NOT allowed -> withdrawn students still owe unit fee
        if (!$refundAllowed) {
            foreach ($withdrawnKids as $student) {
                $gradeName  = (string)$student['grade'];
                $gradeLevel = $this->gradeLevelInt($gradeName);
                $unitFee    = $computeCharge($gradeLevel, $gradeName);
                $studentCharges[$student['student_id']] = ['unit_fee' => $unitFee, 'refund' => 0];
            }
        }

        $eventsList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear);

        // Attach SCHOOL IDs
        $allKids    = array_merge($registeredKids, $withdrawnKids);
        $studentIds = array_column($allKids, 'student_id');

        $schoolIdMap = [];
        if ($studentIds) {
            $rows = $this->studentModel
                ->select('id, school_id')
                ->whereIn('id', $studentIds)
                ->findAll();
            foreach ($rows as $row) {
                $schoolIdMap[(int)$row['id']] = $row['school_id'] ?? 'N/A';
            }
        }

        $students = $allKids;
        foreach ($students as $i => $s) {
            $sid = (int)$s['student_id'];
            $students[$i]['student_school_id'] = $schoolIdMap[$sid] ?? 'N/A';
        }

        $discounts = $this->discountUsageModel
            ->where('invoice_id',  $invoiceId)
            ->where('parent_id',   $parentId)
            ->where('school_year', $schoolYear)
            ->findAll();

        // Refunds PAID for this specific invoice (money returned to the parent)
        $refundsPaidTotal = 0.0;
        try {
            $r = $this->db->table('refunds')
                ->select('COALESCE(SUM(refund_paid_amount),0) AS tot')
                ->where('invoice_id', $invoiceId)
                ->where('school_year', $schoolYear)
                ->whereIn('status', ['Partial','Paid'])
                ->get()->getRowArray();
            $refundsPaidTotal = (float)($r['tot'] ?? 0.0);
        } catch (\Throwable $e) {
            log_message('error', 'Failed to sum refunds for invoice ' . (int)$invoiceId . ': ' . $e->getMessage());
        }

        /* ============================================================
     * ADDITIONAL CHARGES (itemized) for this invoice
     *  - uses the additional_charges table for line items
     *  - uses invoice.additional_charge as the authoritative total (Strategy B)
     * ============================================================ */
        $acRows = $this->additionalChargeModel
            ->select('id, charge_type, title, description, amount, due_date, status, created_at')
            ->where('invoice_id', $invoiceId)
            ->where('status !=', 'void')
            ->orderBy('created_at', 'ASC')
            ->orderBy('id', 'ASC')
            ->findAll();

        $additionalChargeLines  = [];
        $additionalChargesTotal = 0.0;

        foreach ($acRows as $ac) {
            $signed = (float)($ac['amount'] ?? 0);
            $ctype  = strtolower((string)($ac['charge_type'] ?? ''));

            if (in_array($ctype, ['deduct'], true) && $signed > 0) {
                $signed = -$signed;
            } elseif (in_array($ctype, ['add'], true) && $signed < 0) {
                $signed = abs($signed);
            }

            $lineDate = !empty($ac['created_at'])
                ? date('Y-m-d', strtotime($ac['created_at']))
                : (!empty($invoice['created_at']) ? local_date($invoice['created_at'], 'Y-m-d') : local_date(utc_now(), 'Y-m-d'));

            $typeLabel = in_array($ctype, ['deduct'], true) ? 'Deduct' : 'Add';
            $title     = ''; //trim((string)($ac['title'] ?? 'Additional Charge'));
            $desc      = $typeLabel . ': ';

            if (!empty($ac['description'])) {
                $desc = $ac['description'];
            }

            $additionalChargesTotal += $signed;

            $additionalChargeLines[] = [
                'date'        => $lineDate,
                'description' => $desc,
                'amount'      => $signed,
                'meta'        => [
                    'id'       => (int)$ac['id'],
                    'due_date' => $ac['due_date'] ?? null,
                    'status'   => (string)($ac['status'] ?? ''),
                    'type'     => $ctype,
                ],
            ];
        }

        $additionalChargesTotal = round($additionalChargesTotal, 2);

        return [
            'invoice'                => $invoice,
            'parent'                 => $parent,
            'registeredKids'         => $registeredKids,
            'withdrawnKids'          => $withdrawnKids,
            'studentCharges'         => $studentCharges,
            'events'                 => $eventsList,
            'students'               => $students,
            'payments'               => $payments,
            'discounts'              => $discounts,
            'additionalChargesTotal' => $additionalChargesTotal,
            'additionalChargeLines'  => $additionalChargeLines,
            'refundsPaidTotal'       => $refundsPaidTotal,
        ];
    }


    /**
     * Treat common KG spellings as Kindergarten.
     */
    private function isKindergarten(string $grade): bool
    {
        $g = strtolower(trim($grade));
        return in_array($g, ['KG', 'kg', 'k', 'kindergarten', 'k-g', 'k.g', 'k g'], true);
    }


    private function renderPdfInvoice($data)
    {
        // Unpack prepared data
        extract($data);

        // $data keys expected from prepareInvoiceData():
        // - invoice, parent, registeredKids, withdrawnKids, studentCharges, events, students, payments, discounts
        // - additionalChargeLines, additionalChargesTotal

        $pdf = new class extends \FPDF {
            function Footer()
            {
                $this->SetY(-15);
                $this->SetFont('Arial', 'I', 8);
                $this->Cell(0, 10, 'Page ' . $this->PageNo() . '/{nb}', 0, 0, 'C');
            }
        };

        $pdf->AliasNbPages();
        $pdf->AddPage();

        // --- Column widths
        $dateCell    = 30;
        $descripCell = 130;
        $amountCell  = 30;

        // --- Header section
        if (defined('FCPATH')) {
            @$pdf->Image(FCPATH . 'images/logo.png',       170, 8, 30);
            @$pdf->Image(FCPATH . 'images/Isgl_logo.png',   10, 8, 30);
        }
        $pdf->Ln(25);
        $pdf->SetFont('Arial', 'B', 18);
        $pdf->Cell(0, 10, 'Al Rahma Sunday School', 0, 1, 'C');
        $pdf->SetFont('Arial', 'B', 18);
        $pdf->Cell(0, 10, 'Invoice', 0, 1, 'C');
        $pdf->Ln(15);

        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell(40, 6, 'Parent Name:', 0, 0, 'L');
        $pdf->SetFont('Arial', '', 12);
        $pdf->Cell(0, 6, ($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? ''), 0, 1, 'L');

        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell(40, 6, 'Invoice Date:', 0, 0, 'L');
        $pdf->SetFont('Arial', '', 12);
        $issueLocal = null;
        try {
            $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
            if (!empty($invoice['issue_date'])) {
                $issueLocal = (new \DateTimeImmutable($invoice['issue_date'], new \DateTimeZone('UTC')))
                    ->setTimezone(new \DateTimeZone($tzName));
            } elseif (!empty($invoice['created_at'])) {
                $issueLocal = new \DateTimeImmutable($invoice['created_at'], new \DateTimeZone($tzName));
            }
        } catch (\Throwable $e) {}
        if (!$issueLocal) { $issueLocal = new \DateTimeImmutable('now', new \DateTimeZone($tzName ?? 'UTC')); }
        $pdf->Cell(0, 6, $issueLocal->format('m-d-Y'), 0, 1, 'L');

        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell(40, 6, 'Invoice Number:', 0, 0, 'L');
        $pdf->SetFont('Arial', '', 12);
        $pdf->Cell(0, 6, (string)($invoice['invoice_number'] ?? ''), 0, 1, 'L');

        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell(40, 6, 'Due Date:', 0, 0, 'L');
        $pdf->SetFont('Arial', '', 12);
        $dueLocal = null;
        try {
            $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
            if (!empty($invoice['due_date'])) {
                $dueLocal = (new \DateTimeImmutable($invoice['due_date'], new \DateTimeZone('UTC')))
                    ->setTimezone(new \DateTimeZone($tzName));
            } elseif (!empty($invoice['created_at'])) {
                $dueLocal = new \DateTimeImmutable($invoice['created_at'], new \DateTimeZone($tzName));
            }
        } catch (\Throwable $e) {}
        if (!$dueLocal) { $dueLocal = new \DateTimeImmutable('now', new \DateTimeZone($tzName ?? 'UTC')); }
        $pdf->Cell(0, 6, $dueLocal->format('m-d-Y'), 0, 1, 'L');

        $pdf->Ln(5);
        $pdf->SetFont('Arial', '', 9);
        $pdf->MultiCell(0, 5, "Please make the payment by the due date mentioned above. For any queries regarding this invoice, contact the school administration at alrahma.isgl@gmail.com or call +1 978-364-0219.");
        $pdf->Ln(4);

        // --- Table header
        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell($dateCell, 7, 'Date', 1, 0);
        $pdf->Cell($descripCell, 7, 'Description', 1);
        $pdf->Cell($amountCell, 7, 'Amount', 1, 1, 'R');

        // --- Additional charges data presence
        $additionalChargeLines  = $data['additionalChargeLines']  ?? [];
        $additionalChargesTotal = (float)($data['additionalChargesTotal'] ?? 0.0);
        $hasAdditional          = abs($additionalChargesTotal) > 0.00001;

        // --- Helpers for robust date handling & row pushing ---
        $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
        $tz = new \DateTimeZone($tzName);

        /**
         * Normalize a raw date string to configured/user local timezone.
         * @param ?string $raw
         * @param bool $assumeUtc If true, treat $raw as UTC and convert; if false, parse as local.
         */
        $toLocal = function (?string $raw, bool $assumeUtc = false) use ($tz): \DateTimeImmutable {
            $fallback = new \DateTimeImmutable('now', $tz);
            if (!$raw) return $fallback;

            try {
                if ($assumeUtc) {
                    $dt = new \DateTimeImmutable($raw, new \DateTimeZone('UTC'));
                    return $dt->setTimezone($tz);
                }
                // Parse directly with local tz; PHP will interpret Y-m-d and Y-m-d H:i:s fine here
                $dt = new \DateTimeImmutable($raw, $tz);
                return $dt;
            } catch (\Throwable $e) {
                $ts = @strtotime($raw);
                if ($ts === false) return $fallback;
                return (new \DateTimeImmutable('@' . $ts))->setTimezone($tz);
            }
        };

        $transactions  = [];
        $totalPaid     = 0.0;
        $totalDiscount = 0.0;
        $seq = 0; // stable tie-breaker to preserve insertion order for identical timestamps

        $push = function (\DateTimeImmutable $dt, string $desc, float $amount, string $category = 'other') use (&$transactions, &$seq) {
            $transactions[] = [
                'dt'          => $dt,
                'description' => $desc,
                'amount'      => $amount,
                'cat'         => $category,
                'seq'         => $seq++,
            ];
        };

        // --- Tuition/registration lines (charges) ---
        foreach ($registeredKids as $student) {
            $id   = $student['student_id'];
            $unit = (float)($studentCharges[$id]['unit_fee'] ?? 0.0);
            $name = $student['student_firstname'] . ' ' . $student['student_lastname'];

            $classSectionName = $this->classSectionModel->getClassSectionNameByClassId($student['grade']);
            $lowerCaseName = strtolower((string)$classSectionName);
            $gradeName = ($lowerCaseName === 'kg')
                ? 'in Kindergarten '
                : (($lowerCaseName === 'youth') ? 'in Youth ' : ('in Grade ' . $classSectionName));

            $dt = $toLocal($invoice['created_at'] ?? null, false);
            $push($dt, 'Registration of student "' . $name . '" ' . $gradeName, $unit, 'registration');
        }

        // --- Event charges (charges) ---
        foreach ($events as $event) {
            $studentName = 'N/A';
            if (!empty($event['student_id'])) {
                foreach ($students as $st) {
                    if (($st['student_id'] ?? null) == $event['student_id']) {
                        $studentName = $st['student_firstname'] . ' ' . $st['student_lastname'];
                        break;
                    }
                }
            }
            if ($studentName === 'N/A') {
                $externalName = trim((string)($event['external_firstname'] ?? '') . ' ' . (string)($event['external_lastname'] ?? ''));
                if ($externalName !== '') {
                    $studentName = $externalName . ' (external)';
                }
            }

            $dt        = $toLocal($event['created_at'] ?? null, false);
            $amount    = (float)($event['charged'] ?? 0.0);
            $eventName = !empty($event['event_name']) ? $event['event_name'] : 'with no name';

            $push($dt, 'Event ' . $eventName . ' charge for "' . $studentName . '"', $amount, 'event');
        }

        // --- Withdrawn refunds (negative) — only if a non-zero refund figure exists
        foreach ($withdrawnKids as $student) {
            $id   = $student['student_id'];
            $ref  = (float)($studentCharges[$id]['refund'] ?? 0.0);
            if ($ref <= 0) { continue; }
            $name = $student['student_firstname'] . ' ' . $student['student_lastname'];

            $dt = $toLocal($invoice['created_at'] ?? null, false);
            $push($dt, 'Refund for student "' . $name . '"', -1 * $ref, 'refund');
        }

        // --- Payments (negative) — stored in local time
        foreach ($payments as $payment) {
            $dt     = $toLocal($payment['payment_date'] ?? null, false /* local */);
            $amount = (float)($payment['paid_amount'] ?? 0.0);
            $totalPaid += $amount;
            $push($dt, 'Payment (' . ($payment['payment_method'] ?? 'Payment') . ')', -1 * $amount, 'payment');
        }

        // --- Additional charges (already signed: deduct < 0, add > 0)
        foreach ($additionalChargeLines as $l) {
            $dt   = $toLocal($l['date'] ?? null, false);
            $desc = (string)($l['description'] ?? 'Additional Charge');
            $amt  = (float)($l['amount'] ?? 0.0);
            $push($dt, $desc, $amt, 'additional');
        }

        // --- Discounts (negative) integrated into the timeline
        foreach (($discounts ?? []) as $discount) {
            $amt = (float)($discount['discount_amount'] ?? 0.0);
            $totalDiscount += $amt;

            $dt   = $toLocal($discount['used_at'] ?? ($invoice['created_at'] ?? null), false);
            $desc = "Discount applied (Reason: {$discount['description']})";
            $push($dt, $desc, -1 * $amt, 'discount');
        }

        // --- Sort by exact timestamp, then by insertion sequence for stability
        usort($transactions, function ($a, $b) {
            // Different days: keep chronological by timestamp
            $dayA = $a['dt']->format('Y-m-d');
            $dayB = $b['dt']->format('Y-m-d');
            if ($dayA !== $dayB) {
                return $a['dt'] <=> $b['dt'];
            }

            // Same day: ensure desired category priority (registration before payment)
            $pri = [
                'registration' => 10,
                'event'        => 20,
                'additional'   => 25,
                'discount'     => 30,
                'refund'       => 40,
                'payment'      => 90,
                'other'        => 50,
            ];
            $ca = $a['cat'] ?? 'other';
            $cb = $b['cat'] ?? 'other';
            $pa = $pri[$ca] ?? 50;
            $pb = $pri[$cb] ?? 50;
            if ($pa !== $pb) {
                return $pa <=> $pb;
            }

            // Same category: fall back to exact timestamp, then insertion order
            $cmp = $a['dt'] <=> $b['dt'];
            return $cmp !== 0 ? $cmp : ($a['seq'] <=> $b['seq']);
        });

        // --- Render transactions (strict chronological order)
        $pdf->SetFont('Arial', '', 11);
        foreach ($transactions as $t) {
            $pdf->Cell($dateCell,    7, $t['dt']->format('m-d-Y'), 1);
            $pdf->Cell($descripCell, 7, $t['description'],         1);
            $pdf->Cell($amountCell,  7, ($t['amount'] < 0 ? '-$' : '$') . number_format(abs($t['amount']), 2), 1, 1, 'R');
        }

        // ======== SUMMARY (bottom) ========
        // Compute total charges from components (tuition + event charges + additional charges)
        // to ensure the PDF always reflects all elements accurately.
        $tuitionSubtotal = 0.0;
        foreach (($studentCharges ?? []) as $sc) {
            $tuitionSubtotal += (float)($sc['unit_fee'] ?? 0.0);
        }

        $eventSubtotal = 0.0;
        foreach (($events ?? []) as $ev) {
            $eventSubtotal += (float)($ev['charged'] ?? 0.0);
        }

        $additionalSubtotal = (float)($additionalChargesTotal ?? 0.0);

        $totalRefund  = (float)($data['refundsPaidTotal'] ?? 0.0);
        $totalAmount  = round($tuitionSubtotal + $eventSubtotal + $additionalSubtotal, 2);
        $calcBalance  = $totalAmount - $totalPaid - $totalDiscount - $totalRefund;
        // Prefer computed balance in PDF to avoid stale DB values
        $totalBalance = $calcBalance;

        // Display rule: if negative, show as Credit (Overpayment) and clamp balance due to 0.00
        $displayBalance  = $totalBalance;
        $creditOverpay   = 0.0;
        if ($displayBalance < -0.00001) {
            $creditOverpay  = abs($displayBalance);
            $displayBalance = 0.00;
        }

        $pdf->Ln(5);
        $labelWidth  = 165;
        $amountWidth = 25;

        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell($labelWidth, 7, 'Total Charges:', 0, 0, 'R');
        $pdf->SetFont('Arial', '', 12);
        $pdf->Cell($amountWidth, 7, '$' . number_format($totalAmount, 2), 0, 1, 'R');

        // Show Additional Charges subtotal if any (informational)
        if ($hasAdditional) {
            $pdf->SetFont('Arial', 'B', 12);
            $pdf->Cell($labelWidth, 7, 'Additional Charges (included):', 0, 0, 'R');
            $pdf->SetFont('Arial', '', 12);
            $pdf->Cell($amountWidth, 7, '$' . number_format($additionalChargesTotal, 2), 0, 1, 'R');
        }

        if ($totalDiscount > 0.0) {
            $pdf->SetFont('Arial', 'B', 12);
            $pdf->Cell($labelWidth, 7, 'Total Discount:', 0, 0, 'R');
            $pdf->SetFont('Arial', '', 12);
            $pdf->Cell($amountWidth, 7, '$' . number_format($totalDiscount, 2), 0, 1, 'R');
        }

        if ($totalRefund > 0.0) {
            $pdf->SetFont('Arial', 'B', 12);
            $pdf->Cell($labelWidth, 7, 'Refunds Paid:', 0, 0, 'R');
            $pdf->SetFont('Arial', '', 12);
            $pdf->Cell($amountWidth, 7, '$' . number_format($totalRefund, 2), 0, 1, 'R');
        }

        // Show credit if overpaid
        if ($creditOverpay > 0.0) {
            $pdf->SetFont('Arial', 'B', 12);
            $pdf->Cell($labelWidth, 7, 'Credit (Overpayment):', 0, 0, 'R');
            $pdf->SetFont('Arial', '', 12);
            $pdf->Cell($amountWidth, 7, '$' . number_format($creditOverpay, 2), 0, 1, 'R');
        }

        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell($labelWidth, 7, 'Total Paid:', 0, 0, 'R');
        $pdf->SetFont('Arial', '', 12);
        $pdf->Cell($amountWidth, 7, '$' . number_format($totalPaid, 2), 0, 1, 'R');

        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell($labelWidth, 7, 'Balance Due:', 0, 0, 'R');
        $pdf->SetFont('Arial', '', 12);
        $pdf->Cell($amountWidth, 7, '$' . number_format($displayBalance, 2), 0, 1, 'R');

        // --- Output
        header('Content-Type: application/pdf');
        header('Content-Disposition: inline; filename="invoice_' . ($invoice['invoice_number'] ?? 'N/A') . '.pdf"');
        header('Cache-Control: private, max-age=0, must-revalidate');

        $pdf->Output('I', 'invoice_' . ($invoice['invoice_number'] ?? 'N/A') . '.pdf');
        exit;
    }

    // --- Grade helpers ---
    private function compareGrades($gradeA, $gradeB)
    {
        $valA = $this->getGradeLevel($gradeA);
        $valB = $this->getGradeLevel($gradeB);

        // First compare numeric levels
        if ($valA['level'] !== $valB['level']) {
            return $valA['level'] <=> $valB['level'];
        }

        // Same level, compare suffix lexicographically (A < B)
        return strcmp($valA['suffix'] ?? '', $valB['suffix'] ?? '');
    }

    private function gradeLevelInt($grade): int
    {
        $gl = $this->getGradeLevel($grade);
        return (int) ($gl['level'] ?? 0);
    }

    /**
     * Robust grade parser. Returns ['level' => int, 'suffix' => string]
     * Examples:
     *  - 'PK' => -1, 'K'/'Kindergarten' => 0
     *  - '1', '2A', 'Grade 5', 'G5' => 1,2,5
     *  - 'Y', 'Youth', 'Y1', 'Youth 2' => map to > 9 (10, 11, 12 ...)
     */
private function getGradeLevel($grade): array
{
    // Normalize
    if (is_numeric($grade)) {
        $num = (int)$grade;
        if ($num === 13) {
            // KG special case
            return ['level' => 1, 'suffix' => '', 'classId' => 13];
        }
        // If you want, you could also handle Pre-K or Youth by number here
        return ['level' => $num, 'suffix' => '', 'classId' => null];
    }

    if (!is_string($grade)) {
        return ['level' => 999, 'suffix' => '', 'classId' => null];
    }

    $g = strtoupper(trim($grade));
    $g = preg_replace('/\s+/', ' ', $g);
    $g = str_replace(['.', '_'], '', $g);
    $g = str_replace('-', ' ', $g);

    // Kindergarten (string forms)
    $kg = ['K', 'KG', 'K G', 'KINDER', 'KINDERGARTEN'];
    if (in_array($g, $kg, true)) {
        return ['level' => 1, 'suffix' => '', 'classId' => 13];
    }

    // Pre-K
    $pk = ['PK', 'P K', 'PREK', 'PRE K', 'PRE-K', 'PRE KINDER', 'PREKINDER'];
    if (in_array($g, $pk, true)) {
        return ['level' => -1, 'suffix' => '', 'classId' => null];
    }

    // Youth: Y, YOUTH, Y1, YOUTH2, etc.
    if (preg_match('/^Y(?:OUTH)?(?:\s*(\d+))?$/', $g, $m)) {
        $n = isset($m[1]) && $m[1] !== '' ? max(1, (int)$m[1]) : 1;
        return [
            'level'  => (int)$this->gradeFee + $n,
            'suffix' => '',
            'classId'=> null
        ];
    }

    // Numeric grades like "5", "GRADE 5", "G5", "5A"
    if (preg_match('/^(?:GR?ADE\s*)?(\d{1,2})\s*([A-Z]*)$/', $g, $m)) {
        return [
            'level'  => (int) $m[1],
            'suffix' => $m[2] ?? '',
            'classId'=> null
        ];
    }

    return ['level' => 999, 'suffix' => '', 'classId' => null];
}

    private function generateErrorPdf($message)
    {
        // Create a new instance of FPDF
        $pdf = new \FPDF();
        $pdf->AddPage();

        // Set title
        $pdf->SetFont('Arial', 'B', 16);
        $pdf->Cell(0, 10, 'Error', 0, 1, 'C');  // Add the error title centered

        // Set message font and size
        $pdf->SetFont('Arial', '', 12);
        $pdf->Ln(10);  // Add a line break

        // Add the message to the PDF
        $pdf->MultiCell(0, 10, $message);  // Allow the message to wrap in multiple lines

        // Output the PDF
        $pdf->Output('I', 'error.pdf');  // Output PDF inline (browser)
    }

    public function invoicePayment()
    {
        $parentId = session()->get('user_id');

        if (!$parentId) {
            return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
        }

        $currentSchoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
        $selectedYear = $currentSchoolYear;

        // Fetch invoices for the selected year
        $invoices = [];
        if ($selectedYear) {
            $invoices = $this->invoiceModel->getInvoicesByParentId($parentId, $selectedYear);
        }

        // Fetch refund amounts PAID for these invoices
        $invoiceIds = array_column($invoices, 'id');
        $refunds = [];
        if (!empty($invoiceIds)) {
            $refundResults = $this->db->table('refunds')
                ->select('invoice_id, COALESCE(SUM(refund_paid_amount),0) as refund_paid_amount')
                ->whereIn('invoice_id', $invoiceIds)
                ->whereIn('status', ['Partial','Paid'])
                ->groupBy('invoice_id')
                ->get()
                ->getResultArray();

            foreach ($refundResults as $row) {
                $refunds[$row['invoice_id']] = $row['refund_paid_amount'];
            }
        }

        // Fetch last payment data for these invoices
        $lastPayments = [];
        if (!empty($invoiceIds)) {
            $paymentResults = $this->paymentModel->getPaymentsByInvoice($invoiceIds);

            foreach ($paymentResults as $payment) {
                $lastPayments[$payment['invoice_id']] = [
                    'last_paid_amount'  => $payment['last_paid_amount'],
                    'last_payment_date' => $payment['last_payment_date']
                ];
            }
        }

        // Attach refund amount and last payment data to each invoice
        foreach ($invoices as &$invoice) {
            $invoice['refund_amount'] = $refunds[$invoice['id']] ?? 0.00;
            $invoice['last_paid_amount'] = $lastPayments[$invoice['id']]['last_paid_amount'] ?? 0.00;
            $invoice['last_payment_date'] = $lastPayments[$invoice['id']]['last_payment_date'] ?? null;
        }

        $invoiceEventCharges = [];
        foreach ($invoices as $invoice) {
            $year = $invoice['school_year'] ?? $this->schoolYear;
            $sem = $invoice['semester'] ?? $this->semester;
            $invoiceEventCharges[(int)$invoice['id']] = $this->chargesModel->getChargesWithEventInfo(
                $invoice['parent_id'],
                $year,
                $sem
            );
        }

        return view('/parent/invoice_payment', [
            'invoices' => $invoices,
            'selectedYear' => $selectedYear,
            'currentSchoolYear' => $currentSchoolYear,
            'dueDate' => $this->dueDate,
            'invoiceEventCharges' => $invoiceEventCharges,
        ]);
    }

    // API: Create a new invoice
    public function createAPI()
    {
        $data = [
            'parent_id' => $this->request->getPost('parent_id'),
            'registeredKids' => $this->request->getPost('registeredKids'),
            'invoice_number' => $this->request->getPost('invoice_number'),
            'total_amount' => $this->request->getPost('total_amount'),
            'refund_amount' => $this->request->getPost('refund_amount'),
            'balance' => $this->request->getPost('balance'),
            'school_year' => $this->request->getPost('school_year'),
            'issue_date' => $this->request->getPost('issue_date'),
            'refund_issue_date' => $this->request->getPost('refund_issue_date'),
            'due_date' => $this->request->getPost('due_date'),
            'status' => 'Unpaid',
            'description' => $this->request->getPost('description'),
        ];

        if ($this->invoiceModel->save($data)) {
            return $this->respondCreated($data); // Send successful response
        } else {
            return $this->failValidationErrors($this->invoiceModel->errors()); // Return validation errors
        }
    }

    // API: Get invoices by parent ID
    public function getByParentAPI($parentId)
    {
        $invoices = $this->invoiceModel->getInvoicesByUserId($parentId, $this->schoolYear);
        if ($invoices) {
            return $this->respond($invoices); // Send the invoices in the response
        } else {
            return $this->failNotFound('Invoices not found for the parent.');
        }
    }

    // View: Get invoices by parent ID (for web views)
    public function getByParent($parentId)
    {
        $invoices = $this->invoiceModel->getInvoicesByUserId($parentId, $this->schoolYear);
        return view('invoice_list', ['invoices' => $invoices]);
    }

    // View: Create a new invoice
    public function create()
    {
        return view('invoice_create');
    }

    // API: Update invoice status
    public function updateStatusAPI($invoiceId)
    {
        $status = $this->request->getPost('status');
        if ($this->invoiceModel->updateInvoiceStatus($invoiceId, $status)) {
            return $this->respond(['status' => 'success']);
        } else {
            return $this->failNotFound('Invoice not found.');
        }
    }

    // View: Update invoice status
    public function updateStatus($invoiceId)
    {
        $status = $this->request->getPost('status');
        if ($this->invoiceModel->updateInvoiceStatus($invoiceId, $status)) {
            return redirect()->to('/invoices');
        } else {
            return redirect()->back()->with('error', 'Failed to update status.');
        }
    }

    // View: Get unpaid invoices
    public function unpaidInvoices()
    {
        $unpaidInvoices = $this->invoiceModel->getUnpaidInvoices();
        return view('unpaid_invoices', ['invoices' => $unpaidInvoices]);
    }
}
