<?php

namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\ReimbursementModel;
use App\Models\ExpenseModel;
use App\Models\UserModel;
use App\Models\ConfigurationModel;
use App\Models\ReimbursementBatchModel;
use App\Models\ReimbursementBatchItemModel;
use App\Models\ReimbursementBatchAdminFileModel;
use App\Services\EmailService;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Files\UploadedFile;
use ZipArchive;

class ReimbursementController extends BaseController
{
    protected $db;
    protected $configModel;
    protected $semester;
    protected $schoolYear;
    protected $expenseModel;
    protected $reimbModel;
    protected $userModel;
    protected $batchModel;
    protected $batchItemModel;
    protected $batchAdminFileModel;

    public const SPECIAL_RECIPIENTS = [
        // Reserved virtual recipients that should always be selectable.
        990001 => 'Masjid',
        990002 => 'Donation',
    ];

    public function __construct()
    {
        $this->db = \Config\Database::connect();
        if (!$this->db->connect()) {
            log_message('error', 'Database connection failed.');
            throw new \Exception('Database connection failed.');
        }

        $this->configModel   = new ConfigurationModel();
        $this->expenseModel  = new ExpenseModel();
        $this->reimbModel    = new ReimbursementModel();
        $this->userModel     = new UserModel();
        $this->batchModel    = new ReimbursementBatchModel();
        $this->batchItemModel = new ReimbursementBatchItemModel();
        $this->batchAdminFileModel = new ReimbursementBatchAdminFileModel();

        $this->semester   = $this->configModel->getConfig('semester') ?? 'Fall';
        $this->schoolYear = $this->configModel->getConfig('school_year') ?? date('Y');
    }

    /**
     * Return staff users (admins/teachers/etc.) excluding parents/guests.
     */
    private function staffUsers(): array
    {
        $rows = $this->userModel
            ->select('users.id, users.firstname, users.lastname, roles.name AS role_name')
            ->join('user_roles', 'user_roles.user_id = users.id', 'left')
            ->join('roles', 'roles.id = user_roles.role_id', 'left')
            ->where('roles.name IS NOT NULL', null, false)
            ->findAll();

        $excludedRoles = array_map('strtolower', ['parent', 'student', 'guest']);
        $staff = [];

        foreach ($rows as $row) {
            $roleName = strtolower((string) ($row['role_name'] ?? ''));
            $id       = (int) ($row['id'] ?? 0);
            if ($id <= 0 || in_array($roleName, $excludedRoles, true)) {
                continue;
            }
            if (!isset($staff[$id])) {
                $staff[$id] = [
                    'id'        => $id,
                    'firstname' => $row['firstname'] ?? '',
                    'lastname'  => $row['lastname'] ?? '',
                ];
            }
        }

        uasort($staff, static function ($a, $b) {
            $nameA = trim(($a['firstname'] ?? '') . ' ' . ($a['lastname'] ?? ''));
            $nameB = trim(($b['firstname'] ?? '') . ' ' . ($b['lastname'] ?? ''));
            return strcasecmp($nameA, $nameB);
        });

        return array_values($staff);
    }

    private function fetchRecipientUsers(): array
    {
        return $this->staffUsers();
    }

    private function appendSpecialRecipients(array $users): array
    {
        $existingIds = array_map(static function ($row) {
            return isset($row['id']) && is_numeric($row['id']) ? (int) $row['id'] : null;
        }, $users);

        foreach (self::SPECIAL_RECIPIENTS as $id => $label) {
            if (!in_array($id, $existingIds, true)) {
                $users[] = [
                    'id'        => $id,
                    'firstname' => $label,
                    'lastname'  => '',
                ];
            }
        }

        return $users;
    }

    private function recipientOptions(): array
    {
        return $this->appendSpecialRecipients($this->fetchRecipientUsers());
    }

    private function nextYearlyBatchNumberForSchoolYear(?string $schoolYear = null): int
    {
        $year = trim((string) ($schoolYear ?? $this->schoolYear));
        if ($year === '') {
            return 1;
        }
        $record = $this->batchModel
            ->select('MAX(yearly_batch_number) AS max_number')
            ->where('school_year', $year)
            ->first();

        $max = (int) ($record['max_number'] ?? 0);
        return $max + 1;
    }

    private function adminRoster(): array
    {
        $admins = $this->staffUsers();

        if (empty($admins)) {
            $admins = $this->fetchRecipientUsers();
        }

        return $admins;
    }

    private function specialRecipientLabel($value): ?string
    {
        if ($value === null || !is_numeric($value)) {
            return null;
        }

        $id = (int) $value;
        return self::SPECIAL_RECIPIENTS[$id] ?? null;
    }

    /**
     * Ensure a subdirectory exists under WRITEPATH . 'uploads'.
     * Returns the absolute path to the subdirectory.
     */
    private function ensureUploadSubdir(string $subdir): string
    {
        $base = rtrim(WRITEPATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'uploads';
        $dir  = $base . DIRECTORY_SEPARATOR . trim($subdir, '/\\');

        if (!is_dir($dir)) {
            // Try to create the directory tree if missing
            if (!@mkdir($dir, 0755, true) && !is_dir($dir)) {
                log_message('error', 'Failed to create upload directory: ' . $dir);
                throw new \RuntimeException('Upload directory is not available.');
            }
        }
        if (!is_writable($dir)) {
            log_message('error', 'Upload directory not writable: ' . $dir);
            throw new \RuntimeException('Upload directory is not writable.');
        }
        return $dir;
    }

    /**
     * Ensure a subdirectory exists under FCPATH . 'uploads'.
     * Returns the absolute path to the subdirectory.
     */
    /**
     * Save a reimbursement receipt to writable/uploads/reimbursements and
     * return the stored filename (basename). Returns null if no valid file provided.
     */
    private function saveReimbReceipt($file): ?string
    {
        if (!$file instanceof UploadedFile) {
            return null;
        }
        if (!$file->isValid() || $file->hasMoved()) {
            return null;
        }
        // Skip zero-length inputs (when no file chosen for optional uploads)
        $size = (int) ($file->getSize() ?? 0);
        if ($size <= 0) {
            return null;
        }

        // Ensure target directory exists and is writable
        $this->ensureUploadSubdir('reimbursements');

        try {
            // Prefer CI's store() API which places the file under WRITEPATH/uploads/<subdir>
            $stored = $file->store('reimbursements');
            if (!$stored) {
                throw new \RuntimeException('store() returned empty result');
            }
            return basename($stored);
        } catch (\Throwable $e) {
            log_message('error', 'Reimbursement receipt store() failed: {message}', ['message' => $e->getMessage()]);
            // Fallback to move() with a generated filename
            $ext     = $file->getClientExtension() ?: pathinfo($file->getName(), PATHINFO_EXTENSION);
            $ext     = $ext ? ('.' . strtolower($ext)) : '';
            $newName = 'reimb_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . $ext;
            $ok = $file->move(WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'reimbursements', $newName, true);
            if (!$ok) {
                log_message('error', 'Failed to move reimbursement receipt to reimbursements directory.');
                throw new \RuntimeException('Unable to save uploaded file.');
            }
            return $newName;
        }
    }

    private function buildReimbursementQuery($filters = [])
    {
        return $this->expenseModel->getReimbursedExpensesWithDetails($filters);
    }

    public function underProcessing()
    {
        $pendingQuery = $this->db->table('expenses e');
        $pendingQuery->select('
            e.id AS expense_id,
            e.amount AS expense_amount,
            e.description,
            e.receipt_path AS expense_receipt,
            e.retailor,
            e.purchased_by,
            u.firstname AS purchaser_firstname,
            u.lastname  AS purchaser_lastname
        ');
        $pendingQuery->join('users u', 'u.id = e.purchased_by', 'left');
        $pendingQuery->join('reimbursement_batch_items bi', 'bi.expense_id = e.id AND bi.unassigned_at IS NULL', 'left');
        $pendingQuery->where('e.reimbursement_id IS NULL', null, false);
        $pendingQuery->where('e.category !=', 'Donation');
        $pendingQuery->groupStart()
            ->where('e.status', 'approved')
            ->orWhere('e.status', 'Approved')
            ->orWhere('e.status', 'APPROVED')
        ->groupEnd();
        $pendingQuery->where('bi.id IS NULL', null, false);
        $pendingQuery->orderBy('e.created_at', 'DESC');

        $pendingRows = $pendingQuery->get()->getResultArray();

        $pendingItems = [];
        $itemsMap     = [];

        foreach ($pendingRows as $row) {
            $expenseId = (int) ($row['expense_id'] ?? 0);
            if ($expenseId <= 0) {
                continue;
            }
            $receiptUrl = $this->expenseReceiptUrl($row['expense_receipt'] ?? null);
            $fullName   = trim(($row['purchaser_firstname'] ?? '') . ' ' . ($row['purchaser_lastname'] ?? '')) ?: 'Unknown';

            $item = [
                'id'               => $expenseId,
                'expense_id'       => $expenseId,
                'reimbursement_id' => null,
                'expense_amount'   => (float) ($row['expense_amount'] ?? 0),
                'vendor'           => (string) ($row['retailor'] ?? ''),
                'description'      => trim((string) ($row['description'] ?? '')),
                'purchased_by'     => $fullName,
                'receipt_url'      => $receiptUrl,
                'batch_id'         => null,
                'batch_label'      => null,
                'admin_id'         => null,
                'admin_name'       => null,
                'batch_number'     => 0,
            ];

            $pendingItems[]       = $item;
            $itemsMap[$expenseId] = $item;
        }

        $assignedQuery = $this->db->table('reimbursement_batch_items bi');
        $assignedQuery->select('
            bi.id AS batch_item_id,
            bi.batch_id,
            bi.admin_id,
            bi.reimbursement_id,
            bi.expense_id,
            b.title AS batch_title,
            b.status AS batch_status,
            b.yearly_batch_number AS batch_year_number,
            e.amount AS expense_amount,
            e.description,
            e.receipt_path AS expense_receipt,
            e.retailor,
            purchaser.firstname AS purchaser_firstname,
            purchaser.lastname  AS purchaser_lastname,
            admin.firstname     AS admin_firstname,
            admin.lastname      AS admin_lastname
        ');
        $assignedQuery->join('reimbursement_batches b', 'b.id = bi.batch_id', 'inner');
        $assignedQuery->join('expenses e', 'e.id = bi.expense_id', 'inner');
        $assignedQuery->join('users purchaser', 'purchaser.id = e.purchased_by', 'left');
        $assignedQuery->join('users admin', 'admin.id = bi.admin_id', 'left');
        $assignedQuery->where('b.status', 'open');
        $assignedQuery->where('bi.unassigned_at IS NULL', null, false);
        $assignedQuery->where('e.reimbursement_id IS NULL', null, false);
        $assignedQuery->where('e.category !=', 'Donation');
        $assignedQuery->groupStart()
            ->where('e.status', 'approved')
            ->orWhere('e.status', 'Approved')
            ->orWhere('e.status', 'APPROVED')
        ->groupEnd();
        $assignedQuery->orderBy('b.opened_at', 'ASC');
        $assignedQuery->orderBy('bi.assigned_at', 'ASC');

        $assignedRows = $assignedQuery->get()->getResultArray();

        $batchStructures = [];

        foreach ($assignedRows as $row) {
            $expenseId = (int) ($row['expense_id'] ?? 0);
            $batchId   = (int) ($row['batch_id'] ?? 0);
            if ($expenseId <= 0 || $batchId <= 0) {
                continue;
            }

            $receiptUrl = $this->expenseReceiptUrl($row['expense_receipt'] ?? null);
            $purchaser  = trim(($row['purchaser_firstname'] ?? '') . ' ' . ($row['purchaser_lastname'] ?? '')) ?: 'Unknown';
            $yearlyNumber = (int) ($row['batch_year_number'] ?? 0);
            $batchLabel = trim((string) ($row['batch_title'] ?? ''));
            if ($batchLabel === '') {
                $fallbackNumber = $yearlyNumber > 0 ? $yearlyNumber : $batchId;
                $batchLabel = 'Batch #' . $fallbackNumber;
            }
            $adminId    = $row['admin_id'] ? (int) $row['admin_id'] : 0;
            $adminName  = $adminId > 0
                ? (trim(($row['admin_firstname'] ?? '') . ' ' . ($row['admin_lastname'] ?? '')) ?: 'Admin')
                : 'Unassigned';
            $batchStatus = strtolower(trim((string) ($row['batch_status'] ?? ''))) ?: 'open';

            $item = [
                'id'               => $expenseId,
                'expense_id'       => $expenseId,
                'reimbursement_id' => $row['reimbursement_id'] ? (int) $row['reimbursement_id'] : null,
                'expense_amount'   => (float) ($row['expense_amount'] ?? 0),
                'vendor'           => (string) ($row['retailor'] ?? ''),
                'description'      => trim((string) ($row['description'] ?? '')),
                'purchased_by'     => $purchaser,
                'receipt_url'      => $receiptUrl,
                'batch_id'         => $batchId,
                'batch_label'      => $batchLabel,
                'admin_id'         => $adminId ?: null,
                'admin_name'       => $adminId > 0 ? $adminName : null,
                'batch_number'     => $batchId,
            ];

            $itemsMap[$expenseId] = $item;

            if (!isset($batchStructures[$batchId])) {
                $batchStructures[$batchId] = [
                    'batchId'            => $batchId,
                    'label'              => $batchLabel,
                    'slots'              => [],
                    'status'             => $batchStatus,
                    'yearly_batch_number'=> $yearlyNumber,
                ];
            } else {
                $batchStructures[$batchId]['status'] = $batchStatus;
                if ($yearlyNumber > 0) {
                    $batchStructures[$batchId]['yearly_batch_number'] = $yearlyNumber;
                }
            }

            $slotKey = $adminId ?: 0;
            if (!isset($batchStructures[$batchId]['slots'][$slotKey])) {
                $batchStructures[$batchId]['slots'][$slotKey] = [
                    'admin_id'   => $adminId ?: 0,
                    'admin_name' => $adminId > 0 ? $adminName : 'Unassigned',
                    'items'      => [],
                    'check_file' => null,
                ];
            }
            $batchStructures[$batchId]['slots'][$slotKey]['items'][] = $expenseId;
        }

        $adminFileMap = [];
        if (!empty($batchStructures)) {
            $batchIds = array_keys($batchStructures);
            $files = $this->batchAdminFileModel
            ->select('batch_id, admin_id, filename, original_filename')
            ->whereIn('batch_id', $batchIds)
            ->findAll();
            foreach ($files as $file) {
                $batchId = (int) ($file['batch_id'] ?? 0);
                $adminId = (int) ($file['admin_id'] ?? 0);
                if ($batchId <= 0) {
                    continue;
                }
                $adminFileMap[$batchId][$adminId] = [
                    'filename'          => $file['filename'],
                    'original_filename' => $file['original_filename'] ?? null,
                'url'               => base_url('reimbursements/batch/admin-file/' . rawurlencode($file['filename']) . '/inline'),
            ];
        }
        }

        foreach ($batchStructures as $batchId => &$batch) {
            foreach ($batch['slots'] as $slotKey => &$slot) {
                $adminId = $slot['admin_id'] ?? 0;
                $slot['check_file'] = $adminFileMap[$batchId][$adminId] ?? null;
            }
            unset($slot);
        }
        unset($batch);

        $existingBatches = array_map(static function (array $batch) {
            $normalizedSlots = array_map(static function (array $slot) {
                $slot['items'] = array_values(array_map('intval', $slot['items']));
                return $slot;
            }, array_values($batch['slots']));

            $itemIds = [];
            foreach ($normalizedSlots as $slot) {
                $itemIds = array_merge($itemIds, $slot['items']);
            }

            return [
                'batchId'            => $batch['batchId'],
                'batchNumber'        => $batch['batchId'],
                'label'              => $batch['label'],
                'sequence'           => $batch['yearly_batch_number'] ?? 0,
                'yearly_batch_number'=> $batch['yearly_batch_number'] ?? 0,
                'slots'              => $normalizedSlots,
                'itemIds'            => array_values(array_unique($itemIds)),
                'status'             => $batch['status'] ?? 'open',
            ];
        }, array_values($batchStructures));

        $admins = $this->adminRoster();

        return view('reimbursements/under_processing', [
            'pendingItems'    => $pendingItems,
            'existingBatches' => $existingBatches,
            'adminUsers'      => $admins,
            'itemsPayload'    => array_values($itemsMap),
            'createBatchUrl'  => base_url('reimbursements/batch/create'),
            'checkUploadUrl'  => base_url('reimbursements/batch/admin-file/upload'),
        ]);
    }

    public function markDonation()
    {
        if (strtolower($this->request->getMethod()) !== 'post') {
            return $this->response->setStatusCode(405)->setJSON([
                'success' => false,
                'error'   => 'Method not allowed',
            ]);
        }

        $expenseId = (int) $this->request->getPost('expense_id');
        if ($expenseId <= 0) {
            return $this->response->setStatusCode(422)->setJSON([
                'success' => false,
                'error'   => 'Invalid expense id.',
            ]);
        }

        $expense = $this->expenseModel->find($expenseId);
        if (!$expense) {
            return $this->response->setStatusCode(404)->setJSON([
                'success' => false,
                'error'   => 'Expense not found.',
            ]);
        }

        if (!empty($expense['reimbursement_id'])) {
            return $this->response->setStatusCode(409)->setJSON([
                'success' => false,
                'error'   => 'Expense is already tied to a reimbursement.',
            ]);
        }

        $userId = (int) (session()->get('user_id') ?? 0);
        $now    = date('Y-m-d H:i:s');

        $this->db->transBegin();
        try {
            $this->batchItemModel
                ->where('expense_id', $expenseId)
                ->where('unassigned_at IS NULL', null, false)
                ->set('unassigned_at', $now)
                ->update();

            $this->expenseModel->update($expenseId, [
                'category'        => 'Donation',
                'status'          => 'approved',
                'status_reason'   => 'Marked as Donation (non-reimbursable).',
                'approved_by'     => $userId ?: null,
                'updated_by'      => $userId ?: null,
                'reimbursement_id'=> null,
            ]);
        } catch (\Throwable $e) {
            $this->db->transRollback();
            log_message('error', 'Failed to mark expense #{expense} as donation: {msg}', [
                'expense' => $expenseId,
                'msg'     => $e->getMessage(),
            ]);
            return $this->response->setStatusCode(500)->setJSON([
                'success' => false,
                'error'   => 'Unable to mark donation right now.',
            ]);
        }

        if (!$this->db->transStatus()) {
            $this->db->transRollback();
            return $this->response->setStatusCode(500)->setJSON([
                'success' => false,
                'error'   => 'Unable to mark donation right now.',
            ]);
        }
        $this->db->transCommit();

        $newHash = function_exists('csrf_hash') ? csrf_hash() : null;
        return $this->response
            ->setHeader('X-CSRF-HASH', (string) $newHash)
            ->setJSON([
                'success'   => true,
                'csrf_hash' => $newHash,
            ]);
    }

    public function createBatch()
    {
        if (strtolower($this->request->getMethod()) !== 'post') {
            return $this->response->setStatusCode(405)->setJSON([
                'success' => false,
                'error'   => 'Method not allowed',
            ]);
        }

        $title  = trim((string) ($this->request->getPost('title') ?? ''));
        $userId = (int) (session()->get('user_id') ?? 0);
        $now    = date('Y-m-d H:i:s');
        $sequence = $this->nextYearlyBatchNumberForSchoolYear();

        $data = [
            'title'       => $title !== '' ? $title : null,
            'status'      => 'open',
            'created_by'  => $userId ?: null,
            'opened_at'   => $now,
            'school_year' => $this->schoolYear,
            'semester'    => $this->semester,
            'yearly_batch_number' => $sequence,
        ];

        try {
            $this->batchModel->insert($data);
            $batchId = (int) $this->batchModel->getInsertID();
        } catch (\Throwable $e) {
            log_message('error', 'Failed to create reimbursement batch: {msg}', ['msg' => $e->getMessage()]);
            return $this->response->setStatusCode(500)->setJSON([
                'success' => false,
                'error'   => 'Unable to create batch right now.',
            ]);
        }

        if ($batchId <= 0) {
            return $this->response->setStatusCode(500)->setJSON([
                'success' => false,
                'error'   => 'Failed to create batch.',
            ]);
        }

        $label = $title !== '' ? $title : 'Batch #' . $sequence;
        if ($title === '') {
            $this->batchModel->update($batchId, ['title' => $label]);
        }

        $newHash = function_exists('csrf_hash') ? csrf_hash() : null;

        return $this->response
            ->setHeader('X-CSRF-HASH', (string) $newHash)
            ->setJSON([
                'success'  => true,
                'batch_id' => $batchId,
                'label'    => $label,
                'sequence' => $sequence,
                'csrf_hash'=> $newHash,
            ]);
    }

public function updateBatchAssignment()
{
    if (strtolower($this->request->getMethod()) !== 'post') {
        return $this->response->setStatusCode(405)->setJSON([
            'success' => false,
            'error'   => 'Method not allowed',
        ]);
    }

    $expenseId = (int) $this->request->getPost('expense_id');

    $batchIdRaw = $this->request->getPost('batch_id');
    $batchId    = (int) $batchIdRaw;

    $fallbackBatchNumber = $this->request->getPost('batch_number');
    if ($batchId <= 0 && $fallbackBatchNumber !== null && trim((string) $fallbackBatchNumber) !== '') {
        $batchId = (int) $fallbackBatchNumber;
    }

    $adminIdRaw = $this->request->getPost('admin_id');
    $adminId    = ($adminIdRaw === '' || $adminIdRaw === null) ? null : (int) $adminIdRaw;

    $reimbursementId = (int) $this->request->getPost('reimbursement_id') ?: null;

    if ($expenseId <= 0) {
        return $this->response->setStatusCode(422)->setJSON([
            'success' => false,
            'error'   => 'Invalid expense id.',
        ]);
    }

    $now = date('Y-m-d H:i:s');

    $newHashResponse = function (array $payload = []) {
        $newHash = function_exists('csrf_hash') ? csrf_hash() : null;

        return $this->response
            ->setHeader('X-CSRF-HASH', (string) $newHash)
            ->setJSON(array_merge($payload, [
                'csrf_hash' => $newHash,
            ]));
    };

    $this->db->transBegin();

    try {
        $activeItem = $this->batchItemModel
            ->where('expense_id', $expenseId)
            ->where('unassigned_at IS NULL', null, false)
            ->first();

        /**
         * If no batch is provided, remove the item from active batch processing.
         * This means "unassigned from batch", not "unassigned admin".
         */
        if ($batchId <= 0) {
            if ($activeItem) {
                $this->batchItemModel->update((int) $activeItem['id'], [
                    'unassigned_at' => $now,
                ]);

                if (!$reimbursementId && !empty($activeItem['reimbursement_id'])) {
                    $reimbursementId = (int) $activeItem['reimbursement_id'];
                }
            }

            if (!$this->db->transStatus()) {
                throw new \RuntimeException('Transaction failed while unassigning batch item.');
            }

            $this->db->transCommit();

            return $newHashResponse([
                'success'          => true,
                'batch_id'         => 0,
                'admin_id'         => null,
                'reimbursement_id' => $reimbursementId,
            ]);
        }

        $batch = $this->batchModel->find($batchId);
        if (!$batch || strtolower((string) ($batch['status'] ?? 'open')) !== 'open') {
            $this->db->transRollback();

            return $this->response->setStatusCode(404)->setJSON([
                'success' => false,
                'error'   => 'Batch not found or already closed.',
            ]);
        }

        if (!$reimbursementId) {
            if ($activeItem && !empty($activeItem['reimbursement_id'])) {
                $reimbursementId = (int) $activeItem['reimbursement_id'];
            } else {
                $reimbursementId = $this->lookupReimbursementId($expenseId);
            }
        }

        $activeSameBatch = $activeItem && (int) ($activeItem['batch_id'] ?? 0) === $batchId;
        $currentAdmin    = $activeItem ? (int) ($activeItem['admin_id'] ?? 0) : null;
        $requestedAdmin  = $adminId ?? 0;

        /**
         * If the item is already active in the same batch/admin slot, just return success.
         */
        if ($activeSameBatch && $currentAdmin === $requestedAdmin) {
            if (!$this->db->transStatus()) {
                throw new \RuntimeException('Transaction failed while checking existing assignment.');
            }

            $this->db->transCommit();

            return $newHashResponse([
                'success'          => true,
                'batch_id'         => $batchId,
                'admin_id'         => $adminId,
                'reimbursement_id' => $activeItem['reimbursement_id'] ?? $reimbursementId,
            ]);
        }

        /**
         * If same batch but different admin, update the existing active row.
         *
         * Important:
         * admin_id = null means active in the batch but not assigned to an admin.
         * unassigned_at != null means removed from active batch processing.
         */
        if ($activeSameBatch) {
            $this->batchItemModel->update((int) $activeItem['id'], [
                'admin_id'         => $adminId,
                'assigned_at'      => $now,
                'reimbursement_id' => $reimbursementId,
                'unassigned_at'    => null,
            ]);

            if (!$this->db->transStatus()) {
                throw new \RuntimeException('Transaction failed while updating existing assignment.');
            }

            $this->db->transCommit();

            return $newHashResponse([
                'success'          => true,
                'batch_id'         => $batchId,
                'admin_id'         => $adminId,
                'reimbursement_id' => $reimbursementId,
            ]);
        }

        /**
         * If the item is active in another batch, soft-unassign that row first.
         */
        if ($activeItem) {
            $this->batchItemModel->update((int) $activeItem['id'], [
                'unassigned_at' => $now,
            ]);
        }

        /**
         * Critical fix:
         * Check whether this expense was previously assigned to this batch.
         * Because uq_batch_expense(batch_id, expense_id) blocks duplicate rows,
         * we must reactivate/update the old row instead of inserting again.
         */
        $existingBatchItem = $this->batchItemModel
            ->where('batch_id', $batchId)
            ->where('expense_id', $expenseId)
            ->first();

        $assignmentData = [
            'batch_id'         => $batchId,
            'expense_id'       => $expenseId,
            'reimbursement_id' => $reimbursementId,
            'admin_id'         => $adminId,
            'assigned_at'      => $now,
            'unassigned_at'    => null,
            'school_year'      => $this->schoolYear,
            'semester'         => $this->semester,
        ];

        if ($existingBatchItem) {
            $this->batchItemModel->update((int) $existingBatchItem['id'], $assignmentData);
        } else {
            $this->batchItemModel->insert($assignmentData);
        }

        if (!$this->db->transStatus()) {
            throw new \RuntimeException('Transaction failed while saving assignment.');
        }

        $this->db->transCommit();

        return $newHashResponse([
            'success'          => true,
            'batch_id'         => $batchId,
            'admin_id'         => $adminId,
            'reimbursement_id' => $reimbursementId,
        ]);
    } catch (\Throwable $e) {
        $this->db->transRollback();

        log_message('error', 'Failed to update batch assignment for expense #{expense}, batch #{batch}: {msg}', [
            'expense' => $expenseId,
            'batch'   => $batchId,
            'msg'     => $e->getMessage(),
        ]);

        return $this->response->setStatusCode(500)->setJSON([
            'success' => false,
            'error'   => 'Unable to update batch assignment right now.',
        ]);
    }
}

    public function lockBatch()
    {
        if (strtolower($this->request->getMethod()) !== 'post') {
            return $this->response->setStatusCode(405)->setJSON([
                'success' => false,
                'error'   => 'Method not allowed',
            ]);
        }

        $batchId = (int) $this->request->getPost('batch_id');
        if ($batchId <= 0) {
            return $this->response->setStatusCode(422)->setJSON([
                'success' => false,
                'error'   => 'Invalid batch id.',
            ]);
        }

        $batch = $this->batchModel->find($batchId);
        if (!$batch || strtolower((string) ($batch['status'] ?? '')) !== 'open') {
            return $this->response->setStatusCode(404)->setJSON([
                'success' => false,
                'error'   => 'Batch not found or already closed.',
            ]);
        }

        $adminRows = $this->batchItemModel
            ->select('DISTINCT COALESCE(admin_id, 0) AS admin_id')
            ->where('batch_id', $batchId)
            ->where('unassigned_at IS NULL', null, false)
            ->get()
            ->getResultArray();
        $adminIds = array_values(array_unique(array_map(static fn ($row) => isset($row['admin_id']) ? (int) $row['admin_id'] : 0, $adminRows)));
        $requiredAdmins = array_filter($adminIds, static fn ($adminId) => $adminId > 0);
        if (!empty($requiredAdmins)) {
            $uploadedFiles = $this->batchAdminFileModel
                ->select('admin_id')
                ->where('batch_id', $batchId)
                ->whereIn('admin_id', $requiredAdmins)
                ->findAll();
            $uploadedAdminIds = array_values(array_unique(array_map(static fn ($file) => isset($file['admin_id']) ? (int) $file['admin_id'] : 0, $uploadedFiles)));
            foreach ($requiredAdmins as $adminId) {
                if (!in_array($adminId, $uploadedAdminIds, true)) {
                    return $this->response->setStatusCode(409)->setJSON([
                        'success' => false,
                        'error'   => 'All admin sections must have a check file before submitting the batch.',
                    ]);
                }
            }
        }

        $this->db->transBegin();

        // Create reimbursement records for any batch items that don't yet have one
        $items = $this->db->table('reimbursement_batch_items bi')
            ->select('bi.id AS batch_item_id, bi.reimbursement_id AS batch_reimb_id, bi.expense_id, e.amount, e.purchased_by, e.description, e.reimbursement_id AS expense_reimb_id, e.school_year AS expense_school_year, e.semester AS expense_semester')
            ->join('expenses e', 'e.id = bi.expense_id', 'inner')
            ->where('bi.batch_id', $batchId)
            ->where('bi.unassigned_at IS NULL', null, false)
            ->get()
            ->getResultArray();

        $now = date('Y-m-d H:i:s');
        $userId = (int) (session()->get('user_id') ?? 0);

        try {
            foreach ($items as $item) {
                $expenseId   = (int) ($item['expense_id'] ?? 0);
                $recipientId = (int) ($item['purchased_by'] ?? 0);
                if ($expenseId <= 0 || $recipientId <= 0) {
                    continue;
                }

                $reimbId = $item['batch_reimb_id'] ?: ($item['expense_reimb_id'] ?: $this->lookupReimbursementId($expenseId));
                if (!$reimbId) {
                    $payload = [
                        'expense_id'           => $expenseId,
                        'amount'               => (float) ($item['amount'] ?? 0),
                        'reimbursed_to'        => $recipientId,
                        'approved_by'          => $userId ?: null,
                        'description'          => trim((string) ($item['description'] ?? '')),
                        'status'               => 'Paid',
                        'added_by'             => $userId ?: null,
                        'school_year'          => $item['expense_school_year'] ?: $this->schoolYear,
                        'semester'             => $item['expense_semester'] ?: $this->semester,
                        'reimbursement_method' => 'Check',
                        'batch_number'         => $batchId,
                        'created_at'           => $now,
                        'updated_at'           => $now,
                    ];
                    $reimbId = $this->reimbModel->insert($payload);
                }

                if ($reimbId) {
                    $this->reimbModel->update($reimbId, [
                        'batch_number' => $batchId,
                        'approved_by'  => $userId ?: null,
                        'status'       => 'Paid',
                    ]);
                    $this->expenseModel->update($expenseId, ['reimbursement_id' => $reimbId]);
                    if (!empty($item['batch_item_id'])) {
                        $this->batchItemModel->update((int) $item['batch_item_id'], ['reimbursement_id' => $reimbId]);
                    }
                }
            }

            $update = [
                'status'    => 'closed',
                'closed_at' => $now,
            ];

            if ($userId > 0) {
                $update['closed_by'] = $userId;
            }

            $this->batchModel->update($batchId, $update);
        } catch (\Throwable $e) {
            $this->db->transRollback();
            log_message('error', 'Failed to lock reimbursement batch #{batch}: {msg}', [
                'batch' => $batchId,
                'msg'   => $e->getMessage(),
            ]);
            return $this->response->setStatusCode(500)->setJSON([
                'success' => false,
                'error'   => 'Unable to lock batch right now.',
            ]);
        }

        if (!$this->db->transStatus()) {
            $this->db->transRollback();
            return $this->response->setStatusCode(500)->setJSON([
                'success' => false,
                'error'   => 'Unable to lock batch right now.',
            ]);
        }
        $this->db->transCommit();

        $newHash = function_exists('csrf_hash') ? csrf_hash() : null;
        return $this->response
            ->setHeader('X-CSRF-HASH', (string) $newHash)
            ->setJSON([
                'success'  => true,
                'batch_id' => $batchId,
                'status'   => 'closed',
                'csrf_hash'=> $newHash,
            ]);
    }

    public function uploadBatchAdminFile()
    {
        if (strtolower($this->request->getMethod()) !== 'post') {
            return $this->response->setStatusCode(405)->setJSON([
                'success' => false,
                'error'   => 'Method not allowed',
            ]);
        }

        $batchId = (int) $this->request->getPost('batch_id');
        $adminId = (int) $this->request->getPost('admin_id');
        $file    = $this->request->getFile('check_file');

        if ($batchId <= 0 || $adminId < 0) {
            return $this->response->setStatusCode(422)->setJSON([
                'success' => false,
                'error'   => 'Invalid batch or admin specified.',
            ]);
        }

        if (!$file instanceof UploadedFile || !$file->isValid()) {
            return $this->response->setStatusCode(422)->setJSON([
                'success' => false,
                'error'   => 'Please provide a valid file.',
            ]);
        }

        $batch = $this->batchModel->find($batchId);
        if (!$batch) {
            return $this->response->setStatusCode(404)->setJSON([
                'success' => false,
                'error'   => 'Batch not found.',
            ]);
        }
        if (strtolower((string) ($batch['status'] ?? '')) !== 'open') {
            return $this->response->setStatusCode(409)->setJSON([
                'success' => false,
                'error'   => 'Cannot upload files for a locked batch.',
            ]);
        }

        $uploadDir = $this->ensureUploadSubdir('reimbursements');
        $newName   = $file->getRandomName();
        try {
            $file->move($uploadDir, $newName);
        } catch (\Throwable $e) {
            log_message('error', 'Failed to save batch check file: {msg}', ['msg' => $e->getMessage()]);
            return $this->response->setStatusCode(500)->setJSON([
                'success' => false,
                'error'   => 'Unable to store the uploaded file.',
            ]);
        }

        $existing = $this->batchAdminFileModel
            ->where('batch_id', $batchId)
            ->where('admin_id', $adminId)
            ->first();
        if ($existing && !empty($existing['filename'])) {
            $oldFile = $uploadDir . DIRECTORY_SEPARATOR . $existing['filename'];
            if (is_file($oldFile)) {
                @unlink($oldFile);
            }
        }

        $now = date('Y-m-d H:i:s');
        $userId = (int) (session()->get('user_id') ?? 0);
        $payload = [
            'batch_id'         => $batchId,
            'admin_id'         => $adminId,
            'filename'         => $newName,
            'original_filename'=> $file->getClientName(),
            'uploaded_at'      => $now,
            'uploaded_by'      => $userId ?: null,
        ];

        if ($existing) {
            $this->batchAdminFileModel->update((int) $existing['id'], $payload);
        } else {
            $this->batchAdminFileModel->insert($payload);
        }

        $newHash = function_exists('csrf_hash') ? csrf_hash() : null;
        return $this->response
            ->setHeader('X-CSRF-HASH', (string) $newHash)
            ->setJSON([
                'success'           => true,
                'batch_id'          => $batchId,
                'admin_id'          => $adminId,
                'filename'          => $newName,
                'original_filename' => $file->getClientName(),
                'url'               => base_url('reimbursements/batch/admin-file/' . rawurlencode($newName) . '/inline'),
                'csrf_hash'         => $newHash,
            ]);
    }

    public function serveAdminCheckFile(string $filename, string $mode = 'inline')
    {
        $safeName = basename(trim($filename));
        if ($safeName === '') {
            throw new PageNotFoundException('File not specified.');
        }

        $path = $this->ensureUploadSubdir('reimbursements') . DIRECTORY_SEPARATOR . $safeName;
        if (!is_file($path)) {
            throw new PageNotFoundException('File not found.');
        }

        $mime = mime_content_type($path) ?: 'application/octet-stream';
        $disposition = strtolower(trim((string) $mode)) === 'download' ? 'attachment' : 'inline';

        // Use CI's download helper to ensure correct headers/output; override to allow inline display
        return $this->response
            ->download($path, null)
            ->setFileName($safeName)
            ->setHeader('Content-Type', $mime)
            ->setHeader('Content-Disposition', sprintf('%s; filename="%s"', $disposition, $safeName));
    }

    private function expenseReceiptUrl(?string $filename): ?string
    {
        if (!$filename) return null;
        $safe = basename(trim($filename));
        return $safe !== '' ? site_url('receipts/' . $safe) : null;
    }

    private function reimbReceiptUrl(?string $filename): ?string
    {
        if (!$filename) return null;
        $safe = basename(trim($filename));
        return $safe !== '' ? site_url('reimbreceipts/' . $safe) : null;
    }

    private function sanitizeAttachmentComponent(string $value): string
    {
        $clean = preg_replace('/[^A-Za-z0-9]+/', '_', trim((string) $value));
        $clean = trim($clean, '_');
        return $clean === '' ? 'unknown' : $clean;
    }

    private function formatAttachmentDate(?string $value): string
    {
        if (!$value) {
            return 'unknown';
        }
        $timestamp = strtotime($value);
        if ($timestamp === false) {
            return 'unknown';
        }
        return date('Ymd', $timestamp);
    }

    private function uniqueZipEntryName(string $base, string $extension, array &$used): string
    {
        $candidate = $base . $extension;
        $counter = 1;
        while (isset($used[$candidate])) {
            $candidate = $base . '_' . $counter . $extension;
            $counter++;
        }
        $used[$candidate] = true;
        return $candidate;
    }

    private function formatCsvDate(?string $value): string
    {
        if (!$value) {
            return '';
        }
        $timestamp = strtotime($value);
        if ($timestamp === false) {
            return '';
        }
        return date('m-d-Y', $timestamp);
    }

    private function fetchBatchReceiptRows(int $batchId, array $receiptIds = []): array
    {
        $builder = $this->db->table('reimbursement_batch_items bi');
        $builder->select('e.id AS expense_id, e.retailor, e.amount, e.description, e.receipt_path AS receipt_filename, e.date_of_purchase AS purchase_date, u.firstname AS purchaser_firstname, u.lastname AS purchaser_lastname');
        $builder->join('expenses e', 'e.id = bi.expense_id', 'inner');
        $builder->join('users u', 'u.id = e.purchased_by', 'left');
        $builder->where('bi.batch_id', $batchId);
        if (!empty($receiptIds)) {
            $builder->whereIn('e.id', $receiptIds);
        }
        $rows = $builder->get()->getResultArray();
        return array_values(array_filter($rows, static function ($row) {
            return !empty($row['receipt_filename']);
        }));
    }

    private function writeBatchCsvMetadata($handle, string $batchLabel, array $batch): void
    {
        $closedAtRaw = $batch['closed_at'] ?? null;
        fputcsv($handle, ['Batch Title', $batchLabel]);
        fputcsv($handle, ['School Year', $batch['school_year'] ?? '', 'Semester', $batch['semester'] ?? '', 'Closed At', $this->formatCsvDate($closedAtRaw)]);
        fputcsv($handle, []);
        fputcsv($handle, ['Amount', 'Credit', 'Paid', 'Balance', 'Date', 'By Who', 'Vendor', 'Description']);
    }

    private function writeBatchCsvSummary($handle, array $receiptRows, string $totalsDate = ''): void
    {
        $receiptTotalsByRecipient = [];
        foreach ($receiptRows as $row) {
            $recipientName = trim(($row['purchaser_firstname'] ?? '') . ' ' . ($row['purchaser_lastname'] ?? ''));
            if ($recipientName === '') {
                $recipientName = 'Unknown';
            }
            $vendor = trim((string) ($row['retailor'] ?? ''));
            if ($vendor === '') {
                $vendor = 'Vendor N/A';
            }
            $dateText = $this->formatCsvDate($row['purchase_date'] ?? null);
            $descriptionValue = trim((string) ($row['description'] ?? ''));
            $descriptionParts = $descriptionValue !== '' ? [$descriptionValue] : ['Receipt'];
            $amountValue = (float) ($row['amount'] ?? 0);
            $receiptTotalsByRecipient[$recipientName] = ($receiptTotalsByRecipient[$recipientName] ?? 0.0) + $amountValue;
            fputcsv($handle, [
                number_format($amountValue, 2, '.', ''),
                '',
                number_format($amountValue, 2, '.', ''),
                '0.00',
                $dateText,
                $recipientName,
                $vendor,
                implode(' | ', $descriptionParts),
            ]);
        }
        if (!empty($receiptTotalsByRecipient)) {
            fputcsv($handle, []);
            fputcsv($handle, ['Totals by Recipient', '', '', '', '', '', '', '']);
            foreach ($receiptTotalsByRecipient as $recipient => $total) {
                $formatted = number_format($total, 2, '.', '');
                fputcsv($handle, [
                $formatted,
                '',
                $formatted,
                '0.00',
                $totalsDate,
                $recipient,
                '',
                '',
                ]);
            }
        }
    }

    private function parseAttachmentIds($raw): array
    {
        if (is_array($raw)) {
            $decoded = $raw;
        } else {
            $decoded = json_decode((string) $raw, true);
        }
        if (!is_array($decoded)) {
            return [];
        }
        $ids = [];
        foreach ($decoded as $entry) {
            $id = (int) ($entry ?? 0);
            if ($id > 0) {
                $ids[$id] = $id;
            }
        }
        return array_values($ids);
    }

    public function index()
    {
        $semester   = $this->request->getGet('semester') ?: null;
        $status     = $this->request->getGet('status') ?: null;
        $filterYear = $this->request->getGet('school_year') ?: $this->schoolYear;
        $userId     = $this->request->getGet('user_id') ?: null;
        $hasBatchSchoolYear = $this->db->fieldExists('school_year', 'reimbursement_batches');
        $hasExpenseSchoolYear = $this->db->fieldExists('school_year', 'expenses');
        $hasExpenseSemester = $this->db->fieldExists('semester', 'expenses');
        $hasReimbursementSchoolYear = $this->db->fieldExists('school_year', 'reimbursements');

        $filters = [
            'school_year' => $filterYear,
            'semester'    => $semester,
            'status'      => $status,    // e.g., 'Paid'
            'user_id'     => $userId,
        ];

        $expenses = $this->expenseModel
            ->getReimbursedExpensesWithDetails($filters)
            ->get()
            ->getResultArray();

        // Build closed batch summaries/details (all closed batches)
        $batchSummaries = [];
        $batchDetails   = [];
        $batchSelect = "
                b.id AS batch_id,
                b.title AS batch_title,
                b.yearly_batch_number,
                b.closed_at,
                e.id AS expense_id,
                e.amount AS expense_amount,
                e.description,
                e.retailor,
                e.receipt_path AS expense_receipt,
                e.date_of_purchase AS expense_date_of_purchase,
                e.purchased_by,
                u.firstname AS purchaser_firstname,
                u.lastname  AS purchaser_lastname,
                r.amount AS reimb_amount
            ";
        if ($hasBatchSchoolYear) {
            $batchSelect .= ', b.school_year AS batch_school_year';
        }

        $batchQuery = $this->db->table('reimbursement_batches b')
            ->select($batchSelect)
            ->join('reimbursement_batch_items bi', 'bi.batch_id = b.id', 'inner')
            ->join('expenses e', 'e.id = bi.expense_id', 'inner')
            ->join('users u', 'u.id = e.purchased_by', 'left')
            ->join('reimbursements r', 'r.expense_id = e.id', 'left')
            ->where('b.status', 'closed')
            ->where('bi.unassigned_at IS NULL', null, false);

        if (!empty($filterYear)) {
            if ($hasBatchSchoolYear && $hasExpenseSchoolYear) {
                $batchQuery->groupStart()
                    ->where('b.school_year', $filterYear)
                    ->orWhere('e.school_year', $filterYear)
                ->groupEnd();
            } elseif ($hasBatchSchoolYear) {
                $batchQuery->where('b.school_year', $filterYear);
            } elseif ($hasExpenseSchoolYear) {
                $batchQuery->where('e.school_year', $filterYear);
            }
        }
        if (!empty($userId)) {
            $batchQuery->where('e.purchased_by', $userId);
        }

        $batchRows = $batchQuery->get()->getResultArray();
        $batchAttachments = [];
        foreach ($batchRows as $row) {
            $bid = (int) ($row['batch_id'] ?? 0);
            if ($bid <= 0) {
                continue;
            }
            $labelNumber = (int) ($row['yearly_batch_number'] ?? $bid);
            if (!isset($batchSummaries[$bid])) {
                $batchSummaries[$bid] = [
                    'batch_id'  => $bid,
                    'title'     => trim((string) ($row['batch_title'] ?? '')) ?: ('Batch #' . $labelNumber),
                    'sequence'  => $labelNumber,
                    'closed_at' => $row['closed_at'] ?? null,
                    'amount'    => 0.0,
                    'items'     => 0,
                ];
            }
            $amount = $row['reimb_amount'] ?? $row['expense_amount'] ?? 0;
            $batchSummaries[$bid]['amount'] += (float) $amount;
            $batchSummaries[$bid]['items']  += 1;

            if (!isset($batchDetails[$bid])) {
                $batchDetails[$bid] = [
                    'title'      => $batchSummaries[$bid]['title'],
                    'sequence'   => $labelNumber,
                    'closed_at'  => $batchSummaries[$bid]['closed_at'],
                    'items'      => [],
                    'checks'     => [],
                ];
            }
            if (!isset($batchAttachments[$bid])) {
                $batchAttachments[$bid] = [
                    'receipts' => [],
                    'checks'   => [],
                ];
            }
            $batchDetails[$bid]['items'][] = [
                'amount'       => (float) $amount,
                'description'  => $row['description'] ?? '',
                'vendor'       => $row['retailor'] ?? '',
                'purchased_by' => trim(($row['purchaser_firstname'] ?? '') . ' ' . ($row['purchaser_lastname'] ?? '')),
                'receipt_url'  => $this->expenseReceiptUrl($row['expense_receipt'] ?? null),
            ];
            $receiptFilename = trim((string) ($row['expense_receipt'] ?? ''));
            if ($receiptFilename !== '') {
                $batchAttachments[$bid]['receipts'][] = [
                    'expense_id'           => (int) ($row['expense_id'] ?? 0),
                    'receipt_filename'     => $receiptFilename,
                    'receipt_url'          => $this->expenseReceiptUrl($receiptFilename),
                    'purchaser_firstname'  => $row['purchaser_firstname'] ?? '',
                    'purchaser_lastname'   => $row['purchaser_lastname'] ?? '',
                    'vendor'               => $row['retailor'] ?? '',
                    'description'          => $row['description'] ?? '',
                    'amount'               => (float) ($row['expense_amount'] ?? 0),
                    'date_of_purchase'     => $row['expense_date_of_purchase'] ?? '',
                ];
            }
        }

        // Attach uploaded admin check files for all closed batches
        if (!empty($batchSummaries)) {
            $batchIds = array_keys($batchSummaries);
            $files = $this->batchAdminFileModel
                ->select('id AS file_id, batch_id, admin_id, filename, original_filename, uploaded_at')
                ->whereIn('batch_id', $batchIds)
                ->findAll();
            $adminNames = $this->adminRoster();
            $adminNameMap = [];
            foreach ($adminNames as $admin) {
                $adminNameMap[(int) $admin['id']] = trim(($admin['firstname'] ?? '') . ' ' . ($admin['lastname'] ?? ''));
            }
            foreach ($files as $file) {
                $bid = (int) ($file['batch_id'] ?? 0);
                if ($bid <= 0 || !isset($batchDetails[$bid])) {
                    continue;
                }
                $aid = (int) ($file['admin_id'] ?? 0);
                $batchDetails[$bid]['checks'][] = [
                    'admin' => $adminNameMap[$aid] ?? ($aid > 0 ? 'Admin #' . $aid : 'Unassigned'),
                    'filename' => $file['filename'],
                    'original' => $file['original_filename'] ?? $file['filename'],
                    'url' => base_url('reimbursements/batch/admin-file/' . rawurlencode($file['filename']) . '/inline'),
                ];
                if (!isset($batchAttachments[$bid])) {
                    $batchAttachments[$bid] = [
                        'receipts' => [],
                        'checks'   => [],
                    ];
                }
                $batchAttachments[$bid]['checks'][] = [
                    'file_id'           => (int) ($file['file_id'] ?? 0),
                    'filename'          => $file['filename'],
                    'original_filename' => $file['original_filename'] ?? $file['filename'],
                    'admin'             => $adminNameMap[$aid] ?? ($aid > 0 ? 'Admin #' . $aid : 'Unassigned'),
                    'url'               => base_url('reimbursements/batch/admin-file/' . rawurlencode($file['filename']) . '/inline'),
                    'uploaded_at'      => $file['uploaded_at'] ?? null,
                ];
            }
        }

        if (!empty($batchSummaries)) {
            $batchSummaries = array_values($batchSummaries);
            usort($batchSummaries, static function ($a, $b) {
                $aSeq = (int) ($a['sequence'] ?? 0);
                $bSeq = (int) ($b['sequence'] ?? 0);
                return $bSeq <=> $aSeq;
            });
        }

        $donationBatch = null;
        $donationDetails = [
            'items'  => [],
            'checks' => [],
        ];
        $donationSelect = '
                e.id AS expense_id,
                e.amount AS expense_amount,
                e.description,
                e.retailor,
                e.receipt_path AS expense_receipt,
                e.purchased_by,
                e.status,
                u.firstname AS purchaser_firstname,
                u.lastname  AS purchaser_lastname
            ';
        if ($hasExpenseSchoolYear) {
            $donationSelect .= ', e.school_year';
        }
        if ($hasExpenseSemester) {
            $donationSelect .= ', e.semester';
        }

        $donationQuery = $this->db->table('expenses e')
            ->select($donationSelect)
            ->join('users u', 'u.id = e.purchased_by', 'left')
            ->where('e.category', 'Donation');

        if (!empty($filterYear) && $hasExpenseSchoolYear) {
            $donationQuery->where('e.school_year', $filterYear);
        }
        if (!empty($semester) && $hasExpenseSemester) {
            $donationQuery->where('e.semester', $semester);
        }
        if (!empty($userId)) {
            $donationQuery->where('e.purchased_by', $userId);
        }
        if (!empty($status)) {
            $donationQuery->groupStart()
                ->where('e.status', $status)
                ->orWhere('e.status', strtolower($status))
                ->orWhere('e.status', strtoupper($status))
            ->groupEnd();
        }

        $donationRows = $donationQuery->orderBy('e.created_at', 'DESC')->get()->getResultArray();
        if (!empty($donationRows)) {
            $donationTotal = 0.0;
            foreach ($donationRows as $row) {
                $amount = (float) ($row['expense_amount'] ?? 0);
                $donationTotal += $amount;
                $purchaser = trim(($row['purchaser_firstname'] ?? '') . ' ' . ($row['purchaser_lastname'] ?? ''));
                if ($purchaser === '') {
                    $purchaser = 'Unknown';
                }
                $donationDetails['items'][] = [
                    'amount'       => $amount,
                    'description'  => $row['description'] ?? '',
                    'vendor'       => $row['retailor'] ?? '',
                    'purchased_by' => $purchaser,
                    'receipt_url'  => $this->expenseReceiptUrl($row['expense_receipt'] ?? null),
                ];
            }

            $donationBatch = [
                'title'  => 'Donations',
                'items'  => count($donationDetails['items']),
                'amount' => $donationTotal,
            ];
        }

        foreach ($expenses as &$e) {
            if (array_key_exists('expense_receipt', $e)) {
                $e['expense_receipt_url'] = $this->expenseReceiptUrl($e['expense_receipt']);
            } elseif (array_key_exists('receipt_path', $e)) {
                $e['expense_receipt_url'] = $this->expenseReceiptUrl($e['receipt_path']);
            }

            if (array_key_exists('reimb_receipt', $e)) {
                $e['reimb_receipt_url'] = $this->reimbReceiptUrl($e['reimb_receipt']);
            } elseif (array_key_exists('receipt_path_reimb', $e)) {
                $e['reimb_receipt_url'] = $this->reimbReceiptUrl($e['receipt_path_reimb']);
            }

            $recipientId = $e['reimb_recipient_id'] ?? ($e['reimbursed_to'] ?? null);
            $hasName     = trim(($e['reimb_firstname'] ?? '') . ' ' . ($e['reimb_lastname'] ?? '')) !== '';
            if (!$hasName) {
                $label = $this->specialRecipientLabel($recipientId);
                if ($label !== null) {
                    $e['reimb_firstname'] = $label;
                    $e['reimb_lastname']  = '';
                }
            }
        }
        unset($e);

        $users = $this->recipientOptions();

        $schoolYears = [];
        if ($hasReimbursementSchoolYear) {
            $years = $this->db->table('reimbursements')
                ->select('school_year')
                ->distinct()
                ->orderBy('school_year', 'DESC')
                ->get()
                ->getResultArray();
            $schoolYears = array_column($years, 'school_year');
        } elseif ($hasExpenseSchoolYear) {
            $years = $this->db->table('expenses')
                ->select('school_year')
                ->distinct()
                ->where('school_year IS NOT NULL', null, false)
                ->orderBy('school_year', 'DESC')
                ->get()
                ->getResultArray();
            $schoolYears = array_column($years, 'school_year');
        }

        // Add school years from fallback expenses (closed batches without reimbursements)
        if (!empty($fallbackRows)) {
            $fallbackYears = array_values(array_unique(array_filter(array_column($fallbackRows, 'school_year'))));
            $schoolYears = array_values(array_unique(array_merge($schoolYears, $fallbackYears)));
            rsort($schoolYears);
        }

        return view('reimbursements/index', [
            'expenses'     => $expenses,
            'users'        => $users,
            'schoolYears'  => $schoolYears,
            'batchSummaries' => $batchSummaries,
            'batchDetails' => $batchDetails,
            'batchAttachments' => $batchAttachments,
            'donationBatch' => $donationBatch,
            'donationDetails' => $donationDetails,
        ]);
    }

    public function sendBatchEmail()
    {
        $batchId = (int) ($this->request->getPost('batch_id') ?? 0);
        if ($batchId <= 0) {
            return $this->response->setStatusCode(400)->setJSON([
                'success' => false,
                'error'   => 'Batch not specified.',
            ]);
        }

        $recipientEmail = trim((string) ($this->request->getPost('recipient_email') ?? ''));
        if ($recipientEmail === '' || !filter_var($recipientEmail, FILTER_VALIDATE_EMAIL)) {
            return $this->response->setStatusCode(422)->setJSON([
                'success' => false,
                'error'   => 'Please provide a valid recipient email address.',
            ]);
        }

        $message = trim((string) ($this->request->getPost('message') ?? ''));
        $receiptIds = $this->parseAttachmentIds($this->request->getPost('receipts'));
        $checkIds   = $this->parseAttachmentIds($this->request->getPost('checks'));

        $batch = $this->batchModel->find($batchId);
        if (!$batch) {
            return $this->response->setStatusCode(404)->setJSON([
                'success' => false,
                'error'   => 'Requested batch was not found.',
            ]);
        }

        $receiptRows = !empty($receiptIds) ? $this->fetchBatchReceiptRows($batchId, $receiptIds) : [];

        $checkRows = [];
        if (!empty($checkIds)) {
            $checkRows = $this->batchAdminFileModel
                ->select('id AS file_id, admin_id, filename, original_filename, uploaded_at')
                ->where('batch_id', $batchId)
                ->whereIn('id', $checkIds)
                ->findAll();
            $adminLookup = [];
            foreach ($this->adminRoster() as $admin) {
                $adminLookup[(int) ($admin['id'] ?? 0)] = trim((string) (($admin['firstname'] ?? '') . ' ' . ($admin['lastname'] ?? '')));
            }
            foreach ($checkRows as &$row) {
                $aid = (int) ($row['admin_id'] ?? 0);
                $row['admin'] = $adminLookup[$aid] ?? ($aid > 0 ? 'Admin #' . $aid : 'Admin');
            }
            unset($row);
        }

        $tempDir = WRITEPATH . 'tmp';
        if (!is_dir($tempDir)) {
            mkdir($tempDir, 0755, true);
        }
        $cleanupPaths = [];

        $batchLabel = trim((string) ($batch['title'] ?? '')) ?: ('Batch #' . ((int) ($batch['yearly_batch_number'] ?? $batchId)));
        $batchFileLabel = $this->sanitizeAttachmentComponent($batchLabel);

        $csvPath = tempnam($tempDir, 'batch_csv_');
        $cleanupPaths[] = $csvPath;
        $csvHandle = fopen($csvPath, 'w');
        if (!$csvHandle) {
            if (is_file($csvPath)) {
                @unlink($csvPath);
            }
            return $this->response->setStatusCode(500)->setJSON([
                'success' => false,
                'error'   => 'Unable to create the summary file.',
            ]);
        }
        $batchClosedDate = $this->formatCsvDate($batch['closed_at'] ?? null);
        $this->writeBatchCsvMetadata($csvHandle, $batchLabel, $batch);
        $this->writeBatchCsvSummary($csvHandle, $receiptRows, $batchClosedDate);
        fclose($csvHandle);

        $csvAttachmentName = $batchFileLabel . '_summary.csv';

        $receiptsArchivePath = null;
        $receiptZipCreated = false;
        if (!empty($receiptRows)) {
            $archivePath = $tempDir . '/batch_receipts_' . $batchId . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.zip';
            $zip = new ZipArchive();
            $added = 0;
            $usedNames = [];
            if ($zip->open($archivePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
                foreach ($receiptRows as $row) {
                    $sourcePath = WRITEPATH . 'uploads/receipts/' . basename((string) ($row['receipt_filename'] ?? ''));
                    if (!is_file($sourcePath)) {
                        continue;
                    }
                    $firstName = $row['purchaser_firstname'] ?? '';
                    if (trim($firstName) === '' && !empty($row['purchaser_lastname'] ?? '')) {
                        $firstName = $row['purchaser_lastname'];
                    }
                    $firstName = $this->sanitizeAttachmentComponent($firstName);
                    $vendorPart = $this->sanitizeAttachmentComponent($row['retailor'] ?? '');
                    $datePart = $this->formatAttachmentDate($row['purchase_date'] ?? '');
                    $extension = pathinfo($sourcePath, PATHINFO_EXTENSION);
                    $extPart = $extension !== '' ? ('.' . strtolower($extension)) : '';
                    $baseName = $firstName . '_' . $vendorPart . '_' . $datePart;
                    $entryName = $this->uniqueZipEntryName($baseName, $extPart, $usedNames);
                    if ($zip->addFile($sourcePath, $entryName)) {
                        $added++;
                    }
                }
                $zip->close();
            }
            if ($added > 0 && is_file($archivePath)) {
                $receiptsArchivePath = $archivePath;
                $cleanupPaths[] = $archivePath;
                $receiptZipCreated = true;
            } elseif (is_file($archivePath)) {
                @unlink($archivePath);
            }
        }

        $checksArchivePath = null;
        $checksAdded = 0;
        if (!empty($checkRows)) {
            $archivePath = $tempDir . '/batch_checks_' . $batchId . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.zip';
            $zip = new ZipArchive();
            $usedNames = [];
            if ($zip->open($archivePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
                foreach ($checkRows as $row) {
                    $sourcePath = WRITEPATH . 'uploads/reimbursements/' . basename((string) ($row['filename'] ?? ''));
                    if (!is_file($sourcePath)) {
                        continue;
                    }
                    $adminLabel = $row['admin'] ?? 'Admin';
                    $firstToken = strtok($adminLabel, ' ');
                    $namePart = $this->sanitizeAttachmentComponent($firstToken ?: $adminLabel);
                    $extension = pathinfo($sourcePath, PATHINFO_EXTENSION);
                    $extPart = $extension !== '' ? ('.' . strtolower($extension)) : '';
                    $entryName = $this->uniqueZipEntryName($namePart, $extPart, $usedNames);
                    if ($zip->addFile($sourcePath, $entryName)) {
                        $checksAdded++;
                    }
                }
                $zip->close();
            }
            if ($checksAdded > 0 && is_file($archivePath)) {
                $checksArchivePath = $archivePath;
                $cleanupPaths[] = $archivePath;
            } elseif (is_file($archivePath)) {
                @unlink($archivePath);
            }
        }

        $attachmentSummary = ['CSV Summary'];
        if ($receiptZipCreated) {
            $attachmentSummary[] = 'Receipt Files';
        }
        if ($checksArchivePath) {
            $attachmentSummary[] = 'Check Scans';
        }

        $subject = 'Reimbursement Batch Export: ' . $batchLabel;
        $bodyParts = [
            '<p>Please find attached the export for <strong>' . htmlspecialchars($batchLabel, ENT_QUOTES, 'UTF-8') . '</strong>.</p>',
        ];
        if ($message !== '') {
            $bodyParts[] = '<p><strong>Message:</strong><br>' . nl2br(htmlspecialchars($message, ENT_QUOTES, 'UTF-8')) . '</p>';
        }
        $bodyParts[] = '<p>Attachments: ' . htmlspecialchars(implode(', ', $attachmentSummary), ENT_QUOTES, 'UTF-8') . '.</p>';
        $bodyParts[] = '<p>Regards,<br>Al Rahma Sunday School</p>';
        $htmlBody = implode("\n", $bodyParts);

        $attachments = [
            ['path' => $csvPath, 'name' => $csvAttachmentName],
        ];
        if ($receiptsArchivePath) {
            $attachments[] = ['path' => $receiptsArchivePath, 'name' => $batchFileLabel . '_receipts.zip'];
        }
        if ($checksArchivePath) {
            $attachments[] = ['path' => $checksArchivePath, 'name' => $batchFileLabel . '_checks.zip'];
        }

        $responseBody = [
            'success' => true,
            'message' => 'Batch export emailed.',
        ];
        $statusCode = 200;
        try {
            $mailer = new EmailService();
            if (!$mailer->send($recipientEmail, $subject, $htmlBody, 'general', $attachments)) {
                throw new \RuntimeException('EmailService failed to send the batch export.');
            }
            $responseBody['message'] = 'Email sent successfully.';
        } catch (\Throwable $e) {
            log_message('error', 'Failed to send batch export email: {msg}', ['msg' => $e->getMessage()]);
            $statusCode = 500;
            $responseBody = [
                'success' => false,
                'error'   => 'Failed to send email. Please try again.',
            ];
        } finally {
            foreach ($cleanupPaths as $path) {
                if ($path && is_file($path)) {
                    @unlink($path);
                }
            }
        }

        return $this->response->setStatusCode($statusCode)->setJSON($responseBody);
    }

    public function export()
    {
        $type = $this->request->getGet('type'); // 'processed' or 'under_processing'

        if ($type === 'under_processing') {
            // Approved expenses not yet reimbursed
            $builder = $this->db->table('expenses');
            $builder->select('expenses.*, u.firstname AS purchaser_firstname, u.lastname AS purchaser_lastname');
            $builder->join('users u', 'u.id = expenses.purchased_by', 'left');
            $builder->where('expenses.status', 'approved'); // <-- fixed
            $builder->where('expenses.reimbursement_id IS NULL', null, false);
            $builder->where('expenses.category !=', 'Donation');

            $records = $builder->orderBy('expenses.created_at', 'DESC')->get()->getResultArray();

            // Group rows by purchaser so each person appears once in the CSV
            $grouped = [];
            foreach ($records as $row) {
                $personId = (int)($row['purchased_by'] ?? 0);
                $name     = trim(($row['purchaser_firstname'] ?? '') . ' ' . ($row['purchaser_lastname'] ?? ''));
                if ($name === '') {
                    $name = $row['purchaser_firstname'] ?? $row['purchaser_lastname'] ?? 'Unknown';
                }
                $key = $personId > 0 ? 'id_' . $personId : 'name_' . strtolower($name);
                if (!isset($grouped[$key])) {
                    $grouped[$key] = [
                        'name'        => $name ?: 'Unknown',
                        'total'       => 0.0,
                        'vendors'     => [],
                        'descriptions'=> [],
                    ];
                }

                $amount = (float)($row['amount'] ?? 0);
                $grouped[$key]['total'] += $amount;

                $vendor = trim((string)($row['retailor'] ?? ''));
                if ($vendor !== '' && !in_array($vendor, $grouped[$key]['vendors'], true)) {
                    $grouped[$key]['vendors'][] = $vendor;
                }

                $desc = trim((string)($row['description'] ?? ''));
                if ($desc !== '' && !in_array($desc, $grouped[$key]['descriptions'], true)) {
                    $grouped[$key]['descriptions'][] = $desc;
                }
            }

            header('Content-Type: text/csv; charset=UTF-8');
            header('Content-Disposition: attachment; filename=under_processing_expenses.csv');
            $output = fopen('php://output', 'w');
            // UTF-8 BOM for Excel
            fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF));

            fputcsv($output, ['Person', 'Total Amount', 'Vendors', 'Descriptions']);
            foreach ($grouped as $entry) {
                fputcsv($output, [
                    $entry['name'],
                    number_format($entry['total'], 2, '.', ''),
                    implode('; ', $entry['vendors']),
                    implode('; ', $entry['descriptions']),
                ]);
            }
            fclose($output);
            exit;
        }

        // Default: processed reimbursements (filterable)
        $builder = $this->reimbModel
            ->select('reimbursements.*, u.firstname AS reimb_firstname, u.lastname AS reimb_lastname')
            ->join('users u', 'u.id = reimbursements.reimbursed_to', 'left');

        if ($semester = $this->request->getGet('semester')) {
            $builder->where('reimbursements.semester', $semester);
        }
        if ($status = $this->request->getGet('status')) {
            $builder->where('reimbursements.status', $status);
        }
        if ($userId = $this->request->getGet('user_id')) {
            $builder->where('reimbursements.reimbursed_to', $userId);
        }
        if ($schoolYear = $this->request->getGet('school_year')) {
            $builder->where('reimbursements.school_year', $schoolYear);
        }

        $records = $builder->orderBy('reimbursements.created_at', 'DESC')->get()->getResultArray();

        foreach ($records as &$row) {
            $hasName = trim(($row['reimb_firstname'] ?? '') . ' ' . ($row['reimb_lastname'] ?? '')) !== '';
            if (!$hasName) {
                $label = $this->specialRecipientLabel($row['reimbursed_to'] ?? null);
                if ($label !== null) {
                    $row['reimb_firstname'] = $label;
                    $row['reimb_lastname']  = '';
                }
            }
        }
        unset($row);

        header('Content-Type: text/csv; charset=UTF-8');
        header('Content-Disposition: attachment; filename=reimbursements_export.csv');
        $output = fopen('php://output', 'w');
        // UTF-8 BOM for Excel
            fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF));

        fputcsv($output, ['Amount', 'Reimbursed To', 'Status', 'Method', 'Check #', 'School Year', 'Semester', 'Date']);
        foreach ($records as $r) {
            fputcsv($output, [
                $r['amount'],
                trim(($r['reimb_firstname'] ?? '') . ' ' . ($r['reimb_lastname'] ?? '')),
                $r['status'],
                $r['reimbursement_method'] ?? '-',
                $r['check_number'] ?? '-',
                $r['school_year'] ?? '-',
                $r['semester'] ?? '-',
                $r['created_at'],
            ]);
        }
        fclose($output);
        exit;
    }

    public function exportBatch()
    {
        $batchId = (int) ($this->request->getGet('batch_id') ?? 0);
        if ($batchId <= 0) {
            return $this->response->setStatusCode(400)->setBody('Invalid batch specified.');
        }

        $batch = $this->batchModel->find($batchId);
        if (!$batch) {
            throw new PageNotFoundException('Batch not found.');
        }

        $batchLabel = trim((string) ($batch['title'] ?? '')) ?: ('Batch #' . $batchId);
        $receiptRows = $this->fetchBatchReceiptRows($batchId);

        header('Content-Type: text/csv; charset=UTF-8');
        $filename = sprintf('batch_%d_items.csv', $batchId);
        header('Content-Disposition: attachment; filename=' . $filename);
        $output = fopen('php://output', 'w');
        // UTF-8 BOM for Excel
        fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF));

        $batchClosedDate = $this->formatCsvDate($batch['closed_at'] ?? null);
        $this->writeBatchCsvMetadata($output, $batchLabel, $batch);
        $this->writeBatchCsvSummary($output, $receiptRows, $batchClosedDate);

        fclose($output);
        exit;
    }

    public function create()
    {
        $expenseId = $this->request->getGet('expense_id');

        // users list (unchanged)
        $users = $this->recipientOptions();

        // fetch expense to prefill amount
        $prefillAmount = null;
        if ($expenseId) {
            $exp = $this->expenseModel
                ->select('id, amount')
                ->find((int)$expenseId);
            if ($exp) {
                $prefillAmount = $exp['amount'];
            }
        }

        return view('reimbursements/create', [
            'users'          => $users,
            'expense_id'     => $expenseId,
            'prefill_amount' => $prefillAmount,
        ]);
    }


    public function store()
    {
        helper(['form']);

        // Normalize method just in case (e.g., "cash" → "Cash")
        $methodRaw = (string) $this->request->getPost('reimbursement_method');
        $method    = ucfirst(strtolower($methodRaw));
        $expenseId = (int) ($this->request->getPost('expense_id') ?? 0);

        // Base rules
        $rules = [
            'amount'               => 'required|decimal|greater_than[0]',
            'reimbursed_to'        => 'required|is_natural_no_zero',
            'reimbursement_method' => 'required|in_list[Cash,Check]',
        ];

        // Messages (used only when the corresponding rule is present)
        $messages = [
            'receipt' => [
                'uploaded' => 'Receipt file is required.',
                'max_size' => 'Maximum file size is 2MB.',
                'ext_in'   => 'Allowed formats: JPG, JPEG, PNG, WEBP, GIF, or PDF.',
                'mime_in'  => 'Allowed formats: JPG, JPEG, PNG, WEBP, GIF, or PDF.',
            ],
        ];

        // Conditional validation
        if ($method === 'Check') {
            // Require check number + receipt for checks
            $rules['check_number'] = 'required|string|max_length[50]';
            $rules['receipt']      = 'uploaded[receipt]'
                . '|max_size[receipt,2048]'
                . '|ext_in[receipt,jpg,jpeg,png,webp,gif,pdf]'
                . '|mime_in[receipt,image/jpg,image/jpeg,image/png,image/webp,image/gif,application/pdf]';
        } elseif ($method === 'Cash') {
            // Receipt optional for cash; if provided, validate it
            $file = $this->request->getFile('receipt');
            if ($file && $file->isValid() && ($file->getSize() ?? 0) > 0) {
                $rules['receipt'] = 'max_size[receipt,2048]'
                    . '|ext_in[receipt,jpg,jpeg,png,webp,gif,pdf]'
                    . '|mime_in[receipt,image/jpg,image/jpeg,image/png,image/webp,image/gif,application/pdf]';
            }
        }

        if (!$this->validate($rules, $messages)) {
            return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
        }

        if ($expenseId > 0) {
            $expense = $this->expenseModel->find($expenseId);
            if ($expense && strcasecmp($expense['category'] ?? '', 'Donation') === 0) {
                return redirect()->back()->withInput()->with('errors', [
                    'expense_id' => 'Donation expenses are tracked but should not be reimbursed.'
                ]);
            }
        }

        // Store file only if one was actually uploaded
        try {
            $receiptName = $this->saveReimbReceipt($this->request->getFile('receipt'));
        } catch (\Throwable $e) {
            log_message('error', 'Failed to save reimbursement receipt in store(): {msg}', ['msg' => $e->getMessage()]);
            return redirect()->back()->withInput()->with('errors', [
                'receipt' => 'Failed to save uploaded file. Please try again or contact admin.'
            ]);
        }

        $userId      = (int) (session()->get('user_id') ?? 0);
        $recipientId = (int) $this->request->getPost('reimbursed_to');

        // Mark reimbursement as Paid when recorded
        $data = [
            'expense_id'           => $expenseId ?: null,
            'amount'               => $this->request->getPost('amount'),
            'reimbursed_to'        => $recipientId,
            'description'          => $this->request->getPost('description'),
            'reimbursement_method' => $method,
            'check_number'         => $method === 'Check' ? $this->request->getPost('check_number') : null,
            'receipt_path'         => $receiptName,   // may be null for Cash
            'school_year'          => $this->schoolYear,
            'semester'             => $this->semester,
            'added_by'             => $userId,
            'approved_by'          => $userId,
            'status'               => 'Paid',
        ];

        $this->reimbModel->insert($data);
        $reimbursementId = $this->reimbModel->getInsertID();

        if ($expenseId = $this->request->getPost('expense_id')) {
            $this->expenseModel->update($expenseId, ['reimbursement_id' => $reimbursementId]);
        }

        return redirect()->to('/reimbursements')->with('success', 'Reimbursement recorded as Paid.');
    }


    // Optional old flow kept for compatibility (also sets Paid)
    public function process()
    {
        $expenseId = (int) ($this->request->getPost('expense_id') ?? 0);
        if ($expenseId > 0) {
            $expense = $this->expenseModel->find($expenseId);
            if ($expense && strcasecmp($expense['category'] ?? '', 'Donation') === 0) {
                return redirect()->back()->withInput()->with('errors', [
                    'expense_id' => 'Donation expenses are tracked but should not be reimbursed.'
                ]);
            }
        }

        try {
            $receiptName = $this->saveReimbReceipt($this->request->getFile('receipt'));
        } catch (\Throwable $e) {
            log_message('error', 'Failed to save reimbursement receipt in process(): {msg}', ['msg' => $e->getMessage()]);
            return redirect()->back()->withInput()->with('errors', [
                'receipt' => 'Failed to save uploaded file. Please try again or contact admin.'
            ]);
        }

        $userId      = (int) (session()->get('user_id') ?? 0);
        $recipientId = (int) $this->request->getPost('reimbursed_to');

        $reimbursementId = $this->reimbModel->insert([
            'amount'               => $this->request->getPost('amount'),
            'reimbursed_to'        => $recipientId,
            'approved_by'          => $userId,
            'receipt_path'         => $receiptName,
            'description'          => 'Expense reimbursement',
            'status'               => 'Paid',
            'added_by'             => $userId,
            'school_year'          => $this->schoolYear,
            'semester'             => $this->semester,
            'check_number'         => $this->request->getPost('check_number'),
            'reimbursement_method' => $this->request->getPost('reimbursement_method')
        ]);

        $this->expenseModel->update($expenseId, [
            'reimbursement_id' => $reimbursementId
        ]);

        return redirect()->to('/reimbursements/under-processing')->with('success', 'Reimbursement processed!');
    }

    public function reimbursedExpenses()
    {
        $expenses = $this->db->table('reimbursements r')
            ->select('
                e.amount AS expense_amount,
                e.category,
                e.description,
                e.receipt_path AS expense_receipt,
                e.purchased_by,
                r.amount AS reimb_amount,
                r.reimbursement_method,
                r.check_number,
                r.receipt_path AS reimb_receipt,
                r.status       AS reimb_status,
                r.created_at,
                u.firstname AS purchaser_firstname,
                u.lastname  AS purchaser_lastname,
                a.firstname AS approver_firstname,
                a.lastname  AS approver_lastname
            ')
            ->join('expenses e', 'e.id = r.expense_id')
            ->join('users u', 'u.id = e.purchased_by', 'left')
            ->join('users a', 'a.id = r.approved_by', 'left')
            ->orderBy('r.created_at', 'DESC')
            ->get()
            ->getResultArray();

        foreach ($expenses as &$e) {
            $e['expense_receipt_url'] = $this->expenseReceiptUrl($e['expense_receipt'] ?? null);
            $e['reimb_receipt_url']   = $this->reimbReceiptUrl($e['reimb_receipt'] ?? null);
        }

        return view('reimbursements/reimbursed_expenses', ['expenses' => $expenses]);
    }

    // app/Controllers/ReimbursementController.php


    public function edit(int $id)
    {
        $reimb = $this->reimbModel->find($id);
        if (!$reimb) {
            throw PageNotFoundException::forPageNotFound("Reimbursement #$id not found");
        }

        $users = $this->recipientOptions();

        return view('reimbursements/edit', [
            'reimb' => $reimb,
            'users' => $users,
            'receipt_url' => $reimb['receipt_path'] ? site_url('reimbreceipts/' . basename($reimb['receipt_path'])) : null,
        ]);
    }

    public function update(int $id)
    {
        helper(['form']);

        $reimb = $this->reimbModel->find($id);
        if (!$reimb) {
            throw PageNotFoundException::forPageNotFound("Reimbursement #$id not found");
        }

        $methodRaw = (string) $this->request->getPost('reimbursement_method');
        $method    = ucfirst(strtolower($methodRaw));

        $rules = [
            'amount'               => 'required|decimal|greater_than[0]',
            'reimbursed_to'        => 'required|is_natural_no_zero',
            'reimbursement_method' => 'required|in_list[Cash,Check]',
        ];

        // Optional file validation (only if provided)
        $file = $this->request->getFile('receipt');
        if ($method === 'Check') {
            $rules['check_number'] = 'required|string|max_length[50]';
            if ($file && $file->isValid() && ($file->getSize() ?? 0) > 0) {
                $rules['receipt'] = 'max_size[receipt,2048]'
                    . '|ext_in[receipt,jpg,jpeg,png,webp,gif,pdf]'
                    . '|mime_in[receipt,image/jpg,image/jpeg,image/png,image/webp,image/gif,application/pdf]';
            }
        } else { // Cash
            if ($file && $file->isValid() && ($file->getSize() ?? 0) > 0) {
                $rules['receipt'] = 'max_size[receipt,2048]'
                    . '|ext_in[receipt,jpg,jpeg,png,webp,gif,pdf]'
                    . '|mime_in[receipt,image/jpg,image/jpeg,image/png,image/webp,image/gif,application/pdf]';
            }
        }

        if (!$this->validate($rules)) {
            return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
        }

        // Handle new/removed receipt
        $receiptName = $reimb['receipt_path'];
        try {
            $maybeNew    = $this->saveReimbReceipt($file);
        } catch (\Throwable $e) {
            log_message('error', 'Failed to save reimbursement receipt in update(): {msg}', ['msg' => $e->getMessage()]);
            return redirect()->back()->withInput()->with('errors', [
                'receipt' => 'Failed to save uploaded file. Please try again or contact admin.'
            ]);
        }
        if ($maybeNew !== null) {
            $receiptName = $maybeNew;
        }
        if ($this->request->getPost('remove_receipt') === '1') {
            $receiptName = null;
        }

        $this->reimbModel->update($id, [
            'amount'               => $this->request->getPost('amount'),
            'reimbursed_to'        => (int) $this->request->getPost('reimbursed_to'),
            'description'          => $this->request->getPost('description'),
            'reimbursement_method' => $method,
            'check_number'         => $method === 'Check' ? $this->request->getPost('check_number') : null,
            'receipt_path'         => $receiptName,
            'updated_by'           => (int) (session()->get('user_id') ?? 0),
            // keep status as-is; or set from form if you add a status field
        ]);

        return redirect()->to('/reimbursements')->with('success', 'Reimbursement updated.');
    }

    private function lookupReimbursementId(int $expenseId): ?int
    {
        if ($expenseId <= 0) {
            return null;
        }

        $row = $this->reimbModel
            ->select('id')
            ->where('expense_id', $expenseId)
            ->orderBy('id', 'DESC')
            ->first();

        return $row ? (int) $row['id'] : null;
    }
}
