<?php

namespace App\Controllers\View;

use App\Controllers\BaseController;
use App\Models\ClassSectionModel;
use App\Models\InventoryItemModel;
use App\Models\InventoryCategoryModel;
use App\Models\ConfigurationModel;
use App\Models\TeacherClassModel;
use App\Models\StudentClassModel;
use App\Models\UserModel;
use App\Models\InventoryMovementModel;
use App\Models\TeacherModel;

class InventoryController extends BaseController
{
    protected $itemModel;
    protected $teacherModel;
    protected $catModel;
    protected $configModel;
    protected $schoolYear;
    protected $semester;
    protected $movModel;
    protected $teacherClassModel;
    protected $studentClassModel;
    protected $userModel;
    protected $db;
    protected $classSectionModel;
    private array $types = ['classroom', 'book', 'office', 'kitchen'];
    protected array $movementTypes = ['initial', 'in', 'out', 'adjust', 'distribution'];
    protected array $semesters     = ['Spring', 'Fall'];


    public function __construct()
    {
        // Init models 
        $this->movModel = new InventoryMovementModel();
        $this->userModel = new UserModel();
        $this->configModel = new ConfigurationModel();
        $this->itemModel = new InventoryItemModel();
        $this->catModel  = new InventoryCategoryModel();
        $this->teacherClassModel = new TeacherClassModel();
        $this->studentClassModel = new StudentClassModel();
        $this->schoolYear = $this->configModel->getConfig('school_year');
        $this->semester = $this->configModel->getConfig('semester');
        $this->classSectionModel = new ClassSectionModel();
        $this->db = \Config\Database::connect();
        $this->teacherModel = new TeacherModel();
        helper(['form', 'text']);
    }

    // app/Controllers/InventoryController.php  (only the changed bits)

    private function viewForIndex(string $type): string
    {
        // inventory/classroom/index, inventory/book/index, inventory/office/index, inventory/kitchen/index
        return "inventory/{$type}/index";
    }

    private function viewForForm(string $type): string
    {
        // inventory/classroom/form, inventory/book/form, inventory/office/form, inventory/kitchen/form
        return "inventory/{$type}/form";
    }

    public function index(string $type = 'classroom')
    {
        $type = $this->normalizeType($type);

        $schoolYears = $this->getSchoolYearsForType($type);
        $requested     = trim((string) $this->request->getGet('school_year'));
        $selectedYear  = $requested !== '' ? $requested : $this->schoolYear;
        $selectedSemRaw = $this->request->getGet('semester');
        $selectedSem    = $selectedSemRaw === null ? (string) $this->semester : trim((string) $selectedSemRaw);

        $builder = $this->itemModel->where('type', $type);
        if (strtolower($selectedYear) !== 'all') {
            $builder->where('school_year', $selectedYear);
        }
        if ($selectedSem !== '') {
            $builder->where('semester', $selectedSem);
        }

        $items = $builder->orderBy('name', 'ASC')->findAll();
        $categories = $this->catModel->optionsForType($type);

        // NEW: build UpdatedBy name map
        $userIds   = array_values(array_unique(array_map(fn($i) => (int)($i['updated_by'] ?? 0), $items)));
        $userIds   = array_values(array_filter($userIds));
        $userNames = $this->userNamesByIds($userIds);

        return view($this->viewForIndex($type), [
            'type'            => $type,
            'items'           => $items,
            'categories'      => $categories,
            'conditionOptions' => ['good' => 'Good condition', 'needs_repair' => 'Needs repair', 'need_replace' => 'Need replace', 'cannot_find' => 'Cannot find'],
            'schoolYears'     => $schoolYears,
            'selectedYear'    => $selectedYear,
            'currentYear'     => $this->schoolYear,
            'semester'        => $selectedSem,
            'userNames'       => $userNames,   // <-- pass to views
        ]);
    }

    public function edit(int $id)
    {
        $item = $this->itemModel->find($id);
        if (!$item) return redirect()->back()->with('error', 'Item not found.');

        // Load categories for THIS item type
        $categories = $this->catModel->optionsForType($item['type']);

        return view($this->viewForForm($item['type']), [
            'type'        => $item['type'],
            'item'        => $item,
            'categories'  => $categories,
            'conditionOptions' => [
                'good' => 'Good condition',
                'needs_repair' => 'Needs repair',
                'need_replace' => 'Need replace',
                'cannot_find' => 'Cannot find'
            ],
            'isEdit'      => true, // <- tell the view we're editing
        ]);
    }

    public function update(int $id)
    {
        $item = $this->itemModel->find($id);
        if (!$item) return redirect()->back()->with('error', 'Item not found.');

        // lock to original type so users can’t switch books -> office, etc.
        $data = $this->filterItemData($this->request->getPost(), $item['type']);

        // Do not allow changing the PK via payload
        unset($data['id']);

        if (!$this->itemModel->update($id, $data)) {
            return redirect()->back()->withInput()->with('error', implode(', ', $this->itemModel->errors()));
        }

        return redirect()->to(site_url('inventory/' . $item['type']))->with('success', 'Item updated.');
    }

    public function store()
    {
        $data = $this->request->getPost();

        $itemData = $this->filterItemData($data);
        // Use insert() to get ID cleanly
        $itemId = $this->itemModel->insert($itemData, true);
        if ($itemId) {
            $initialQty = (int) ($itemData['quantity'] ?? 0);
            if ($initialQty !== 0) {
                $this->recordMovement($itemId, $initialQty, 'initial', 'Initial stock');
            } else {
                // ensure quantity is synced even if zero
                $this->recalcQuantity($itemId);
            }
            return redirect()->to(site_url("inventory/{$itemData['type']}"))->with('success', 'Item added.');
        }
        return redirect()->back()->withInput()->with('error', implode(', ', $this->itemModel->errors()));
    }

    // Lazily ensure the very first movement = current on-hand, for legacy items
    private function ensureInitialMovement(int $itemId): void
    {
        // Any movement already?
        $hasMov = $this->movModel->where('item_id', $itemId)->limit(1)->countAllResults();
        if ($hasMov > 0) return; // legacy seed only

        $item = $this->itemModel->select('quantity')->find($itemId);
        $initialQty = (int)($item['quantity'] ?? 0);

        $this->movModel->insert([
            'item_id'       => $itemId,
            'qty_change'    => $initialQty,
            'movement_type' => 'initial',
            'semester'      => $this->semester,
            'school_year'   => $this->schoolYear,
            'performed_by'  => $this->currentUserId(),
        ], false);
    }

    public function delete(int $id)
    {
        $item = $this->itemModel->find($id);
        if ($item) {
            $type = $item['type'];
            $this->itemModel->delete($id);
            return redirect()->to(site_url("inventory/{$type}"))->with('success', 'Item deleted.');
        }
        return redirect()->back()->with('error', 'Item not found.');
    }

    // Categories
    public function saveCategory()
    {
        // Normalize inputs
        $rawType = (string)($this->request->getPost('type') ?? '');
        $type = strtolower(trim($rawType));
        $id   = $this->request->getPost('id');

        // Collapse multiple spaces in the name and trim
        $name = preg_replace('/\s+/', ' ', trim((string)$this->request->getPost('name')));

        $data = [
            'type'        => in_array($type, ['classroom', 'book', 'office', 'kitchen'], true) ? $type : 'office',
            'name'        => $name,
            'description' => (string)$this->request->getPost('description'),
        ];

        // Only books have grade range
        if ($data['type'] === 'book') {
            $gmin = $this->request->getPost('grade_min');
            $gmax = $this->request->getPost('grade_max');

            $gmin = ($gmin === '' || $gmin === null) ? null : max(0, (int)$gmin);
            $gmax = ($gmax === '' || $gmax === null) ? null : max(0, (int)$gmax);

            if ($gmin !== null && $gmax !== null && $gmin > $gmax) {
                return redirect()->back()->withInput()->with('error', 'Grade Min cannot be greater than Grade Max.');
            }
            if ($gmin !== null && $gmin > 13) $gmin = 13;
            if ($gmax !== null && $gmax > 13) $gmax = 13;

            $data['grade_min'] = $gmin;
            $data['grade_max'] = $gmax;
        } else {
            $data['grade_min'] = null;
            $data['grade_max'] = null;
        }

        // --- Duplicate guard for (type, name) pair ---
        $existing = $this->catModel
            ->where('type', $data['type'])
            ->where('name', $data['name'])
            ->first();

        try {
            if ($id) {
                // Updating: allow if the found record is THIS record, block otherwise
                $pk = $this->catModel->primaryKey ?? 'id';
                if ($existing && (int)$existing[$pk] !== (int)$id) {
                    return redirect()->back()->withInput()->with(
                        'error',
                        'A category with this Type & Name already exists. Please choose a different name or edit the existing one.'
                    );
                }
                $ok = $this->catModel->update((int)$id, $data);
            } else {
                // Creating: block if already exists
                if ($existing) {
                    // If you PREFER auto-converting this into an update, uncomment the two lines below:
                    // $pk = $this->catModel->primaryKey ?? 'id';
                    // return $this->catModel->update((int)$existing[$pk], $data)
                    //     ? redirect()->back()->with('success', 'Category already existed; details updated.')
                    //     : redirect()->back()->withInput()->with('error', 'Failed to update existing category.');

                    return redirect()->back()->withInput()->with(
                        'error',
                        'This category already exists: ' . $data['type'] . ' — ' . $data['name'] . '.'
                    );
                }
                $ok = (bool) $this->catModel->insert($data);
            }
        } catch (\Throwable $e) {
            // Handle duplicate at DB level (safety net)
            $msg = $e->getMessage();
            if (strpos($msg, 'Duplicate entry') !== false && strpos($msg, 'type_name') !== false) {
                return redirect()->back()->withInput()->with(
                    'error',
                    'Duplicate Type/Name. A category with this combination already exists.'
                );
            }
            log_message('critical', 'Failed to save category: {msg}', ['msg' => $msg]);
            return redirect()->back()->withInput()->with('error', 'Failed to save category.');
        }

        if (!$ok) {
            return redirect()->back()->withInput()->with('error', 'Failed to save category.');
        }

        return redirect()->back()->with('success', 'Category saved.');
    }

    public function optionsForType(string $type): array
    {
        return $this->asArray()
            ->select('id, name, description, grade_min, grade_max')
            ->where('type', $type)
            ->orderBy('name', 'ASC')
            ->findAll();
    }

    private function normalizeType(?string $type): string
    {
        $type = strtolower((string)$type);
        return in_array($type, $this->types, true) ? $type : 'classroom';
    }

    private function currentUserId(): ?int
    {
        foreach (['user_id', 'id', 'uid', 'userId'] as $k) {
            $v = session()->get($k);
            if ($v !== null && $v !== '') return (int)$v;
        }
        return null;
    }


    // add a tiny helper (optional, but keeps things tidy)
    private function post(string $key, $default = null)
    {
        $v = $this->request->getPost($key);
        if ($v === '' || $v === null) return $default;
        return is_string($v) ? trim($v) : $v;
    }

    private function filterItemData(array $data, ?string $lockedType = null): array
    {
        $type = $lockedType ?? $this->normalizeType($this->post('type', 'classroom'));

        $qtyRaw   = $this->request->getPost('quantity');
        $quantity = is_numeric($qtyRaw) ? (int)$qtyRaw : 0;

        $base = [
            // 'id' should NOT be required; we pass PK separately on update()
            'type'        => $type,
            'category_id' => ($this->post('category_id') !== null && $this->post('category_id') !== '') ? (int)$this->post('category_id') : null,
            'name'        => (string) $this->post('name', ''),
            'description' => $this->post('description'),
            'quantity'    => $quantity,
            'unit'        => $this->post('unit'),
            'sku'         => $this->post('sku'),
            'notes'       => $this->post('notes'),
            'semester'    => $this->semester,
            'school_year' => $this->schoolYear,
            'updated_by'  => $this->currentUserId(),
        ];

        $base['condition'] = ($type === 'classroom') ? $this->post('condition') : null;

        if ($type === 'book') {
            $base['isbn']    = $this->post('isbn');
            $base['edition'] = $this->post('edition');
        } else {
            $base['isbn'] = $base['edition'] = null;
        }

        return $base;
    }

    public function auditClassroomForm(int $itemId)
    {
        $item = $this->itemModel->find($itemId);
        if (!$item || $item['type'] !== 'classroom') {
            return redirect()->back()->with('error', 'Invalid classroom item.');
        }

        // Defaults if columns were just added
        $item['good_qty']         = (int)($item['good_qty']         ?? 0);
        $item['needs_repair_qty'] = (int)($item['needs_repair_qty'] ?? 0);
        $item['need_replace_qty'] = (int)($item['need_replace_qty'] ?? 0);
        $item['cannot_find_qty']  = (int)($item['cannot_find_qty']  ?? 0);

        return view('inventory/classroom/audit_form', ['item' => $item]);
    }

    public function auditClassroomStore(int $itemId)
    {
        $item = $this->itemModel->find($itemId);
        if (!$item || $item['type'] !== 'classroom') {
            return redirect()->back()->with('error', 'Invalid classroom item.');
        }

        // Previous buckets
        $prevGood   = (int)($item['good_qty']         ?? 0);
        $prevRepair = (int)($item['needs_repair_qty'] ?? 0);
        $prevLoss   = (int)($item['need_replace_qty'] ?? 0);
        $prevMiss   = (int)($item['cannot_find_qty']  ?? 0);

        // New buckets (non-negative ints)
        $newRepair = max(0, (int)$this->request->getPost('needs_repair_qty'));
        $newLoss   = max(0, (int)$this->request->getPost('need_replace_qty'));
        $newMiss   = max(0, (int)$this->request->getPost('cannot_find_qty'));

        // Good is derived: keep total physical = good + repair (loss/missing are not on hand)
        // We base it on current on-hand (item.quantity) and newRepair.
        // If you want to enter "good" directly, you can switch to reading it from POST.
        $onHandNow = (int)$item['quantity'];
        $newGood   = max(0, $onHandNow - $newRepair);

        // Movements for loss/missing deltas (affects physical on-hand)
        $deltaLoss = $newLoss - $prevLoss;
        $deltaMiss = $newMiss - $prevMiss;

        // Positive deltas mean more items are now total-loss/missing → stock OUT
        $totalOut = 0;
        if ($deltaLoss > 0) $totalOut += $deltaLoss;
        if ($deltaMiss > 0) $totalOut += $deltaMiss;

        // Negative deltas mean we recovered some previously lost/missing → stock IN
        $totalIn = 0;
        if ($deltaLoss < 0) $totalIn += -$deltaLoss;
        if ($deltaMiss < 0) $totalIn += -$deltaMiss;

        // Apply stock movements
        if ($totalOut > 0) {
            if (!$this->recordMovement($itemId, -$totalOut, 'out', 'Audit: loss/missing increased')) {
                return redirect()->back()->withInput(); // error flashed inside recordMovement
            }
        }
        if ($totalIn > 0) {
            $this->recordMovement($itemId, +$totalIn, 'in', 'Audit: loss/missing decreased');
        }

        // Recalc on-hand now that movements applied
        $this->recalcQuantity($itemId);

        // Persist buckets
        $this->itemModel->update($itemId, [
            'good_qty'         => $newGood,
            'needs_repair_qty' => $newRepair,
            'need_replace_qty' => $newLoss,
            'cannot_find_qty'  => $newMiss,
            // also stamp audit user/time
            'updated_by'       => $this->currentUserId(),
            'updated_at'       => utc_now(),
        ]);

        return redirect()->to(site_url('inventory/classroom'))->with('success', 'Audit saved.');
    }

    private function userNamesByIds(array $ids): array
    {
        $ids = array_values(array_unique(array_filter($ids, fn($v) => !empty($v))));
        if (!$ids) return [];

        if (!$this->db->tableExists('users')) return [];

        // Detect available columns to build a safe SELECT
        $cols = $this->db->getFieldNames('users'); // returns [] if empty
        $hasFirst   = in_array('first_name', $cols, true) || in_array('firstname', $cols, true);
        $hasLast    = in_array('last_name',  $cols, true) || in_array('lastname',  $cols, true);
        $firstCol   = in_array('first_name', $cols, true) ? 'first_name' : (in_array('firstname', $cols, true) ? 'firstname' : null);
        $lastCol    = in_array('last_name',  $cols, true) ? 'last_name'  : (in_array('lastname',  $cols, true)  ? 'lastname'  : null);
        $nameCol    = in_array('name',       $cols, true) ? 'name'       : null;
        $emailCol   = in_array('email',      $cols, true) ? 'email'      : null;

        if ($firstCol || $lastCol) {
            $select = "id, TRIM(CONCAT(IFNULL($firstCol,''),' ',IFNULL($lastCol,''))) AS display_name";
        } elseif ($nameCol) {
            $select = "id, $nameCol AS display_name";
        } elseif ($emailCol) {
            $select = "id, $emailCol AS display_name";
        } else {
            // Last resort: just return IDs as labels
            $out = [];
            foreach ($ids as $id) $out[(int)$id] = 'User #' . $id;
            return $out;
        }

        $rows = $this->db->table('users')->select($select)->whereIn('id', $ids)->get()->getResultArray();
        $map  = [];
        foreach ($rows as $r) {
            $disp = trim((string)($r['display_name'] ?? ''));
            $map[(int)$r['id']] = $disp !== '' ? $disp : ('User #' . $r['id']);
        }
        return $map;
    }

    // Distinct school years for a given inventory type (newest first).
    private function getSchoolYearsForType(string $type): array
    {
        // Pull distinct non-null school_year values for this type
        $years = $this->itemModel
            ->select('school_year')
            ->where('type', $type)
            ->where('school_year IS NOT NULL', null, false)
            ->groupBy('school_year')
            ->orderBy('school_year', 'DESC')
            ->findColumn('school_year') ?? [];

        // Normalize & de-dup
        $years = array_values(array_unique(array_filter(array_map('trim', $years))));

        // Ensure the current year (from config) appears as an option, even if no rows yet
        $currentYear = $this->schoolYear;
        if ($currentYear && !in_array($currentYear, $years, true)) {
            array_unshift($years, $currentYear);
            $years = array_values(array_unique($years));
        }

        return $years;
    }

    private function recordMovement(
        int $itemId,
        int $qtyChange,
        string $type,        // initial|in|out|adjust|distribution
        ?string $reason = null,
        ?string $note = null,
        ?int $classSectionId = null,
        ?int $studentId = null
    ): bool {
        $me = $this->currentUserId();
        $isInitial = ($type === 'initial');

        // ✅ Only seed legacy/year opening for NON-initial inserts
        if (!$isInitial) {
            // Seed a one-time legacy opening movement if *no* movement exists for this item
            $this->ensureInitialMovement($itemId);

            // Ensure there is an opening for *this* school year
            $this->ensureInitialMovementForYear($itemId, $this->schoolYear);

            // Sync on-hand before validating outflow
            $this->recalcQuantity($itemId);
        }

        // Normalize sign for out/distribution
        if (in_array($type, ['out', 'distribution'], true) && $qtyChange > 0) {
            $qtyChange = -abs($qtyChange);
        }

        // Prevent going negative on out/distribution
        if (in_array($type, ['out', 'distribution'], true)) {
            $onHand = (int) ($this->itemModel->select('quantity')->where('id', $itemId)->first()['quantity'] ?? 0);
            if ($onHand + $qtyChange < 0) {
                session()->setFlashdata('error', 'Not enough stock to complete this deduction.');
                return false;
            }
        }

        $ok = $this->movModel->insert([
            'item_id'          => $itemId,
            'qty_change'       => $qtyChange,
            'movement_type'    => $type,
            'reason'           => $reason,
            'note'             => $note,
            'semester'         => $this->semester,
            'school_year'      => $this->schoolYear,
            'performed_by'     => $me,
            'teacher_id'       => $me,
            'student_id'       => $studentId,
            'class_section_id' => $classSectionId,
        ], false);

        if ($ok) {
            $this->recalcQuantity($itemId);
            return true;
        }
        return false;
    }

    private function recalcQuantity(int $itemId): void
    {
        // Sum movements
        $sumRow = $this->movModel
            ->selectSum('qty_change')
            ->where('item_id', $itemId)
            ->first();

        $sum = (int)($sumRow['qty_change'] ?? 0);
        if ($sum < 0) $sum = 0;

        // Fetch current values we compare against
        $item = $this->itemModel
            ->select('id, type, quantity, needs_repair_qty, good_qty')
            ->find($itemId);

        if (!$item) return;

        $updates = [];

        // (optional) if you want quantity to mirror movement sum, keep this block
        // If you prefer to leave quantity as entered, just delete this IF.
        if ((int)$item['quantity'] !== $sum) {
            $updates['quantity'] = $sum;
        }

        // Keep classroom Good = OnHand − NeedsRepair (derived)
        if (($item['type'] ?? null) === 'classroom') {
            $needsRepair = (int)($item['needs_repair_qty'] ?? 0);
            $goodCalc    = max(0, $sum - $needsRepair);
            if (!array_key_exists('good_qty', $item) || (int)$item['good_qty'] !== $goodCalc) {
                $updates['good_qty'] = $goodCalc;
            }
        }

        // ✅ Only update if there is something to update
        if (!empty($updates)) {
            try {
                $this->itemModel->update($itemId, $updates);
            } catch (\CodeIgniter\Database\Exceptions\DataException $e) {
                // This would happen if allowedFields stripped everything
                log_message('error', 'recalcQuantity() skipped update: ' . $e->getMessage());
            }
        }
    }

    public function summary(string $type = 'classroom')
    {
        $type = $this->normalizeType($type);
        $currentSem = $this->semester;
        $currentYear = $this->schoolYear;
        $selectedYear = trim((string) $this->request->getGet('school_year')) ?: $currentYear;

        // Items of this type
        $items = $this->itemModel->where('type', $type)->orderBy('name', 'ASC')->findAll();
        $itemIds = array_map(fn($i) => (int)$i['id'], $items);

        // Aggregate movements for the selected year (or all)
        $agg = [];
        if ($itemIds) {
            $qb = $this->movModel->select('item_id,
             SUM(CASE WHEN qty_change>0 THEN qty_change ELSE 0 END) AS received,
             SUM(CASE WHEN qty_change<0 THEN -qty_change ELSE 0 END) AS issued')
                ->whereIn('item_id', $itemIds)
                ->groupBy('item_id');

            if (strtolower($selectedYear) !== 'all') {
                $qb->where('school_year', $selectedYear);
            }

            foreach ($qb->findAll() as $row) {
                $agg[(int)$row['item_id']] = [
                    'received' => (int)$row['received'],
                    'issued'   => (int)$row['issued'],
                ];
            }
        }

        // school year options
        $schoolYears = $this->getSchoolYearsForType($type);

        return view('inventory/summary', [
            'type'         => $type,
            'items'        => $items,
            'agg'          => $agg,
            'selectedYear' => $selectedYear,
            'currentYear'  => $currentYear,
            'schoolYears'  => $schoolYears,
        ]);
    }

    public function adjustForm(int $itemId)
    {
        $item = $this->itemModel->find($itemId);
        if (!$item) return redirect()->back()->with('error', 'Item not found.');
        return view('inventory/adjust_form', ['item' => $item, 'type' => $item['type']]);
    }

    public function adjustStore(int $itemId)
    {
        $item = $this->itemModel->find($itemId);
        if (!$item) return redirect()->back()->with('error', 'Item not found.');

        $mode   = $this->request->getPost('mode'); // 'in' or 'out' or 'adjust'
        $qty    = (int) $this->request->getPost('quantity');
        $reason = (string) $this->request->getPost('reason');
        $note   = (string) $this->request->getPost('note');

        if ($qty <= 0) return redirect()->back()->with('error', 'Quantity must be > 0.');

        $delta = ($mode === 'out') ? -abs($qty) : (($mode === 'in') ? abs($qty) : $qty);
        $type  = ($mode === 'out') ? 'out' : (($mode === 'in') ? 'in' : 'adjust');

        if (!$this->recordMovement((int)$itemId, $delta, $type, $reason, $note)) {
            return redirect()->back()->withInput(); // error is flashed inside recordMovement
        }

        return redirect()->to(site_url('inventory/summary/' . $item['type']))->with('success', 'Stock updated.');
    }

    /**
     * Teacher book distribution page:
     * - Locks to teacher’s class section if only one, else uses ?class_section_id
     * - Filters BOOKS by category grade range (grade_min/grade_max) against class grade (class_id)
     * - Groups books by CATEGORY (with grade range in the group label) for an <optgroup>-style select
     * - Shows per-student prior distribution history for the selected book in the current school year
     */
    public function teacherDistributeForm()
    {
        // 1) Build teacher/class context (auth/role/no-class handled inside)
        $ctx = $this->buildTeacherClassContext();
        if ($ctx instanceof \CodeIgniter\HTTP\RedirectResponse) {
            return $ctx; // early exit on redirect (not logged in / no class / invalid role)
        }

        // 2) Decide selected class section (lock if teacher has only one)
        $lockClassSection = \is_array($ctx['classSectionIds'] ?? null) && \count($ctx['classSectionIds']) === 1;
        $classSectionId   = $lockClassSection
            ? (int) ($ctx['defaultClassSectionId'] ?? 0)
            : (int) ($this->request->getGet('class_section_id') ?? ($ctx['defaultClassSectionId'] ?? 0));

        // 3) Convert class section -> class grade (class_id as integer grade level)
        $classID = null;
        if (!empty($classSectionId)) {
            $classID = $this->classSectionModel->getClassId($classSectionId); // e.g., 3 for Grade 3
            if ($classID !== null) $classID = (int) $classID;
        }

        // 4) Selected book (optional, to render student history and on-hand)
        $itemId = (int) ($this->request->getGet('item_id') ?? 0);

        // 5) Fetch BOOKS filtered by category grade range and GROUP BY CATEGORY
        $db = \Config\Database::connect();
        $builder = $db->table('inventory_items i')
            ->select('i.id,i.name,i.isbn,i.edition,i.quantity,i.category_id,i.type,
                  c.name AS category_name, c.grade_min, c.grade_max')
            ->join('inventory_categories c', 'c.id = i.category_id', 'left')
            ->where('i.type', 'book');

        if (!empty($classID)) {
            $builder->groupStart()
                // Case A: both bounds set and classID between [min..max]
                ->groupStart()
                ->where('c.grade_min IS NOT NULL', null, false)
                ->where('c.grade_max IS NOT NULL', null, false)
                ->where('c.grade_min <=', (int)$classID)
                ->where('c.grade_max >=', (int)$classID)
                ->groupEnd()

                // Case B: only min set AND equals min
                ->orGroupStart()
                ->where('c.grade_min IS NOT NULL', null, false)
                ->where('c.grade_max', null)               // IS NULL
                ->where('c.grade_min', (int)$classID)      // =
                ->groupEnd()

                // Case C: only max set AND equals max
                ->orGroupStart()
                ->where('c.grade_max IS NOT NULL', null, false)
                ->where('c.grade_min', null)               // IS NULL
                ->where('c.grade_max', (int)$classID)      // =
                ->groupEnd()
                ->groupEnd();
        }

        $rows = $builder->orderBy('i.name', 'ASC')->get()->getResultArray();

        // Build CATEGORY groups keyed by category_id
        $labelFor = static function (?array $r): string {
            $name = $r['category_name'] ?? 'Uncategorized';
            $gmin = $r['grade_min'] ?? null;
            $gmax = $r['grade_max'] ?? null;
            if ($gmin === null && $gmax === null) return $name;
            if ($gmin !== null && $gmax !== null) return $name . ' (G' . (int)$gmin . '–G' . (int)$gmax . ')';
            if ($gmin !== null) return $name . ' (G' . (int)$gmin . '+)';
            return $name . ' (≤G' . (int)$gmax . ')';
        };

        // booksGrouped: [catId => ['label'=>string, 'items'=>[['id'=>..,'display'=>..,'quantity'=>..], ...]]]
        $booksGrouped = [];
        foreach ($rows as $r) {
            $catId = (int)($r['category_id'] ?? 0);
            if (!isset($booksGrouped[$catId])) {
                $booksGrouped[$catId] = [
                    'label' => $labelFor($r),
                    'items' => [],
                ];
            }
            $display = $r['name'];
            if (!empty($r['isbn']))    $display .= ' — ISBN ' . $r['isbn'];
            if (!empty($r['edition'])) $display .= ' (' . $r['edition'] . ')';

            $booksGrouped[$catId]['items'][] = [
                'id'       => (int)$r['id'],
                'display'  => $display,
                'quantity' => (int)($r['quantity'] ?? 0),
            ];
        }

        // Sort groups by label, and each group’s items by display
        uasort($booksGrouped, fn($a, $b) => strnatcasecmp($a['label'], $b['label']));
        foreach ($booksGrouped as &$grp) {
            usort($grp['items'], fn($x, $y) => strnatcasecmp($x['display'], $y['display']));
        }
        unset($grp);

        // 6) Students of the selected class section (provided by context)
        $students = $ctx['studentsData'][$classSectionId] ?? [];

        // 7) History map: how many of the selected book each student already received this school year
        $already = [];
        if ($itemId && $classSectionId) {
            $rowsHist = $this->movModel
                ->select('student_id, SUM(CASE WHEN qty_change < 0 THEN -qty_change ELSE 0 END) AS qty')
                ->where([
                    'item_id'          => $itemId,
                    'movement_type'    => 'distribution',
                    'class_section_id' => $classSectionId,
                    'school_year'      => $ctx['schoolYear'],
                ])
                ->groupBy('student_id')
                ->findAll();

            foreach ($rowsHist as $r) {
                $sid = (int) ($r['student_id'] ?? 0);
                if ($sid) $already[$sid] = (int) ($r['qty'] ?? 0);
            }
        }

        // 8) On-hand for the selected book (to show live available stock)
        $onHand = 0;
        if ($itemId) {
            $book = $this->itemModel->find($itemId);
            if ($book && ($book['type'] ?? '') === 'book') {
                $onHand = (int) ($book['quantity'] ?? 0);
            }
        }

        // 9) Render view
        return view('inventory/teacher_distribute', [
            'booksGrouped'     => $booksGrouped,     // grouped by CATEGORY for <optgroup>
            'items'            => $rows,             // raw list if needed elsewhere (optional)

            'classes'          => $ctx['classes'],
            'class_section_id' => $classSectionId,
            'lockClassSection' => $lockClassSection,

            'item_id'          => $itemId,
            'students'         => $students,
            'already'          => $already,
            'onHand'           => $onHand,

            'schoolYear'       => $ctx['schoolYear'],
            'semester'         => $ctx['semester'],
        ]);
    }

    public function teacherDistributeStore()
    {
        $ctx = $this->buildTeacherClassContext();
        if ($ctx instanceof \CodeIgniter\HTTP\RedirectResponse) {
            return $ctx;
        }

        $itemId        = (int)$this->request->getPost('item_id');
        $postedClassId = (int)$this->request->getPost('class_section_id');
        $selectedIds   = (array)$this->request->getPost('student_ids');
        $note          = (string)$this->request->getPost('note');

        // Enforce teacher’s classes
        $classIds = $ctx['classSectionIds'];
        $classSectionId = (count($classIds) === 1)
            ? (int)$classIds[0]
            : (in_array($postedClassId, $classIds, true) ? $postedClassId : 0);

        if (!$classSectionId) {
            return redirect()->back()->with('error', 'Invalid class selection.');
        }

        // Validate book
        $book = $this->itemModel->find($itemId);
        if (!$book || $book['type'] !== 'book') {
            return redirect()->back()->with('error', 'Invalid book.');
        }

        // Validate students belong to the class
        $students = $ctx['studentsData'][$classSectionId] ?? [];
        $validIds = [];
        foreach ($students as $st) {
            $sid = $st['student_id'] ?? ($st['id'] ?? ($st['user_id'] ?? null));
            if ($sid) $validIds[(int)$sid] = true;
        }
        $selectedIds = array_values(array_unique(array_map('intval', $selectedIds)));
        $selectedIds = array_values(array_filter($selectedIds, fn($sid) => isset($validIds[$sid])));

        // Already distributed this year for this class/book
        $already = [];
        $rows = $this->movModel
            ->select('student_id, SUM(CASE WHEN qty_change < 0 THEN -qty_change ELSE 0 END) AS qty')
            ->where([
                'item_id'          => $itemId,
                'movement_type'    => 'distribution',
                'class_section_id' => $classSectionId,
                'school_year'      => $ctx['schoolYear'],
            ])->groupBy('student_id')->findAll();
        foreach ($rows as $r) {
            $sid = (int)($r['student_id'] ?? 0);
            if ($sid) $already[$sid] = (int)$r['qty'];
        }

        // Only assign to students who don't already have it
        $toGive = [];
        foreach ($selectedIds as $sid) {
            if (($already[$sid] ?? 0) < 1) $toGive[] = $sid;
        }

        // Stock check
        $needed = count($toGive);
        $onHand = (int)$book['quantity'];
        if ($needed > $onHand) {
            return redirect()->back()->withInput()->with('error', 'Not enough stock: need ' . $needed . ', on hand ' . $onHand . '.');
        }

        // Record one movement per student
        $okCount = 0;
        foreach ($toGive as $sid) {
            $ok = $this->recordMovement($itemId, -1, 'distribution', 'Teacher distribution', $note, $classSectionId, $sid);
            if ($ok) $okCount++;
        }

        if ($okCount === 0) {
            return redirect()
                ->to(site_url('inventory/books/distribute?class_section_id=' . $classSectionId . '&item_id=' . $itemId))
                ->with('warning', 'No changes (everyone selected already has this book).');
        }

        return redirect()
            ->to(site_url('inventory/books/distribute?class_section_id=' . $classSectionId . '&item_id=' . $itemId))
            ->with('success', 'Distributed to ' . $okCount . ' student(s).');
    }


    /**
     * Builds the same context your TeacherController::classView() creates.
     * Returns an array with: user, classes, roleNames, classSectionIds, studentsData, taNames, defaultClassSectionId
     * Throws a redirect Response (return it) when access is invalid (not logged in / no classes / invalid role).
     */
    private function buildTeacherClassContext()
    {
        // 1) Logged-in user?
        $user_id = session()->get('user_id');
        if (!$user_id) {
            return redirect()->to('/login')->with('error', 'Please log in first');
        }

        // 2) All assignments (main or TA) for current school year
        $classes = $this->teacherClassModel->getClassAssignmentsByUserId($user_id, $this->schoolYear);

        if (empty($classes)) {
            return redirect()->to('no-classes')->with('message', 'You do not have an assigned class yet. Please contact the administration.');
        }

        // 3) Identify user & role
        $user = $this->userModel->find($user_id);
        if (!$user) {
            return redirect()->to('/error')->with('error', 'User not found');
        }

        $roles = $this->db->table('user_roles ur')
            ->select('r.name')
            ->join('roles r', 'r.id = ur.role_id')
            ->where('ur.user_id', $user_id)
            ->get()->getResultArray();

        $roleNames = array_map(fn($r) => $r['name'], $roles);

        if (in_array('teacher', $roleNames, true)) {
            $user['role_name'] = 'teacher';
        } elseif (in_array('teacher_assistant', $roleNames, true)) {
            $user['role_name'] = 'teacher_assistant';
        } else {
            return redirect()->to('/error')->with('error', 'Access denied');
        }

        // 4) Collect class_section IDs
        $classSectionIds = array_column($classes, 'class_section_id');
        $studentsData = [];

        // 5) Group students by class_section_id
        if (!empty($classSectionIds)) {
            $students = $this->studentClassModel->getStudentsByClassSectionIds($classSectionIds);
            foreach ($students as $student) {
                $studentsData[$student['class_section_id']][] = $student;
            }
        }

        // 6) TA names per class_section (same as your code)
        $taAssignments = $this->teacherClassModel
            ->whereIn('class_section_id', $classSectionIds)
            ->where('school_year', $this->schoolYear)
            ->where('position', 'ta')
            ->findAll();

        $taNames = [];
        foreach ($taAssignments as $ta) {
            $taUser = $this->userModel->find($ta['teacher_id']);
            if ($taUser) {
                $csid = $ta['class_section_id'];
                $taNames[$csid][] = ($taUser['firstname'] ?? '') . ' ' . ($taUser['lastname'] ?? '');
            }
        }

        // 7) Default selection = active session choice (if valid) or first class
        $sessionId = (int)(session()->get('class_section_id') ?? 0);
        if ($sessionId && in_array($sessionId, $classSectionIds, true)) {
            $defaultClassSectionId = $sessionId;
        } else {
            $defaultClassSectionId = $classSectionIds[0] ?? null;
            if ($defaultClassSectionId) {
                session()->set('class_section_id', $defaultClassSectionId);
            }
        }

        return [
            'user'                  => $user,
            'roleNames'             => $roleNames,
            'classes'               => $classes,
            'classSectionIds'       => $classSectionIds,
            'studentsData'          => $studentsData,
            'taNames'               => $taNames,
            'defaultClassSectionId' => $defaultClassSectionId,
            'schoolYear'            => $this->schoolYear,
            'semester'              => $this->semester,
            'user_id'               => $user_id,
        ];
    }

    // Ensure an initial movement exists for THIS school year, even if we started mid-year
    private function ensureInitialMovementForYear(int $itemId, string $schoolYear): void
    {
        // If we already have any movement for this item in this year, make sure an 'initial' exists; else create one.
        $hasAnyThisYear = $this->movModel
            ->where('item_id', $itemId)
            ->where('school_year', $schoolYear)
            ->limit(1)->countAllResults();

        // Already has any movement this year?
        if ($hasAnyThisYear === 0) {
            // No movement at all this year → opening = current on-hand
            $onHand = (int) ($this->itemModel->select('quantity')->where('id', $itemId)->first()['quantity'] ?? 0);
            $this->movModel->insert([
                'item_id'       => $itemId,
                'qty_change'    => $onHand,
                'movement_type' => 'initial',
                'school_year'   => $schoolYear,
                'semester'      => $this->semester ?? null,
                'performed_by'  => $this->currentUserId(),
            ], false);
            return;
        }

        // We have some movement this year. If there's no 'initial' yet, backfill a correct opening.
        $hasInitialThisYear = $this->movModel
            ->where('item_id', $itemId)
            ->where('school_year', $schoolYear)
            ->where('movement_type', 'initial')
            ->limit(1)->countAllResults();

        if ($hasInitialThisYear === 0) {
            // Opening = current_on_hand - net_year_movements
            $onHand  = (int) ($this->itemModel->select('quantity')->where('id', $itemId)->first()['quantity'] ?? 0);
            $netYear = (int) ($this->movModel->selectSum('qty_change')
                ->where('item_id', $itemId)
                ->where('school_year', $schoolYear)
                ->first()['qty_change'] ?? 0);

            $opening = $onHand - $netYear; // reconstruct beginning-of-year truth
            $this->movModel->insert([
                'item_id'       => $itemId,
                'qty_change'    => $opening,
                'movement_type' => 'initial',
                'school_year'   => $schoolYear,
                'semester'      => $this->semester ?? null,
                'performed_by'  => $this->currentUserId(),
            ], false);
        }
    }

    public function create(string $type = 'classroom')
    {
        $categories = $this->catModel->optionsForType($type);
        return view("inventory/{$type}/form", [
            'type'             => $type,
            'item'             => [], // important for create
            'categories'       => $categories,
            'conditionOptions' => [
                'good' => 'Good condition',
                'needs_repair' => 'Needs repair',
                'need_replace' => 'Need replace',
                'cannot_find' => 'Cannot find',
            ],
        ]);
    }

    public function summaryAll()
    {
        // -------- Inputs --------
        $selectedYear = trim((string) $this->request->getGet('school_year')) ?: (string) $this->schoolYear;
        $selectedType = strtolower((string) ($this->request->getGet('type') ?? 'all')); // optional: all|book|classroom|office|kitchen
        $isAllYears   = (strtolower($selectedYear) === 'all');

        // -------- Items query (single source of truth) --------
        $qbItems = $this->db->table('inventory_items i')
            ->select('i.*, c.name AS category_name, CONCAT(u.firstname, " ", u.lastname) AS updated_by_name')
            ->join('inventory_categories c', 'c.id = i.category_id', 'left')
            ->join('users u', 'u.id = i.updated_by', 'left')
            ->orderBy('i.name', 'ASC');

        // Filter by school year unless "all"
        if (!$isAllYears) {
            $qbItems->where('i.school_year', $selectedYear);
        }

        // Optional: filter by type if provided
        if (in_array($selectedType, ['book', 'classroom', 'office', 'kitchen'], true)) {
            $qbItems->where('i.type', $selectedType);
        }

        $items = $qbItems->get()->getResultArray();

        // -------- Prepare output structures --------
        $rows   = [];
        $totals = [
            'opening'     => 0,
            'received'    => 0,
            'distributed' => 0,
            'other_out'   => 0,
            'adjust_net'  => 0,
            'ending'      => 0,
            'onhand'      => 0,
            'variance'    => 0,
        ];

        // If no items, render an empty table gracefully
        if (empty($items)) {
            return view('inventory/summary', [
                'selectedYear' => $selectedYear,
                'currentYear'  => $this->schoolYear,
                'schoolYears'  => $this->getSchoolYearsForType('book') ?: [$this->schoolYear, 'All'],
                'rows'         => $rows,
                'totals'       => $totals,
                // optionally pass the selected type if your view supports it
                'selectedType' => $selectedType,
            ]);
        }

        // -------- Aggregate movements for the fetched items --------
        $itemIds = array_map('intval', array_column($items, 'id'));

        $aggById = [];
        $qb = $this->db->table('inventory_movements m')
            ->select("
                m.item_id,
                SUM(CASE WHEN m.movement_type = 'initial' THEN m.qty_change ELSE 0 END) AS opening,
                SUM(CASE WHEN m.qty_change > 0 AND m.movement_type IN ('in','adjust') THEN m.qty_change ELSE 0 END) AS received,
                SUM(CASE WHEN m.qty_change < 0 AND m.movement_type = 'distribution' THEN -m.qty_change ELSE 0 END) AS distributed,
                SUM(CASE WHEN m.qty_change < 0 AND m.movement_type = 'out' THEN -m.qty_change ELSE 0 END) AS other_out,
                SUM(CASE WHEN m.movement_type = 'adjust' THEN m.qty_change ELSE 0 END) AS adjust_net
            ")
            ->whereIn('m.item_id', $itemIds)
            ->groupBy('m.item_id');

        if (!$isAllYears) {
            $qb->where('m.school_year', $selectedYear);
        }

        foreach ($qb->get()->getResultArray() as $r) {
            $aggById[(int)$r['item_id']] = [
                'opening'     => (int)($r['opening'] ?? 0),
                'received'    => (int)($r['received'] ?? 0),
                'distributed' => (int)($r['distributed'] ?? 0),
                'other_out'   => (int)($r['other_out'] ?? 0),
                'adjust_net'  => (int)($r['adjust_net'] ?? 0),
            ];
        }

        // -------- Build rows + totals --------
        foreach ($items as $it) {
            $id = (int)$it['id'];
            $a  = $aggById[$id] ?? ['opening' => 0, 'received' => 0, 'distributed' => 0, 'other_out' => 0, 'adjust_net' => 0];

            $opening     = (int)$a['opening'];
            $received    = (int)$a['received'];
            $distributed = (int)$a['distributed'];
            $otherOut    = (int)$a['other_out'];
            $adjustNet   = (int)$a['adjust_net'];

            $ending   = $opening + $received - $distributed - $otherOut + $adjustNet;
            $onhand   = (int)$it['quantity'];
            $variance = $onhand - $ending;

            $rows[] = [
                'id'          => $id,
                'name'        => (string)$it['name'],
                'type'        => (string)$it['type'],
                'category'    => (string)($it['category_name'] ?? ''),
                'opening'     => $opening,
                'received'    => $received,
                'distributed' => $distributed,
                'other_out'   => $otherOut,
                'adjust_net'  => $adjustNet,
                'ending'      => $ending,
                'onhand'      => $onhand,
                'variance'    => $variance,
            ];

            $totals['opening']     += $opening;
            $totals['received']    += $received;
            $totals['distributed'] += $distributed;
            $totals['other_out']   += $otherOut;
            $totals['adjust_net']  += $adjustNet;
            $totals['ending']      += $ending;
            $totals['onhand']      += $onhand;
            $totals['variance']    += $variance;
        }

        // -------- Year dropdown options --------
        $schoolYears = $this->getSchoolYearsForType('book') ?: [$this->schoolYear, 'All'];

        return view('inventory/summary', [
            'selectedYear' => $selectedYear,
            'currentYear'  => $this->schoolYear,
            'schoolYears'  => $schoolYears,
            'rows'         => $rows,
            'totals'       => $totals,
            'selectedType' => $selectedType,
        ]);
    }

    /** =========================
     *  LIST
     *  ========================= */
    public function movementsIndex()
    {
        // Academic filters
        $selectedYearRaw = $this->request->getGet('school_year');
        $selectedSemRaw  = $this->request->getGet('semester');
        $selectedYear = $selectedYearRaw === null ? (string) $this->schoolYear : trim((string) $selectedYearRaw);
        $selectedSem  = $selectedSemRaw  === null ? (string) $this->semester   : trim((string) $selectedSemRaw);

        // Pull movements with friendly names when possible
        $builder = $this->db->table('inventory_movements m')
            ->select([
                'm.*',
                'ii.name AS item_name',

                // Performed by (users)
                "CONCAT(pb.firstname, ' ', pb.lastname) AS performed_by_name",

                // Teacher (users)
                "CONCAT(t.firstname, ' ', t.lastname) AS teacher_name",

                // Student (students)
                "CONCAT(s.firstname, ' ', s.lastname) AS student_name",

                // Class section
                'cs.class_section_name',
            ])
            ->join('inventory_items ii', 'ii.id = m.item_id', 'left')
            ->join('users pb', 'pb.id = m.performed_by', 'left')
            ->join('users t',  't.id  = m.teacher_id',  'left')
            ->join('students s', 's.id = m.student_id',  'left')
            ->join('classSection cs', 'cs.class_section_id = m.class_section_id', 'left')
            ->orderBy('m.id', 'DESC');

        if ($selectedYear !== '') $builder->where('m.school_year', $selectedYear);
        if ($selectedSem  !== '') $builder->where('m.semester',    $selectedSem);

        $movements = $builder->get()->getResultArray();

        return view('inventory/movements/index', [
            'movements' => $movements,
            'schoolYear'=> $selectedYear,
            'semester'  => $selectedSem,
        ]);
    }

    /** =========================
     *  CREATE (form)
     *  ========================= */
    public function movementsCreate()
    {
        return view('inventory/movements/form', [
            'movement'      => $this->blankMovement(),
            'movementTypes' => $this->movementTypes,
            'semesters'     => $this->semesters,
            'items'         => $this->getItems(),
            'classSections' => $this->getClassSections(),
            'teachers'      => $this->getTeachers(),
            'students'      => $this->getStudents(),
            'action'        => site_url('inventory/movements/store'),
            'title'         => 'Create Inventory Movement',
            'isCreate'      => true,
        ]);
    }

    /** =========================
     *  STORE (POST)
     *  ========================= */
    public function movementsStore()
    {
        $rules = [
            'item_id'       => 'required|is_natural_no_zero',
            'qty_change'    => 'required|integer',
            'movement_type' => 'required|in_list[' . implode(',', $this->movementTypes) . ']',
            'reason'        => 'permit_empty|max_length[120]',
            'note'          => 'permit_empty|string',
            'semester'      => 'permit_empty|in_list[' . implode(',', $this->semesters) . ']',
            'school_year'   => 'permit_empty|max_length[16]',
            'teacher_id'    => 'permit_empty|is_natural_no_zero',
            'student_id'    => 'permit_empty|is_natural_no_zero',
            'class_section_id' => 'permit_empty|is_natural_no_zero',
        ];

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

        $userId = (int) (session('user_id') ?? 0);
        $itemId = (int) $this->request->getPost('item_id');
        $movementType = (string) $this->request->getPost('movement_type');

        $item = $this->itemModel->find($itemId);
        if (!$item) {
            return redirect()->back()->withInput()->with('status', 'error')
                ->with('message', 'Selected item not found.');
        }

        $qtyChange = $this->normalizeMovementQty($movementType, (int) $this->request->getPost('qty_change'));
        if ($this->wouldGoNegative($itemId, $qtyChange)) {
            return redirect()->back()->withInput();
        }

        $semester = trim((string) $this->request->getPost('semester'));
        $schoolYear = trim((string) $this->request->getPost('school_year'));
        if ($semester === '') $semester = (string) $this->semester;
        if ($schoolYear === '') $schoolYear = (string) $this->schoolYear;

        $data = [
            'item_id'         => $itemId,
            'qty_change'      => $qtyChange,
            'movement_type'   => $movementType,
            'reason'          => trim((string)$this->request->getPost('reason')),
            'note'            => trim((string)$this->request->getPost('note')),
            'semester'        => $semester ?: null,
            'school_year'     => $schoolYear ?: null,
            'performed_by'    => $userId ?: null,
            'teacher_id'      => $this->nullableInt($this->request->getPost('teacher_id')),
            'student_id'      => $this->nullableInt($this->request->getPost('student_id')),
            'class_section_id' => $this->nullableInt($this->request->getPost('class_section_id')),
            'created_at'      => utc_now(),
            'updated_at'      => utc_now(),
        ];

        $ok = $this->db->table('inventory_movements')->insert($data);

        if (! $ok) {
            return redirect()->back()->withInput()->with('status', 'error')
                ->with('message', 'Could not save movement.');
        }

        $this->recalcQuantity($itemId);

        return redirect()->to(site_url('inventory/movements'))
            ->with('status', 'success')
            ->with('message', 'Movement created.');
    }

    /** =========================
     *  EDIT (form)
     *  ========================= */
    public function movementsEdit(int $id)
    {
        $movement = $this->db->table('inventory_movements')->where('id', $id)->get()->getRowArray();
        if (!$movement) {
            return redirect()->to(site_url('inventory/movements'))
                ->with('status', 'error')->with('message', 'Movement not found.');
        }

        // enrich for display (names)
        $movement = $this->hydrateNames($movement);

        return view('inventory/movements/form', [
            'movement'      => $movement, // already hydrated by your helper
            'movementTypes' => $this->movementTypes,
            'semesters'     => $this->semesters,
            'items'         => $this->getItems(),
            'classSections' => $this->getClassSections(),
            'teachers'      => $this->getTeachers(),
            'students'      => $this->getStudents(),
            'action'        => site_url('inventory/movements/update/' . $id),
            'title'         => 'Edit Inventory Movement',
            'isCreate'      => false,
        ]);
    }

    /** =========================
     *  UPDATE (POST)
     *  ========================= */
    public function movementsUpdate(int $id)
    {
        $rules = [
            'qty_change'    => 'required|integer',
            'movement_type' => 'required|in_list[' . implode(',', $this->movementTypes) . ']',
            'reason'        => 'permit_empty|max_length[120]',
            'note'          => 'permit_empty|string',
            'semester'      => 'permit_empty|in_list[' . implode(',', $this->semesters) . ']',
            'school_year'   => 'permit_empty|max_length[16]',
            'teacher_id'    => 'permit_empty|is_natural_no_zero',
            'student_id'    => 'permit_empty|is_natural_no_zero',
            'class_section_id' => 'permit_empty|is_natural_no_zero',
        ];

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

        $existing = $this->db->table('inventory_movements')->where('id', $id)->get()->getRowArray();
        if (!$existing) {
            return redirect()->to(site_url('inventory/movements'))
                ->with('status', 'error')->with('message', 'Movement not found.');
        }

        $movementType = (string) $this->request->getPost('movement_type');
        $qtyChange = $this->normalizeMovementQty($movementType, (int) $this->request->getPost('qty_change'));

        $itemId = (int) ($existing['item_id'] ?? 0);
        if ($itemId > 0) {
            $delta = $qtyChange - (int) ($existing['qty_change'] ?? 0);
            if ($delta !== 0 && $this->wouldGoNegative($itemId, $delta)) {
                return redirect()->back()->withInput();
            }
        }

        $semester = trim((string) $this->request->getPost('semester'));
        $schoolYear = trim((string) $this->request->getPost('school_year'));
        if ($semester === '') $semester = (string) $this->semester;
        if ($schoolYear === '') $schoolYear = (string) $this->schoolYear;

        $data = [
            // item_id is often immutable after posting history; include if you want to allow editing:
            // 'item_id'         => (int) $this->request->getPost('item_id'),
            'qty_change'      => $qtyChange,
            'movement_type'   => $movementType,
            'reason'          => trim((string)$this->request->getPost('reason')),
            'note'            => trim((string)$this->request->getPost('note')),
            'semester'        => $semester ?: null,
            'school_year'     => $schoolYear ?: null,
            'teacher_id'      => $this->nullableInt($this->request->getPost('teacher_id')),
            'student_id'      => $this->nullableInt($this->request->getPost('student_id')),
            'class_section_id' => $this->nullableInt($this->request->getPost('class_section_id')),
            'updated_at'      => utc_now(),
        ];

        $ok = $this->db->table('inventory_movements')->where('id', $id)->update($data);

        if (! $ok) {
            return redirect()->back()->withInput()->with('status', 'error')
                ->with('message', 'Could not update movement.');
        }

        if ($itemId > 0) {
            $this->recalcQuantity($itemId);
        }

        return redirect()->to(site_url('inventory/movements'))
            ->with('status', 'success')
            ->with('message', 'Movement updated.');
    }

    /** =========================
     *  DELETE (POST)
     *  ========================= */
    public function movementsDelete(int $id)
    {
        // Optional: authorization checks here
        log_message('info', 'Inventory: delete one attempt', ['id' => $id, 'user' => (int)(session('user_id') ?? 0)]);
        $movement = $this->db->table('inventory_movements')->select('id, item_id')->where('id', $id)->get()->getRowArray();
        if (!$movement) {
            return redirect()->back()->with('status', 'error')->with('message', 'Movement not found.');
        }

        $ok = $this->db->table('inventory_movements')->where('id', $id)->delete();
        log_message('info', 'Inventory: delete one result', ['id' => $id, 'ok' => (bool)$ok, 'affected' => $this->db->affectedRows()]);

        if (! $ok) {
            return redirect()->back()->with('status', 'error')
                ->with('message', 'Could not delete movement.');
        }

        if (!empty($movement['item_id'])) {
            $this->recalcQuantity((int) $movement['item_id']);
        }
        return redirect()->to(site_url('inventory/movements'))
            ->with('status', 'success')->with('message', 'Movement deleted.');
    }

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

    /** Default structure for create form */
    private function blankMovement(): array
    {
        return [
            'id'               => null,
            'item_id'          => null,
            'qty_change'       => 0,
            'movement_type'    => 'adjust',
            'reason'           => '',
            'note'             => '',
            'semester'         => null,
            'school_year'      => null,
            'performed_by'     => session('user_id') ?? null,
            'teacher_id'       => null,
            'student_id'       => null,
            'class_section_id' => null,
            'created_at'       => null,
            'updated_at'       => null,
        ];
    }

    /** Coerce to nullable int */
    private function nullableInt($val): ?int
    {
        if ($val === null || $val === '' || $val === '0') {
            return null;
        }
        return (int) $val;
    }

    private function normalizeMovementQty(string $movementType, int $qtyChange): int
    {
        if (in_array($movementType, ['out', 'distribution'], true) && $qtyChange > 0) {
            return -abs($qtyChange);
        }
        return $qtyChange;
    }

    private function wouldGoNegative(int $itemId, int $delta): bool
    {
        $onHand = (int) ($this->itemModel->select('quantity')->where('id', $itemId)->first()['quantity'] ?? 0);
        if ($onHand + $delta < 0) {
            session()->setFlashdata('status', 'error');
            session()->setFlashdata('message', 'Not enough stock to apply this movement.');
            return true;
        }
        return false;
    }

    /** Hydrate display names for a row (used in edit) */
    private function hydrateNames(array $m): array
    {
        // Item
        if (!empty($m['item_id'])) {
            $row = $this->db->table('inventory_items')->select('name')->where('id', $m['item_id'])->get()->getRowArray();
            $m['item_name'] = $row['name'] ?? null;
        }

        // Performed By (users)
        if (!empty($m['performed_by'])) {
            $row = $this->db->table('users')
                ->select("CONCAT(firstname, ' ', lastname) AS fullname")
                ->where('id', $m['performed_by'])
                ->get()->getRowArray();
            $m['performed_by_name'] = $row['fullname'] ?? null;
        }

        // Teacher (users)
        if (!empty($m['teacher_id'])) {
            $row = $this->db->table('users')
                ->select("CONCAT(firstname, ' ', lastname) AS fullname")
                ->where('id', $m['teacher_id'])
                ->get()->getRowArray();
            $m['teacher_name'] = $row['fullname'] ?? null;
        }

        // Student (students)
        if (!empty($m['student_id'])) {
            $row = $this->db->table('students')
                ->select("CONCAT(firstname, ' ', lastname) AS fullname")
                ->where('id', $m['student_id'])
                ->get()->getRowArray();
            $m['student_name'] = $row['fullname'] ?? null;
        }

        // Class section (by CODE)
        if (!empty($m['class_section_id'])) {
            $row = $this->db->table('classSection')
                ->select('class_section_name')
                ->where('class_section_id', $m['class_section_id'])
                ->get()->getRowArray();
            $m['class_section_name'] = $row['class_section_name'] ?? null;
        }

        return $m;
    }

    private function getTeachers(): array
    {
        $rows = $this->teacherModel->getTeachersAndTAs();

        // Normalize to include a ready-to-use fullname
        return array_map(static function ($r) {
            $fullname = trim(($r['firstname'] ?? '') . ' ' . ($r['lastname'] ?? ''));
            return [
                'id'        => (int) $r['id'],
                'firstname' => $r['firstname'] ?? '',
                'lastname'  => $r['lastname'] ?? '',
                'fullname'  => $fullname,
                'email'     => $r['email'] ?? '',
                'cellphone' => $r['cellphone'] ?? '',
                'role'      => $r['role'] ?? '',
            ];
        }, $rows);
    }


    /** Dropdowns */
    private function getItems(): array
    {
        return $this->db->table('inventory_items')->select('id, name')->orderBy('name', 'ASC')->get()->getResultArray();
    }
    private function getClassSections(): array
    {
        return $this->db->table('classSection')
            ->select('class_section_id, class_section_name')
            ->orderBy('class_section_name', 'ASC')->get()->getResultArray();
    }


    private function getStudents(): array
{
    // Returns: id, fullname
    return $this->db->table('students')
        ->select("id, CONCAT(firstname, ' ', lastname) AS fullname", false)
        ->orderBy('lastname', 'ASC')
        ->orderBy('firstname', 'ASC')
        ->get()
        ->getResultArray();
}

    public function movementsBulkDelete()
    {
    $ids = (array) $this->request->getPost('ids');

    // sanitize to ints and drop invalids
    $ids = array_values(array_filter(array_map('intval', $ids), fn($v) => $v > 0));

    if (empty($ids)) {
        log_message('warning', 'Inventory: bulk delete called with empty ids', ['user' => (int)(session('user_id') ?? 0)]);
        return redirect()->back()->with('status','error')->with('message','No movements selected.');
    }

    log_message('info', 'Inventory: bulk delete attempt', ['count' => count($ids), 'ids' => $ids, 'user' => (int)(session('user_id') ?? 0)]);
    $itemIds = $this->db->table('inventory_movements')
        ->select('item_id')
        ->whereIn('id', $ids)
        ->get()->getResultArray();
    $itemIds = array_values(array_unique(array_filter(array_map(
        static fn($r) => (int) ($r['item_id'] ?? 0),
        $itemIds
    ))));

    $ok = $this->db->table('inventory_movements')->whereIn('id', $ids)->delete();
    log_message('info', 'Inventory: bulk delete result', ['ok' => (bool)$ok, 'affected' => $this->db->affectedRows()]);

    if (! $ok) {
        return redirect()->back()->with('status','error')->with('message','Bulk delete failed.');
    }

    foreach ($itemIds as $itemId) {
        $this->recalcQuantity($itemId);
    }

    return redirect()->to(site_url('inventory/movements'))
        ->with('status','success')
        ->with('message', count($ids) . ' movement(s) deleted.');
}

}
