<?php
/**
 * RetirementsController — Slice 8Z-d · Batch 2
 *
 * One retirement per internal-cash LPO. Requester adds spend lines (with receipts) + cash returned,
 * submits; Accountant reviews, approves or rejects. On approval, the LPO is marked RETIRED and a
 * best-effort journal is posted. Blocks further cash requests by the same user until all open
 * advances are retired (wired into CashRequests::saveAj).
 *
 * Endpoints:
 *   /Retirements/new/{lpo_id}        GET  — draft form (creates DRAFT if none exists)
 *   /Retirements/saveHeaderAj        POST — updates cash_returned on DRAFT/REJECTED
 *   /Retirements/addLineAj           POST (multipart) — add spend line with optional receipt
 *   /Retirements/updateLineAj        POST — edit a line (amount / description / notes / coa)
 *   /Retirements/removeLineAj        POST — delete a line
 *   /Retirements/submitAj            POST — DRAFT/REJECTED → SUBMITTED (balance-gated)
 *   /Retirements/view/{id}           GET  — read-only detail (requester + Accountant use)
 *   /Retirements/queue               GET  — SUBMITTED queue (Accountant / FM / Admin)
 *   /Retirements/approveAj           POST — Accountant approves (posts journal best-effort)
 *   /Retirements/rejectAj            POST — Accountant rejects with mandatory reason
 *   /Retirements/my                  GET  — requester's own retirements
 *   /Retirements/receiptDownload/{lineId} GET — stream a receipt attachment
 */
class RetirementsController extends Controller
{
    /** Open the draft form for an LPO (auto-creates DRAFT if missing). */
    public function newAction($lpoId = 0)
    {
        $lpoId = (int)$lpoId;
        if ($lpoId <= 0) { $this->fb('Missing LPO id.'); return; }

        $lpo = (new Lpos())->byId($lpoId);
        if (!$lpo) { $this->fb('LPO not found.'); return; }

        // Only internal-cash LPOs retire; SUPPLIER closes via GRN + invoice.
        if ($lpo['lpo_type'] === 'SUPPLIER') {
            $this->fb('Supplier LPOs are closed via GRN + supplier invoice, not the retirement pipeline.');
            return;
        }

        // Only the original requester (or Accountant/FM/Admin) can open the form.
        $role = strtolower(trim($this->currentUserGroup()));
        $isReviewer = in_array($role, ['accountant','finance manager','administrator'], true);
        if ((int)$lpo['requested_by'] !== $this->currentUserId() && !$isReviewer) {
            $this->fb('Only the requester can retire this LPO.');
            return;
        }

        // Cash must have been issued/paid before retirement makes sense.
        // Slice 8Z-m · escape hatch — partial-paid LPOs (BOOKED with amount_paid > 0) can be
        // retired against the paid portion once 30 days have elapsed since the first partial pay.
        $canRetireOnPartial = false;
        if ($lpo['cash_out_status'] === 'BOOKED' && (float)($lpo['amount_paid'] ?? 0) > 0 && !empty($lpo['first_partial_paid_on'])) {
            $daysSince = (time() - strtotime((string)$lpo['first_partial_paid_on'])) / 86400;
            if ($daysSince >= 30) $canRetireOnPartial = true;
        }
        if (!in_array($lpo['cash_out_status'], ['ISSUED','PAID','RETIRED'], true) && !$canRetireOnPartial) {
            if ($lpo['cash_out_status'] === 'BOOKED' && (float)($lpo['amount_paid'] ?? 0) > 0) {
                $daysSince = !empty($lpo['first_partial_paid_on']) ? floor((time() - strtotime((string)$lpo['first_partial_paid_on'])) / 86400) : 0;
                $this->fb('This LPO has a partial payment (' . $lpo['currency'] . ' ' . number_format((float)$lpo['amount_paid']) . ' of ' . number_format((float)$lpo['amount']) . '). Retirement on the paid portion unlocks after 30 days — you\'re at day ' . (int)$daysSince . '.');
            } else {
                $this->fb('This LPO has not been disbursed yet — nothing to retire (current status: ' . $lpo['cash_out_status'] . ').');
            }
            return;
        }

        $m   = new LpoRetirements();
        $rid = $m->ensureDraft($lpoId, $this->currentUserId());
        $r   = $m->find($rid);
        $lines = $m->linesFor($rid);
        // Batch 4 · Discovery LPOs need the findings capture section as well.
        $isDiscovery = ($lpo['lpo_type'] === 'PRODUCT_DISCOVERY');
        $findings    = $isDiscovery ? $m->findingsFor($rid) : [];

        // Slice 8Z-q · receipts + per-line attachments
        $receipts    = $m->receiptsFor($rid);
        $lineAtts    = [];
        foreach ($lines as $ln) { $lineAtts[(int)$ln['c0d']] = $m->lineAttachmentsFor((int)$ln['c0d']); }

        $db   = new DB();
        $coa  = $db->mySelect("SELECT c0d, account_code, account_name FROM chart_of_accounts WHERE is_active = 1 AND account_type IN ('EXPENSE','ASSET') ORDER BY account_code", []) ?: [];

        $this->view->setLayout($this->roleLayout());
        $this->view->render('Retirements/new', [
            'page_title'   => 'Retire · ' . $lpo['lpo_no'],
            'lpo'          => $lpo,
            'retirement'   => $r,
            'lines'        => $lines,
            'coa'          => $coa,
            'is_discovery' => $isDiscovery,
            'findings'     => $findings,
            'receipts'     => $receipts,
            'line_atts'    => $lineAtts,
        ]);
    }

    public function saveHeaderAjAction()
    {
        header('Content-Type: application/json');
        try {
            $rid = (int)($_POST['retirement_id'] ?? 0);
            if ($rid <= 0) throw new InvalidArgumentException('retirement_id required.');
            $this->assertOwnerOrReviewer($rid);
            (new LpoRetirements())->saveHeader($rid, [
                'cash_returned' => (float)($_POST['cash_returned'] ?? 0),
            ], $this->currentUserId());
            $r = (new LpoRetirements())->find($rid);
            echo json_encode(['ok' => true, 'totals' => [
                'total_spent'   => (float)$r['total_spent'],
                'cash_returned' => (float)$r['cash_returned'],
                'variance'      => (float)$r['variance'],
            ]]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    public function addLineAjAction()
    {
        header('Content-Type: application/json');
        try {
            $rid = (int)($_POST['retirement_id'] ?? 0);
            if ($rid <= 0) throw new InvalidArgumentException('retirement_id required.');
            $this->assertOwnerOrReviewer($rid);
            $file = $_FILES['receipt'] ?? null;
            $lineId = (new LpoRetirements())->addLine($rid, [
                'description'    => (string)($_POST['description'] ?? ''),
                'amount'         => (float) ($_POST['amount']      ?? 0),
                'coa_account_id' => (int)   ($_POST['coa_account_id'] ?? 0),
                'notes'          => (string)($_POST['notes'] ?? ''),
            ], $file, $this->currentUserId());
            $r = (new LpoRetirements())->find($rid);
            echo json_encode(['ok' => true, 'line_id' => $lineId, 'totals' => [
                'total_spent'   => (float)$r['total_spent'],
                'cash_returned' => (float)$r['cash_returned'],
                'variance'      => (float)$r['variance'],
            ]]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    public function updateLineAjAction()
    {
        header('Content-Type: application/json');
        try {
            $lineId = (int)($_POST['line_id'] ?? 0);
            if ($lineId <= 0) throw new InvalidArgumentException('line_id required.');
            $db = new DB();
            $rid = (int)($db->mySelect("SELECT retirement_id FROM lpo_retirement_lines WHERE c0d = :id", [':id' => $lineId])[0]['retirement_id'] ?? 0);
            $this->assertOwnerOrReviewer($rid);
            (new LpoRetirements())->updateLine($lineId, [
                'description'    => $_POST['description']    ?? null,
                'amount'         => $_POST['amount']         ?? null,
                'coa_account_id' => $_POST['coa_account_id'] ?? null,
                'notes'          => $_POST['notes']          ?? null,
            ], $this->currentUserId());
            $r = (new LpoRetirements())->find($rid);
            echo json_encode(['ok' => true, 'totals' => [
                'total_spent'   => (float)$r['total_spent'],
                'cash_returned' => (float)$r['cash_returned'],
                'variance'      => (float)$r['variance'],
            ]]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    public function removeLineAjAction()
    {
        header('Content-Type: application/json');
        try {
            $lineId = (int)($_POST['line_id'] ?? 0);
            if ($lineId <= 0) throw new InvalidArgumentException('line_id required.');
            $db = new DB();
            $rid = (int)($db->mySelect("SELECT retirement_id FROM lpo_retirement_lines WHERE c0d = :id", [':id' => $lineId])[0]['retirement_id'] ?? 0);
            $this->assertOwnerOrReviewer($rid);
            (new LpoRetirements())->removeLine($lineId);
            $r = (new LpoRetirements())->find($rid);
            echo json_encode(['ok' => true, 'totals' => [
                'total_spent'   => (float)$r['total_spent'],
                'cash_returned' => (float)$r['cash_returned'],
                'variance'      => (float)$r['variance'],
            ]]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    public function submitAjAction()
    {
        header('Content-Type: application/json');
        try {
            $rid = (int)($_POST['retirement_id'] ?? 0);
            if ($rid <= 0) throw new InvalidArgumentException('retirement_id required.');
            $this->assertOwnerOrReviewer($rid);
            (new LpoRetirements())->submit($rid, $this->currentUserId(), $_SESSION['full_name'] ?? '');
            echo json_encode(['ok' => true, 'msg' => 'Submitted for Accountant review.']);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    /** Accountant queue: SUBMITTED retirements awaiting review. */
    public function queueAction()
    {
        $m = new LpoRetirements();
        $rows = $m->pendingReviewQueue();
        $this->view->setLayout($this->roleLayout());
        $this->view->render('Retirements/queue', [
            'page_title' => 'Retirements to review',
            'rows'       => $rows,
            'types'      => Lpos::TYPES(),
        ]);
    }

    /** Read-only detail — requester + reviewer both use this after submit. */
    public function viewAction($id = 0)
    {
        $id = (int)$id;
        if ($id <= 0) { $this->fb('Missing retirement id.'); return; }
        $m = new LpoRetirements();
        $r = $m->find($id);
        if (!$r) { $this->fb('Retirement not found.'); return; }
        $lpo = (new Lpos())->byId((int)$r['lpo_id']);

        // Reviewer or requester only.
        $role = strtolower(trim($this->currentUserGroup()));
        $isReviewer = in_array($role, ['accountant','finance manager','administrator','executive'], true);
        if ((int)$lpo['requested_by'] !== $this->currentUserId() && !$isReviewer) {
            $this->fb('You do not have access to this retirement.');
            return;
        }

        $lines = $m->linesFor($id);
        $isDiscovery = ($lpo['lpo_type'] === 'PRODUCT_DISCOVERY');
        $findings = $isDiscovery ? $m->findingsFor($id) : [];
        $receipts = $m->receiptsFor($id);
        $lineAtts = [];
        foreach ($lines as $ln) { $lineAtts[(int)$ln['c0d']] = $m->lineAttachmentsFor((int)$ln['c0d']); }
        $this->view->setLayout($this->roleLayout());
        $this->view->render('Retirements/view', [
            'page_title'  => 'Retirement · ' . $lpo['lpo_no'],
            'retirement'  => $r,
            'lpo'         => $lpo,
            'lines'       => $lines,
            'is_reviewer' => $isReviewer,
            'is_discovery'=> $isDiscovery,
            'findings'    => $findings,
            'receipts'    => $receipts,
            'line_atts'   => $lineAtts,
        ]);
    }

    public function approveAjAction()
    {
        header('Content-Type: application/json');
        try {
            $rid = (int)($_POST['retirement_id'] ?? 0);
            if ($rid <= 0) throw new InvalidArgumentException('retirement_id required.');
            $notes = trim((string)($_POST['notes'] ?? ''));
            $jeId = (new LpoRetirements())->approve($rid, $this->currentUserId(), $_SESSION['full_name'] ?? '', $notes);
            echo json_encode(['ok' => true, 'journal_entry_id' => $jeId, 'msg' => $jeId > 0
                ? 'Approved · journal posted (#' . $jeId . ').'
                : 'Approved · journal skipped (COA not fully configured).']);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    public function rejectAjAction()
    {
        header('Content-Type: application/json');
        try {
            $rid = (int)($_POST['retirement_id'] ?? 0);
            $reason = trim((string)($_POST['reason'] ?? ''));
            if ($rid <= 0)     throw new InvalidArgumentException('retirement_id required.');
            if ($reason === '') throw new InvalidArgumentException('Please explain why the retirement is being bounced back.');
            (new LpoRetirements())->reject($rid, $this->currentUserId(), $_SESSION['full_name'] ?? '', $reason);
            echo json_encode(['ok' => true, 'msg' => 'Bounced back to requester.']);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    /** Requester's own list. */
    public function myAction()
    {
        $rows = (new LpoRetirements())->forUser($this->currentUserId());
        $this->view->setLayout($this->roleLayout());
        $this->view->render('Retirements/my', [
            'page_title' => 'My retirements',
            'rows'       => $rows,
            'types'      => Lpos::TYPES(),
        ]);
    }

    /** Stream a receipt attachment (requester or reviewer). */
    public function receiptDownloadAction($lineId = 0)
    {
        $lineId = (int)$lineId;
        if ($lineId <= 0) { http_response_code(400); echo 'Bad request.'; return; }
        $db = new DB();
        $ln = $db->mySelect("SELECT * FROM lpo_retirement_lines WHERE c0d = :id", [':id' => $lineId]);
        if (empty($ln))                       { http_response_code(404); echo 'Not found.'; return; }
        if (empty($ln[0]['receipt_attachment_path'])) { http_response_code(404); echo 'No receipt on this line.'; return; }

        $rid = (int)$ln[0]['retirement_id'];
        try { $this->assertOwnerOrReviewer($rid); }
        catch (\Throwable $e) { http_response_code(403); echo 'Forbidden.'; return; }

        $abs = ROOT . '/' . ltrim((string)$ln[0]['receipt_attachment_path'], '/');
        if (!is_file($abs)) { http_response_code(404); echo 'File missing on disk.'; return; }
        $mime = (string)($ln[0]['receipt_mime'] ?: 'application/octet-stream');
        $name = (string)($ln[0]['receipt_original_name'] ?: basename($abs));
        header('Content-Type: ' . $mime);
        header('Content-Length: ' . (int)filesize($abs));
        header('Content-Disposition: inline; filename="' . str_replace('"', '', $name) . '"');
        readfile($abs);
        exit();
    }

    /* ── Slice 8Z-q · Cross-cutting receipts + line attachments ─────────── */

    public function addReceiptAjAction()
    {
        header('Content-Type: application/json');
        try {
            $rid = (int)($_POST['retirement_id'] ?? 0);
            if ($rid <= 0) throw new InvalidArgumentException('retirement_id required.');
            $this->assertOwnerOrReviewer($rid);
            $file = $_FILES['attachment'] ?? null;
            $recId = (new LpoRetirements())->addReceipt($rid, [
                'receipt_type'   => (string)($_POST['receipt_type'] ?? 'NORMAL'),
                'subject'        => (string)($_POST['subject'] ?? ''),
                'supplier_name'  => (string)($_POST['supplier_name'] ?? ''),
                'efd_receipt_no' => (string)($_POST['efd_receipt_no'] ?? ''),
                'efd_date'       => (string)($_POST['efd_date'] ?? ''),
                'supplier_tin'   => (string)($_POST['supplier_tin'] ?? ''),
                'gross_amount'   => (float) ($_POST['gross_amount'] ?? 0),
                'vat_amount'     => $_POST['vat_amount'] ?? null,
                'notes'          => (string)($_POST['notes'] ?? ''),
            ], $file, $this->currentUserId(), $_SESSION['full_name'] ?? '');
            echo json_encode(['ok' => true, 'receipt_id' => $recId]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    public function updateReceiptAjAction()
    {
        header('Content-Type: application/json');
        try {
            $recId = (int)($_POST['receipt_id'] ?? 0);
            if ($recId <= 0) throw new InvalidArgumentException('receipt_id required.');
            $rec = (new LpoRetirements())->findReceipt($recId);
            if (empty($rec)) throw new RuntimeException('Receipt not found.');
            $this->assertOwnerOrReviewer((int)$rec['retirement_id']);
            (new LpoRetirements())->updateReceipt($recId, [
                'receipt_type'   => $_POST['receipt_type']   ?? null,
                'subject'        => $_POST['subject']        ?? null,
                'supplier_name'  => $_POST['supplier_name']  ?? null,
                'efd_receipt_no' => $_POST['efd_receipt_no'] ?? null,
                'efd_date'       => $_POST['efd_date']       ?? null,
                'supplier_tin'   => $_POST['supplier_tin']   ?? null,
                'gross_amount'   => $_POST['gross_amount']   ?? null,
                'vat_amount'     => $_POST['vat_amount']     ?? null,
                'notes'          => $_POST['notes']          ?? null,
            ]);
            echo json_encode(['ok' => true]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    public function removeReceiptAjAction()
    {
        header('Content-Type: application/json');
        try {
            $recId = (int)($_POST['receipt_id'] ?? 0);
            if ($recId <= 0) throw new InvalidArgumentException('receipt_id required.');
            $rec = (new LpoRetirements())->findReceipt($recId);
            if (empty($rec)) throw new RuntimeException('Receipt not found.');
            $this->assertOwnerOrReviewer((int)$rec['retirement_id']);
            (new LpoRetirements())->removeReceipt($recId);
            echo json_encode(['ok' => true]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    public function setLineReceiptAjAction()
    {
        header('Content-Type: application/json');
        try {
            $lineId = (int)($_POST['line_id'] ?? 0);
            $rIn    = $_POST['receipt_id'] ?? '';
            $receiptId = ($rIn === '' || $rIn === '0') ? null : (int)$rIn;
            if ($lineId <= 0) throw new InvalidArgumentException('line_id required.');
            $db  = new DB();
            $rid = (int)($db->mySelect("SELECT retirement_id FROM lpo_retirement_lines WHERE c0d = :id", [':id' => $lineId])[0]['retirement_id'] ?? 0);
            $this->assertOwnerOrReviewer($rid);
            (new LpoRetirements())->setLineReceipt($lineId, $receiptId);
            echo json_encode(['ok' => true]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    public function addLineAttachmentAjAction()
    {
        header('Content-Type: application/json');
        try {
            $lineId = (int)($_POST['line_id'] ?? 0);
            if ($lineId <= 0) throw new InvalidArgumentException('line_id required.');
            $db  = new DB();
            $rid = (int)($db->mySelect("SELECT retirement_id FROM lpo_retirement_lines WHERE c0d = :id", [':id' => $lineId])[0]['retirement_id'] ?? 0);
            $this->assertOwnerOrReviewer($rid);
            $file = $_FILES['attachment'] ?? null;
            if (!$file || empty($file['tmp_name'])) throw new InvalidArgumentException('No file uploaded.');
            $attId = (new LpoRetirements())->addLineAttachment($lineId, $file, $this->currentUserId(), $_SESSION['full_name'] ?? '');
            echo json_encode(['ok' => true, 'attachment_id' => $attId]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    public function removeLineAttachmentAjAction()
    {
        header('Content-Type: application/json');
        try {
            $attId = (int)($_POST['attachment_id'] ?? 0);
            if ($attId <= 0) throw new InvalidArgumentException('attachment_id required.');
            $m = new LpoRetirements();
            $att = $m->findLineAttachment($attId);
            if (empty($att)) throw new RuntimeException('Attachment not found.');
            $db  = new DB();
            $rid = (int)($db->mySelect("SELECT retirement_id FROM lpo_retirement_lines WHERE c0d = :id", [':id' => (int)$att['line_id']])[0]['retirement_id'] ?? 0);
            $this->assertOwnerOrReviewer($rid);
            $m->removeLineAttachment($attId);
            echo json_encode(['ok' => true]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    /** Stream a receipt's own attachment (cross-cutting receipt entity). */
    public function receiptFileDownloadAction($receiptId = 0)
    {
        $receiptId = (int)$receiptId;
        if ($receiptId <= 0) { http_response_code(400); echo 'Bad request.'; return; }
        $rec = (new LpoRetirements())->findReceipt($receiptId);
        if (empty($rec) || empty($rec['attachment_path'])) { http_response_code(404); echo 'Not found.'; return; }
        try { $this->assertOwnerOrReviewer((int)$rec['retirement_id']); }
        catch (\Throwable $e) { http_response_code(403); echo 'Forbidden.'; return; }
        $abs = ROOT . '/' . ltrim((string)$rec['attachment_path'], '/');
        if (!is_file($abs)) { http_response_code(404); echo 'File missing on disk.'; return; }
        header('Content-Type: ' . (string)($rec['attachment_mime'] ?: 'application/octet-stream'));
        header('Content-Length: ' . (int)filesize($abs));
        header('Content-Disposition: inline; filename="' . str_replace('"', '', (string)($rec['attachment_original_name'] ?: basename($abs))) . '"');
        readfile($abs);
        exit();
    }

    /** Stream a per-line attachment (photo / proof). */
    public function lineAttachmentDownloadAction($attId = 0)
    {
        $attId = (int)$attId;
        if ($attId <= 0) { http_response_code(400); echo 'Bad request.'; return; }
        $m   = new LpoRetirements();
        $att = $m->findLineAttachment($attId);
        if (empty($att)) { http_response_code(404); echo 'Not found.'; return; }
        $db  = new DB();
        $rid = (int)($db->mySelect("SELECT retirement_id FROM lpo_retirement_lines WHERE c0d = :id", [':id' => (int)$att['line_id']])[0]['retirement_id'] ?? 0);
        try { $this->assertOwnerOrReviewer($rid); }
        catch (\Throwable $e) { http_response_code(403); echo 'Forbidden.'; return; }
        $abs = ROOT . '/' . ltrim((string)$att['file_path'], '/');
        if (!is_file($abs)) { http_response_code(404); echo 'File missing on disk.'; return; }
        header('Content-Type: ' . (string)($att['mime'] ?: 'application/octet-stream'));
        header('Content-Length: ' . (int)filesize($abs));
        header('Content-Disposition: inline; filename="' . str_replace('"', '', (string)($att['original_name'] ?: basename($abs))) . '"');
        readfile($abs);
        exit();
    }

    /* ── Batch 4 · Product Discovery findings ─────────────────────────── */

    public function addFindingAjAction()
    {
        header('Content-Type: application/json');
        try {
            $rid = (int)($_POST['retirement_id'] ?? 0);
            if ($rid <= 0) throw new InvalidArgumentException('retirement_id required.');
            $this->assertOwnerOrReviewer($rid);
            $id = (new LpoRetirements())->addFinding($rid, $this->collectFindingFields(), $this->currentUserId());
            echo json_encode(['ok' => true, 'finding_id' => $id]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    public function updateFindingAjAction()
    {
        header('Content-Type: application/json');
        try {
            $fid = (int)($_POST['finding_id'] ?? 0);
            if ($fid <= 0) throw new InvalidArgumentException('finding_id required.');
            $db = new DB();
            $rid = (int)($db->mySelect("SELECT retirement_id FROM lpo_retirement_findings WHERE c0d = :id", [':id' => $fid])[0]['retirement_id'] ?? 0);
            $this->assertOwnerOrReviewer($rid);
            (new LpoRetirements())->updateFinding($fid, $this->collectFindingFields());
            echo json_encode(['ok' => true]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    public function removeFindingAjAction()
    {
        header('Content-Type: application/json');
        try {
            $fid = (int)($_POST['finding_id'] ?? 0);
            if ($fid <= 0) throw new InvalidArgumentException('finding_id required.');
            $db = new DB();
            $rid = (int)($db->mySelect("SELECT retirement_id FROM lpo_retirement_findings WHERE c0d = :id", [':id' => $fid])[0]['retirement_id'] ?? 0);
            $this->assertOwnerOrReviewer($rid);
            (new LpoRetirements())->removeFinding($fid);
            echo json_encode(['ok' => true]);
        } catch (\Throwable $e) { echo json_encode(['ok' => false, 'msg' => $e->getMessage()]); }
    }

    private function collectFindingFields(): array
    {
        return [
            'supplier_name'        => (string)($_POST['supplier_name'] ?? ''),
            'contact_name'         => (string)($_POST['contact_name'] ?? ''),
            'phone'                => (string)($_POST['phone'] ?? ''),
            'email'                => (string)($_POST['email'] ?? ''),
            'tin'                  => (string)($_POST['tin'] ?? ''),
            'country'              => (string)($_POST['country'] ?? ''),
            'capability_notes'     => (string)($_POST['capability_notes'] ?? ''),
            'quote_received'       => !empty($_POST['quote_received']) ? 1 : 0,
            'quote_amount'         => $_POST['quote_amount'] ?? null,
            'quote_currency'       => (string)($_POST['quote_currency'] ?? ''),
            'quote_lead_time_days' => $_POST['quote_lead_time_days'] ?? null,
            'sample_received'      => !empty($_POST['sample_received']) ? 1 : 0,
            'recommendation'       => (string)($_POST['recommendation'] ?? 'MAYBE'),
            'rationale'            => (string)($_POST['rationale'] ?? ''),
        ];
    }

    /* ── Internals ─────────────────────────────────────────────────────── */

    private function assertOwnerOrReviewer(int $retirementId): void
    {
        $db = new DB();
        $rs = $db->mySelect(
            "SELECT r.c0d, l.requested_by
               FROM lpo_retirements r
               JOIN lpos l ON l.c0d = r.lpo_id
              WHERE r.c0d = :id",
            [':id' => $retirementId]
        );
        if (empty($rs)) throw new RuntimeException('Retirement not found.');
        $role = strtolower(trim($this->currentUserGroup()));
        $isReviewer = in_array($role, ['accountant','finance manager','administrator'], true);
        if ((int)$rs[0]['requested_by'] !== $this->currentUserId() && !$isReviewer) {
            throw new RuntimeException('You do not have access to this retirement.');
        }
    }

    private function fb(string $msg): void
    {
        $this->view->setLayout($this->roleLayout());
        $this->view->render('generals/fb_page', [
            'page_title' => 'Retirements',
            'fb_msg'     => '<div class="alert alert-warning">' . htmlspecialchars($msg) . '</div>',
        ]);
    }
}
