<?php
declare(strict_types=1);

namespace App\Controller;

use Cake\Event\EventInterface;
use Cake\Datasource\ConnectionManager;
use Cake\Core\Configure;
use Stripe\Stripe;
use Stripe\Checkout\Session;
use Stripe\Webhook;
use Cake\Routing\Router;
use Cake\Mailer\Mailer;

require_once ROOT . DS . 'vendor' . DS . 'autoload.php';

class OrdersController extends AppController
{


    /** quick guard used across admin actions */
    private function isAdmin(): bool
    {
        $id = $this->request->getAttribute('identity');
        return (bool)($id && $id->get('role') === 'admin');
    }
    private function isStaff(): bool
    {
        $id = $this->request->getAttribute('identity');
        return (bool)($id && $id->get('role') === 'staff');
    }

    public function beforeFilter(EventInterface $event): void
    {
        parent::beforeFilter($event);
        // guests can checkout
        if ($this->components()->has('Authentication')) {
            $this->Authentication->allowUnauthenticated(['checkout', 'place', 'confirmation', 'pay', 'webhook']);
        }
        if ($this->components()->has('Authorization')) {
            $this->Authorization->skipAuthorization();
        }
    }

    /** Build items from the session cart (shared with CartController logic) */
    private function buildCart(): array
    {
        $session = $this->request->getSession();
        // Your cart format: [product_id => qty]
        $raw = (array)$session->read('Cart');
        if (!$raw) {
            return ['items' => [], 'subtotal' => 0.0, 'totalQty' => 0];
        }

        $ids = array_keys($raw);
        $Products = $this->fetchTable('Products');

        $rows = $Products->find()
            ->where(['Products.id IN' => $ids])
            ->all()
            ->indexBy('id')
            ->toArray();

        $items = [];
        $subtotal = 0.0;
        $totalQty = 0;

        foreach ($raw as $pid => $qty) {
            if (!isset($rows[$pid])) { continue; }
            $p = $rows[$pid];

            $price = (isset($p->sale_price) && $p->sale_price !== null && (float)$p->sale_price < (float)$p->price)
                ? (float)$p->sale_price
                : (float)$p->price;

            $line = $price * (int)$qty;
            $subtotal += $line;
            $totalQty += (int)$qty;

            $items[] = [
                'product'  => $p,          // full product (for view)
                'product_id' => (int)$p->id,
                'name'     => (string)$p->name,
                'qty'      => (int)$qty,
                'price'    => $price,
                'subtotal' => $line,
            ];
        }

        return compact('items', 'subtotal', 'totalQty');
    }

    /** GET/POST /orders/checkout  */
    public function checkout()
    {
        $session = $this->request->getSession();
        $removed = 0;
        if ($this->request->is('post')) {
            $action = (string)$this->request->getData('action');

            if ($action === 'save_note') {
                $session->write('Order.note', (string)$this->request->getData('note'));
                $session->write('Order.email', (string)$this->request->getData('email'));
            } elseif ($action === 'apply_promo') {
                $code = trim((string)$this->request->getData('promo'));  // <input name="promo">
                if (strcasecmp($code, 'BrewhubLover123') === 0) {
                    $session->write('Order.promo', $code);
                    $removed = 0;
                    $this->Flash->success('Promo applied: 5% off your order.');
                } else {
                    $session->delete('Order.promo');
                    $this->Flash->error('Invalid promo code.');
                }
            } elseif ($action === 'remove_promo') {
                $session->delete('Order.promo');
                $removed = 1;
                $this->Flash->success('Promo removed.');

            } elseif ($action === 'update_qty') {
                $id  = (int)$this->request->getData('id');
                $qty = max(0, (int)$this->request->getData('qty'));
                $cart = (array)$session->read('Cart');
                if ($qty > 0) {
                    $cart[$id] = $qty;
                } else {
                    unset($cart[$id]);
                }
                $session->write('Cart', $cart);
            } elseif ($action === 'remove_item') {
                $id = (int)$this->request->getData('id');
                $cart = (array)$session->read('Cart');
                unset($cart[$id]);
                $session->write('Cart', $cart);
            }

            return $this->redirect(['action' => 'checkout']);
        }

        $cart = $this->buildCart();
        $note  = (string)$session->read('Order.note') ?: '';
        $email = (string)$session->read('Order.email') ?: '';
        $promo = (string)$session->read('Order.promo') ?: '';
        if ($removed == 0){
            $discount = 0.05 * $cart['subtotal'];
            $total = $cart['subtotal'] - $discount;
        } else {
            $discount = 0;
            $total = $cart['subtotal'] - $discount;
        }
        

        $this->set([
            'items'    => $cart['items'],
            'subtotal' => $cart['subtotal'],
            'discount' => $discount,
            'total'    => $total,
            'notes'     => $note,
            'email'    => $email,
            'promo'    => $promo,
        ]);
        
    }

    /** POST /orders/place — actually creates orders + order_items */
    public function place()
    {
        $this->request->allowMethod(['post']);
        $session = $this->request->getSession();

        $cart = $this->buildCart();
        if (empty($cart['items'])) {
            $this->Flash->error('Your cart is empty.');
            return $this->redirect(['action' => 'checkout']);
        }

        // Build order payload
        $subtotal = (float)($cart['subtotal'] ?? 0);
$promo    = (string)$session->read('Order.promo') ?: '';     // from checkout
$rate     = strcasecmp($promo, 'BrewhubLover123') === 0 ? 0.05 : 0.0;

$discount = $rate > 0 ? round($subtotal * $rate, 2) : 0.0;
$total    = max(0.0, round($subtotal - $discount, 2));

        $orderItems = [];
        foreach ($cart['items'] as $it) {
            $orderItems[] = [
                'product_id' => $it['product_id'],
                'name'       => $it['name'],
                'price'      => $it['price'],
                'qty'        => $it['qty'],
                'line_total' => $it['subtotal'],
                'unit_price' => $it['price'],

            ];
        }

        $Orders = $this->fetchTable('Orders');
        $Products = $this->fetchTable('Products');

        $conn = ConnectionManager::get('default');
        $conn->begin();
        $userId    = $this->request->getAttribute('identity')?->get('id') ?? null;
        $userEmail = $this->request->getAttribute('identity')?->get('email') ?? null;
        $sessionEmail = (string)$session->read('Order.email') ?: null;
        $note = $session->read('Order.note') ?: $session->read('Order.email');
        if (empty($userEmail) && empty($sessionEmail)) {
            $this->Flash->error('Email is required.');
            return $this->redirect(['action' => 'checkout']);
        }
        try {
            $order = $this->Orders->newEntity([
                // guest checkout fields (nullable)
                'user_id' => $userId,
                'customer_name'  => $this->request->getData('name') ?? null,
                'email' => $userEmail ?? $sessionEmail,
                'notes' => $note,
                'subtotal' => $subtotal,
                'discount' => $discount,
                'total'    => $total,
                'status'   => 'waiting',
                'order_items' => $orderItems,
            ], ['associated' => ['OrderItems']]);

            // If you didn’t add beforeSave generateOrderNo(), use this fallback:
            if (empty($order->order_no)) {
                $order->order_no = $this->generateOrderNoFallback();
            }

            if (!$this->Orders->save($order, ['associated' => ['OrderItems']])) {
                throw new \RuntimeException('Save failed: ' . json_encode($order->getErrors(), JSON_PRETTY_PRINT));
            }

            foreach ($orderItems as $it) {
                $product = $Products->get
                ($it['product_id'],
                    fields: ['id', 'name', 'sku', 'stock', 'threshold']
                );

                $prevStock  = (int)$product->stock;
                $newStock   = max(0, $prevStock - (int)$it['qty']);
                $threshold = (int)($product->threshold ?? 10); // use the product’s own threshold, fallback 10

                $product->stock = $newStock;
                if (!$Products->save($product)) {
                    throw new \RuntimeException("Failed to update stock for product {$product->id}");
                }

                if ($prevStock > $threshold && $newStock <= $threshold) {
                    try {
                        $mailer = new Mailer('internal'); // same as ContactController
                        $mailer->setTo('brewhub@u25s2212.iedev.org')
                            ->setSubject('[BrewHub] Low Stock Alert')
                            ->deliver(
                                "Product: {$product->name}\n" .
                                "SKU: {$product->sku}\n" .
                                "Previous stock: {$prevStock}\n" .
                                "Current stock: {$newStock}\n" .
                                "Threshold: {$threshold}\n" .
                                "Link: " . Router::url(
                                    ['controller' => 'Products', 'action' => 'view', $product->id],
                                    true // full base URL
                                )
                            );
                    } catch (\Throwable $mailEx) {
                        // Don’t break checkout if email fails; just log it.
                        \Cake\Log\Log::error('Low-stock email failed: ' . $mailEx->getMessage());
                    }
                }
            }

            // Clear cart + ephemeral order session
            $session->delete('Cart');
            $session->delete('Order');

            $conn->commit();

            return $this->redirect(['action' => 'confirmation', $order->order_no]);

        } catch (\Throwable $e) {
            $conn->rollback();
            if (Configure::read('debug')) {
                $this->Flash->error('Checkout failed: ' . $e->getMessage());
            } else {
                $this->Flash->error('Checkout failed. Please try again.');
            }
            return $this->redirect(['action' => 'checkout']);
        }

    }

    /** Build compact, safe metadata for Stripe (<=500 chars per value) */
    private function buildStripeMetadata(string $orderNo, array $cart): array
    {
        // Minimal, deterministic metadata
        $itemCodes = [];
        foreach ($cart['items'] as $it) {
            // "pid x qty", e.g. "33x1"
            $itemCodes[] = ((int)$it['product_id']) . 'x' . ((int)$it['qty']);
        }

        $meta = [
            'order_no'    => $orderNo,
            'item_count'  => (string)count($cart['items']),
            'items_compact' => implode(',', $itemCodes),               // e.g. "33x1,32x3,15x1"
            'subtotal_c'  => (string)(int)round(((float)$cart['subtotal']) * 100),
            'discount_c'  => '0',
            'total_c'     => (string)(int)round(((float)$cart['subtotal']) * 100), // no promo yet
        ];

        // Guard-rail: never exceed Stripe's 500 chars per value
        foreach ($meta as $k => $v) {
            if (strlen($v) > 480) {
                $meta[$k] = substr($v, 0, 480);
            }
        }
        return $meta;
    }

    // composer require stripe/stripe-php
    // for Stripe payment
    public function pay()
    {
        $this->request->allowMethod(['post']);
        $session = $this->request->getSession();
        @ini_set('serialize_precision', '-1');
        @ini_set('precision', '14');

        // 1) Build cart
        $cart = $this->buildCart();
        if (empty($cart['items'])) {
            $this->Flash->error('Your cart is empty.');
            return $this->redirect(['action' => 'checkout']);
        }

        // 2) Prepare order items (reuse structure from place())
        $orderItems = [];
        foreach ($cart['items'] as $it) {
            $orderItems[] = [
                'product_id' => $it['product_id'],
                'name'       => $it['name'],
                'price'      => $it['price'],
                'qty'        => $it['qty'],
                'line_total' => $it['subtotal'],
                'unit_price' => $it['price'],
            ];
        }

        // Totals (plug promo/discount logic here if needed)
        $subtotal = (float)$cart['subtotal'];
        $discount = 0.0;
        $total    = max(0, $subtotal - $discount);

        // 3) Create a pending order FIRST, so we can show a full receipt after Stripe
        $Orders = $this->fetchTable('Orders');
        $Products = $this->fetchTable('Products');

        $conn   = ConnectionManager::get('default');
        $conn->begin();
        $userId    = $this->request->getAttribute('identity')?->get('id') ?? null;
        $userEmail = $this->request->getAttribute('identity')?->get('email') ?? null;
        $sessionEmail = (string)$session->read('Order.email') ?: null;
        $note = $session->read('Order.note') ?: $session->read('Order.email');
        if (empty($userEmail) && empty($sessionEmail)) {
            $this->Flash->error('Email is required.');
            return $this->redirect(['action' => 'checkout']);
        }
        try {
            $order = $Orders->newEntity([
                'user_id' => $userId,
                'customer_name'  => $this->request->getData('name') ?? null,
                'email' => $userEmail ?? $sessionEmail ,
                'subtotal'       => $subtotal,
                'discount'       => $discount,
                'notes' => $note,
                'total'          => $total,
                'status'         => 'waiting',         // will be completed via webhook after payment
                'order_items'    => $orderItems,
            ], ['associated' => ['OrderItems']]);

            if (empty($order->order_no)) {
                $order->order_no = $this->generateOrderNoFallback();
            }

            if (!$Orders->save($order, ['associated' => ['OrderItems']])) {
                throw new \RuntimeException('Order save failed: ' . json_encode($order->getErrors(), JSON_PRETTY_PRINT));
            }

            // 4) Build Stripe line items
            $lineItems = [];
            foreach ($cart['items'] as $it) {
                $lineItems[] = [
                    'quantity'   => (int)$it['qty'],
                    'price_data' => [
                        'currency'     => 'aud',
                        'unit_amount'  => (int)round($it['price'] * 100), // cents
                        'product_data' => [
                            'name'     => (string)$it['name'],
                            'metadata' => ['product_id' => (string)$it['product_id']],
                        ],
                    ],
                ];
            }

            // 5) Stripe session that routes back to confirmation/{orderNo}
            \Stripe\Stripe::setApiKey(Configure::read('Stripe.secret'));

            $successUrl = Router::url(
                    ['controller' => 'Orders', 'action' => 'confirmation', $order->order_no, '_full' => true]
                ) . '?sid={CHECKOUT_SESSION_ID}';
            $cancelUrl  = Router::url(['controller' => 'Orders', 'action' => 'checkout', '_full' => true]);

            // --- Compact, bounded metadata (no huge JSON) ---
            $codes = [];
            foreach ($cart['items'] as $it) {
                $codes[] = ((int)$it['product_id']) . 'x' . ((int)$it['qty']); // e.g. "33x1"
            }
            $metadata = [
                'order_no'      => $order->order_no,
                'item_count'    => (string)count($cart['items']),
                'items_compact' => implode(',', $codes),                         // e.g. "33x1,32x3,15x1"
                'subtotal_c'    => (string)(int)round($subtotal * 100),
                'discount_c'    => (string)(int)round($discount * 100),
                'total_c'       => (string)(int)round($total * 100),
            ];
            // Guard-rail: ensure no single value can exceed Stripe's 500-char limit
            foreach ($metadata as $k => $v) {
                if (strlen($v) > 480) { // leave headroom
                    $metadata[$k] = substr($v, 0, 480);
                }
            }

            $session = \Stripe\Checkout\Session::create([
                'mode'                 => 'payment',
                'line_items'           => $lineItems,
                'success_url'          => $successUrl,
                'cancel_url'           => $cancelUrl,
                'client_reference_id'  => $order->order_no,
                'metadata'             => $metadata,
            ]);

            foreach ($orderItems as $it) {
                $product = $Products->get(
                    $it['product_id'],
                    fields: ['id', 'name', 'sku', 'stock', 'threshold']
                );

                $prevStock = (int)$product->stock;
                $newStock   = max(0, $prevStock - (int)$it['qty']);
                $threshold = (int)($product->threshold ?? 10); // per-product threshold

                $product->stock = $newStock;
                if (!$Products->save($product)) {
                    throw new \RuntimeException("Failed to update stock for product {$product->id}");
                }

                if ($prevStock > $threshold && $newStock <= $threshold) {
                    try {
                        $mailer = new \Cake\Mailer\Mailer('internal'); // same as ContactController
                        $mailer->setTo('brewhub@u25s2212.iedev.org')
                            ->setSubject('[BrewHub] Low Stock Alert')
                            ->deliver(
                                "Product: {$product->name}\n" .
                                "SKU: {$product->sku}\n" .
                                "Previous stock: {$prevStock}\n" .
                                "Current stock: {$newStock}\n" .
                                "Threshold: {$threshold}\n" .
                                "Link: " . \Cake\Routing\Router::url(
                                    ['controller' => 'Products', 'action' => 'view', $product->id],
                                    true
                                )
                            );
                    } catch (\Throwable $mailEx) {
                        \Cake\Log\Log::error('Low-stock email failed: ' . $mailEx->getMessage());
                    }
                }

            }

            // Keep cart until payment succeeds
            $conn->commit();
            return $this->redirect($session->url);

        } catch (\Stripe\Exception\ApiErrorException $e) {
            $conn->rollback();
            $this->Flash->error('Payment setup failed. Please try again. (' . $e->getMessage() . ')');
            return $this->redirect(['action' => 'checkout']);
        } catch (\Throwable $e) {
            $conn->rollback();
            if (Configure::read('debug')) {
                $this->Flash->error('Checkout failed: ' . $e->getMessage());
            } else {
                $this->Flash->error('Checkout failed. Please try again.');
            }
            return $this->redirect(['action' => 'checkout']);
        }
    }

    /** GET /orders/confirmation/{orderNo} */
    public function confirmation(string $orderNo = '')
    {
        // FOR TESTING PURPOSES
        $session = $this->request->getSession();

        // Don’t 404 if someone refreshes without param; just show a plain page
        $order = null;

        if ($orderNo !== '') {
            $order = $this->fetchTable('Orders')->find()
                ->where(['order_no' => $orderNo])
                ->contain(['OrderItems'])
                ->first(); // may be null if someone pastes a bad URL
        }

        // Clear via verified Checkout Session (no webhook needed)
        $sid = (string)$this->request->getQuery('sid');
        if ($sid && $order) {
            try {
                \Stripe\Stripe::setApiKey(\Cake\Core\Configure::read('Stripe.secret'));
                $sess = \Stripe\Checkout\Session::retrieve($sid);

                // Stripe marks this "paid" when payment has succeeded
                if (($sess->payment_status ?? null) === 'paid') {
                    if ($session->check('Cart')) {
                        $session->delete('Cart');
                        $session->delete('Order');
                    }
                }
            } catch (\Throwable $e) {
            }
        }

        if ($order && ($order->status ?? null) === 'completed' && $session->check('Cart')) {
            $session->delete('Cart');
            $session->delete('Order');
        }

        $this->set(compact('order', 'orderNo'));
    }

    /** Admin list: /admin/orders (you’re guarding manually) */
    public function adminIndex()
    {
        if (!$this->isStaff() && !$this->isAdmin()) {
            throw new \Cake\Http\Exception\ForbiddenException('Staffs only.');
        }

        $Orders = $this->fetchTable('Orders');

        // Optional filters (q, status)
        $qParam      = trim((string)$this->request->getQuery('q'));
        $status = trim((string)$this->request->getQuery('status'));

        $query = $Orders->find()
            ->where(['status IN' => ['Waiting','Approved','Processing']]);
        $func  = $query->func();

        // SUM(qty) as items_count
        $query
            ->select([
                'Orders.id', 'Orders.order_no', 'Orders.created',
                'Orders.customer_name', 'Orders.email',
                'Orders.total', 'Orders.status',
                // SUM(OrderItems.qty), wrapped in COALESCE to avoid nulls
                'items_count' => $func->coalesce([$func->sum('OrderItems.qty'), 0]),
            ])
            ->leftJoinWith('OrderItems')     // requires Orders hasMany OrderItems
            ->groupBy(['Orders.id'])
            ->orderByDesc('Orders.created');

        if ($status && $status !== 'all') {
            $query->where(['Orders.status' => $status]);
        }
        if ($qParam !== '') {
            $like = "%{$qParam}%";
            $query->where([
                'OR' => [
                    'Orders.order_no LIKE'      => $like,
                    'Orders.customer_name LIKE' => $like,
                    'Orders.email LIKE'          => $like,
                ]
            ]);
        }

        $this->paginate = ['limit' => 20];
        $orders = $this->paginate($query);
        $this->set(compact('orders'));

        $statuses = ['waiting','approved','processing','completed','canceled'];
        $this->set(compact('orders', 'statuses'));
    }

    /** Admin detail: /admin/orders/view/{id} */
    public function adminView($id = null)
    {
        if (!$this->isStaff() && !$this->isAdmin()) {
            throw new \Cake\Http\Exception\ForbiddenException('Staffs only.');
        }

        if (!$id) {
            $this->Flash->error('Order ID is missing.');
            return $this->redirect(['action' => 'adminIndex']);
        }

        $Orders = $this->fetchTable('Orders');

        try {
            $order = $Orders->get($id, contain: ['OrderItems']);

            $statuses = ['waiting','approved','processing','completed','canceled'];

            $this->set(compact('order', 'statuses'));
        } catch (\Cake\Datasource\Exception\RecordNotFoundException $e) {
            $this->Flash->error('Order not found.');
            return $this->redirect(['action' => 'adminIndex']);
        }
    }

    public function archived()
    {
        if (!$this->isStaff() && !$this->isAdmin()) {
            throw new \Cake\Http\Exception\ForbiddenException('Staffs only.');
        }

        $Orders = $this->fetchTable('Orders');

        // Optional filters (q, status)
        $qParam      = trim((string)$this->request->getQuery('q'));
        $status = trim((string)$this->request->getQuery('status'));

        $query = $this->Orders->find()
            ->where(['status IN' => ['completed','canceled']]);

        $func  = $query->func();

        // SUM(qty) as items_count
        $query
            ->select([
                'Orders.id', 'Orders.order_no', 'Orders.created',
                'Orders.customer_name', 'Orders.email',
                'Orders.total', 'Orders.status',
                // SUM(OrderItems.qty), wrapped in COALESCE to avoid nulls
                'items_count' => $func->coalesce([$func->sum('OrderItems.qty'), 0]),
            ])
            ->leftJoinWith('OrderItems')     // requires Orders hasMany OrderItems
            ->groupBy(['Orders.id'])
            ->orderByDesc('Orders.created');

        if ($status && $status !== 'all') {
            $query->where(['Orders.status' => $status]);
        }
        if ($qParam !== '') {
            $like = "%{$qParam}%";
            $query->where([
                'OR' => [
                    'Orders.order_no LIKE'      => $like,
                    'Orders.customer_name LIKE' => $like,
                    'Orders.email LIKE'          => $like,
                ]
            ]);
        }

        $this->paginate = ['limit' => 20];
        $orders = $this->paginate($query);
        $this->set(compact('orders'));

        $statuses = ['waiting','approved','processing','completed','canceled'];
        $this->set(compact('orders', 'statuses'));
    }

    /** Admin: update status (single “l” → 'canceled' to match DB ENUM) */
    public function setStatus($id = null)
    {
        if (!$this->isStaff() && !$this->isAdmin()) {
            throw new \Cake\Http\Exception\ForbiddenException('Staffs only.');
        }
        $this->request->allowMethod(['post']);

        $status = (string)$this->request->getData('status');
        $valid  = ['waiting','approved','processing','completed','canceled'];
        if (!in_array($status, $valid, true)) {
            $this->Flash->error('Invalid status.');
            return $this->redirect($this->referer(['action' => 'adminIndex'], true));
        }

        $Orders = $this->fetchTable('Orders');
        $order  = $Orders->get($id);
        $order->status = $status;

        if ($Orders->save($order)) {
            $this->Flash->success('Status updated.');
        } else {
            $this->Flash->error('Could not update status.');
        }
        return $this->redirect($this->referer(['action' => 'adminIndex'], true));
    }

    /** Admin: cancel */
    public function cancel($id = null)
    {
        if (!$this->isStaff() && !$this->isAdmin()) {
            throw new \Cake\Http\Exception\ForbiddenException('Staffs only.');
        }
        $this->request->allowMethod(['post']);

        $Orders = $this->fetchTable('Orders');
        $order  = $Orders->get($id);
        $order->status = 'canceled'; // single “l” to match schema

        if ($Orders->save($order)) {
            $this->Flash->success('Order canceled.');
        } else {
            $this->Flash->error('Could not cancel order.');
        }
        return $this->redirect(['action' => 'adminView', $id]);
    }

    public function webhook()
    {
        $this->request->allowMethod(['post']);

        $payload   = @file_get_contents('php://input');
        $sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
        $secret    = Configure::read('Stripe.webhook_secret');

        try {
            $event = \Stripe\Webhook::constructEvent($payload, $sigHeader, $secret);
        } catch (\UnexpectedValueException $e) {
            // Invalid payload
            return $this->response->withStatus(400);
        } catch (\Stripe\Exception\SignatureVerificationException $e) {
            // Invalid signature
            return $this->response->withStatus(400);
        }

        if ($event->type === 'checkout.session.completed') {
            /** @var \Stripe\Checkout\Session $session */
            $session = $event->data->object;

            $orderNo = $session->client_reference_id ?? ($session->metadata->order_no ?? null);
            if ($orderNo) {
                $Orders = $this->fetchTable('Orders');
                $order  = $Orders->find()->where(['order_no' => $orderNo])->first();

                if ($order) {
                    $order->status = 'completed';
                    $Orders->save($order);
                }
            }
        }

        // Always return 200 to tell Stripe we received it
        return $this->response->withStatus(200);
    }

    /** Fallback order number generator if Table hook not present */
    private function generateOrderNoFallback(): string
    {
        $prefix = 'BH-' . date('Ymd') . '-';
        $Orders = $this->fetchTable('Orders');
        do {
            $candidate = $prefix . strtoupper(bin2hex(random_bytes(2))); // e.g., BH-20250918-7F3C
        } while ($Orders->exists(['order_no' => $candidate]));
        return $candidate;
    }
}
