<?php

namespace routes;

use Exception;
use attachments\helpers\attachment_content;
use classes\attachment_store;
use classes\attachments;
use classes\authentication;
use classes\economic;
use classes\order_reference_suggestions_service;
use classes\orders_input_normalizer;
use classes\pdf_store;
use classes\response;
use classes\stripe;
use JetBrains\PhpStorm\NoReturn;
use objects\collected_order_invoices_o;
use objects\departments_o;
use objects\economic_module_orders;
use objects\logs_o;
use objects\order_bookings_o;
use objects\orders_o;
use objects\stripe_module_orders_o;
use objects\stripe_payment_intents_o;
use objects\users_o;
use traits\route_t;
use modules\subusers\helpers\subusers_permission_node_key;

class ordersRoute
{
    use route_t;

    public function run(): void
    {
        $this->get('/orders/reference-suggestions', function () {
            global $response;
            $auth = new authentication();
            $user = $auth->get_user();
            if ($user === false) {
                (new logs_o())->add('orders', 'global', 1, 0, 'LIST_ORDER_REFERENCE_SUGGESTIONS', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            $this->requirePermission('list_orders');
            self::requireParameters(['department_id']);
            $departmentId = (int)self::getParameter('department_id');
            self::requireParameterIntPositive($departmentId, 'department_id');
            self::requireDepartmentAccess((string)$departmentId);

            $search = trim((string)(self::getParameter('search') ?? ''));
            if (strlen($search) > 255) {
                $response->error('Parameter search must be at most 255 characters long', 400);
            }

            foreach (['reg_1', 'reg_2', 'reg_3'] as $plateParameter) {
                $plateValue = (string)(self::getParameter($plateParameter) ?? '');
                if (strlen($plateValue) > 32) {
                    $response->error('Parameter ' . $plateParameter . ' must be at most 32 characters long', 400);
                }
            }

            $suggestions = (new order_reference_suggestions_service())->suggest([
                'search' => $search,
                'department_id' => $departmentId,
                'customer_id' => self::getParameter('customer_id') ?? null,
                'reg_1' => self::getParameter('reg_1') ?? '',
                'reg_2' => self::getParameter('reg_2') ?? '',
                'reg_3' => self::getParameter('reg_3') ?? '',
                'limit' => self::getParameter('limit') ?? null,
            ]);

            (new logs_o())->add('orders', (string)$departmentId, 1, (int)$user->id, 'LIST_ORDER_REFERENCE_SUGGESTIONS', 'Successfully listed POS reference suggestions');
            $response->success($suggestions);
        },
            [
                'list_orders' => 'List POS order reference suggestions',
                'department_access_:id' => 'Access to the department used for reference suggestions',
            ]
        );

        $this->get('/orders', function () {
            // Require the user to be logged in
            global $response;
            /** Authentication */
            $auth = new authentication();
            $user = $auth->get_user();
            if ($user === false) {
                (new logs_o())->add('orders', 'global', 1, 0, 'LIST_ORDERS', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }
            /** Permissions (subuser-aware) */
            $permission_own = self::definePermission('list_own_orders', subusers_permission_node_key::ORDERS_LIST);
            $permission_other = self::definePermission('list_orders');
            $has_permission_other = self::hasPermission($permission_other);
            $targetCustomerNumber = self::resolveEffectiveCustomerNumber();
            self::allowOwnOrDepartmentAccess(
                $permission_own,
                $permission_other,
                $targetCustomerNumber,
                null,
                null,
                'You do not have permission to list orders.'
            );

            // Log the incident
            (new logs_o())->add('orders', 'global', 1, $user->id, 'LIST_ORDERS', 'Successfully listed orders');

            // Build department filter when listing as department/admin
            $department_ids = [];
            if ($has_permission_other) {
                $department_ids = $user->getGroup()->getDepartments();
            }
            if (self::isParametersSet(['show_wash_subscription'])) {
                if (self::getParameter('show_wash_subscription') === 'true') {
                    $department_ids[] = '10';
                }
            }

            $orders = new orders_o();
            $orders->setView('orders_with_invoice_collections');
            $effectiveCustomer = self::resolveEffectiveCustomerNumber();
            $forcedFilters = $orders->forceRestrictFilters([
                ...($has_permission_other ? [
                    'department_id' => $department_ids
                ] : []),
                ...(!$has_permission_other && $effectiveCustomer !== null ? [
                    'customer_id' => $effectiveCustomer
                ] : []),
            ]);
            $rawOrders = $orders->listObjectsWithPaginationIfSet(
                null,
                $forcedFilters
            );

            $response->success(
                $this->enrichOrderListRows($rawOrders)
            );
        },
            [
                'list_orders' => 'List all orders',
                'list_own_orders' => 'List own orders. Subusers require node: ORDERS_LIST and X-Customer-Number header.',
            ]
        );


        $this->post('/orders', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('add_order');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Get the post data
                $data = json_decode(file_get_contents('php://input'), true);
                if (!is_array($data)) {
                    $data = [];
                }
                // Check if the required fields are set
                $data = $this->getData($data, $response);
                // This is used to determine if the order is created from a handheld device,
                // If it is, we will add an indicator "pending" to the order, in the cache.
                // This will automatically be removed at midnight, or when the order is completed.
                if (isset($data['is_handheld']) && !is_bool($data['is_handheld'])) {
                    $response->error('is_handheld must be a boolean', 400);
                }
                if (isset($data['is_handheld'])) {
                    $isHandHeld = (bool)$data['is_handheld'];
                    unset($data['is_handheld']);
                } else {
                    $isHandHeld = false;
                }
                // Validate the department
                if (!(new departments_o())->getDepartmentById((int)$data['department_id'])) {
                    $response->error('Department not found', 400);
                }
                // Check if the user has access to the department
                self::requireDepartmentAccess((string)(int)$data['department_id']);
                // Make sure the customer number set is valid
                $targetUser = (new users_o())->getUserByCustomerNumber((int)$data['customer_id']);
                if (!$targetUser->exists()) {
                    $response->error('Customer not found', 400);
                }

                // Check if the user requires a reference
                if ($targetUser->requiresReference() && empty($data['reference'])) {
                    $response->error('Reference is required by the customer', 400);
                }
                try {
                    $reg_1 = orders_input_normalizer::normalizeRegistrationNumber($data['reg_1']);
                    $reg_2 = orders_input_normalizer::normalizeRegistrationNumber($data['reg_2'] ?? '');
                    $reg_3 = orders_input_normalizer::normalizeRegistrationNumber($data['reg_3'] ?? '');
                    $createdAt = orders_input_normalizer::normalizeCreatedAt($data['created_at'] ?? date('Y-m-d H:i:s'));
                    $includeInInvoice = array_key_exists('include_in_invoice', $data)
                        ? orders_input_normalizer::normalizeIncludeInInvoice($data['include_in_invoice'])
                        : null;
                } catch (\InvalidArgumentException $e) {
                    $response->error($e->getMessage(), 400);
                }

                $bookingId = !empty($data['booking_id']) ? (int)$data['booking_id'] : null;
                $po = $this->resolveOrderPoForBookingDefault(
                    array_key_exists('po', $data) ? $data['po'] : null,
                    array_key_exists('po', $data),
                    $bookingId,
                    (int)$data['customer_id'],
                    (int)$data['department_id']
                );
                $new_data = [
                    'customer_id' => (int)$data['customer_id'],
                    'department_id' => (int)$data['department_id'],
                    'reference' => (string)$data['reference'] ?? '',
                    'cashier_id' => (int)$user->id, // The user who created the order
                    'notes' => (string)$data['notes'] ?? '',
                    ...($po !== null ? ['po' => $po] : []),
                    'reg_1' => (string)$reg_1,
                    'reg_2' => (string)$reg_2,
                    'reg_3' => (string)$reg_3,
                    ...(!empty($data['lane']) ? ['lane' => (int)$data['lane']] : []), // Optional lane
                    ...(!empty($data['wash_id']) ? ['wash_id' => (string)$data['wash_id']] : []), // Optional wash ID
                    ...($bookingId !== null ? ['booking_id' => $bookingId] : []), // Optional booking ID
                    'created_at' => $createdAt, // Default to current time if not set
                    ...(array_key_exists('include_in_invoice', $data) ? ['include_in_invoice' => $includeInInvoice] : []),
                    ...(array_key_exists('safety_seal', $data) ? ['safety_seal' => orders_o::normalizeSafetySealValue($data['safety_seal'])] : []),
                ];
                // Create the order
                //$order = (new orders_o())->add((int)$data['customer_id'], $user->id, $data['reference'], $data['notes'], (int)$data['department_id'], (string)$reg_1, (string)$reg_2, (string)$reg_3);
                $order = (new orders_o())->addArray($new_data);
                // If the order is created from a handheld device, we will add an indicator "pending" to the order, in the cache.
                if ($isHandHeld) {
                    $order->setPendingHandheldIndicator();
                }
                // Log the incident
                (new logs_o())->add('orders', $data['department_id'], 1, $user->id, 'ADD_ORDER', 'Successfully added an order (ID: ' . $data['department_id'] . ')');
                // Return a success message, containing the orders array
                $response->success($order->asArray());
            } else { 
                // Log the incident
                (new logs_o())->add('orders', 'global', 1, 0, 'ADD_ORDER', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'add_order' => 'Add an order'
            ]
        );

        $this->put('/order', function () {
            self::updateOrder();
        },
            [
                'edit_order' => 'Edit an order'
            ]
        );

        $this->put('/orders', function () {
            self::updateOrder();
        },
            [
                'edit_order' => 'Edit an order'
            ]
        );

        $this->delete('/orders', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('delete_order');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Get the payload
                $id = $this->fromRequest('id');
                // Check if the ID is set
                if (!$id) {
                    $response->error('ID is required', 400);
                }
                // Get the current order
                $order = (new orders_o())->getOrderById((int)$id);
                // Check if the order exists
                if (!isset($order->id) || (int)$order->id < 1 || !$order->exists()) {
                    $response->error('Order not found', 400);
                }
                // Check if the user has access to the department
                self::requireDepartmentAccess((int)$order->department_id->value());
                $confirmed = filter_var($this->fromRequest('confirmed'), FILTER_VALIDATE_BOOLEAN) === true;
                $deleteProtection = $order->getDeleteProtectionSummary();
                if ($deleteProtection['requires_confirmation'] && !$confirmed) {
                    (new logs_o())->add(
                        'orders',
                        $order->department_id->value(),
                        1,
                        $user->id,
                        'DELETE_ORDER_CONFIRMATION_REQUIRED',
                        'Order deletion requires confirmation (ID: ' . $id
                        . '; reasons: ' . implode(',', $deleteProtection['protected_reasons'])
                        . '; order_items: ' . $deleteProtection['order_item_count']
                        . '; attachments: ' . $deleteProtection['attachment_count'] . ')'
                    );
                    $response->error([
                        'message' => 'Order deletion requires confirmation',
                        ...$deleteProtection,
                    ], 409);
                }
                // Delete the order
                $order->delete();
                // Log the incident
                if ($deleteProtection['requires_confirmation']) {
                    (new logs_o())->add(
                        'orders',
                        $order->department_id->value(),
                        1,
                        $user->id,
                        'DELETE_ORDER_CONFIRMED',
                        'Successfully deleted a protected order after confirmation (ID: ' . $id
                        . '; reasons: ' . implode(',', $deleteProtection['protected_reasons'])
                        . '; order_items: ' . $deleteProtection['order_item_count']
                        . '; attachments: ' . $deleteProtection['attachment_count'] . ')'
                    );
                } else {
                    (new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'DELETE_ORDER', 'Successfully deleted an order (ID: ' . $id . ')');
                }
                // Return a success message
                $response->success(['message' => 'Order deleted successfully']);
            } else {
                // Log the incident
                (new logs_o())->add('orders', 'global', 1, 0, 'DELETE_ORDER', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'delete_order' => 'Delete an order'
            ]
        );

        $this->get('/orders/attachments/download', function () {
            global $response;
            $context = $this->requireOrderAttachmentDownloadContext();
            $downloadLink = $context['store'] instanceof pdf_store
                ? $context['store']->getPresignedUrl($context['object_key'])
                : $context['store']->generateDirectDownloadUrl($context['object_key']);
            $this->logOrderAttachmentDownload($context, 'DOWNLOAD_ORDER_ATTACHMENT');
            $response->success(['download_link' => $downloadLink]);
        },
            [
                'download_order_attachments' => 'Download attachments for an order',
                'download_order_attachments_own' => 'Download attachments for an order (Only for own orders). Subusers require node: ORDERS_LIST and X-Customer-Number header.'
            ]
        );

        $this->get('/orders/attachments/content', function () {
            global $response;

            $context = $this->requireOrderAttachmentDownloadContext();
            $disposition = strtolower(trim((string)(self::getParameter('disposition') ?? 'inline')));
            if (!in_array($disposition, ['inline', 'attachment'], true)) {
                $response->error('disposition must be either inline or attachment', 400);
            }

            $temporaryPath = null;
            try {
                $temporaryPath = $context['store']->downloadToTemporaryFile($context['object_key']);
                $mimeType = $this->detectAttachmentMimeType($temporaryPath);
                $fileName = $this->sanitizeAttachmentDownloadFileName(
                    $context['file_name'],
                    $context['object_key']
                );
                $fileSize = filesize($temporaryPath);
                if ($fileSize === false) {
                    throw new \RuntimeException('Unable to determine attachment size');
                }

                $this->logOrderAttachmentDownload($context, 'STREAM_ORDER_ATTACHMENT');
                header('Content-Type: ' . $mimeType);
                header('Content-Disposition: ' . $disposition . '; filename="' . addcslashes($fileName, "\\\"") . '"; filename*=UTF-8\'\'' . rawurlencode($fileName));
                header('Content-Length: ' . $fileSize);
                header('Cache-Control: private, no-store');
                header('X-Content-Type-Options: nosniff');
                http_response_code(200);
                readfile($temporaryPath);
            } finally {
                if (is_string($temporaryPath) && file_exists($temporaryPath)) {
                    unlink($temporaryPath);
                }
            }
            exit;
        },
            [
                'download_order_attachments' => 'Stream attachments for an order',
                'download_order_attachments_own' => 'Stream attachments for an order (Only for own orders). Subusers require node: ORDERS_LIST and X-Customer-Number header.'
            ]
        );

        $this->get('/orders/attachments', function () {
            // Require the user to be logged in
            global $response;
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Get the order ID from the request
                self::requireParameters([
                    'id'
                ]);
                $order_id = self::getParameter('id');
                if (!is_numeric($order_id) || (int)$order_id < 1) {
                    $response->error('Invalid order ID', 400);
                }
                // Get the current order
                $order = (new orders_o())->getOrderById((int)$order_id);
                // Check if the order exists
                if (!$order->exists()) {
                    $response->error('Order not found', 400);
                }
                // Permissions (subuser-aware)
                $permission_own = self::definePermission('list_own_order_attachments', subusers_permission_node_key::ORDERS_LIST);
                $permission_other = self::definePermission('list_order_attachments');
                $has_permission_other = self::hasPermission($permission_other);
                if (!$has_permission_other) {
                    self::requirePermission($permission_own);
                    $effectiveCustomer = self::resolveEffectiveCustomerNumber();
                    if ($effectiveCustomer === null || (int)$order->customer_id->value() !== (int)$effectiveCustomer) {
                        $response->forbidden([$permission_other->permission]);
                    }
                }
                // Get the attachments
                $attachments = $order->listAttachments();
                // Log the incident
                (new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'LIST_ORDER_ATTACHMENTS', 'Successfully listed attachments for an order (ID: ' . $order_id . ')');
                // Return the attachments
                $response->success($attachments);
            } else {
                // Log the incident
                (new logs_o())->add('orders', 'global', 1, 0, 'LIST_ORDER_ATTACHMENTS', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'list_order_attachments' => 'List attachments for an order',
                'list_own_order_attachments' => 'List attachments for an order (Only for own orders). Subusers require node: ORDERS_LIST and X-Customer-Number header.'
            ]
        );

        $this->post('/orders/attachments/upload', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('add_order_attachments');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Check if the order ID and file are set
                $this->requireParameters(['order_id', 'base64_file', 'file_name']);
                $order_id = (int)$this->getParameter('order_id');
                if (!is_numeric($order_id) || (int)$order_id < 1) {
                    $response->error('Invalid order ID', 400);
                }
                // Get the current order
                $order = (new orders_o())->getOrderById((int)$order_id);
                // Check if the order exists
                if (!$order->exists()) {
                    $response->error('Order not found', 400);
                }
                // Check if the user has access to the department
                self::requireDepartmentAccess((string)(int)$order->department_id->value());
                // Get the base64 file
                $base64_file = (string)$this->getParameter('base64_file');
                $attachment_store = new attachment_store();
                // Determine the file extension from the file name
                $file_name = (string)$this->getParameter('file_name');
                $extension = pathinfo($file_name, PATHINFO_EXTENSION);
                $object_name = $attachment_store->storeTempFileFromBase64(
                    $base64_file,
                    $extension
                );
                if ($object_name === false) {
                    $response->error('Failed to store the attachment file.', 500);
                }
                $object_attachment = $order->addAttachment((new attachment_content())->setDocument($object_name)->setOther((string)self::getParameter('file_name')));
                // Log the incident
                (new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'ADD_ORDER_ATTACHMENT', 'Successfully added an attachment for an order (Order ID: ' . $order_id . ')');
                // Return a success message
                $attachments = new attachments();
                $response->success($attachments->format($attachments->get($object_attachment->id)));
            } else {
                // Log the incident
                (new logs_o())->add('orders', 'global', 1, 0, 'ADD_ORDER_ATTACHMENT', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'add_order_attachments' => 'Add attachments for an order'
            ]
        );

        $this->delete('/orders/attachments', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('delete_order_attachments');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Get the order ID and attachment ID from the request
                self::requireParameters([
                    'order_id',
                    'attachment_id'
                ]);
                $order_id = self::getParameter('order_id');
                $attachment_id = self::getParameter('attachment_id');
                if (!is_numeric($order_id) || (int)$order_id < 1) {
                    $response->error('Invalid order ID', 400);
                }
                if (!is_numeric($attachment_id) || (int)$attachment_id < 1) {
                    $response->error('Invalid attachment ID', 400);
                }
                // Get the current order
                $order = (new orders_o())->getOrderById((int)$order_id);
                // Check if the order exists
                if (!$order->exists()) {
                    $response->error('Order not found', 400);
                }
                // Check if the user has access to the department
                self::requireDepartmentAccess((string)(int)$order->department_id->value());
                // Delete the attachment
                $order->removeAttachment((int)$attachment_id);
                // Log the incident
                (new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'DELETE_ORDER_ATTACHMENT', 'Successfully deleted an attachment for an order (Order ID: ' . $order_id . ', Attachment ID: ' . $attachment_id . ')');
                // Return a success message
                $response->success(['message' => 'Attachment deleted successfully']);
            } else {
                // Log the incident
                (new logs_o())->add('orders', 'global', 1, 0, 'DELETE_ORDER_ATTACHMENT', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'delete_order_attachments' => 'Delete attachments for an order'
            ]
        );

        $this->post('/orders/mark_as_completed', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('mark_order_as_completed');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Get the post data
                $data = json_decode(file_get_contents('php://input'), true);
                // Check if the required fields are set
                if (!isset($data['id'])) {
                    $response->error('ID is required', 400);
                }
                // Get the current order
                $order = (new orders_o())->getOrderById((int)$data['id']);
                // Check if the order exists
                if (!$order->exists()) {
                    $response->error('Order not found', 400);
                }
                // Check if the user has access to the department
                self::requireDepartmentAccess((string)(int)$order->department_id->value());
                // Mark the order as completed
                $order->markAsCompleted((string)$user->display_name->value());
                // Log the incident
                (new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'MARK_ORDER_AS_COMPLETED', 'Successfully marked an order as completed (ID: ' . $data['id'] . ')');
                // Return a success message
                $response->success(['message' => 'Order marked as completed successfully']);
            } else {
                // Log the incident
                (new logs_o())->add('orders', 'global', 1, 0, 'MARK_ORDER_AS_COMPLETED', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'mark_order_as_completed' => 'Mark an order as completed'
            ]
        );

        $this->post('/orders/module/stripe/payment_intent', function () {
            global $response;
            $this->requirePermission('charge_order');
            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('orders', 'global', 1, 0, 'CHARGE_ORDER', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            $data = json_decode(file_get_contents('php://input'), true);
            if (!is_array($data)) {
                $data = [];
            }
            if (!isset($data['id'])) {
                $response->error('ID is required', 400);
            }

            $order = (new orders_o())->getOrderById((int)$data['id']);
            if (!$order->exists()) {
                $response->error('Order not found', 400);
            }
            self::requireDepartmentAccess((int)$order->department_id->value());

            $department = (new departments_o())->selectId((int)$order->department_id->value());
            if (!$department->isStripeConfigured()) {
                $response->error([
                    'message' => 'Card payments are not ready for this department. Open Stripe setup and choose a terminal location.',
                    'code' => 'stripe_terminal_setup_required',
                ], 409);
            }

            $readerId = trim((string)($data['reader'] ?? ''));
            if ($readerId === '') {
                $response->error('Reader ID is required', 400);
            }

            $tax_percentage = isset($data['tax_percentage']) ? (int)$data['tax_percentage'] : null;
            if ($tax_percentage !== null && ($tax_percentage < 0 || $tax_percentage > 100)) {
                $response->error('Invalid tax percentage', 400);
            }

            $stripe = new stripe();
            $stripePaymentIntents = new stripe_payment_intents_o();
            $expectedPaymentIntentAmount = $this->getStripePaymentIntentAmountForOrder($order, $tax_percentage ?? 0);

            if ($stripePaymentIntents->doesOrderHavePaymentIntent((int)$order->id)) {
                $stripePaymentIntents->selectOrderPaymentIntent((int)$order->id);

                try {
                    $storedPaymentIntent = $stripe->payment_intents->get($stripePaymentIntents->payment_intent_id->value());
                    $stripePaymentIntents->updateStoredPaymentIntent($storedPaymentIntent);
                    $stripePaymentIntents->setReaderId($readerId);
                    if ($tax_percentage !== null) {
                        $stripePaymentIntents->tax_percentage->set($tax_percentage);
                    }

                    if (
                        $this->isStripePaymentIntentReusable($storedPaymentIntent)
                        && $this->doesStripePaymentIntentMatchOrder($storedPaymentIntent, $expectedPaymentIntentAmount, $tax_percentage ?? 0)
                    ) {
                        (new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'CHARGE_ORDER', 'Reused Stripe payment intent for order (ID: ' . $data['id'] . ')');
                        $response->success($this->buildStripePaymentIntentResponse($storedPaymentIntent, $stripePaymentIntents, [
                            'reused' => true,
                        ]));
                    }

                    $stripePaymentIntents->deletePermanently();
                } catch (\Stripe\Exception\InvalidRequestException) {
                    $stripePaymentIntents->deletePermanently();
                }
            }

            $paymentIntent = $stripe->payment_intents->create(
                $expectedPaymentIntentAmount,
                [
                    'description' => 'Order ID: ' . $order->id,
                    'metadata' => [
                        'order_id' => (string)$order->id,
                        'customer_id' => (string)$order->customer_id->value(),
                        'department_id' => (string)$order->department_id->value(),
                        'tax_percentage' => (string)($tax_percentage ?? 0),
                        'reader_id' => $readerId,
                        'reader' => $readerId,
                    ],
                    'payment_method_types' => ['card_present'],
                    'capture_method' => 'manual',
                ]
            );

            $stripePaymentIntents->add(
                (int)$order->id,
                $paymentIntent->id,
                $paymentIntent->client_secret,
                $paymentIntent->toJSON(),
                $readerId,
                $tax_percentage
            );

            try {
                $stripe->readers->sendPaymentIntent($readerId, $paymentIntent->id);
            } catch (\Stripe\Exception\InvalidRequestException) {
                try {
                    $stripePaymentIntents->delete();
                } catch (Exception) {
                    $stripePaymentIntents->deletePermanently();
                }
                $response->error('Unable to start payment on the selected reader', 409);
            }

            try {
                $paymentIntent = $stripe->payment_intents->get($paymentIntent->id);
                $stripePaymentIntents->updateStoredPaymentIntent($paymentIntent);
            } catch (\Stripe\Exception\InvalidRequestException) {
                // Keep the created intent payload if Stripe retrieve is temporarily unavailable.
            }

            (new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'CHARGE_ORDER', 'Successfully charged an order (ID: ' . $data['id'] . ')');

            $response->success($this->buildStripePaymentIntentResponse($paymentIntent, $stripePaymentIntents, [
                'reused' => false,
            ]));
        },
            [
                'charge_order' => 'Charge an order'
            ]
        );

        $this->get('/orders/module/stripe/payment_intent', function () {
            global $response;
            $this->requirePermission('get_payment_intent');
            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('orders', 'global', 1, 0, 'GET_PAYMENT_INTENT', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            self::requireParameters([
                'id'
            ]);
            $id = self::getParameter('id');
            if (!isset($id)) {
                $response->error('ID is required', 400);
            }

            $order = (new orders_o())->getOrderById((int)$id);
            if (!$order->exists()) {
                $response->error('Order not found', 400);
            }
            self::requireDepartmentAccess((int)$order->department_id->value());

            $stripePaymentIntents = new stripe_payment_intents_o();
            if (!$stripePaymentIntents->doesOrderHavePaymentIntent((int)$order->id)) {
                $response->success($this->buildStripePaymentIntentResponse(null, null, [
                    'message' => 'No active payment intent for this order.',
                ]));
            }

            $stripePaymentIntents->selectOrderPaymentIntent((int)$order->id);
            $stripe = new stripe();

            try {
                $paymentIntent = $stripe->payment_intents->get($stripePaymentIntents->payment_intent_id->value());
            } catch (\Stripe\Exception\InvalidRequestException) {
                $stripePaymentIntents->deletePermanently();
                $response->success($this->buildStripePaymentIntentResponse(null, null, [
                    'message' => 'No active payment intent for this order.',
                ]));
            }

            $status = strtolower((string)($paymentIntent->status ?? ''));
            if ($status === 'canceled') {
                $stripePaymentIntents->deletePermanently();
                $response->success($this->buildStripePaymentIntentResponse(null, null, [
                    'message' => 'No active payment intent for this order.',
                ]));
            }

            $stripePaymentIntents->updateStoredPaymentIntent($paymentIntent);
            $response->success($this->buildStripePaymentIntentResponse($paymentIntent, $stripePaymentIntents));
        },
            [
                'get_payment_intent' => 'Get a payment intent'
            ]
        );

        $this->delete('/orders/module/stripe/payment_intent', function () {
            global $response;
            $this->requirePermission('charge_order');
            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('orders', 'global', 1, 0, 'DELETE_PAYMENT_INTENT', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            $data = json_decode(file_get_contents('php://input'), true);
            if (!is_array($data)) {
                $data = [];
            }
            $id = $data['id'] ?? self::fromRequest('id');
            if (!isset($id)) {
                $response->error('ID is required', 400);
            }

            $order = (new orders_o())->getOrderById((int)$id);
            if (!$order->exists()) {
                $response->error('Order not found', 400);
            }
            self::requireDepartmentAccess((int)$order->department_id->value());

            $stripePaymentIntents = new stripe_payment_intents_o();
            if (!$stripePaymentIntents->doesOrderHavePaymentIntent((int)$order->id)) {
                $response->success($this->buildStripePaymentIntentResponse(null, null, [
                    'message' => 'Payment intent cleared successfully.',
                    'cleared' => true,
                ]));
            }

            $stripePaymentIntents->selectOrderPaymentIntent((int)$order->id);

            try {
                $stripePaymentIntents->delete();
            } catch (\Stripe\Exception\InvalidRequestException) {
                $stripePaymentIntents->deletePermanently();
            }

            (new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'DELETE_PAYMENT_INTENT', 'Successfully deleted a payment intent (ID: ' . $id . ')');
            $response->success($this->buildStripePaymentIntentResponse(null, null, [
                'message' => 'Payment intent cleared successfully.',
                'cleared' => true,
            ]));
        },
            [
                'charge_order' => 'Delete a payment intent'
            ]
        );

        $this->post('/orders/module/stripe/payment_intent/capture', function () {
            global $response;
            $this->requirePermission('confirm_payment_intent');
            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('orders', 'global', 1, 0, 'CONFIRM_PAYMENT_INTENT', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            $data = json_decode(file_get_contents('php://input'), true);
            if (!is_array($data)) {
                $data = [];
            }
            if (!isset($data['id'])) {
                $response->error('ID is required', 400);
            }

            $order = (new orders_o())->getOrderById((int)$data['id']);
            if (!$order->exists()) {
                $response->error('Order not found', 400);
            }

            $stripePaymentIntents = new stripe_payment_intents_o();
            if (!$stripePaymentIntents->doesOrderHavePaymentIntent((int)$order->id)) {
                $response->error('No active payment intent for this order.', 409);
            }

            $stripePaymentIntents->selectOrderPaymentIntent((int)$order->id);
            $stripe = new stripe();

            try {
                $paymentIntent = $stripe->payment_intents->get($stripePaymentIntents->payment_intent_id->value());
            } catch (\Stripe\Exception\InvalidRequestException) {
                $stripePaymentIntents->deletePermanently();
                $response->error('Stored payment intent is stale. Start the payment again.', 409);
            }

            $stripePaymentIntents->updateStoredPaymentIntent($paymentIntent);
            $status = strtolower((string)($paymentIntent->status ?? ''));

            if ($status === 'succeeded') {
                $response->error('Payment intent has already been captured.', 409);
            }
            if ($status === 'canceled') {
                $stripePaymentIntents->deletePermanently();
                $response->error('Payment intent was cancelled. Start the payment again.', 409);
            }
            if ($status !== 'requires_capture') {
                $response->error('Payment intent is not ready to capture.', 409);
            }

            try {
                $paymentIntent = $stripe->payment_intents->capture(
                    $stripePaymentIntents->payment_intent_id->value(),
                    []
                );
            } catch (\Stripe\Exception\InvalidRequestException) {
                $stripePaymentIntents->deletePermanently();
                $response->error('Stored payment intent is stale. Start the payment again.', 409);
            }

            $stripePaymentIntents->updateStoredPaymentIntent($paymentIntent);
            if (strtolower((string)($paymentIntent->status ?? '')) !== 'succeeded') {
                $response->error('Payment intent is not ready to capture.', 409);
            }

            $order_collection = $order->getOrderCollection();
            $order_collection->paidWithStripe($paymentIntent->id);

            (new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'CONFIRM_PAYMENT_INTENT', 'Successfully confirmed a payment intent (ID: ' . $data['id'] . ')');
            $response->success($this->buildStripePaymentIntentResponse($paymentIntent, $stripePaymentIntents));
        },
            [
                'confirm_payment_intent' => 'Confirm a payment intent'
            ]
        );

        $this->post('/orders/module/stripe/debug/simulate_payment', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('debug_simulate_payment_intent');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Get the post data
                $data = json_decode(file_get_contents('php://input'), true);
                // Check if the required fields are set
                if (!isset($data['id'])) {
                    $response->error('ID is required', 400);
                }
                // Get the current order
                $order = (new orders_o())->getOrderById((int)$data['id']);
                // Check if the order exists
                if (!$order->exists()) {
                    $response->error('Order not found', 400);
                }
                // Check if the order has a payment intent
                $stripe_payment_intents = new stripe_payment_intents_o();
                if (!$stripe_payment_intents->doesOrderHavePaymentIntent((int)$order->id)) {
                    $response->error('Order does not have a payment intent', 400);
                }
                $stripe_payment_intents->selectOrderPaymentIntent((int)$order->id);
                // Simulate the payment
                $stripe = new stripe();
                //$paymentIntent = $stripe->readers->simulatePayment(
                //    $stripe_payment_intents->payment_intent_id->value(),
                //);
                $response->success('TEST_SUCCESS', 200);
            } else {
                // Log the incident
                (new logs_o())->add('orders', 'global', 1, 0, 'SIMULATE_PAYMENT_INTENT', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'simulate_payment_intent' => 'Simulate a payment intent, this is only for testing purposes and should under no circumstances be used in production'
            ]
        );
    }

    private function addTaxNetAmount(float $net_amount, ?int $tax_percentage): float
    {
        if (empty($tax_percentage) || $tax_percentage <= 0) {
            return $net_amount;
        }

        return $net_amount + ($net_amount * ($tax_percentage / 100));
    }

    private function getStripePaymentIntentAmountForOrder(orders_o $order, ?int $tax_percentage): int
    {
        return (int)round($this->addTaxNetAmount(
            (float)$order->getNetAmount() * 100,
            $tax_percentage ?? 0
        ));
    }

    private function doesStripePaymentIntentMatchOrder(object $paymentIntent, int $expectedAmount, ?int $tax_percentage): bool
    {
        if (!isset($paymentIntent->amount) || (int)$paymentIntent->amount !== $expectedAmount) {
            return false;
        }

        $metadata = $paymentIntent->metadata ?? null;
        $storedTaxPercentage = null;
        if (is_array($metadata)) {
            $storedTaxPercentage = $metadata['tax_percentage'] ?? null;
        } elseif (is_object($metadata)) {
            $storedTaxPercentage = $metadata->tax_percentage ?? null;
        }

        if ($storedTaxPercentage === null || !is_numeric($storedTaxPercentage)) {
            return ($tax_percentage ?? 0) === 0;
        }

        return (int)$storedTaxPercentage === ($tax_percentage ?? 0);
    }

    private function isStripePaymentIntentReusable(object $paymentIntent): bool
    {
        $status = strtolower((string)($paymentIntent->status ?? ''));

        return in_array($status, [
            'requires_payment_method',
            'requires_confirmation',
            'requires_action',
            'processing',
            'requires_capture',
            'succeeded',
        ], true);
    }

    private function buildStripePaymentIntentResponse(?object $paymentIntent, ?stripe_payment_intents_o $storedIntent, array $extra = []): array
    {
        $paymentIntentPayload = null;
        if ($paymentIntent !== null) {
            if (method_exists($paymentIntent, 'toJSON')) {
                $decoded = json_decode($paymentIntent->toJSON(), true);
                $paymentIntentPayload = is_array($decoded) ? $decoded : null;
            } else {
                $decoded = json_decode(json_encode($paymentIntent), true);
                $paymentIntentPayload = is_array($decoded) ? $decoded : null;
            }
        }

        if ($paymentIntentPayload !== null) {
            $metadata = $paymentIntentPayload['metadata'] ?? [];
            if (!is_array($metadata)) {
                $metadata = [];
            }

            if ($storedIntent !== null && isset($storedIntent->reader_id) && !empty($storedIntent->reader_id->value())) {
                $metadata['reader_id'] = (string)$storedIntent->reader_id->value();
                if (empty($metadata['reader'])) {
                    $metadata['reader'] = $metadata['reader_id'];
                }
            }

            if ($storedIntent !== null && isset($storedIntent->tax_percentage) && $storedIntent->tax_percentage->value() !== null) {
                $metadata['tax_percentage'] = (string)$storedIntent->tax_percentage->value();
            }

            $paymentIntentPayload['metadata'] = $metadata;
        }

        return array_merge([
            'payment_intent' => $paymentIntentPayload,
            'has_payment_intent' => $paymentIntentPayload !== null,
        ], $extra);
    }

    /**
     * @throws \Exception
     */
    #[NoReturn] private function updateOrder(): void
    {
        // Require the user to be logged in
        global $response;
        // Auth & permissions (subuser-aware)
        $auth = new authentication();
        $user = $auth->get_user();
        $permission_own = self::definePermission('edit_own_orders', subusers_permission_node_key::ORDERS_EDIT);
        $permission_other = self::definePermission('edit_order');
        $has_permission_other = self::hasPermission($permission_other);
        // Classic user own-edit path (legacy behaviour)
        $classic_own_path = ($user !== false && $user->hasPermission('user') && !$has_permission_other);
        // Subuser own-edit path via node ORDERS_EDIT
        $subuser_own_path = (!$has_permission_other && self::hasPermission($permission_own));
        $isOwnPath = $classic_own_path || $subuser_own_path;
        if (!$isOwnPath && !$has_permission_other) {
            // Neither own nor admin permission — deny via admin requirement to unify error shape
            self::requirePermission($permission_other);
        }
        // Check if the request was successful
        if ($user) {
            // Get the post data
            $data = json_decode(file_get_contents('php://input'), true);
            if (!is_array($data)) {
                $data = [];
            }
            $data = $this->normalizeLegacyEditableFieldPayload($data, $response);
            // Check if the required fields are set
            if (!isset($data['id'])) {
                $response->error('ID is required', 400);
            }
            // Get the current order
            $order = (new orders_o())->getOrderById((int)$data['id']);
            // Check if the order exists
            if (!$order->exists()) {
                $response->error('Order not found', 400);
            }
            // Own path (classic or subuser) — limited field edits only
            if ($isOwnPath) {
                // Validate that the order belongs to the effective customer context
                if ($subuser_own_path) {
                    $effectiveCustomer = self::resolveEffectiveCustomerNumber();
                    if ($effectiveCustomer === null || (int)$order->customer_id->value() !== (int)$effectiveCustomer) {
                        $response->forbidden([$permission_other->permission]);
                    }
                } else {
                    // Classic own path — compare against authenticated user customer number
                    if ($order->customer_id->value() !== $user->customer_number->value()) {
                        $response->forbidden([$permission_other->permission]);
                    }
                    // Ensure classic own-path requires user permission
                    $this->requirePermission('user');
                }
                // Allowed to edit list
                $allowed_to_edit = [
                    // Include the order ID (Even though it is not editable)
                    'id',
                    'po',
                    'reference',
                    'notes',
                    'safety_seal',
                    'reg_1',
                    'reg_2',
                    'reg_3',
                ];
                // Check if the $data contains any non-allowed keys
                foreach ( $data as $key => $value ) {
                    if (!in_array($key, $allowed_to_edit)) {
                        $response->error('You do not have permission to edit this order field (key: ' . $key . ')', 400);
                    }
                };
                $shouldRefreshAttachedWashCertificate = false;
                // PO
                if (isset($data['po'])) {
                    $order->po->set((string)$data['po']);
                }
                // Registration numbers
                if (isset($data['reg_1'])) {
                    $normalizedReg1 = $this->normalizeRegistrationNumberOrError($data['reg_1']);
                    $shouldRefreshAttachedWashCertificate = true;
                    $order->reg_1->set($normalizedReg1);
                }
                if (isset($data['reg_2'])) {
                    $normalizedReg2 = $this->normalizeRegistrationNumberOrError($data['reg_2']);
                    $shouldRefreshAttachedWashCertificate = true;
                    $order->reg_2->set($normalizedReg2);
                }
                if (isset($data['reg_3'])) {
                    $normalizedReg3 = $this->normalizeRegistrationNumberOrError($data['reg_3']);
                    $shouldRefreshAttachedWashCertificate = true;
                    $order->reg_3->set($normalizedReg3);
                }
                // Reference
                if (isset($data['reference'])) {
                    $order->reference->set((string)$data['reference']);
                }
                // Notes
                if (isset($data['notes'])) {
                    $order->notes->set((string)$data['notes']);
                }
                if (array_key_exists('safety_seal', $data)) {
                    $normalizedSafetySeal = orders_o::normalizeSafetySealValue($data['safety_seal']);
                    $shouldRefreshAttachedWashCertificate = true;
                    $order->setSafetySealValue($normalizedSafetySeal);
                }
                if ($shouldRefreshAttachedWashCertificate) {
                    $order->regenerateAttachedWashCertificate();
                }
                // Register the change
                $order->objectChanged();
                // Return a success message
                $response->success($order->asArray());
            }
            // Admin/department path (requires edit_order)
            self::requirePermission($permission_other);
            /** Departmental access — user must have access to the order's current department */
            self::requireDepartmentAccess((string)(int)$order->department_id->value());
            $originalCustomerNumber = (int)$order->customer_id->value();
            $newCustomerNumber = $originalCustomerNumber;
            $shouldAutoReassignInvoiceCollection = false;
            $shouldRefreshAttachedWashCertificate = false;
            // If the customer ID is set, validate it
            if (isset($data['customer_id'])) {
                if (!(new users_o())->getUserByCustomerNumber((int)$data['customer_id'])->exists() || empty($data['customer_id'])) {
                    $response->error('Customer not found or invalid', 400);
                }
                $newCustomerNumber = (int)$data['customer_id'];
                $shouldRefreshAttachedWashCertificate = true;
                $shouldAutoReassignInvoiceCollection = $this->shouldAutoReassignInvoiceCollectionForDraftTransition(
                    $originalCustomerNumber,
                    $newCustomerNumber
                );
                $order->customer_id->set($newCustomerNumber);
            }
            $targetCustomerNumber = isset($data['customer_id'])
                ? (int)$data['customer_id']
                : (int)$order->customer_id->value();
            // If the reference is set, validate it
            if (isset($data['reference'])) {
                $order->reference->set($data['reference']);
            }
            // If the notes are set, validate them
            if (isset($data['notes'])) {
                $order->notes->set($data['notes']);
            }
            // If the registration number is set, validate it
            if (isset($data['reg_1'])) {
                $normalizedReg1 = $this->normalizeRegistrationNumberOrError($data['reg_1']);
                $shouldRefreshAttachedWashCertificate = true;
                $order->reg_1->set($normalizedReg1);
            }
            // If the registration number 2 is set, validate it
            if (isset($data['reg_2'])) {
                $normalizedReg2 = $this->normalizeRegistrationNumberOrError($data['reg_2']);
                $shouldRefreshAttachedWashCertificate = true;
                $order->reg_2->set($normalizedReg2);
            }
            // If the registration number 3 is set, validate it
            if (isset($data['reg_3'])) {
                $normalizedReg3 = $this->normalizeRegistrationNumberOrError($data['reg_3']);
                $shouldRefreshAttachedWashCertificate = true;
                $order->reg_3->set($normalizedReg3);
            }
            // If the PO is set, validate it
            if (isset($data['po'])) {
                $order->po->set((string)$data['po']);
            }
            if (array_key_exists('safety_seal', $data)) {
                $normalizedSafetySeal = orders_o::normalizeSafetySealValue($data['safety_seal']);
                $shouldRefreshAttachedWashCertificate = true;
                $order->setSafetySealValue($normalizedSafetySeal);
            }
            // If the lane is set, validate it
            if (isset($data['lane'])) {
                $order->lane->set((int)$data['lane']);
            }
            // If the department ID is set, validate it
            if (isset($data['department_id'])) {
                if (!(new departments_o())->getDepartmentById((int)$data['department_id'])) {
                    $response->error('Department not found', 400);
                }
                // Check if the user has access to the target department
                self::requireDepartmentAccess((string)(int)$data['department_id']);
                $order->department_id->set((int)$data['department_id']);
            }
            // If the booking ID is set, validate it
            if (isset($data['booking_id'])) {
                $bookingId = (int)$data['booking_id'];
                $order->booking_id->set($bookingId);
                $this->applyBookingPoDefaultToOrder($order, $bookingId);
            }
            // Check if the invoice collection is set
            if (isset($data['invoice_collection_id']) && !$shouldAutoReassignInvoiceCollection) {
                $invoiceCollectionId = (int)$data['invoice_collection_id'];
                if ($invoiceCollectionId > 0) {
                    $invoiceCollection = (new collected_order_invoices_o())->select($invoiceCollectionId);
                    if (!$invoiceCollection->exists()) {
                        $response->error('Invoice collection not found', 400);
                    }
                    if ((int)$invoiceCollection->customer_number->value() !== $targetCustomerNumber) {
                        $response->error('Invoice collection does not belong to the order customer', 400);
                    }
                }
                $order->invoice_collection_id->set($invoiceCollectionId);
            }
            // Check if the wash_id is set
            if (isset($data['wash_id'])) {
                $order->wash_id->set($data['wash_id']);
            }
            // Check if the created_at is set
            if (isset($data['created_at'])) {
                try {
                    $order->created_at->set(orders_input_normalizer::normalizeCreatedAt($data['created_at']));
                } catch (\InvalidArgumentException $e) {
                    $response->error($e->getMessage(), 400);
                }
            }
            // Check if include_in_invoice is set
            if (array_key_exists('include_in_invoice', $data)) {
                try {
                    $order->include_in_invoice->set(
                        orders_input_normalizer::normalizeIncludeInInvoice($data['include_in_invoice'])
                    );
                } catch (\InvalidArgumentException $e) {
                    $response->error($e->getMessage(), 400);
                }
            }
            if ($shouldAutoReassignInvoiceCollection) {
                $order->assignToInvoiceCollection(null, false);
            }
            if ($shouldRefreshAttachedWashCertificate) {
                $order->regenerateAttachedWashCertificate();
            }
            // Void any cached key for the order
            $order->objectChanged();
            // Log the incident
            (new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'EDIT_ORDER', 'Successfully updated an order (ID: ' . $data['id'] . ')');
            // Return a success message
            $response->success(['message' => 'Order updated successfully']);
        } else {
            // Log the incident
            (new logs_o())->add('orders', 'global', 1, 0, 'EDIT_ORDER', 'No user found, or invalid session');
            // Return an error
            $response->error('Invalid session', 400);
        }
    }

    private function resolveOrderPoForBookingDefault(
        mixed $po,
        bool $poProvided,
        ?int $bookingId,
        int $customerNumber,
        int $departmentId
    ): ?string
    {
        $currentPo = is_scalar($po) || $po === null ? trim((string)$po) : '';
        if ($currentPo !== '') {
            return $currentPo;
        }

        $bookingPo = $this->getBookingPoDefault($bookingId, $customerNumber, $departmentId);
        if ($bookingPo !== null) {
            return $bookingPo;
        }

        return $poProvided ? '' : null;
    }

    private function applyBookingPoDefaultToOrder(orders_o $order, ?int $bookingId = null): void
    {
        $currentPo = trim((string)($order->po->value() ?? ''));
        if ($currentPo !== '') {
            return;
        }

        $bookingPo = $this->getBookingPoDefault(
            $bookingId ?? (int)($order->booking_id->value() ?? 0),
            (int)$order->customer_id->value(),
            (int)$order->department_id->value()
        );
        if ($bookingPo === null) {
            return;
        }

        $order->po->set($bookingPo);
    }

    private function getBookingPoDefault(?int $bookingId, int $customerNumber, int $departmentId): ?string
    {
        if ($bookingId === null || $bookingId <= 0 || $customerNumber <= 0 || $departmentId <= 0) {
            return null;
        }

        if (!$this->canUseBookingPoDefault($customerNumber, $departmentId)) {
            return null;
        }

        try {
            $booking = (new order_bookings_o())->select($bookingId);
            if (!$booking->exists()) {
                return null;
            }

            if ((int)$booking->customer_number->value() !== $customerNumber) {
                return null;
            }

            if ((int)$booking->department->value() !== $departmentId) {
                return null;
            }

            if (trim((string)($booking->deleted_at->value() ?? '')) !== '') {
                return null;
            }

            $bookingPo = trim((string)($booking->po->value() ?? ''));
            return $bookingPo !== '' ? $bookingPo : null;
        } catch (\Throwable) {
            return null;
        }
    }

    private function canUseBookingPoDefault(int $customerNumber, int $departmentId): bool
    {
        try {
            $user = (new authentication())->get_user();
            return $user !== false && isset($user->customer_number) && (int)$user->customer_number->value() === $customerNumber;
        } catch (\Throwable) {
            return false;
        }
    }

    private function normalizeLegacyEditableFieldPayload(array $data, response $response): array
    {
        if (!array_key_exists('field', $data) && !array_key_exists('value', $data)) {
            return $data;
        }

        if (!array_key_exists('field', $data) || !array_key_exists('value', $data)) {
            $response->error('Both legacy field and value are required', 400);
        }

        $field = is_string($data['field']) ? trim($data['field']) : '';
        $allowedLegacyFields = ['reference', 'notes', 'safety_seal', 'reg_1', 'reg_2', 'reg_3'];

        if ($field === '' || !in_array($field, $allowedLegacyFields, true)) {
            $displayField = is_scalar($data['field']) || $data['field'] === null
                ? (string)$data['field']
                : gettype($data['field']);

            $response->error('Unsupported legacy order field: ' . $displayField, 400);
        }

        if (!array_key_exists($field, $data)) {
            $data[$field] = $data['value'];
        }

        unset($data['field'], $data['value']);

        return $data;
    }

    private function normalizeRegistrationNumberOrError(mixed $value): string
    {
        global $response;

        try {
            return orders_input_normalizer::normalizeRegistrationNumber($value);
        } catch (\InvalidArgumentException $e) {
            $response->error($e->getMessage(), 400);
        }
    }

    private function shouldAutoReassignInvoiceCollectionForDraftTransition(int $originalCustomerNumber, int $newCustomerNumber): bool
    {
        if ($originalCustomerNumber <= 0 || $newCustomerNumber <= 0 || $originalCustomerNumber === $newCustomerNumber) {
            return false;
        }

        $economic = new economic();
        return $economic->isDraftCustomerNumber($originalCustomerNumber)
            || $economic->isDraftCustomerNumber($newCustomerNumber);
    }

    /**
     * @param array<int, array<string, mixed>> $orders
     * @return array<int, array<string, mixed>>
     */
    private function enrichOrderListRows(array $orders): array
    {
        if (empty($orders)) {
            return [];
        }

        $orderIds = array_values(array_unique(array_filter(array_map('intval', array_column($orders, 'id')), static fn(int $id): bool => $id > 0)));
        $customerNumbers = array_values(array_unique(array_filter(array_map('intval', array_column($orders, 'customer_id')), static fn(int $id): bool => $id > 0)));
        $cashierIds = array_values(array_unique(array_filter(array_map('intval', array_column($orders, 'cashier_id')), static fn(int $id): bool => $id > 0)));

        $netAmountsByOrderId = (new orders_o())->getNetAmountForOrders($orderIds);
        $economicModules = new economic_module_orders();
        $economicModules->ensureRowsForOrderIds($orderIds);
        $economicByOrderId = $economicModules->getByOrderIdsAsArray($orderIds);
        $stripeByOrderId = $this->getStripeModulesByOrderIds($orderIds);

        $users = new users_o();
        $customerNamesByCustomerNumber = $users->getCustomerNames($customerNumbers);
        $userIdsByCustomerNumber = $this->getUserIdsByCustomerNumbers($customerNumbers);
        $cashierNamesById = $users->getCashierNames($cashierIds);
        $pendingHandheldByOrderId = $this->getPendingHandheldFlags($orderIds);
        $attachmentsByOrderId = (new attachments())->listMany('orders', $orderIds);

        foreach ($orders as &$order) {
            $orderId = (int)($order['id'] ?? 0);
            $customerNumber = (int)($order['customer_id'] ?? 0);
            $cashierId = (int)($order['cashier_id'] ?? 0);

            $order['economic_invoice_module'] = $economicByOrderId[$orderId] ?? [
                'id' => $orderId,
                'invoice_draft_id' => null,
                'invoice_id' => null,
            ];
            $order['total_net_amount'] = (float)($netAmountsByOrderId[$orderId] ?? 0);

            if (isset($stripeByOrderId[$orderId])) {
                $order['stripe_invoice_module'] = $stripeByOrderId[$orderId];
            }

            if (!empty($order['invoice_collection_id'])) {
                $order['invoice_collection'] = [
                    'id' => $order['invoice_collection_id'],
                    'closed_at' => $order['closed_at'] ?? null,
                    'booked_invoice_id' => $order['booked_invoice_id'] ?? null,
                    'processor' => (int)($order['processor'] ?? 0),
                ];
            }

            $customerName = $customerNamesByCustomerNumber[(string)$customerNumber] ?? null;
            if ($customerName === null && $customerNumber > 0) {
                $customerName = $users->getCustomerName($customerNumber);
            }
            $order['customer_name'] = $customerName;
            $order['user_id'] = (int)($userIdsByCustomerNumber[$customerNumber] ?? 0);
            $order['cashier_name'] = $cashierNamesById[$cashierId] ?? 'Unknown Cashier';
            $order['pending_handheld'] = (bool)($pendingHandheldByOrderId[$orderId] ?? false);
            $order['attachments'] = $attachmentsByOrderId[$orderId] ?? [];
            $order['po'] = $order['po'] ?? null;
            $order['safety_seal'] = $order['safety_seal'] ?? null;
            $order['lane'] = $order['lane'] ?? null;
        }
        unset($order);

        return $orders;
    }

    /**
     * @param int[] $orderIds
     * @return array<int, array<string, mixed>>
     */
    private function getStripeModulesByOrderIds(array $orderIds): array
    {
        $orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds), static fn(int $id): bool => $id > 0)));
        if (empty($orderIds)) {
            return [];
        }

        $rows = (new stripe_module_orders_o())->getFieldsWhereIn(
            ['id' => $orderIds],
            ['id', 'invoice_id', 'customer_id', 'url', 'created_at']
        );
        $byOrderId = [];
        foreach ($rows as $row) {
            $orderId = (int)($row['id'] ?? 0);
            if ($orderId <= 0) {
                continue;
            }
            $invoiceId = trim((string)($row['invoice_id'] ?? ''));
            if ($invoiceId === '') {
                continue;
            }
            $stripeSnapshot = $this->getStripeInvoiceSnapshot($invoiceId);
            $byOrderId[$orderId] = [
                'id' => $orderId,
                'invoice_id' => $invoiceId,
                'customer_id' => (string)($row['customer_id'] ?? ''),
                'url' => (string)($row['url'] ?? ''),
                'created_at' => (string)($row['created_at'] ?? ''),
                'paid' => (bool)($stripeSnapshot['paid'] ?? false),
                'status' => $stripeSnapshot['status'] ?? null,
                'amount_due' => $stripeSnapshot['amount_due'] ?? null,
                'amount_paid' => $stripeSnapshot['amount_paid'] ?? null,
            ];
        }

        return $byOrderId;
    }

    /**
     * @return array{paid: bool, status: mixed, amount_due: mixed, amount_paid: mixed}
     */
    private function getStripeInvoiceSnapshot(string $invoiceId): array
    {
        $cached = $this->getCachedStripeInvoiceSnapshot($invoiceId);
        if ($cached !== null) {
            return $cached;
        }

        $invoice = (new stripe())->invoice->retrieve($invoiceId);
        $snapshot = [
            'paid' => (bool)($invoice->paid ?? false),
            'status' => $invoice->status ?? null,
            'amount_due' => $invoice->amount_due ?? null,
            'amount_paid' => $invoice->amount_paid ?? null,
        ];
        $this->cacheStripeInvoiceSnapshot($invoiceId, $snapshot);

        return $snapshot;
    }

    /**
     * @return array{paid: bool, status: mixed, amount_due: mixed, amount_paid: mixed}|null
     */
    private function getCachedStripeInvoiceSnapshot(string $invoiceId): ?array
    {
        if (!defined('redis')) {
            return null;
        }

        $cacheKey = 'orders_stripe_invoice_snapshot_' . $invoiceId;
        $cachedRaw = redis->get($cacheKey);
        if (!is_string($cachedRaw) || $cachedRaw === '') {
            return null;
        }
        $decoded = json_decode($cachedRaw, true);
        return is_array($decoded) ? $decoded : null;
    }

    /**
     * @param array{paid: bool, status: mixed, amount_due: mixed, amount_paid: mixed} $snapshot
     */
    private function cacheStripeInvoiceSnapshot(string $invoiceId, array $snapshot): void
    {
        if (!defined('redis')) {
            return;
        }

        $encoded = json_encode($snapshot);
        if (!is_string($encoded) || $encoded === '') {
            return;
        }

        $cacheKey = 'orders_stripe_invoice_snapshot_' . $invoiceId;
        redis->set($cacheKey, $encoded);
        redis->expire($cacheKey, 30);
    }

    /**
     * @param int[] $customerNumbers
     * @return array<int, int> map: customer_number => user_id
     */
    private function getUserIdsByCustomerNumbers(array $customerNumbers): array
    {
        $customerNumbers = array_values(array_unique(array_filter(array_map('intval', $customerNumbers), static fn(int $id): bool => $id > 0)));
        if (empty($customerNumbers)) {
            return [];
        }

        $rows = (new users_o())->getFieldsWhereIn(
            ['customer_number' => $customerNumbers],
            ['id', 'customer_number']
        );

        usort($rows, static fn(array $a, array $b): int => ((int)($a['id'] ?? 0)) <=> ((int)($b['id'] ?? 0)));
        $map = [];
        foreach ($rows as $row) {
            $customerNumber = (int)($row['customer_number'] ?? 0);
            if ($customerNumber <= 0 || isset($map[$customerNumber])) {
                continue;
            }
            $map[$customerNumber] = (int)($row['id'] ?? 0);
        }

        // Keep parity with existing behavior that imports missing customer users via getUserByCustomerNumber.
        foreach ($customerNumbers as $customerNumber) {
            if (isset($map[$customerNumber])) {
                continue;
            }
            $user = (new users_o())->getUserByCustomerNumber($customerNumber);
            if ($user->exists()) {
                $map[$customerNumber] = (int)$user->id;
            }
        }

        return $map;
    }

    /**
     * @param int[] $orderIds
     * @return array<int, bool> map: order_id => pending_handheld
     */
    private function getPendingHandheldFlags(array $orderIds): array
    {
        $orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds), static fn(int $id): bool => $id > 0)));
        if (empty($orderIds)) {
            return [];
        }

        $flags = [];
        foreach ($orderIds as $orderId) {
            $flags[$orderId] = false;
        }
        if (!defined('redis')) {
            return $flags;
        }

        $cached = (new orders_o())->getCachedForMultipleObjects('pending_handheld_cache_indicator', $orderIds);
        foreach ($orderIds as $index => $orderId) {
            $flags[$orderId] = ((int)($cached[$index] ?? 0) === 1);
        }

        return $flags;
    }

    /**
     * @return array{user:object,order:orders_o,order_id:int,attachment_id:int,object_key:string,file_name:string,store:attachment_store|pdf_store}
     */
    private function requireOrderAttachmentDownloadContext(): array
    {
        global $response;

        $permissionOwn = self::definePermission('download_order_attachments_own', subusers_permission_node_key::ORDERS_LIST);
        $permissionOther = self::definePermission('download_order_attachments');
        $hasPermissionOther = self::hasPermission($permissionOther);
        if (!$hasPermissionOther) {
            self::requirePermission($permissionOwn);
        }

        $user = (new authentication())->get_user();
        if (!$user) {
            (new logs_o())->add('orders', 'global', 1, 0, 'DOWNLOAD_ORDER_ATTACHMENT', 'No user found, or invalid session');
            $response->error('Invalid session', 400);
        }

        self::requireParameters(['order_id', 'attachment_id']);
        $orderId = self::getParameter('order_id');
        $attachmentId = self::getParameter('attachment_id');
        if (!is_numeric($orderId) || (int)$orderId < 1) {
            $response->error('Invalid order ID', 400);
        }
        if (!is_numeric($attachmentId) || (int)$attachmentId < 1) {
            $response->error('Invalid attachment ID', 400);
        }
        $orderId = (int)$orderId;
        $attachmentId = (int)$attachmentId;

        $order = (new orders_o())->getOrderById($orderId);
        if (!$order->exists()) {
            $response->error('Order not found', 404);
        }
        if (!$hasPermissionOther) {
            $effectiveCustomer = self::resolveEffectiveCustomerNumber();
            if ($effectiveCustomer === null || (int)$order->customer_id->value() !== (int)$effectiveCustomer) {
                $response->forbidden([$permissionOther->permission]);
            }
        }

        $attachment = $order->getAttachment($attachmentId);
        $attachmentType = $attachment->exists()
            ? trim((string)$attachment->object_type->value(), '`')
            : '';
        if (
            !$attachment->exists()
            || $attachmentType !== 'orders'
            || (int)$attachment->object_id->value() !== $orderId
            || !empty($attachment->deleted_at->value())
        ) {
            $response->error('Attachment not found for order', 404);
        }

        $formattedAttachment = (new attachments())->format($attachment);
        $objectKey = trim((string)(
            $formattedAttachment->content->document
            ?? $formattedAttachment->content->image
            ?? ''
        ));
        if ($objectKey === '') {
            $response->error('Attachment has no stored file', 404);
        }

        $attachmentStore = new attachment_store();
        if (!$attachmentStore->isValidFilePath($objectKey)) {
            $response->error('Attachment contains an invalid stored file path', 400);
        }
        $isWashCertificate = $formattedAttachment->isWashCertificate();
        $store = $isWashCertificate ? new pdf_store() : $attachmentStore;
        try {
            if (!$store->doesObjectExist($objectKey)) {
                $response->error('Attachment file not found', 404);
            }
        } catch (\Throwable) {
            $response->error('Attachment storage is unavailable', 502);
        }

        $originalFileName = is_string($formattedAttachment->content->other)
            ? $formattedAttachment->content->other
            : '';
        if ($isWashCertificate) {
            $originalFileName = 'wash_certificate.pdf';
        }

        return [
            'user' => $user,
            'order' => $order,
            'order_id' => $orderId,
            'attachment_id' => $attachmentId,
            'object_key' => $objectKey,
            'file_name' => $originalFileName,
            'store' => $store,
        ];
    }

    private function logOrderAttachmentDownload(array $context, string $action): void
    {
        (new logs_o())->add(
            'orders',
            $context['order']->department_id->value(),
            1,
            $context['user']->id,
            $action,
            'Successfully accessed an attachment for an order (Order ID: '
                . $context['order_id']
                . ', Attachment ID: '
                . $context['attachment_id']
                . ')'
        );
    }

    private function detectAttachmentMimeType(string $path): string
    {
        $mimeType = false;
        if (class_exists(\finfo::class)) {
            $mimeType = (new \finfo(FILEINFO_MIME_TYPE))->file($path);
        }
        if ((!is_string($mimeType) || $mimeType === '') && function_exists('mime_content_type')) {
            $mimeType = mime_content_type($path);
        }

        return is_string($mimeType) && preg_match('#^[a-z0-9.+-]+/[a-z0-9.+-]+$#i', $mimeType) === 1
            ? $mimeType
            : 'application/octet-stream';
    }

    private function sanitizeAttachmentDownloadFileName(string $fileName, string $objectKey): string
    {
        $fileName = trim(str_replace(["\r", "\n", "\0"], '', basename($fileName)));
        if ($fileName === '' || $fileName === '.' || $fileName === '..') {
            $fileName = basename($objectKey);
        }

        $fileName = preg_replace('/[\\x00-\\x1F\\x7F\\/\\\\]/u', '_', $fileName) ?? 'attachment';
        return trim($fileName) !== '' ? $fileName : 'attachment';
    }

    /**
     * @param mixed $data
     * @param response $response
     * @return mixed
     */
    private function getData(mixed $data, response $response): mixed
    {
        if (!isset($data['customer_id'])) {
            $response->error('Customer ID is required', 400);
        }
        if ((int)$data['customer_id'] < 1 || !is_numeric((int)$data['customer_id'])) {
            $response->error('Customer ID is required', 400);
        }
        if (!isset($data['department_id'])) {
            $response->error('Department ID is required', 400);
        }
        if (!isset($data['reference'])) {
            $response->error('Reference is required', 400);
        }
        if (!isset($data['notes'])) {
            $response->error('Notes is required', 400);
        }
        if (!isset($data['reg_1'])) {
            $response->error('Registration number 1 is required', 400);
        }
        try {
            $data['reg_1'] = orders_input_normalizer::normalizeRegistrationNumber($data['reg_1']);
            if (array_key_exists('reg_2', $data)) {
                $data['reg_2'] = orders_input_normalizer::normalizeRegistrationNumber($data['reg_2']);
            }
            if (array_key_exists('reg_3', $data)) {
                $data['reg_3'] = orders_input_normalizer::normalizeRegistrationNumber($data['reg_3']);
            }
        } catch (\InvalidArgumentException $e) {
            $response->error($e->getMessage(), 400);
        }
        if (strlen($data['reg_1']) < 4) {
            $response->error('Registration number 1 must be at least 4 characters', 400);
        }
        // Optional fields are not checked here, as they are optional and can be empty
        return $data;
    }
}
