<?php

namespace App\Controllers\View;

use App\Models\PaymentModel;
use App\Models\AdditionalChargeModel;
use App\Models\ClassSectionModel;
use App\Models\ManualPaymentModel;
use App\Models\EventChargesModel;
use App\Models\UserModel;
use App\Models\StudentModel;
use App\Models\EnrollmentModel;
use App\Models\ConfigurationModel;
use App\Models\StudentClassModel;
use App\Models\PaymentErrorModel;
use App\Models\InvoiceModel;
use App\Models\TeacherClassModel;
use App\Models\DiscountUsageModel;
use Config\PaypalConfig;
use PayPal\Api\Amount;
use PayPal\Api\Payment;
use PayPal\Api\PaymentExecution;
use PayPal\Api\Payer;
use PayPal\Api\Transaction;
use PayPal\Rest\ApiContext;
use PayPal\Auth\OAuthTokenCredential;

use CodeIgniter\RESTful\ResourceController;
use CodeIgniter\Events\Events;

class PaymentController extends ResourceController
{
    protected $paymentModel;
    protected $paypalConfig;
    protected $apiContext;
    protected $request;
    protected $db;
    protected $invoiceModel;
    protected $configModel;
    protected $manualPaymentModel;
    protected $eventChargesModel;
    protected $userModel;
    protected $schoolYear;
    protected $semester;
    protected $enrollmentModel;
    protected $studentModel;
    protected $studentClassModel;
    protected $paymentErrorModel;
    protected $installmentDate;
    protected $teacherClassModel;
    protected $discountUsageModel;
    protected $additionalChargeModel;
    protected $classSectionModel;

    public function __construct()
    {
        $this->teacherClassModel = new TeacherClassModel();
        $this->paymentErrorModel = new PaymentErrorModel();
        $this->userModel = new UserModel();
        $this->eventChargesModel = new EventChargesModel();
        $this->manualPaymentModel = new ManualPaymentModel();
        $this->paymentModel = new PaymentModel();
        $this->paypalConfig = new PaypalConfig();
        $this->request = \Config\Services::request();
        $this->db = \Config\Database::connect();
        $this->invoiceModel = new InvoiceModel();
        $this->configModel = new ConfigurationModel();
        $this->enrollmentModel = new EnrollmentModel();
        $this->studentModel = new StudentModel();
        $this->studentClassModel = new StudentClassModel();
        $this->schoolYear = $this->configModel->getConfig('school_year');
        $this->semester = $this->configModel->getConfig('semester'); //installment_date
        $this->installmentDate = $this->configModel->getConfig('installment_date');
        $this->discountUsageModel = new DiscountUsageModel();
        $this->additionalChargeModel = new AdditionalChargeModel();
        $this->classSectionModel     = new ClassSectionModel();

        // Set up PayPal API context
        $this->apiContext = new ApiContext(
            new OAuthTokenCredential(
                $this->paypalConfig->paypalClientId,
                $this->paypalConfig->paypalSecret
            )
        );

        $this->apiContext->setConfig([
            'mode' => $this->paypalConfig->paypalMode, // 'sandbox' or 'live'
            'http.headers' => ['Connection' => 'Close']
        ]);
    }

    // API: Create a new payment plan
    public function createAPI()
    {
        $data = [
            'parent_id' => $this->request->getPost('parent_id'),
            'total_amount' => $this->request->getPost('total_amount'),
            'number_of_installments' => $this->request->getPost('number_of_installments'),
            'paid_amount' => 0,
            'balance_amount' => $this->request->getPost('total_amount'),
            'payment_date' => $this->request->getPost('payment_date'),
            'payment_type' => $this->request->getPost('payment_type'),
            'status' => 'Pending',
            'semester' => $this->request->getPost('semester'),
            'school_year' => $this->request->getPost('school_year'),
        ];

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

    // API: Get payments by parent ID
    public function getByParentAPI($parentId)
    {
        $payments = $this->paymentModel->getPaymentsByParentId($parentId);
        if ($payments) {
            return $this->respond($payments);
        } else {
            return $this->failNotFound('Payments not found for the parent.');
        }
    }

    // View: Get payments by parent ID (for web views)
    public function getByParent($parentId)
    {
        // Fetch payments with invoice number (if available)
        $payments = $this->paymentModel
            ->select('payments.*, invoices.invoice_number')
            ->join('invoices', 'invoices.id = payments.invoice_id', 'left')
            ->where('payments.parent_id', (int)$parentId)
            ->orderBy('payments.payment_date', 'DESC')
            ->findAll();

        // Parent display name
        $parent = $this->userModel->select('firstname, lastname')->find((int)$parentId) ?: [];
        $parentName = trim(($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? ''));

        // If requested for modal, return a fragment (no layout)
        $isModal = (string)($this->request->getGet('modal') ?? '') === '1';
        if ($isModal) {
            return view('payment/_list_modal', [
                'payments'   => $payments,
                'parentId'   => (int)$parentId,
                'parentName' => $parentName,
            ], ['saveData' => true]);
        }

        // Full page fallback
        return view('payment_list', [
            'payments'   => $payments,
            'parentId'   => (int)$parentId,
            'parentName' => $parentName,
        ]);
    }

    // View: Create a new payment plan
    public function create()
    {
        return view('payment_create');
    }

    // API: Update balance after payment
    public function updateBalanceAPI($paymentId)
    {
        $amountPaid = $this->request->getPost('paid_amount');
        if ($this->paymentModel->updateBalance($paymentId, $amountPaid)) {
            return $this->respond(['status' => 'success']);
        } else {
            return $this->failNotFound('Payment not found.');
        }
    }

    // View: Update balance after payment
    public function updateBalance($paymentId)
    {
        $amountPaid = $this->request->getPost('paid_amount');
        if ($this->paymentModel->updateBalance($paymentId, $amountPaid)) {
            return redirect()->to('/payments');
        } else {
            return redirect()->back()->with('error', 'Failed to update balance.');
        }
    }


    // Create a PayPal payment
    public function createPaypalPayment($paymentId)
    {
        // Fetch the payment details from the database
        $payment = $this->paymentModel->find($paymentId);

        // Create the payer object (who is making the payment)
        $payer = new Payer();
        $payer->setPaymentMethod('paypal');

        // Set up the payment amount
        $amount = new Amount();
        $amount->setCurrency('USD')
            ->setTotal($payment['balance_amount']);  // The balance amount to be paid

        // Set up the transaction details
        $transaction = new Transaction();
        $transaction->setAmount($amount)
            ->setDescription('Payment for school fees')
            ->setInvoiceNumber(uniqid());

        // Create the payment and set the redirect URLs
        $payment = new Payment();
        $payment->setIntent('sale')
            ->setPayer($payer)
            ->setTransactions([$transaction]);

        // Set the approval URL for the payment
        $redirectUrls = new \PayPal\Api\RedirectUrls();
        $redirectUrls->setReturnUrl(base_url('payments/executePaypalPayment')) // Set this to where the user will be redirected after approval
            ->setCancelUrl(base_url('payments/cancelPaypalPayment'));

        $payment->setRedirectUrls($redirectUrls);

        // Create the payment and get the approval URL
        try {
            $payment->create($this->apiContext);
            // Store the payment ID in the session to retrieve it later
            session()->set('paypalPaymentId', $payment->getId());
            session()->set('paymentId', $paymentId);
            $approvalUrl = $payment->getApprovalLink();

            return redirect()->to($approvalUrl);  // Redirect the user to PayPal's approval page
        } catch (\PayPal\Exception\PayPalConnectionException $ex) {
            // Handle errors
            echo $ex->getData();
            exit;
        }
    }

    // Execute the PayPal payment after user approval
    public function executePaypalPayment()
    {
        // Get the payment ID and Payer ID from the request
        $paymentId = session()->get('paypalPaymentId');
        $payerId = $this->request->getGet('PayerID');

        // Get the payment object using the payment ID
        $payment = Payment::get($paymentId, $this->apiContext);

        // Create an execution object to execute the payment
        $execution = new PaymentExecution();
        $execution->setPayerId($payerId);

        // Execute the payment
        try {
            $result = $payment->execute($execution, $this->apiContext);

            // Payment successful, update the payment status in the database
            $paymentId = session()->get('paymentId');
            $this->paymentModel->update($paymentId, ['status' => 'Completed']);

            return redirect()->to('/payments');  // Redirect to the payments page after success
        } catch (\PayPal\Exception\PayPalConnectionException $ex) {
            // Handle payment execution failure
            echo $ex->getData();
            exit;
        }
    }

    // Cancel the PayPal payment
    public function cancelPaypalPayment()
    {
        // Payment was canceled by the user
        return redirect()->to('/payments')->with('error', 'Payment was canceled.');
    }


    public function redirectPage()
    {
        $modeCheck = $this->configModel->getConfig('paypal_mode');

        $parentId = session()->get('user_id'); // assuming this is the logged-in parent

        // Get parent name and school ID
        $parent = $this->db->table('users')
            ->select('firstname, lastname, school_id')
            ->where('id', $parentId)
            ->get()
            ->getRowArray();

        $parentName = isset($parent['firstname'], $parent['lastname'])
            ? $parent['firstname'] . ' ' . $parent['lastname']
            : 'Parent';

        $schoolId = $parent['school_id'] ?? null;

        // Get latest invoice total_amount
        $latestInvoice = $this->invoiceModel->where('parent_id', $parentId)
            ->orderBy('created_at', 'DESC')
            ->select('total_amount')
            ->get()
            ->getRowArray();

        $totalAmount = $latestInvoice['total_amount'] ?? 0;

        return view('payment/payment_redirect', [
            'parentName'   => $parentName,
            'totalAmount'  => $totalAmount,
            'schoolId'     => $schoolId,
            'modeCheck'    => $modeCheck,
        ]);
    }


    public function paypal()
    {
        // Redirect to PayPal API or show instructions
        return redirect()->to('https://www.paypal.com/ncp/payment/87FJL3EV8C7NE');
    }

    public function manual()
    {
        if (strtolower($this->request->getMethod()) === 'post') {
            $data = [
                'invoice_number'   => $this->request->getPost('invoice_number'),
                'amount'           => $this->request->getPost('amount'),
                'payment_method'   => $this->request->getPost('payment_method'),
                'reference'        => $this->request->getPost('reference'),
                'created_at'       => utc_now(),
            ];

            // Handle file upload if present
            $proof = $this->request->getFile('proof');
            if ($proof && $proof->isValid() && !$proof->hasMoved()) {
                $filename = $proof->getRandomName();
                $proof->move(WRITEPATH . 'uploads/payments/', $filename);
                $data['proof_path'] = $filename;
            }

            $this->manualPaymentModel->insert($data);

            return redirect()->back()->with('success', 'Payment info submitted successfully!');
        }

        return view('payment/manual_payment');
    }

    public function eventChargesShow()
    {
        $parents = $this->userModel->getParents();
        $school_Year = $this->request->getGet('school_year') ?? $this->schoolYear;
        $seme_ster = $this->request->getGet('semester') ?? $this->semester;

        // Join with users and students for full info
        $charges = $this->eventChargesModel
            ->select('event_charges.*, users.firstname AS parent_firstname, users.lastname AS parent_lastname, students.firstname AS student_firstname, students.lastname AS student_lastname')
            ->join('users', 'users.id = event_charges.parent_id', 'left')
            ->join('students', 'students.id = event_charges.student_id', 'left')
            ->where('event_charges.school_year', $school_Year)
            ->where('event_charges.semester', $seme_ster)
            ->orderBy('event_charges.created_at', 'DESC')
            ->findAll();

        return view('payment/event_charges', [
            'charges' => $charges,
            'parents' => $parents,
            'school_year' => $school_Year,
            'semester' => $seme_ster,
        ]);
    }

    public function eventChargesUpdate()
    {
        $studentIds = $this->request->getPost('student_ids') ?? [];
        $userId = session()->get('user_id');

        $commonData = [
            'parent_id'   => $this->request->getPost('parent_id'),
            'event_name'  => $this->request->getPost('event_name'),
            'description' => $this->request->getPost('description'),
            'amount'      => $this->request->getPost('amount'),
            'semester'    => $this->request->getPost('semester'),
            'school_year' => $this->request->getPost('school_year'),
            'charged'     => 0,
            'updated_by'  => $userId,
        ];

        $success = false;

        if (!empty($studentIds)) {
            foreach ($studentIds as $studentId) {
                $data = array_merge($commonData, ['student_id' => $studentId]);
                $this->eventChargesModel->insert($data);
                $success = true;
            }
        } elseif ($this->request->getPost('student_id')) {
            $data = array_merge($commonData, ['student_id' => $this->request->getPost('student_id')]);
            $this->eventChargesModel->insert($data);
            $success = true;
        }

        if (!$success) {
            return redirect()->back()->with('error', 'No student selected. Please select at least one.');
        }

        return redirect()->back()->with('success', 'Event charge(s) added successfully.');
    }

    public function getEnrolledStudents($parentId)
    {
        $enrollments = $this->enrollmentModel->getEnrolledStudents((int) $parentId, $this->schoolYear);

        $students = [];
        foreach ($enrollments as $row) {
            $students[] = [
                'id' => $row['id'],
                'firstname' => $row['firstname'],
                'lastname' => $row['lastname'],
                'grade' => $this->studentClassModel->getStudentGrade($row['id']) ?? 'N/A',
            ];
        }

        return $this->response->setJSON($students);
    }

    public function manualPaySearch()
    {
        helper('form');

        $parentData = null;
        $students   = [];
        $payments   = [];
        $invoices   = [];
        $pager      = null;

        // Read the search term (email or phone)
        $searchTerm = trim((string) $this->request->getGet('search_term'));

        // --- Installment end date comes ONLY from config ---
        $installmentDateRaw = (string) ($this->installmentDate ?? '');
        $installmentEndYmd  = '';
        if ($installmentDateRaw) {
            $ts = strtotime($installmentDateRaw);
            if ($ts !== false) {
                $installmentEndYmd = date('Y-m-d', $ts); // ensure 'YYYY-MM-DD'
            }
        }

        if ($searchTerm !== '') {
            $searchClean     = preg_replace('/\D/', '', $searchTerm);
            $searchFormatted = $this->formatPhone($searchClean);

            $builder = $this->db->table('users');
            $builder->select('*');

            // Build search conditions
            $builder->groupStart();
            $builder->where('email', $searchTerm);

            if (!empty($searchClean)) {
                if (strlen($searchClean) === 10) {
                    $formattedPhone = substr($searchClean, 0, 3) . '-' . substr($searchClean, 3, 3) . '-' . substr($searchClean, 6, 4);
                    $builder->orWhere('cellphone', $formattedPhone);
                }

                // compare stripped formatting
                $builder->orWhere(
                    "REPLACE(REPLACE(REPLACE(REPLACE(cellphone, '(', ''), ')', ''), '-', ''), ' ', '') =",
                    $this->db->escapeString($searchClean),
                    false
                );
            }

            if (!empty($searchFormatted)) {
                $builder->orWhere('cellphone', $searchFormatted);
            }
            $builder->groupEnd();

            // Execute query
            $parent = $builder->get()->getRowArray();

            if (is_array($parent) && array_key_exists('password', $parent)) {
                unset($parent['password']);
            }

            if ($parent && !empty($parent['id'])) {
                $parentData = $parent;
                $parentId   = (int) $parent['id'];

                // Students
                $students = $this->studentModel
                    ->where('parent_id', $parentId)
                    ->findAll();

                // Payments (paginated)
                $payments = $this->paymentModel
                    ->where('parent_id', $parentId)
                    ->orderBy('payment_date', 'DESC')
                    ->paginate(10);
                $pager = $this->paymentModel->pager;

                // Invoices
                $rawInvoices = $this->invoiceModel
                    ->where('parent_id', $parentId) // <- fixed (removed "value:")
                    ->orderBy('issue_date', 'DESC')
                    ->findAll();

                // Preload sums of actual payments/discounts/refunds per invoice to avoid stale invoice.paid_amount/balance
                $paidByInvoice = [];
                $discountByInvoice = [];
                $refundByInvoice = [];
                if (!empty($rawInvoices)) {
                    $invoiceIds = array_values(array_unique(array_map(static fn($r) => (int)($r['id'] ?? 0), $rawInvoices)));
                    if (!empty($invoiceIds)) {
                        $rows = $this->paymentModel
                            ->select('invoice_id, COALESCE(SUM(paid_amount),0) AS total_paid')
                            ->whereIn('invoice_id', $invoiceIds)
                            ->groupBy('invoice_id')
                            ->get()->getResultArray();
                        foreach ($rows as $r) {
                            $iid = (int)($r['invoice_id'] ?? 0);
                            $paidByInvoice[$iid] = (float)($r['total_paid'] ?? 0);
                        }

                        // Sum discounts per invoice_id
                        $discRows = $this->db->table('discount_usages')
                            ->select('invoice_id, COALESCE(SUM(discount_amount),0) AS total_disc')
                            ->whereIn('invoice_id', $invoiceIds)
                            ->groupBy('invoice_id')
                            ->get()->getResultArray();
                        foreach ($discRows as $dr) {
                            $iid = (int)($dr['invoice_id'] ?? 0);
                            $discountByInvoice[$iid] = (float)($dr['total_disc'] ?? 0);
                        }

                        // Sum PAID refunds per invoice_id (only Partial/Paid reduce liability)
                        $refRows = $this->db->table('refunds')
                            ->select('invoice_id, COALESCE(SUM(refund_paid_amount),0) AS total_refund_paid')
                            ->whereIn('invoice_id', $invoiceIds)
                            ->whereIn('status', ['Partial','Paid'])
                            ->groupBy('invoice_id')
                            ->get()->getResultArray();
                        foreach ($refRows as $rr) {
                            $iid = (int)($rr['invoice_id'] ?? 0);
                            $refundByInvoice[$iid] = (float)($rr['total_refund_paid'] ?? 0);
                        }
                    }
                }

                // Normalize invoices for the view/JS; force due_ymd to the CONFIG date
                $invoices = array_map(function (array $inv) use ($installmentEndYmd, $paidByInvoice, $discountByInvoice, $refundByInvoice) {
                    // Cast numeric fields we rely on
                    $inv['total_amount'] = isset($inv['total_amount']) ? (float) $inv['total_amount'] : 0.0;
                    // Prefer actual sum of payments for paid_amount
                    $iid = (int)($inv['id'] ?? 0);
                    $actualPaid = isset($paidByInvoice[$iid]) ? (float)$paidByInvoice[$iid] : null;
                    $inv['paid_amount']  = is_numeric($actualPaid)
                        ? (float)$actualPaid
                        : (isset($inv['paid_amount']) ? (float)$inv['paid_amount'] : 0.0);
                    // Per-invoice discount
                    $inv['discount'] = isset($discountByInvoice[$iid]) ? (float)$discountByInvoice[$iid] : (float)($inv['discount'] ?? 0.0);

                    // Per-invoice refunds (paid out)
                    $inv['refund_paid'] = isset($refundByInvoice[$iid]) ? (float)$refundByInvoice[$iid] : 0.0;

                    // Derive balance from total - paid - discount - refundsPaid (never below 0)
                    $inv['balance'] = max(0.0, (float)$inv['total_amount'] - (float)$inv['paid_amount'] - (float)$inv['discount'] - (float)$inv['refund_paid']);

                    // Always use the configured end date for installments
                    $inv['due_ymd'] = $installmentEndYmd;

                    // Optional: keep a start marker if you ever need it elsewhere
                    $issueYmd = '';
                    foreach (['issue_date', 'start_date', 'created_at'] as $k) {
                        if (!empty($inv[$k])) {
                            $its = strtotime($inv[$k]);
                            if ($its !== false) {
                                $issueYmd = date('Y-m-d', $its);
                                break;
                            }
                        }
                    }
                    $inv['issue_ymd'] = $issueYmd;

                    return $inv;
                }, $rawInvoices);
            }
        }

        return view('payment/manual_pay', [
            'parent'                 => $parentData,
            'students'               => $students,
            'payments'               => $payments,
            'invoices'               => $invoices,
            'pager'                  => $pager,
            'searchTermUsedInSearch' => $searchTerm,
            'todayYmd'               => utc_now(),
            'installmentEndYmd'      => $installmentEndYmd, // SELECT will use this in data-end-date
        ]);
    }

    public function manualPaySuggest()
    {
        $q = trim((string)($this->request->getGet('q') ?? ''));
        if ($q === '') {
            return $this->response->setJSON(['items' => []]);
        }

        $db = $this->db;
        $qs = '%' . $db->escapeLikeString($q) . '%';
        $digits = preg_replace('/\D/', '', $q);

        $sql = "SELECT id, firstname, lastname, email, cellphone
                  FROM users
                 WHERE (
                        CONCAT_WS(' ', firstname, lastname) LIKE ?
                     OR email LIKE ?
                     OR cellphone LIKE ?";
        $params = [$qs, $qs, $qs];

        if ($digits !== '') {
            $sql .= " OR REPLACE(REPLACE(REPLACE(REPLACE(cellphone, '(', ''), ')', ''), '-', ''), ' ', '') LIKE ?";
            $params[] = '%' . $digits . '%';
        }

        $sql .= ")
                 ORDER BY lastname, firstname
                 LIMIT 20";

        $rows = $db->query($sql, $params)->getResultArray();
        $items = [];
        foreach ($rows as $row) {
            $label = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? ''));
            $email = trim((string)($row['email'] ?? ''));
            $phone = trim((string)($row['cellphone'] ?? ''));
            $sub = trim($email . ' ' . $phone);
            $value = $email !== '' ? $email : ($phone !== '' ? $phone : $label);

            $items[] = [
                'id'    => (int)($row['id'] ?? 0),
                'label' => $label,
                'sub'   => $sub,
                'value' => $value,
            ];
        }

        return $this->response->setJSON(['items' => $items]);
    }

    private function updateEnrollmentStatusIfPaid(int $invoiceId): int
{
    // 1) Fetch invoice
    $invoice = $this->invoiceModel->find($invoiceId);
    if (!$invoice) {
        log_message('warning', 'Invoice not found: {id}', ['id' => $invoiceId]);
        return 0;
    }

    // 2) Payment check: recompute current balance from payments (authoritative)
    $total         = (float) ($invoice['total_amount'] ?? 0);
    $currentBal    = $this->getCurrentInvoiceBalance($invoiceId); // <-- uses the safe helper
    if (!($total > 0 && $currentBal < $total)) {
        log_message('info', 'No payment yet (or still full balance). Skipping enrollment update. Invoice #{id}', ['id' => $invoiceId]);
        return 0;
    }

    $parentId   = (int) ($invoice['parent_id'] ?? 0);
    $schoolYear = (string) $this->schoolYear;
    $semester   = isset($this->semester) && $this->semester !== '' ? (string)$this->semester : null;

    if ($parentId <= 0 || $schoolYear === '') {
        log_message('warning', 'updateEnrollmentStatusIfPaid: missing parent_id/school_year for invoice #{id}', ['id' => $invoiceId]);
        return 0;
    }

    // 3) Try to limit to enrollments actually present on the invoice (optional)
    $paidEnrollmentIds = [];
    try {
        $hasInvoiceItems = false;
        try {
            $hasInvoiceItems = (bool) $this->db->query("SHOW TABLES LIKE 'invoice_items'")->getNumRows();
        } catch (\Throwable $e) {
            $hasInvoiceItems = false;
        }

        if ($hasInvoiceItems) {
            $rows = $this->db->table('invoice_items')
                ->select('enrollment_id')
                ->where('invoice_id', $invoiceId)
                ->where('enrollment_id IS NOT NULL', null, false)
                ->get()->getResultArray();

            foreach ($rows as $r) {
                $eid = (int)($r['enrollment_id'] ?? 0);
                if ($eid > 0) $paidEnrollmentIds[] = $eid;
            }
            $paidEnrollmentIds = array_values(array_unique($paidEnrollmentIds));
        }
    } catch (\Throwable $e) {
        // proceed without narrowing
        $paidEnrollmentIds = [];
    }

    // 4) Preview IDs that will be updated (good for debugging “partials”)
    $previewBuilder = $this->db->table('enrollments')
        ->select('id')
        ->where('parent_id', $parentId)
        ->where('school_year', $schoolYear)
        ->groupStart()
            // tolerant match for 'payment pending' (case/space/nbsp)
            ->where('enrollment_status', 'payment pending')
            ->orWhere('enrollment_status', 'Payment pending')
            ->orWhere('enrollment_status', 'Payment Pending')
            ->orWhere('enrollment_status', 'PAYMENT PENDING')
            ->orWhere("LOWER(TRIM(REPLACE(enrollment_status, CHAR(160), ' '))) = 'payment pending'", null, false)
        ->groupEnd();

    if ($semester !== null) {
        $previewBuilder->where('semester', $semester);
    }
    if (!empty($paidEnrollmentIds)) {
        $previewBuilder->whereIn('id', $paidEnrollmentIds);
    }

    $toUpdateIds = array_map(
        static fn($r) => (int)$r['id'],
        $previewBuilder->get()->getResultArray()
    );

    if (empty($toUpdateIds)) {
        log_message('info', 'No enrollments matched pending status for parent {p}, year {y}, sem {s}.', [
            'p' => $parentId,
            'y' => $schoolYear,
            's' => $semester ?? 'ALL'
        ]);
        return 0;
    }

    // 5) Perform update only if we have rows to change
    $db = $this->db;
    $db->transBegin();
    try {
        $builder = $db->table('enrollments');
        $builder->set('enrollment_status', 'enrolled')
                ->set('enrollment_date', local_date(utc_now(), 'Y-m-d')) // or 'CURRENT_DATE()' with false flag
                ->whereIn('id', $toUpdateIds);

        if ($builder->update() === false) {
            $err = $db->error();
            throw new \RuntimeException('Enrollments update failed: ' . ($err['message'] ?? 'unknown DB error'));
        }

        $affected = $db->affectedRows();
        $db->transCommit();

        log_message(
            'info',
            'Enrollment status -> enrolled for {n} row(s). parent={p}, year={y}, sem={s}, ids=[{ids}]',
            [
                'n'   => $affected,
                'p'   => $parentId,
                'y'   => $schoolYear,
                's'   => $semester ?? 'ALL',
                'ids' => implode(',', $toUpdateIds),
            ]
        );

        return $affected;
    } catch (\Throwable $e) {
        $db->transRollback();
        log_message('error', 'updateEnrollmentStatusIfPaid error: ' . $e->getMessage());
        return 0;
    }
}



    private function buildStudentEnrolledEventData(
        int $parentId,
        array $studentIds,
        ?string $schoolYear = null,
        ?string $semester   = null,
        ?string $portalLink = null
    ): array {
        $schoolYear = $schoolYear ?? $this->schoolYear;
        $semester   = $semester   ?? $this->semester;
        $portalLink = $portalLink ?? site_url('/login');

        // --- Parent/User info ---
        $parent = $this->db->table('users')
            ->select('id, firstname, lastname, email')
            ->where('id', $parentId)
            ->get()->getRowArray();

        if (!$parent) {
            throw new \RuntimeException("Parent user #{$parentId} not found");
        }

        // Normalize student ids and guard against empty arrays
        $ids = array_values(array_unique(array_map('intval', $studentIds)));
        if (empty($ids)) {
            // If none provided, fetch all of this parent's students for the current term (optional behavior)
            $ids = array_map(
                fn($r) => (int)$r['id'],
                $this->db->table('students')
                    ->select('id')
                    ->where('parent_id', $parentId)
                    ->get()->getResultArray()
            );
        }

        // If still empty, return minimal payload
        if (empty($ids)) {
            $data = [
                'user_id'     => (int) $parent['id'],
                'email'       => $parent['email'] ?? null,
                'firstname'   => $parent['firstname'] ?? '',
                'lastname'    => $parent['lastname'] ?? '',
                'school_year' => $schoolYear,
            ];
            return [$data, []];
        }

        // --- One-shot enrichment query ---
        // NOTE on classSection PK:
        // If your classSection PK is `class_section_id` (common in your code), keep the join below.
        // If it’s `id`, change the ON to `cs.id = sc.class_section_id`.
        $qb = $this->db->table('students s');
        $qb->select("
        s.id AS student_id,
        s.firstname AS s_firstname,
        s.lastname  AS s_lastname,
        s.registration_grade,
        sc.class_section_id,
        cs.class_section_name,
        tu.firstname AS t_firstname,
        tu.lastname  AS t_lastname
    ");

        // current term assignment (left join so unassigned students still appear)
        $qb->join(
            'student_class sc',
            'sc.student_id = s.id AND sc.school_year = ' . $this->db->escape($schoolYear),
            'left'
        );

        // section name
        $qb->join(
            'classSection cs',
            'cs.class_section_id = sc.class_section_id',
            'left'
        );

        // main teacher for that section in the same term
        $qb->join(
            'teacher_class tc',
            'tc.class_section_id = sc.class_section_id
         AND tc.position = "main"
         AND tc.school_year = ' . $this->db->escape($schoolYear),
            'left'
        );

        // teacher user for name
        $qb->join('users tu', 'tu.id = tc.teacher_id', 'left');

        $qb->where('s.parent_id', $parentId)
            ->whereIn('s.id', $ids);

        // If a student can have multiple sc rows for the same term, you may need a strategy to pick one.
        // The simplest is to prefer the most recently updated:
        $qb->orderBy('sc.updated_at', 'DESC');

        $rows = $qb->get()->getResultArray();

        // Fallback minimal data if nothing came back (should be rare)
        if (!$rows) {
            $rows = $this->db->table('students s')
                ->select('s.id AS student_id, s.firstname AS s_firstname, s.lastname AS s_lastname, s.registration_grade')
                ->whereIn('s.id', $ids)
                ->where('s.parent_id', $parentId)
                ->get()->getResultArray();
        }

        // Build normalized student array
        $studentdata = [];
        $seen = []; // to avoid duplicates if joins produce multiple rows per student
        foreach ($rows as $r) {
            $sid = (int) ($r['student_id'] ?? 0);
            if ($sid <= 0 || isset($seen[$sid])) continue;
            $seen[$sid] = true;

            $teacherFull = trim(
                trim((string)($r['t_firstname'] ?? '')) . ' ' .
                    trim((string)($r['t_lastname'] ?? ''))
            );
            if ($teacherFull === '') {
                $teacherFull = null; // keep null if no teacher yet
            }

            $studentdata[] = [
                'id'               => $sid,
                'student_id'       => $sid,
                'firstname'        => (string)($r['s_firstname'] ?? ''),
                'lastname'         => (string)($r['s_lastname'] ?? ''),
                'name'             => trim(((string)($r['s_firstname'] ?? '')) . ' ' . ((string)($r['s_lastname'] ?? ''))),
                'registration_grade' => (string)($r['registration_grade'] ?? ''), // what they registered for
                'class_section_id' => isset($r['class_section_id']) ? (int)$r['class_section_id'] : null,
                'class_section_name' => $r['class_section_name'] ?? null,        // actual placed section (if any)
                'teacher_name'     => $teacherFull,                               // main teacher for placed section/term
                'start_date'       => '',                                         // fill if you have it elsewhere
                'portal_link'      => $portalLink,
            ];
        }

        // Top-level envelope consumed by handleStudentEnrolled()
        $data = [
            'user_id'     => (int) $parent['id'],
            'email'       => $parent['email'] ?? null,
            'firstname'   => $parent['firstname'] ?? '',
            'lastname'    => $parent['lastname'] ?? '',
            'school_year' => $schoolYear,
            'semester'    => $semester,
            'portal_link' => $portalLink,
        ];

        return [$data, $studentdata];
    }

    private function buildPaymentEventData(
        int $invoiceId,
        string $transactionId,
        float $amount,
        string $paymentMethod,
        string $paymentDate,
        ?string $checkNumber,
        int $installmentSeq,
        float $preBalance,
        float $postBalance,
        ?float $invoiceDiscount = null,
        ?float $parentYearDiscountTotal = null
    ): array {
        $invoice  = $this->invoiceModel->find($invoiceId) ?: [];
        $parentId = (int) ($invoice['parent_id'] ?? 0);

        $parent = $parentId
            ? ($this->db->table('users')->select('id, firstname, lastname, email')->where('id', $parentId)->get()->getRowArray() ?: [])
            : [];

        $eventData = [
            'invoice_id'        => $invoiceId,
            'invoice_number'    => $invoice['invoice_number'] ?? null,

            // both keys, to satisfy any template
            'transaction_id'    => $transactionId,
            'transactionId'     => $transactionId,

            // payment
            'amount'            => (float) $amount,
            'payment_method'    => $paymentMethod,
            'method'            => $paymentMethod,
            'payment_date'      => $paymentDate,
            'check_number'      => $checkNumber ?: null,
            'installment_seq'   => (int) $installmentSeq,

            // balances
            'pre_balance'       => (float) $preBalance,
            'post_balance'      => (float) $postBalance,

            // totals
            'invoice_total'     => (float) ($invoice['total_amount'] ?? 0),

            // parent
            'user_id'           => (int) ($parent['id'] ?? $parentId),
            'email'             => $parent['email'] ?? null,
            'firstname'         => $parent['firstname'] ?? null,
            'lastname'          => $parent['lastname'] ?? null,

            'school_year'       => $this->schoolYear,
            'semester'          => $this->semester,
        ];

        if ($invoiceDiscount !== null) {
            $eventData['invoice_discount'] = (float) $invoiceDiscount;
        }
        if ($parentYearDiscountTotal !== null) {
            $eventData['parent_year_discount_total'] = (float) $parentYearDiscountTotal;
        }

        // lightweight student list
        $students = [];
        if ($parentId) {
            $studentRows = $this->db->table('students')
                ->select('id, firstname, lastname')
                ->where('parent_id', $parentId)
                ->orderBy('lastname', 'ASC')
                ->get()->getResultArray();
            foreach ($studentRows as $r) {
                $students[] = [
                    'id'        => (int) $r['id'],
                    'firstname' => (string) ($r['firstname'] ?? ''),
                    'lastname'  => (string) ($r['lastname'] ?? ''),
                    'name'      => trim(($r['firstname'] ?? '') . ' ' . ($r['lastname'] ?? '')),
                ];
            }
        }

        return [$eventData, $students];
    }

    public function manualPayUpdate()
    {
        $invoiceId      = (int) $this->request->getPost('invoice_id');
        $amount         = (float) $this->request->getPost('amount');
        $paymentMethod  = strtolower(trim((string) $this->request->getPost('payment_method'))); // cash|check|card
        $checkNumber    = trim((string) $this->request->getPost('check_number'));
        $searchTerm     = $this->request->getPost('search_term') ?? $this->request->getGet('search_term') ?? '';

        // UI-only
        $paymentType    = strtolower((string) ($this->request->getPost('payment_type') ?? 'full')); // full|installment

        // Accept user-entered date or now()
        $paymentDateStr = $this->request->getPost('payment_date');
        $paymentDate    = $paymentDateStr ? date('Y-m-d H:i:s', strtotime($paymentDateStr)) : utc_now();

        // Basic validation
        if (!$invoiceId || !$amount || !$paymentMethod) {
            return redirect()->back()->withInput()->with('error', 'Missing required fields (invoice, amount, or method).');
        }

        $allowedMethods = ['cash', 'check', 'card'];
        if (!in_array($paymentMethod, $allowedMethods, true)) {
            return redirect()->back()->withInput()->with('error', 'Invalid payment method.');
        }

        if ($paymentMethod === 'check' && $checkNumber === '') {
            return redirect()->back()->withInput()->with('error', 'Check number is required for check payments.');
        }

        if ($amount <= 0) {
            return redirect()->back()->withInput()->with('error', 'Invalid amount: ' . number_format($amount, 2));
        }

        $invoice = $this->invoiceModel->find($invoiceId);
        if (!$invoice) {
            return redirect()->back()->with('error', 'Invoice not found.');
        }

        // Snapshot pre-payment balance
        $initialPreBalance = $this->getCurrentInvoiceBalance($invoiceId);

        // Card must equal full balance
        if ($paymentMethod === 'card' && (float)round($amount, 2) !== (float)round($initialPreBalance, 2)) {
            return redirect()->back()->withInput()->with(
                'error',
                'Debit/Credit Card payments must equal the full remaining balance (' . number_format($initialPreBalance, 2) . ').'
            );
        }

        // Optional receipt upload
        $checkFile   = null;
        $paymentFile = $this->request->getFile('payment_file');
        if ($paymentFile && $paymentFile->isValid() && !$paymentFile->hasMoved()) {
            $okTypes = ['image/jpeg', 'image/png', 'application/pdf'];
            $okExts  = ['jpg', 'jpeg', 'png', 'pdf'];

            $mime = $paymentFile->getMimeType();
            $ext  = strtolower($paymentFile->getClientExtension());

            if (!in_array($mime, $okTypes, true) || !in_array($ext, $okExts, true)) {
                return redirect()->back()->withInput()->with('error', 'Unsupported file type. Use JPG, PNG, or PDF.');
            }
            if ($paymentFile->getSize() > 5 * 1024 * 1024) {
                return redirect()->back()->withInput()->with('error', 'File too large. Max 5MB.');
            }

            $fileName  = $paymentFile->getRandomName();
            $subdir    = ($paymentMethod === 'check') ? 'checks' : (($paymentMethod === 'card') ? 'cards' : 'misc');
            $targetDir = WRITEPATH . 'uploads/' . $subdir . '/';
            if (!is_dir($targetDir)) @mkdir($targetDir, 0775, true);
            $paymentFile->move($targetDir, $fileName);
            $checkFile = $fileName;
        }
        $this->db->transBegin();

        try {
            // Lock invoice & get context (also ensure totals are up-to-date before validation)
            $row = $this->db->query(
                'SELECT id, parent_id, total_amount, school_year FROM invoices WHERE id = ? FOR UPDATE',
                [$invoiceId]
            )->getRowArray();

            if (!$row) {
                $this->db->transRollback();
                return redirect()->back()->with('error', 'Invoice not found.');
            }

            $parentId = (int)($row['parent_id'] ?? 0);
            $invYear  = (string)($row['school_year'] ?? $this->schoolYear);

            // Recompute invoice totals from tuition + events + additional charges
            $this->recalculateInvoice($invoiceId, $invYear);

            // Authoritative balance
            $currentBalance = $this->getCurrentInvoiceBalance($invoiceId);

            if ($amount > $currentBalance + 0.00001) {
                $this->db->transRollback();
                return redirect()->back()->withInput()->with(
                    'error',
                    'Entered amount (' . number_format($amount, 2) . ') exceeds remaining balance (' . number_format($currentBalance, 2) . ').'
                );
            }

            if ($paymentMethod === 'card' && (float)round($amount, 2) !== (float)round($currentBalance, 2)) {
                $this->db->transRollback();
                return redirect()->back()->withInput()->with(
                    'error',
                    'Debit/Credit Card payments must equal the full remaining balance (' . number_format($currentBalance, 2) . ').'
                );
            }

            // Installment sequence
            $paidCount      = $this->getSuccessfulPaymentCount($invoiceId);
            $installmentSeq = $paidCount + 1;

            // Generate a stable transaction id
            $transactionId = 'INV-' . $invoiceId . '-' . str_replace('.', '', (string)microtime(true));

            // Record payment
            $ok = $this->processPayment(
                $invoiceId,
                $amount,
                $paymentMethod,
                $checkFile,
                $transactionId,
                $paymentDate,
                $this->schoolYear,
                $this->semester,
                $checkNumber,
                $installmentSeq
            );

            if (!$ok) {
                $this->db->transRollback();
                return redirect()->back()->with('error', 'Failed to record payment.');
            }

            // Ensure invoice totals/balance reflect discounts and this payment
            $this->recalculateInvoice($invoiceId, $this->schoolYear);

            // Post-payment balance from snapshot
            $postBalance = (float)round($initialPreBalance - $amount, 2);
            if ($postBalance < 0) $postBalance = 0.0;

            // Optional enrollment update
            $enrollmentupdated = $this->updateEnrollmentStatusIfPaid($invoiceId);
            if ($enrollmentupdated != 0) {
                $studentIds = $this->studentModel->getStudentIdsByParentId($parentId);
                [$eventDataEnroll, $studentDataEnroll] = $this->buildStudentEnrolledEventData($parentId, $studentIds);
                Events::trigger('studentEnrolled', $eventDataEnroll, $studentDataEnroll);
            }

            // --- Discounts (keep payment and discount separate!)
            // Per-invoice discount: sum by invoice_id ONLY (no year filter so we never miss it)
            $invoiceDiscount = 0.0;
            $rowDisc = $this->db->table('discount_usages')
                ->selectSum('discount_amount', 'sum_disc')
                ->where('invoice_id', $invoiceId)
                ->get()->getRowArray();
            if ($rowDisc && isset($rowDisc['sum_disc'])) {
                $invoiceDiscount = (float)$rowDisc['sum_disc'];
            }

            // Parent YTD (same school year)
            $parentYearDiscountTotal = 0.0;
            $rowParentDisc = $this->db->table('discount_usages')
                ->selectSum('discount_amount', 'sum_disc')
                ->where('parent_id', $parentId)
                ->where('school_year', $this->schoolYear)
                ->get()->getRowArray();
            if ($rowParentDisc && isset($rowParentDisc['sum_disc'])) {
                $parentYearDiscountTotal = (float)$rowParentDisc['sum_disc'];
            }

            $this->db->transCommit();

            // Build payload & notify
            [$eventData, $studentData] = $this->buildPaymentEventData(
                $invoiceId,
                $transactionId,
                $amount,
                $paymentMethod,
                $paymentDate,
                $checkNumber,
                $installmentSeq,
                $initialPreBalance,
                $postBalance,
                $invoiceDiscount,
                $parentYearDiscountTotal
            );

            Events::trigger('paymentReceived', $eventData, $studentData);

            return redirect()
                ->to(site_url('payment/manual_pay?search_term=' . urlencode($searchTerm)))
                ->with('success', 'Payment recorded successfully (Installment #' . $installmentSeq . '). Transaction ID: ' . $transactionId);
        } catch (\Throwable $e) {
            if ($this->db->transStatus()) $this->db->transRollback();
            log_message('error', '[manualPayUpdate] ' . $e->getMessage());
            return redirect()->back()->withInput()->with('error', 'Unexpected error while recording payment.');
        }
    }


    public function manualPayEdit()
    {
        $paymentId     = (int) $this->request->getPost('payment_id');
        $paidAmount    = (float) $this->request->getPost('paid_amount');
        $paymentMethod = $this->request->getPost('payment_method');
        $checkNumber   = $this->request->getPost('check_number'); // ✅ Added check_number

        if (!$paymentId || !$paidAmount || !$paymentMethod) {
            return redirect()->back()->with('error', 'Missing required fields.');
        }

        $payment = $this->paymentModel->find($paymentId);
        if (!$payment) {
            return redirect()->back()->with('error', 'Original payment not found.');
        }

        $invoice = $this->invoiceModel->find($payment['invoice_id']);
        if (!$invoice) {
            return redirect()->back()->with('error', 'Linked invoice not found.');
        }

        //$schoolYear = (new ConfigurationModel())->getConfig('school_year');
        $checkFile  = $payment['check_file']; // default keep old file

        // Handle optional check or card payment receipt upload
        if (strtolower($paymentMethod) === 'check') {
            $paymentFile = $this->request->getFile('payment_file'); // ✅ Updated to payment_file
            if ($paymentFile && $paymentFile->isValid() && !$paymentFile->hasMoved()) {
                $fileName = $paymentFile->getRandomName();
                $paymentFile->move(WRITEPATH . 'uploads/checks/', $fileName);
                $checkFile = $fileName;
            }
        } elseif (strtolower($paymentMethod) === 'card') {
            $paymentFile = $this->request->getFile('payment_file'); // ✅ Updated to payment_file
            if ($paymentFile && $paymentFile->isValid() && !$paymentFile->hasMoved()) {
                $fileName = $paymentFile->getRandomName();
                $paymentFile->move(WRITEPATH . 'uploads/cards/', $fileName);
                $checkFile = $fileName; // reuse for compatibility
            }
        }

        // ❌ Validate amount - negative or zero
        if ($paidAmount <= 0) {
            return redirect()->back()->with(
                'error',
                'Invalid amount entered: ' . number_format($paidAmount, 2) . '. Amount must be greater than zero.'
            );
        }

        // 🔄 Recalculate invoice first to ensure totals reflect tuition + events + additional
        $this->recalculateInvoice($payment['invoice_id'], $this->schoolYear);

        // 🔄 Get current balance based on actual payments (with school_year filter)
        // We need to calculate what the balance would be WITHOUT the current payment being edited
        $currentBalance = $this->getCurrentInvoiceBalanceExcludingPayment($payment['invoice_id'], $paymentId);

        if ($paidAmount > $currentBalance) {
            return redirect()->back()->with(
                'error',
                'Entered amount (' . number_format($paidAmount, 2) . ') exceeds remaining balance (' . number_format($currentBalance, 2) . ').'
            );
        }

        // ✅ Update edited payment with check_number
        $updateData = [
            'paid_amount'   => $paidAmount,
            'payment_method' => strtolower($paymentMethod),
            'check_file'    => $checkFile,
            'updated_by'    => session()->get('user_id')
        ];

        // ✅ Only add check_number if payment method is check
        if (strtolower($paymentMethod) === 'check') {
            $updateData['check_number'] = $checkNumber;
        } else {
            $updateData['check_number'] = null; // Clear check number for non-check payments
        }

        $this->paymentModel->update($paymentId, $updateData);

        // 🔄 Always recalc invoice after change
        $this->recalculateInvoice($payment['invoice_id'], $this->schoolYear);

        return redirect()->back()->with('success', 'Payment updated successfully.');
    }


    /**
     * 🔄 Helper: Recalculate invoice totals and status based on all payments for current school year
     */
    private function recalculateInvoice($invoiceId, $schoolYear)
    {
        $invoice = $this->invoiceModel->find($invoiceId);
        if (!$invoice) return;

        $parentId = (int)($invoice['parent_id'] ?? 0);
        if ($parentId <= 0) return;

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

        $registered = [];
        $withdrawn  = [];
        foreach ($enrollments as $e) {
            $row = [
                'student_id'       => (int)($e['student_id'] ?? 0),
                'class_section_id' => $e['class_section_id'] ?? null,
                'enrollment_status'=> (string)($e['enrollment_status'] ?? ''),
            ];
            if (in_array($row['enrollment_status'], ['enrolled','payment pending'], true)) {
                $registered[] = $row;
            } elseif (in_array($row['enrollment_status'], ['withdrawn','refund pending','withdraw under review'], true)) {
                $withdrawn[] = $row;
            }
        }

        // Refund window check – if after deadline, withdrawn still billed
        $refundDeadline = (string)($this->configModel->getConfig('refund_deadline') ?? '');
        $refundAllowed  = true;
        try {
            $tzName   = (string) (config('School')->attendance['timezone'] ?? user_timezone());
            $tz       = new \DateTimeZone($tzName);
            $today    = new \DateTimeImmutable('today', $tz);
            $deadline = new \DateTimeImmutable($refundDeadline, $tz);
            $refundAllowed = $today <= $deadline;
        } catch (\Throwable $e) {
            $refundAllowed = true;
        }

        $tuitionStudents = $registered;
        if (!$refundAllowed) {
            $tuitionStudents = array_merge($tuitionStudents, $withdrawn);
        }

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

        // Grade threshold and fees
        $gradeFee         = (int)($this->configModel->getConfig('grade_fee') ?? 9);
        $firstStudentFee  = (float)($this->configModel->getConfig('first_student_fee')  ?? 350);
        $secondStudentFee = (float)($this->configModel->getConfig('second_student_fee') ?? 200);
        $youthFee         = (float)($this->configModel->getConfig('youth_fee')          ?? 180);

        // Normalize grades for tuition students
        foreach ($tuitionStudents as &$s) {
            $name = null;
            if (!empty($s['class_section_id'])) {
                $name = $this->classSectionModel->getClassSectionNameBySectionId($s['class_section_id']);
            }
            $s['grade_name'] = is_string($name) ? strtoupper(trim($name)) : 'N/A';
        }
        unset($s);

        // Count regular vs youth and compute tuition
        $regularCount = 0;
        $youthCount   = 0;
        foreach ($tuitionStudents as $s) {
            $lvl = $this->parseGradeLevel($s['grade_name']);
            if ($lvl > $gradeFee) $youthCount++; else $regularCount++;
        }

        $tuitionSubtotal = 0.0;
        $tuitionSubtotal += $youthCount * $youthFee;
        if ($regularCount >= 2) {
            $tuitionSubtotal += $firstStudentFee + ($regularCount - 1) * $secondStudentFee;
        } elseif ($regularCount === 1) {
            $tuitionSubtotal += $firstStudentFee;
        }

        // ---- Event charges (parent-year) ----
        $eventSubtotal = 0.0;
        try {
            $events = $this->eventChargesModel->getChargesWithEventInfo($parentId, $schoolYear) ?? [];
            foreach ($events as $ev) { $eventSubtotal += (float)($ev['charged'] ?? 0.0); }
        } catch (\Throwable $e) {}

        // ---- Additional charges (per-invoice) ----
        $additionalSubtotal = 0.0;
        try {
            $rows = $this->additionalChargeModel
                ->select('charge_type, amount')
                ->where('invoice_id', $invoiceId)
                ->where('status', 'applied')
                ->findAll();
            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);
                $additionalSubtotal += $amt;
            }
        } catch (\Throwable $e) {}

        $newTotal = round($tuitionSubtotal + $eventSubtotal + $additionalSubtotal, 2);

        // ---- Payments / Discounts / Refunds ----
        $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('invoice_id', $invoiceId)
            ->where('school_year', $schoolYear);

        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();
        $totalPaid = 0.0;
        foreach ($payments as $p) { $totalPaid += (float)($p['paid_amount'] ?? 0); }

        $discRow = $this->db->table('discount_usages')
            ->select('COALESCE(SUM(discount_amount),0) AS total_disc')
            ->where('invoice_id', $invoiceId)
            ->get()->getRowArray();
        $totalDisc = (float)($discRow['total_disc'] ?? 0);

        $refundRow = $this->db->table('refunds')
            ->select('COALESCE(SUM(refund_paid_amount),0) AS total_refund_paid')
            ->where('invoice_id', $invoiceId)
            ->whereIn('status', ['Partial','Paid'])
            ->get()->getRowArray();
        $totalRefundPaid = (float)($refundRow['total_refund_paid'] ?? 0);

        $newBalance = max(0.0, $newTotal - $totalDisc - $totalPaid - $totalRefundPaid);
        $newStatus  = ($newBalance <= 0.00001) ? 'Paid' : (($totalPaid > 0) ? 'Partially Paid' : 'Unpaid');

        $this->invoiceModel->update($invoiceId, [
            'total_amount' => $newTotal,
            'paid_amount'  => $totalPaid,
            'balance'      => $newBalance,
            'status'       => $newStatus,
        ]);
    }

    /**
     * Recalculate invoice totals by invoice number or ID (school-year only).
     */
    public function recalculateInvoiceByNumber(string $invoiceNumber)
    {
        $invoiceNumber = trim($invoiceNumber);
        $invoice = null;

        if ($invoiceNumber !== '' && ctype_digit($invoiceNumber)) {
            $invoice = $this->invoiceModel->find((int)$invoiceNumber);
        }
        if (!$invoice) {
            $invoice = $this->invoiceModel
                ->where('invoice_number', $invoiceNumber)
                ->first();
        }

        if (!$invoice) {
            return redirect()->back()->with('error', 'Invoice not found.');
        }

        $schoolYear = (string)($invoice['school_year'] ?? $this->schoolYear);
        $this->recalculateInvoice((int)$invoice['id'], $schoolYear);

        return redirect()->back()->with('success', 'Invoice recalculated.');
    }

    /**
     * Parse a grade name into an integer level for tuition rules.
     * Kindergarten -> 1, Youth -> > 9, numeric grades passthrough.
     */
    private function parseGradeLevel($grade): int
    {
        if (is_numeric($grade)) return (int)$grade;
        if (!is_string($grade)) return 999;
        $g = strtoupper(trim((string)$grade));
        $g = preg_replace('/\s+/', ' ', $g);
        $g = str_replace(['.', '_', '-'], ['', '', ' '], $g);
        // KG/K/Kindergarten
        $kg = ['K','KG','K G','KINDER','KINDERGARTEN'];
        if (in_array($g, $kg, true)) return 1;
        // Pre-K -> treat below regular
        $pk = ['PK','P K','PREK','PRE K','PRE KINDER','PREKINDER'];
        if (in_array($g, $pk, true)) return -1;
        // Youth variants: Y, YOUTH, Y1, YOUTH2 -> map to 10+
        if (preg_match('/^Y(?:OUTH)?\s*(\d+)?$/', $g, $m)) {
            $n = isset($m[1]) && $m[1] !== '' ? max(1, (int)$m[1]) : 1;
            $gradeFee = (int)($this->configModel->getConfig('grade_fee') ?? 9);
            return $gradeFee + $n;
        }
        if (preg_match('/^(?:GR?ADE\s*)?(\d{1,2})\s*([A-Z]*)$/', $g, $m)) {
            return (int)$m[1];
        }
        return 999;
    }

    /** Get current invoice balance = total - payments - discounts - refundsPaid (no school_year filter). */
    private function getCurrentInvoiceBalance(int $invoiceId): float
    {
        $invoice = $this->invoiceModel->find($invoiceId);
        if (!$invoice) return 0.0;

        $db       = $this->db;
        $table    = $this->paymentModel->table;        // usually 'payments'
        $hasStatus = $db->fieldExists('status', $table);
        $hasVoid  = $db->fieldExists('is_void', $table);

        $qb = $this->paymentModel
            ->select('COALESCE(SUM(paid_amount),0) AS total_paid')
            ->where('invoice_id', $invoiceId);

        if ($hasStatus) {
            $qb->groupStart()
                ->whereNotIn('status', ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'])
                ->orWhere('status IS NULL', null, false)
                ->groupEnd();
        }

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

        $row       = $qb->first();
        $totalPaid = (float)($row['total_paid'] ?? 0);

        // Discount sum on this invoice
        $discRow = $this->db->table('discount_usages')
            ->select('COALESCE(SUM(discount_amount),0) AS total_disc')
            ->where('invoice_id', $invoiceId)
            ->get()->getRowArray();
        $totalDisc = (float)($discRow['total_disc'] ?? 0);

        // Refunds paid
        $refundRow = $this->db->table('refunds')
            ->select('COALESCE(SUM(refund_paid_amount),0) AS total_refund_paid')
            ->where('invoice_id', $invoiceId)
            ->whereIn('status', ['Partial','Paid'])
            ->get()->getRowArray();
        $totalRefundPaid = (float)($refundRow['total_refund_paid'] ?? 0);

        $total     = (float)($invoice['total_amount'] ?? 0);

        return max(0.0, round($total - $totalPaid - $totalDisc - $totalRefundPaid, 2));
    }

    /** Balance excluding a specific payment (for edit scenarios) and subtracting discounts/refunds. */
    private function getCurrentInvoiceBalanceExcludingPayment(int $invoiceId, int $excludePaymentId): float
    {
        $invoice = $this->invoiceModel->find($invoiceId);
        if (!$invoice) return 0.0;

        $db       = $this->db;
        $table    = $this->paymentModel->table;
        $hasStatus = $db->fieldExists('status', $table);
        $hasVoid  = $db->fieldExists('is_void', $table);

        $qb = $this->paymentModel
            ->select('COALESCE(SUM(paid_amount),0) AS total_paid')
            ->where('invoice_id', $invoiceId)
            ->where('id !=', $excludePaymentId);

        if ($hasStatus) {
            $qb->groupStart()
                ->whereNotIn('status', ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'])
                ->orWhere('status IS NULL', null, false)
                ->groupEnd();
        }

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

        $row       = $qb->first();
        $totalPaid = (float)($row['total_paid'] ?? 0);

        // Discount sum on this invoice
        $discRow = $this->db->table('discount_usages')
            ->select('COALESCE(SUM(discount_amount),0) AS total_disc')
            ->where('invoice_id', $invoiceId)
            ->get()->getRowArray();
        $totalDisc = (float)($discRow['total_disc'] ?? 0);

        // Refunds paid
        $refundRow = $this->db->table('refunds')
            ->select('COALESCE(SUM(refund_paid_amount),0) AS total_refund_paid')
            ->where('invoice_id', $invoiceId)
            ->whereIn('status', ['Partial','Paid'])
            ->get()->getRowArray();
        $totalRefundPaid = (float)($refundRow['total_refund_paid'] ?? 0);

        $total     = (float)($invoice['total_amount'] ?? 0);

        return max(0.0, round($total - $totalPaid - $totalDisc - $totalRefundPaid, 2));
    }


    /**
     * 🔄 Helper: Get current invoice balance based on actual payments
     */

    function formatPhone($input)
    {
        // Remove any non-digits just in case
        $digits = preg_replace('/\D/', '', $input);

        if (strlen($digits) !== 10) {
            return $input; // Return original if not exactly 10 digits
        }

        return sprintf(
            '(%s)-%s-%s',
            substr($digits, 0, 3),
            substr($digits, 3, 3),
            substr($digits, 6, 4)
        );
    }

    private function processPayment(
        $invoiceId,
        $amount,
        $paymentMethod,
        $checkFile = null,
        $transactionId = null,
        $paymentDate = null,
        $schoolYear = null,
        $semester = null,
        $checkNumber = null,
        ?int $installmentSeq = null // <-- NOW: the installment sequence (1,2,3,...) for this invoice
    ) {
        $invoice = $this->invoiceModel->find($invoiceId);
        if (!$invoice) {
            return false;
        }

        $transactionId = $transactionId ?? 'INV-' . $invoiceId . '-' . time();
        $paymentDate   = $paymentDate   ?? utc_now();

        // If sequence wasn't provided (legacy callers), compute it here.
        // NOTE: If you call this from inside a transaction (recommended), this will be consistent.
        if ($installmentSeq === null || $installmentSeq < 1) {
            $exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
            $priorCount = $this->paymentModel
                ->where('invoice_id', $invoiceId)
                ->where('paid_amount >', 0)
                ->whereNotIn('status', $exclude)
                ->countAllResults();
            $installmentSeq = $priorCount + 1;
        }


        // Compute new totals
        $newPaid    = (float) $invoice['paid_amount'] + (float) $amount;
        $newBalance = (float) $invoice['balance'] - (float) $amount;
        if ($newBalance == $invoice['total_amount']) {
            $paymentStatus = 'Unpaid';
        } elseif ($newBalance > 0 && $newBalance < $invoice['total_amount']) {
            $paymentStatus = 'Partially Paid';
        } elseif ($newBalance <= 0.00001) {
            $paymentStatus = 'Paid';
        } else {
            $paymentStatus = $invoice['status']; // fallback
        }

        // Update invoice
        $invoiceUpdateData = [
            'paid_amount' => $newPaid,
            'balance'     => $newBalance,
            'status'      => $paymentStatus,
            'updated_by'  => session()->get('user_id'),
        ];
        if (!$this->invoiceModel->update($invoiceId, $invoiceUpdateData)) {
            return false;
        }

        // Store the *sequence* in number_of_installments (kept for schema compatibility)
        // Consider renaming the column to `installment_seq` in a future migration.
        $paymentData = [
            'parent_id'              => $invoice['parent_id'],
            'invoice_id'             => $invoiceId,
            'total_amount'           => $invoice['total_amount'],
            'paid_amount'            => $amount,
            'balance'                => $newBalance,
            'number_of_installments' => $installmentSeq, // <-- installment sequence (1,2,3,...)
            'transaction_id'         => $transactionId,
            'payment_method'         => strtolower($paymentMethod),
            'payment_date'           => $paymentDate,
            'status'                 => $paymentStatus,
            'check_file'             => $checkFile,
            'check_number'           => (strtolower($paymentMethod) === 'check') ? $checkNumber : null,
            'updated_by'             => session()->get('user_id'),
            'school_year'            => $schoolYear ?? $this->schoolYear,
            'semester'               => $semester   ?? $this->semester,
            // 'installment_index'    => $installmentSeq, // use if you add a dedicated column later
        ];

        if (!$this->paymentModel->insert($paymentData)) {
            return false;
        }

        return true;
    }

    private function getSuccessfulPaymentCount(int $invoiceId): int
    {
        $exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];

        return $this->paymentModel
            ->where('invoice_id', $invoiceId)
            ->where('paid_amount >', 0)
            ->whereNotIn('status', $exclude)
            ->countAllResults();
    }

    public function serveCheckFile($filename, $mode = 'download')
    {
        $filename = basename($filename);
        $roots = [
            WRITEPATH . 'uploads/checks/' . $filename,
            WRITEPATH . 'uploads/cards/'  . $filename,
            WRITEPATH . 'uploads/misc/'   . $filename,
            WRITEPATH . 'uploads/'        . $filename, // final fallback
        ];

        $path = null;
        foreach ($roots as $candidate) {
            if (is_file($candidate)) {
                $path = $candidate;
                break;
            }
        }

        if (!$path) {
            throw new \CodeIgniter\Exceptions\PageNotFoundException('Payment file not found: ' . esc($filename));
        }

        if ($mode === 'inline') {
            return $this->response
                ->setHeader('Content-Type', mime_content_type($path))
                ->setHeader('Content-Disposition', 'inline; filename="' . $filename . '"')
                ->setBody(file_get_contents($path));
        }

        return $this->response->download($path, null);
    }
}
