<?php
defined('ABSPATH') or die('Direct access not allowed');

/**
 * VollstartRD_Admin — Admin pages and AJAX handler.
 *
 * SRP: Registers admin menu, renders admin page shell, dispatches AJAX.
 * Content is loaded via AJAX/JS — PHP only renders the shell.
 */
class VollstartRD_Admin {

    private $MAIN;

    public function __construct($MAIN) {
        $this->MAIN = $MAIN;
        add_action('admin_menu', [$this, 'registerMenu']);
        add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
        add_action('wp_dashboard_setup', [$this, 'registerDashboardWidget']);

        if (basename(sanitize_text_field(wp_unslash($_SERVER['SCRIPT_NAME'] ?? ''))) === 'admin-ajax.php') {
            add_action('wp_ajax_vollstart_rd_admin', [$this, 'handleAjax']);
        }
        add_action('admin_init', [$this, 'handlePdfDownload']);
    }

    /**
     * Sanitize and JSON-decode a $_POST value.
     *
     * @param string $key      POST key
     * @param string $default  Default if key missing
     * @return array|null      Decoded array or null on failure
     */
    private function jsonPost(string $key, string $default = '{}'): ?array {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce is verified by the caller (handleAjax)
        $raw = sanitize_text_field(wp_unslash($_POST[$key] ?? $default));
        $decoded = json_decode($raw, true);
        return is_array($decoded) ? $decoded : null;
    }

    public function registerMenu(): void {
        add_menu_page(
            __('Repair Desk', 'vollstart-repair-desk'),
            __('Repair Desk', 'vollstart-repair-desk'),
            'manage_options',
            'vollstart-repair-desk',
            [$this, 'renderPage'],
            'dashicons-admin-tools',
            30
        );
    }

    public function enqueueAssets(string $hook): void {
        if ($hook !== 'toplevel_page_vollstart-repair-desk') {
            return;
        }

        $version = $this->MAIN->getPluginVersion();
        $is_dev = (defined('WP_DEBUG') && WP_DEBUG) || wp_get_environment_type() === 'development';
        $debug_suffix = $is_dev ? '.' . time() : '';

        // DataTables + Responsive extension
        wp_enqueue_style(
            'datatables',
            VOLLSTART_RD_PLUGIN_DIR_URL . 'assets/js/libs/datatables/dataTables.dataTables.min.css',
            [],
            '2.3.7'
        );
        wp_enqueue_style(
            'datatables-responsive',
            VOLLSTART_RD_PLUGIN_DIR_URL . 'assets/js/libs/datatables/responsive.dataTables.min.css',
            ['datatables'],
            '3.0.6'
        );
        wp_enqueue_script(
            'datatables',
            VOLLSTART_RD_PLUGIN_DIR_URL . 'assets/js/libs/datatables/dataTables.min.js',
            ['jquery'],
            '2.3.7',
            true
        );
        wp_enqueue_script(
            'datatables-responsive',
            VOLLSTART_RD_PLUGIN_DIR_URL . 'assets/js/libs/datatables/dataTables.responsive.min.js',
            ['datatables'],
            '3.0.6',
            true
        );

        wp_enqueue_style(
            'vollstart-rd-admin',
            VOLLSTART_RD_PLUGIN_DIR_URL . 'assets/css/backend.css',
            ['datatables'],
            $version . $debug_suffix
        );

        wp_register_script('qrcodejs', VOLLSTART_RD_PLUGIN_DIR_URL . 'assets/js/libs/qrcode/qrcode.min.js', [], '1.0.0', true);
        wp_register_script('html5-qrcode', VOLLSTART_RD_PLUGIN_DIR_URL . 'assets/js/libs/html5-qrcode/html5-qrcode.min.js', [], '2.3.8', true);

        wp_enqueue_script(
            'vollstart-rd-admin',
            VOLLSTART_RD_PLUGIN_DIR_URL . 'assets/js/backend.js',
            ['jquery', 'wp-i18n', 'datatables', 'qrcodejs', 'html5-qrcode'],
            $version . $debug_suffix,
            true
        );

        // Only send what JS needs at boot — no DB queries here.
        // Status data is static (defined in PHP, not DB) so it's cheap.
        $bootData = [
            'ajaxurl'     => admin_url('admin-ajax.php'),
            'nonce'       => wp_create_nonce('vollstart_rd_admin'),
            'pluginVersion' => $this->MAIN->getPluginVersion(),
            'proInstalled' => defined('VOLLSTART_RD_PRO_VERSION'),
            'proVersion'   => defined('VOLLSTART_RD_PRO_VERSION') ? VOLLSTART_RD_PRO_VERSION : '',
            'statuses'    => $this->MAIN->getRepairs()->getActiveStatuses(),
            'allStatuses' => $this->MAIN->getRepairs()->getAllStatuses(),
            // function_exists() doesn't trigger WC's autoloader (class_exists()
            // does, and WC's bootstrap registers translations on autoload which
            // trips WP 6.7's _load_textdomain_just_in_time notice).
            'woocommerceActive' => function_exists('WC'),
            'currency'    => $this->MAIN->getCurrency(),
            'statusPageUrl' => home_url($this->MAIN->getFrontend()->getSlug() . '/'),
            'pluginUrl'     => VOLLSTART_RD_PLUGIN_DIR_URL,
            'isDebug'       => defined('WP_DEBUG') && WP_DEBUG,
            'pdfNonce'      => wp_create_nonce('rd_pdf_download'),
            'proAdminJs'    => defined('VOLLSTART_RD_PRO_DIR_PATH')
                ? plugins_url('assets/js/pro-admin.js', VOLLSTART_RD_PRO_DIR_PATH . 'vollstart-repair-desk-pro.php')
                : '',
        ];
        
        $bootData = apply_filters('vollstart_rd_admin_boot_data', $bootData);
        wp_localize_script('vollstart-rd-admin', 'vollstartRdAdmin', array_merge($bootData, $this->MAIN->getOptions()->getValuesMap()));

        wp_set_script_translations('vollstart-rd-admin', 'vollstart-repair-desk', VOLLSTART_RD_PLUGIN_DIR_PATH . 'languages');
    }

    /**
     * Render admin page shell. Content loaded via JS.
     */
    public function renderPage(): void {
        ?>
        <div id="rd-app">
            <div class="rd-loading" style="display:flex;align-items:center;justify-content:center;min-height:300px;gap:10px">
                <span class="spinner is-active" style="float:none"></span>
                <span><?php esc_html_e('Loading...', 'vollstart-repair-desk'); ?></span>
            </div>
        </div>
        <?php
    }

    /**
     * Enrich a repair array with computed fields.
     *
     * A registered add-on can hook `vollstart_rd_enrich_repair` to add staff
     * names (`assigned_to_name`, `created_by_staff_name`) and other extra data.
     * The default returns the input unchanged.
     */
    private function enrichRepairWithStaffName(?array $repair): ?array {
        if (!$repair) return $repair;
        return apply_filters('vollstart_rd_enrich_repair', $repair);
    }

    /**
     * Direct PDF download via GET — streams PDF inline so browser can display natively.
     * URL: admin.php?page=vollstart-repair-desk&rd_pdf=receipt&id=123&_wpnonce=xxx
     * Tax/billing-aware invoice PDFs are handled by an add-on if installed.
     */
    public function handlePdfDownload(): void {
        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- nonce verified below via wp_verify_nonce
        if (empty($_GET['rd_pdf']) || empty($_GET['page']) || $_GET['page'] !== 'vollstart-repair-desk') {
            return;
        }
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('Permission denied.', 'vollstart-repair-desk'));
        }
        if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'rd_pdf_download')) {
            wp_die(esc_html__('Invalid nonce.', 'vollstart-repair-desk'));
        }

        $type = sanitize_text_field(wp_unslash($_GET['rd_pdf']));
        $id = intval(wp_unslash($_GET['id'] ?? 0));
        $full = !empty(sanitize_text_field(wp_unslash($_GET['full'] ?? '')));
        // phpcs:enable WordPress.Security.NonceVerification.Recommended

        $pdfContent = '';
        $filename = 'document.pdf';

        switch ($type) {
            case 'receipt':
                $pdfContent = $this->MAIN->getPdf()->generateReceipt($id);
                $repair = $this->MAIN->getRepairs()->getById($id);
                $filename = 'receipt-' . sanitize_file_name($repair['repair_number'] ?? $id) . '.pdf';
                break;

            case 'device':
                $pdfContent = $this->MAIN->getPdf()->generateDevicePdf($id, $full);
                $product = $this->MAIN->getProducts()->getById($id);
                $deviceLabel = trim(($product['brand'] ?? '') . '-' . ($product['model'] ?? ''));
                if (empty($deviceLabel) || $deviceLabel === '-') $deviceLabel = (string) $id;
                $filename = ($full ? 'device-full-' : 'device-') . sanitize_file_name($deviceLabel) . '.pdf';
                break;

            case 'customer':
                $pdfContent = $this->MAIN->getPdf()->generateCustomerPdf($id, $full);
                $customer = $this->MAIN->getCustomers()->getById($id);
                $customerLabel = sanitize_file_name($customer['name'] ?? (string) $id);
                $filename = ($full ? 'customer-full-' : 'customer-') . $customerLabel . '.pdf';
                break;

            default:
                wp_die(esc_html__('Unknown PDF type.', 'vollstart-repair-desk'));
        }

        if (empty($pdfContent)) {
            wp_die(esc_html__('Could not generate PDF.', 'vollstart-repair-desk'));
        }

        // Stream PDF inline
        header('Content-Type: application/pdf');
        header('Content-Disposition: inline; filename="' . $filename . '"');
        header('Content-Length: ' . strlen($pdfContent));
        header('Cache-Control: no-cache, no-store, must-revalidate');
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- binary PDF output
        echo $pdfContent;
        exit;
    }

    /**
     * AJAX dispatcher.
     */
    // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- nonce verified via check_ajax_referer() at method start; data passed to manager methods is sanitized via VollstartRD::sanitizePostArray() (wp_unslash + sanitize_text_field/sanitize_textarea_field per declared field type); individual scalar reads in this method use wp_unslash + sanitize_text_field/intval inline.
    public function handleAjax(): void {
        check_ajax_referer('vollstart_rd_admin', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Permission denied', 'vollstart-repair-desk')]);
        }

        // Guarantee a clean JSON response. With WP_DEBUG enabled, a wpdb error
        // echoed into the output stream corrupts the JSON body, which the admin
        // UI then surfaces as a generic "Connection error. Please try again."
        // Keep wpdb from printing errors (they are still recorded in the debug
        // log via WP_DEBUG_LOG). We do NOT touch PHP's global display_errors /
        // error_reporting — the output buffer below strips any stray noise that
        // another plugin may emit before our payload.
        $GLOBALS['wpdb']->hide_errors();

        // Belt-and-suspenders: another active plugin (on the reviewer's or a
        // customer's site) may echo output while our response is built, which
        // would prepend noise to the body and make the JSON unparseable. Buffer
        // the handler and, on flush, strip anything before the JSON payload so
        // only valid JSON reaches the browser.
        ob_start(function ($buffer) {
            if ($buffer === '' || $buffer[0] === '{' || $buffer[0] === '[') {
                return $buffer;
            }
            $brace   = strpos($buffer, '{');
            $bracket = strpos($buffer, '[');
            if ($brace === false && $bracket === false) {
                return $buffer;
            }
            $start = ($brace === false) ? $bracket
                   : (($bracket === false) ? $brace : min($brace, $bracket));
            return substr($buffer, $start);
        });

        $actions_raw = sanitize_text_field(wp_unslash($_POST['sub_action'] ?? ''));

        // Multi-action support: comma-separated actions in one call.
        // Response keys are grouped by action name.
        // Single action: response is flat. Multiple: response[action] = data.
        $actions = array_map('trim', explode(',', $actions_raw));
        $is_multi = count($actions) > 1;
        $multi_response = [];

        foreach ($actions as $action) {


        $response = [];

        switch ($action) {
            // --- Repairs ---
            case 'get_repairs':
                $response = $this->MAIN->getRepairs()->getPaginated([
                    'page'     => intval(wp_unslash($_POST['page'] ?? 1)),
                    'per_page' => intval(wp_unslash($_POST['per_page'] ?? 20)),
                    'search'   => sanitize_text_field(wp_unslash($_POST['search'] ?? '')),
                    'status'   => sanitize_text_field(wp_unslash($_POST['status'] ?? '')),
                    'orderby'  => sanitize_text_field(wp_unslash($_POST['orderby'] ?? 'created_at')),
                    'order'    => sanitize_text_field(wp_unslash($_POST['order'] ?? 'DESC')),
                ]);
                // A registered add-on can enrich status_log entries via this filter.
                if (!empty($response['repairs'])) {
                    foreach ($response['repairs'] as &$repair) {
                        if (!empty($repair['status_log']) && is_array($repair['status_log'])) {
                            foreach ($repair['status_log'] as &$entry) {
                                $entry = apply_filters('vollstart_rd_enrich_status_log_entry', $entry);
                            }
                            unset($entry);
                        }
                    }
                    unset($repair);
                }
                break;

            case 'regenerate_access_code':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $newCode = $this->MAIN->getRepairs()->regenerateAccessCode($id);
                $response['access_code'] = $newCode;
                break;

            case 'bulk_change_status':
                $ids = array_map('intval', explode(',', sanitize_text_field(wp_unslash($_POST['ids'] ?? ''))));
                $status = sanitize_text_field(wp_unslash($_POST['status'] ?? ''));
                $ids = array_filter($ids);
                if (!empty($ids) && !empty($status)) {
                    $user = wp_get_current_user()->display_name ?: 'Admin';
                    foreach ($ids as $rid) {
                        $repair = $this->MAIN->getRepairs()->getById($rid);
                        if ($repair && $repair['status'] !== $status) {
                            $this->MAIN->getRepairs()->changeStatus($rid, $status, $user, __('Bulk action', 'vollstart-repair-desk'));
                        }
                    }
                }
                break;

            case 'bulk_delete_repairs':
                $ids = array_map('intval', explode(',', sanitize_text_field(wp_unslash($_POST['ids'] ?? ''))));
                $ids = array_filter($ids);
                foreach ($ids as $rid) {
                    $this->MAIN->getRepairs()->delete($rid);
                }
                break;

            case 'search_repair_by_number':
                $number = strtoupper(sanitize_text_field(wp_unslash($_POST['repair_number'] ?? '')));
                $db = $this->MAIN->getDB();
                $tbl = $db->getTabelle('repairs');
                $repairId = $db->getVar("SELECT id FROM %i WHERE repair_number = %s", $tbl, $number);
                $response['repair_id'] = $repairId ? (int) $repairId : null;
                break;

            case 'get_repair':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $repair = $this->MAIN->getRepairs()->getById($id);
                if (!$repair) {
                    wp_send_json_error(['message' => __('Repair not found', 'vollstart-repair-desk')]);
                }
                // Resolve creator name from WP user
                $creatorName = '';
                if (!empty($repair['created_by_user_id'])) {
                    $wpUser = get_userdata((int) $repair['created_by_user_id']);
                    if ($wpUser) $creatorName = $wpUser->display_name;
                }
                $repair['created_by_name'] = $creatorName;
                // A registered add-on can add createdByStaffName / assignedStaffName via this filter.
                $response = apply_filters('vollstart_rd_enrich_repair_response', $response, $repair);
                $response['repair'] = $repair;
                $response['customer'] = $repair['customer_id'] ? $this->MAIN->getCustomers()->getById($repair['customer_id']) : null;
                $response['product'] = !empty($repair['product_id']) ? $this->MAIN->getProducts()->getById((int) $repair['product_id']) : null;
                $response['timeline'] = $this->MAIN->getRepairs()->getStatusLog($id);
                $response['checklistProgress'] = $this->MAIN->getChecklists()->getProgress($id);
                $response['is_final'] = $this->MAIN->getRepairs()->isFinalStatus($repair['status']);
                $files = $this->MAIN->getFileUpload()->getByEntity('repair', $id);
                foreach ($files as &$f) {
                    $f['url'] = $this->MAIN->getFileUpload()->getFileUrl($f);
                    $f['is_image'] = strpos($f['mime_type'], 'image/') === 0;
                }
                $response['files'] = $files;
                // Mark customer messages as read when staff opens repair
                $this->MAIN->getRepairs()->markMessagesRead($id);
                // Ratings
                if ($this->MAIN->getOptions()->isCheckboxActive('enableRatings')) {
                    $response['ratings'] = $this->MAIN->getRatings()->getForRepair($id);
                    if ($repair['customer_id']) {
                        $response['customerAvgRating'] = $this->MAIN->getRatings()->getAverageForCustomer((int) $repair['customer_id']);
                    }
                }
                break;

            case 'create_repair':
                $id = $this->MAIN->getRepairs()->create(VollstartRD::sanitizePostArray($_POST, ['defect_description', 'internal_notes']));
                // Claim temp files uploaded during create form
                $tempToken = sanitize_text_field(wp_unslash($_POST['temp_token'] ?? ''));
                if (!empty($tempToken)) {
                    $this->MAIN->getFileUpload()->claimTempFiles($tempToken, 'repair', $id);
                }
                // Auto-apply checklists from repair type
                $repairTypeId = intval(wp_unslash($_POST['repair_type_id'] ?? 0));
                if ($repairTypeId > 0) {
                    $this->MAIN->getChecklists()->applyToRepair($id, $repairTypeId);
                }

                // Auto-create product from device data if checkbox checked
                $saveAsDevice = sanitize_text_field(wp_unslash($_POST['save_as_device'] ?? '0'));
                $customerId = intval(wp_unslash($_POST['customer_id'] ?? 0));
                if ($saveAsDevice === '1' && $customerId > 0) {
                    $deviceBrand = sanitize_text_field(wp_unslash($_POST['device_brand'] ?? ''));
                    $deviceModel = sanitize_text_field(wp_unslash($_POST['device_model'] ?? ''));
                    $deviceType = sanitize_text_field(wp_unslash($_POST['device_type'] ?? ''));
                    $deviceSerial = sanitize_text_field(wp_unslash($_POST['device_serial'] ?? ''));
                    // Only create if there's actual device data
                    if ($deviceBrand || $deviceModel || $deviceSerial || $deviceType) {
                        $productId = $this->MAIN->getProducts()->create([
                            'customer_id'   => $customerId,
                            'name'          => $deviceType,
                            'brand'         => $deviceBrand,
                            'model'         => $deviceModel,
                            'serial_number' => $deviceSerial,
                        ]);
                        // Link product to repair
                        $this->MAIN->getRepairs()->update($id, ['product_id' => $productId]);
                    }
                }
                // Notify admin about new repair
                $this->MAIN->getNotifications()->sendNewRepairNotification($id);

                $response['repair'] = $this->enrichRepairWithStaffName($this->MAIN->getRepairs()->getById($id));
                // Include list + counts so JS can refresh without extra call
                $response['repairs'] = $this->MAIN->getRepairs()->getAll();
                $response['counts'] = $this->MAIN->getRepairs()->countByStatus();
                break;

            case 'update_repair':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $this->MAIN->getRepairs()->update($id, VollstartRD::sanitizePostArray($_POST, ['defect_description', 'internal_notes']));
                $response['repair'] = $this->enrichRepairWithStaffName($this->MAIN->getRepairs()->getById($id));
                $response['auditLog'] = $this->MAIN->getRepairs()->getAuditLog($id);
                $response['repairs'] = $this->MAIN->getRepairs()->getAll();
                $response['counts'] = $this->MAIN->getRepairs()->countByStatus();
                break;

            case 'change_status':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $status = sanitize_text_field(wp_unslash($_POST['status'] ?? ''));
                $note = sanitize_textarea_field(wp_unslash($_POST['note'] ?? ''));
                $this->MAIN->getRepairs()->changeStatus($id, $status, '', $note);
                $response['repair'] = $this->enrichRepairWithStaffName($this->MAIN->getRepairs()->getById($id));
                $response['timeline'] = $this->MAIN->getRepairs()->getStatusLog($id);
                $response['auditLog'] = $this->MAIN->getRepairs()->getAuditLog($id);
                $response['counts'] = $this->MAIN->getRepairs()->countByStatus();
                break;

            case 'get_audit_log':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $response['auditLog'] = $this->MAIN->getRepairs()->getAuditLog($id);
                break;

            case 'delete_repair':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $this->MAIN->getRepairs()->delete($id);
                $response['repairs'] = $this->MAIN->getRepairs()->getAll();
                $response['counts'] = $this->MAIN->getRepairs()->countByStatus();
                break;

            // --- Customers ---
            case 'get_customers':
                $response = $this->MAIN->getCustomers()->getPaginated([
                    'page'     => intval(wp_unslash($_POST['page'] ?? 1)),
                    'per_page' => intval(wp_unslash($_POST['per_page'] ?? 20)),
                    'search'   => sanitize_text_field(wp_unslash($_POST['search'] ?? '')),
                    'orderby'  => sanitize_text_field(wp_unslash($_POST['orderby'] ?? 'name')),
                    'order'    => sanitize_text_field(wp_unslash($_POST['order'] ?? 'ASC')),
                ]);
                break;

            case 'get_customer':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $customer = $this->MAIN->getCustomers()->getById($id);
                if (!$customer) {
                    wp_send_json_error(['message' => __('Customer not found', 'vollstart-repair-desk')]);
                }
                $response['customer'] = $customer;
                $response['customerRepairs'] = $this->MAIN->getRepairs()->getAll(['customer_id' => $id]);
                $products = $this->MAIN->getProducts()->getByCustomerId($id);
                foreach ($products as &$p) {
                    $p['thumbnail_url'] = $this->MAIN->getFileUpload()->getThumbnailUrl('product', (int) $p['id']);
                }
                $response['customerProducts'] = $products;
                if ($this->MAIN->getOptions()->isCheckboxActive('enableRatings')) {
                    $response['customerRatings'] = $this->MAIN->getRatings()->getForCustomer($id);
                    $response['customerAvgRating'] = $this->MAIN->getRatings()->getAverageForCustomer($id);
                }
                break;

            case 'create_customer':
                $id = $this->MAIN->getCustomers()->create(VollstartRD::sanitizePostArray($_POST, ['address', 'notes']));
                $response['customer'] = $this->MAIN->getCustomers()->getById($id);
                $response['customers'] = $this->MAIN->getCustomers()->getAll();
                break;

            case 'update_customer':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $this->MAIN->getCustomers()->update($id, VollstartRD::sanitizePostArray($_POST, ['address', 'notes']));
                $response['customer'] = $this->MAIN->getCustomers()->getById($id);
                $response['customers'] = $this->MAIN->getCustomers()->getAll();
                break;

            case 'delete_customer':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $this->MAIN->getCustomers()->delete($id);
                $response['customers'] = $this->MAIN->getCustomers()->getAll();
                break;

            case 'export_customer_data':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $response['data'] = $this->MAIN->getCustomers()->exportCustomerData($id);
                break;

            case 'anonymize_customer':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $this->MAIN->getCustomers()->anonymize($id);
                break;

            // --- Products ---
            case 'get_products':
                $customerId = intval(wp_unslash($_POST['customer_id'] ?? 0));
                if ($customerId > 0) {
                    $response['products'] = $this->MAIN->getProducts()->getByCustomerId($customerId);
                } else {
                    // Paginated for Devices tab DataTable
                    $response = $this->MAIN->getProducts()->getPaginated([
                        'page'     => intval(wp_unslash($_POST['page'] ?? 1)),
                        'per_page' => intval(wp_unslash($_POST['per_page'] ?? 20)),
                        'search'   => sanitize_text_field(wp_unslash($_POST['search'] ?? '')),
                        'orderby'  => sanitize_text_field(wp_unslash($_POST['orderby'] ?? 'name')),
                        'order'    => sanitize_text_field(wp_unslash($_POST['order'] ?? 'ASC')),
                    ]);
                }
                break;

            case 'get_product':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $product = $this->MAIN->getProducts()->getById($id);
                if (!$product) {
                    wp_send_json_error(['message' => __('Product not found', 'vollstart-repair-desk')]);
                }
                $response['product'] = $product;
                break;

            case 'create_product':
                $id = $this->MAIN->getProducts()->create(VollstartRD::sanitizePostArray($_POST, ['notes']));
                $response['product'] = $this->MAIN->getProducts()->getById($id);
                $response['products'] = $this->MAIN->getProducts()->getByCustomerId(intval(wp_unslash($_POST['customer_id'] ?? 0)));
                break;

            case 'update_product':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $this->MAIN->getProducts()->update($id, VollstartRD::sanitizePostArray($_POST, ['notes']));
                $response['product'] = $this->MAIN->getProducts()->getById($id);
                break;

            case 'delete_product':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $product = $this->MAIN->getProducts()->getById($id);
                $this->MAIN->getProducts()->delete($id);
                if ($product) {
                    $response['products'] = $this->MAIN->getProducts()->getByCustomerId((int) $product['customer_id']);
                }
                break;

            case 'get_products_dropdown':
                $customerId = intval(wp_unslash($_POST['customer_id'] ?? 0));
                $response['products'] = $this->MAIN->getProducts()->getForDropdown($customerId);
                break;

            // --- Files ---
            case 'upload_file':
                $entityType = sanitize_text_field(wp_unslash($_POST['entity_type'] ?? ''));
                $entityId = intval(wp_unslash($_POST['entity_id'] ?? 0));
                $caption = sanitize_text_field(wp_unslash($_POST['caption'] ?? ''));
                $phase = sanitize_text_field(wp_unslash($_POST['phase'] ?? ''));
                $tempToken = sanitize_text_field(wp_unslash($_POST['temp_token'] ?? ''));

                if (!empty($_POST['base64_data'])) {
                    $result = $this->MAIN->getFileUpload()->handleBase64Upload(
                        sanitize_text_field(wp_unslash($_POST['base64_data'])), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- base64 data validated in handleBase64Upload
                        $entityType, $entityId, $caption, $phase, $tempToken
                    );
                } elseif (!empty($_FILES['file']) && is_array($_FILES['file'])) {
                    $file = VollstartRD_FileUpload::sanitizeUploadedFile($_FILES['file']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized inside helper
                    if (!$file) {
                        wp_send_json_error(['message' => __('Invalid upload', 'vollstart-repair-desk')]);
                    }
                    $result = $this->MAIN->getFileUpload()->handleUpload(
                        $file,
                        $entityType, $entityId, $caption, $phase, $tempToken
                    );
                } else {
                    wp_send_json_error(['message' => __('No file data', 'vollstart-repair-desk')]);
                }

                if (is_wp_error($result)) {
                    wp_send_json_error(['message' => $result->get_error_message()]);
                }

                $response['file'] = $this->MAIN->getFileUpload()->getById($result);
                $response['file']['url'] = $this->MAIN->getFileUpload()->getFileUrl($response['file']);
                // Log audit event for repairs + return updated audit log
                if ($entityType === 'repair' && $entityId > 0) {
                    $this->MAIN->getRepairs()->logEvent($entityId, 'file_uploaded', '', $response['file']['original_name']);
                    $response['auditLog'] = $this->MAIN->getRepairs()->getAuditLog($entityId);
                }
                // Return files list: by temp_token or by entity
                if (!empty($tempToken)) {
                    $response['files'] = $this->getFilesWithUrlsByToken($tempToken);
                } else {
                    $response['files'] = $this->getFilesWithUrls($entityType, $entityId);
                }
                break;

            case 'cleanup_temp_files':
                $cleaned = $this->MAIN->getFileUpload()->cleanupOldTempFiles(24);
                $response['cleaned'] = $cleaned;
                break;

            case 'cancel_temp_files':
                $tempToken = sanitize_text_field(wp_unslash($_POST['temp_token'] ?? ''));
                if (!empty($tempToken)) {
                    $this->MAIN->getFileUpload()->deleteTempFiles($tempToken);
                }
                break;

            case 'get_temp_files':
                $tempToken = sanitize_text_field(wp_unslash($_POST['temp_token'] ?? ''));
                $response['files'] = !empty($tempToken) ? $this->getFilesWithUrlsByToken($tempToken) : [];
                break;

            case 'get_messages':
                $repairId = intval(wp_unslash($_POST['repair_id'] ?? 0));
                $tbl = $this->MAIN->getDB()->getTabelle('messages');
                $response['messages'] = $this->MAIN->getDB()->getResults(
                    "SELECT * FROM %i WHERE repair_id = %d ORDER BY created_at ASC",
                    $tbl,
                    $repairId
                );
                break;

            case 'send_message':
                $repairId = intval(wp_unslash($_POST['repair_id'] ?? 0));
                $message = sanitize_textarea_field(wp_unslash($_POST['message'] ?? ''));
                if ($repairId > 0 && !empty($message)) {
                    $user = wp_get_current_user();
                    $this->MAIN->getDB()->insert('messages', [
                        'repair_id'   => $repairId,
                        'sender_type' => 'staff',
                        'sender_name' => $user->display_name ?: 'Admin',
                        'message'     => $message,
                        'created_at'  => current_time('mysql'),
                    ]);
                    // Email customer about the message
                    $this->MAIN->getNotifications()->sendCustomerMessageEmail($repairId, $message);
                }
                break;

            case 'get_files':
                $entityType = sanitize_text_field(wp_unslash($_POST['entity_type'] ?? ''));
                $entityId = intval(wp_unslash($_POST['entity_id'] ?? 0));
                $response['files'] = $this->getFilesWithUrls($entityType, $entityId);
                break;

            case 'delete_file':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $file = $this->MAIN->getFileUpload()->getById($id);
                if ($file && $file['entity_type'] === 'repair' && $file['entity_id'] > 0) {
                    $this->MAIN->getRepairs()->logEvent((int) $file['entity_id'], 'file_deleted', '', $file['original_name']);
                    $this->MAIN->getFileUpload()->delete($id);
                    $response['files'] = $this->getFilesWithUrls($file['entity_type'], (int) $file['entity_id']);
                    $response['auditLog'] = $this->MAIN->getRepairs()->getAuditLog((int) $file['entity_id']);
                } else {
                    $this->MAIN->getFileUpload()->delete($id);
                    if ($file) {
                        $response['files'] = $this->getFilesWithUrls($file['entity_type'], (int) $file['entity_id']);
                    }
                }
                break;

            case 'set_thumbnail':
                $entityType = sanitize_text_field(wp_unslash($_POST['entity_type'] ?? ''));
                $entityId = intval(wp_unslash($_POST['entity_id'] ?? 0));
                $fileId = intval(wp_unslash($_POST['file_id'] ?? 0));
                $this->MAIN->getFileUpload()->setThumbnail($entityType, $entityId, $fileId);
                $response['files'] = $this->getFilesWithUrls($entityType, $entityId);
                break;

            case 'get_device_detail':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $product = $this->MAIN->getProducts()->getById($id);
                if (!$product) {
                    wp_send_json_error(['message' => __('Device not found', 'vollstart-repair-desk')]);
                }
                $response['product'] = $product;
                $response['product']['thumbnail_url'] = $this->MAIN->getFileUpload()->getThumbnailUrl('product', $id);
                $response['customer'] = $product['customer_id'] ? $this->MAIN->getCustomers()->getById((int) $product['customer_id']) : null;
                $response['repairs'] = $this->MAIN->getRepairs()->getAll(['product_id' => $id]);
                $response['files'] = $this->getFilesWithUrls('product', $id);
                break;

            case 'get_product_history':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $response['history'] = $this->MAIN->getProducts()->getHistory($id);
                break;

            case 'dismiss_maintenance':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $this->MAIN->getProducts()->dismissReminder($id);
                break;

            case 'get_customer_history':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $response['history'] = $this->MAIN->getCustomers()->getHistory($id);
                break;

            case 'get_customers_dropdown':
                $response['customers'] = $this->MAIN->getCustomers()->getForDropdown();
                break;

            // --- Settings ---
            case 'get_settings':
                $response['options'] = $this->MAIN->getOptions()->getOptions();
                break;

            case 'save_settings':
                $settings = $this->jsonPost('settings');
                if ($settings) {
                    $opts = $this->MAIN->getOptions();
                    foreach ($settings as $key => $value) {
                        $cleanKey = sanitize_text_field($key);
                        // Email body fields allow HTML (wp_kses_post), others use sanitize_textarea_field
                        $cleanValue = str_ends_with($cleanKey, 'Body') ? wp_kses_post($value) : sanitize_textarea_field($value);
                        $opts->changeOption(['key' => $cleanKey, 'value' => $cleanValue]);
                    }
                }
                break;

            case 'save_option':
                $key = sanitize_text_field(wp_unslash($_POST['key'] ?? ''));
                // Email body fields allow HTML (wp_kses_post), others use sanitize_textarea_field
                $rawValue = wp_unslash($_POST['value'] ?? ''); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized per type below (wp_kses_post for HTML, sanitize_textarea_field for rest)
                if (str_ends_with($key, 'Body')) {
                    $value = wp_kses_post($rawValue);
                } else {
                    $value = sanitize_textarea_field($rawValue);
                }
                $this->MAIN->getOptions()->changeOption(['key' => $key, 'value' => $value]);
                break;

            case 'export_options':
                $response = $this->MAIN->getOptions()->exportOptions();
                break;

            case 'import_options':
                $optionsData = $this->jsonPost('options');
                if ($optionsData) {
                    foreach ($optionsData as $k => $v) {
                        if (is_string($v)) {
                            // Email body fields allow HTML (wp_kses_post), others use sanitize_textarea_field
                            $optionsData[$k] = str_ends_with((string) $k, 'Body') ? wp_kses_post($v) : sanitize_textarea_field($v);
                        }
                    }
                }
                $response = $this->MAIN->getOptions()->importOptions($optionsData ?: []);
                break;

            case 'reset_options':
                $this->MAIN->getOptions()->resetAllToDefault();
                break;

            // --- Export / Import ---

            case 'export_repairs_csv':
                $db = $this->MAIN->getDB();
                $tbl = $db->getTabelle('repairs');
                $ctbl = $db->getTabelle('customers');
                $rows = $db->getResults("SELECT r.*, c.name AS customer_name, c.customer_number, c.email AS customer_email FROM %i r LEFT JOIN %i c ON r.customer_id = c.id ORDER BY r.id ASC", $tbl, $ctbl);
                $response['rows'] = $rows;
                break;

            case 'export_customers_csv':
                $db = $this->MAIN->getDB();
                $tbl = $db->getTabelle('customers');
                $response['rows'] = $db->getResults("SELECT id, customer_number, name, email, phone, address, notes, created_at FROM %i ORDER BY id ASC", $tbl);
                break;

            case 'import_customers':
                $items = $this->jsonPost('data', '[]');
                $imported = 0;
                if ($items) {
                    foreach ($items as $row) {
                        if (!is_array($row) || empty($row['name'])) continue;
                        $sanitized = array_map('sanitize_text_field', array_filter($row, 'is_string'));
                        $this->MAIN->getCustomers()->create($sanitized);
                        $imported++;
                    }
                }
                $response['imported'] = $imported;
                break;

            case 'export_devices_csv':
                $db = $this->MAIN->getDB();
                $tbl = $db->getTabelle('products');
                $ctbl = $db->getTabelle('customers');
                $response['rows'] = $db->getResults("SELECT p.*, c.name AS customer_name FROM %i p LEFT JOIN %i c ON p.customer_id = c.id ORDER BY p.id ASC", $tbl, $ctbl);
                break;

            case 'export_repair_types_json':
                $types = $this->MAIN->getRepairTypes()->getAll();
                // Include checklist assignments
                foreach ($types as &$t) {
                    $t['checklists'] = $this->MAIN->getChecklists()->getChecklistsForType((int) $t['id']);
                }
                $response['data'] = $types;
                break;

            case 'import_repair_types':
                $items = $this->jsonPost('data', '[]');
                $imported = 0;
                if ($items) {
                    foreach ($items as $row) {
                        if (!is_array($row) || empty($row['name'])) continue;
                        $sanitized = array_map('sanitize_text_field', array_filter($row, 'is_string'));
                        $this->MAIN->getRepairTypes()->create($sanitized);
                        $imported++;
                    }
                }
                $response['imported'] = $imported;
                break;

            case 'export_work_steps_json':
                $cls = $this->MAIN->getChecklists()->getAllWithItems();
                $response['data'] = $cls;
                break;

            case 'import_work_steps':
                $items = $this->jsonPost('data', '[]');
                $imported = 0;
                if ($items) {
                    foreach ($items as $cl) {
                        if (!is_array($cl) || empty($cl['name'])) continue;
                        $sanitizedCl = array_map('sanitize_text_field', array_filter($cl, 'is_string'));
                        $clId = $this->MAIN->getChecklists()->createChecklist($sanitizedCl);
                        if (!empty($cl['items']) && is_array($cl['items'])) {
                            foreach ($cl['items'] as $idx => $item) {
                                if (!is_array($item)) continue;
                                $sanitizedItem = array_map('sanitize_text_field', array_filter($item, 'is_string'));
                                $this->MAIN->getChecklists()->addItem($clId, $sanitizedItem['title'] ?? '', $idx, $sanitizedItem);
                            }
                        }
                        $imported++;
                    }
                }
                $response['imported'] = $imported;
                break;

            case 'get_ratings':
                $repairId = intval(wp_unslash($_POST['repair_id'] ?? 0));
                $response['ratings'] = $this->MAIN->getRatings()->getForRepair($repairId);
                break;

            case 'get_email_log':
                $repairId = intval(wp_unslash($_POST['repair_id'] ?? 0));
                $response['emails'] = $this->MAIN->getNotifications()->getEmailLog($repairId);
                break;

            case 'get_customer_email_log':
                $customerId = intval(wp_unslash($_POST['customer_id'] ?? 0));
                $response['emails'] = $this->MAIN->getNotifications()->getEmailLogForCustomer($customerId);
                break;

            case 'resend_email':
                $logId = intval(wp_unslash($_POST['log_id'] ?? 0));
                $response['sent'] = $this->MAIN->getNotifications()->resendEmail($logId);
                break;

            case 'send_manual_email':
                $repairId = intval(wp_unslash($_POST['repair_id'] ?? 0));
                $response['sent'] = $this->MAIN->getNotifications()->sendManualEmail($repairId);
                break;

            case 'get_options_history':
                $response = $this->getOptionsHistory();
                break;

            case 'revert_option':
                $historyId = intval(wp_unslash($_POST['history_id'] ?? 0));
                $response = $this->revertOption($historyId);
                break;


            // --- Repair Types ---
            case 'get_repair_types':
                $response['repairTypes'] = $this->MAIN->getRepairTypes()->getAll();
                $response['icons'] = $this->MAIN->getRepairTypes()->getIconOptions();
                break;

            case 'create_repair_type':
                $id = $this->MAIN->getRepairTypes()->create(VollstartRD::sanitizePostArray($_POST, ['description']));
                $response['repairType'] = $this->MAIN->getRepairTypes()->getById($id);
                $response['repairTypes'] = $this->MAIN->getRepairTypes()->getAll();
                break;

            case 'update_repair_type':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $this->MAIN->getRepairTypes()->update($id, VollstartRD::sanitizePostArray($_POST, ['description']));
                $response['repairType'] = $this->MAIN->getRepairTypes()->getById($id);
                $response['repairTypes'] = $this->MAIN->getRepairTypes()->getAll();
                break;

            case 'delete_repair_type':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $this->MAIN->getRepairTypes()->delete($id);
                $response['repairTypes'] = $this->MAIN->getRepairTypes()->getAll();
                break;

            case 'get_repair_type_detail':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $type = $this->MAIN->getRepairTypes()->getById($id);
                if (!$type) wp_send_json_error(['message' => __('Repair type not found', 'vollstart-repair-desk')]);
                $response['repairType'] = $type;
                $response['checklists'] = $this->MAIN->getChecklists()->getChecklistsByType($id);
                $response['repairs'] = $this->MAIN->getRepairs()->getAll(['repair_type_id' => $id]);
                $repairCount = count($response['repairs']);
                $response['stats'] = ['repair_count' => $repairCount];
                break;

            case 'get_repair_type_history':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $response['history'] = $this->MAIN->getRepairTypes()->getHistory($id);
                break;

            case 'save_repair_types_order':
                $order = $this->jsonPost('order', '[]');
                if ($order) {
                    $order = array_map('intval', $order);
                    $this->MAIN->getRepairTypes()->updateSortOrder($order);
                }
                $response['repairTypes'] = $this->MAIN->getRepairTypes()->getAll();
                break;

            case 'get_repair_types_dropdown':
                $response['repairTypes'] = $this->MAIN->getRepairTypes()->getAll(true);
                $response['favorites'] = $this->MAIN->getRepairTypes()->getFavorites();
                break;

            // --- Checklists ---
            case 'get_checklists':
                $checklists = $this->MAIN->getChecklists()->getAllChecklists();
                foreach ($checklists as &$cl) {
                    $items = $this->MAIN->getChecklists()->getItems((int) $cl['id']);
                    foreach ($items as &$item) {
                        $files = $this->getFilesWithUrls('checklist_item', (int) $item['id']);
                        $item['file_count'] = count($files);
                        $item['files'] = $files;
                    }
                    $cl['items'] = $items;
                    $cl['repair_types'] = $this->MAIN->getChecklists()->getRepairTypesForChecklist((int) $cl['id']);
                }
                $response['checklists'] = $checklists;
                break;

            case 'get_checklist_detail':
                $id = intval(wp_unslash($_POST['id'] ?? 0));
                $cl = $this->MAIN->getChecklists()->getChecklist($id);
                if (!$cl) wp_send_json_error(['message' => __('Not found', 'vollstart-repair-desk')]);
                $items = $this->MAIN->getChecklists()->getItems($id);
                foreach ($items as &$item) {
                    $files = $this->getFilesWithUrls('checklist_item', (int) $item['id']);
                    $item['file_count'] = count($files);
                    $item['files'] = $files;
                }
                $cl['items'] = $items;
                $cl['repair_types'] = $this->MAIN->getChecklists()->getRepairTypesForChecklist($id);
                $response['checklist'] = $cl;
                break;

            case 'get_checklists_for_type':
                $repairTypeId = intval(wp_unslash($_POST['repair_type_id'] ?? 0));
                $response['assignedIds'] = $this->MAIN->getChecklists()->getChecklistIdsForType($repairTypeId);
                $checklists = $this->MAIN->getChecklists()->getAllChecklists();
                foreach ($checklists as &$cl) {
                    $cl['items'] = $this->MAIN->getChecklists()->getItems((int) $cl['id']);
                }
                $response['checklists'] = $checklists;
                break;

            case 'save_checklist':
                // Only saves list properties (name, description, sort_order) — NOT items
                $clId = intval(wp_unslash($_POST['checklist_id'] ?? 0));
                $clData = [
                    'name'                 => sanitize_text_field(wp_unslash($_POST['name'] ?? '')),
                    'description'          => sanitize_textarea_field(wp_unslash($_POST['description'] ?? '')),
                    'trigger_status_start' => sanitize_text_field(wp_unslash($_POST['trigger_status_start'] ?? '')),
                    'trigger_status'       => sanitize_text_field(wp_unslash($_POST['trigger_status'] ?? '')),
                    'sort_order'           => intval(wp_unslash($_POST['sort_order'] ?? 0)),
                ];
                if ($clId > 0) {
                    $this->MAIN->getChecklists()->updateChecklist($clId, $clData);
                } else {
                    $clId = $this->MAIN->getChecklists()->createChecklist($clData);
                }
                $cl = $this->MAIN->getChecklists()->getChecklist($clId);
                $cl['items'] = $this->MAIN->getChecklists()->getItems($clId);
                $response['checklist'] = $cl;
                break;

            case 'add_checklist_item':
                $clId = intval(wp_unslash($_POST['checklist_id'] ?? 0));
                $title = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
                if ($clId > 0 && !empty($title)) {
                    $sortOrder = count($this->MAIN->getChecklists()->getItems($clId));
                    $this->MAIN->getChecklists()->addItem($clId, $title, $sortOrder);
                }
                $response['checklist'] = $this->MAIN->getChecklists()->getChecklist($clId);
                $response['checklist']['items'] = $this->MAIN->getChecklists()->getItems($clId);
                break;

            case 'update_checklist_item':
                $itemId = intval(wp_unslash($_POST['item_id'] ?? 0));
                $updateFields = [
                    'title'            => sanitize_text_field(wp_unslash($_POST['title'] ?? '')),
                    'instructions'     => sanitize_textarea_field(wp_unslash($_POST['instructions'] ?? '')),
                    'default_duration' => (float) sanitize_text_field(wp_unslash($_POST['default_duration'] ?? 0)),
                    'default_price'    => (float) sanitize_text_field(wp_unslash($_POST['default_price'] ?? 0)),
                ];
                $this->MAIN->getChecklists()->updateItem($itemId, $updateFields);
                break;

            case 'delete_checklist_item':
                $itemId = intval(wp_unslash($_POST['item_id'] ?? 0));
                $this->MAIN->getFileUpload()->deleteByEntity('checklist_item', $itemId);
                $this->MAIN->getChecklists()->deleteItem($itemId);
                break;

            case 'delete_checklist':
                $this->MAIN->getChecklists()->deleteChecklist(intval(wp_unslash($_POST['id'] ?? 0)));
                $all = $this->MAIN->getChecklists()->getAllChecklists();
                foreach ($all as &$a) { $a['items'] = $this->MAIN->getChecklists()->getItems((int) $a['id']); }
                $response['checklists'] = $all;
                break;

            case 'assign_checklists_to_type':
                $repairTypeId = intval(wp_unslash($_POST['repair_type_id'] ?? 0));
                $ids = $this->jsonPost('checklist_ids', '[]');
                if ($ids && $repairTypeId > 0) {
                    $ids = array_map('intval', $ids);
                    $this->MAIN->getChecklists()->assignChecklistsToType($repairTypeId, $ids);
                }
                break;

            case 'toggle_timer':
                if (!$this->MAIN->getOptions()->isCheckboxActive('enableTimeTracking')) {
                    wp_send_json_error(['message' => __('Time tracking is disabled', 'vollstart-repair-desk')]);
                }
                $stepId = intval(wp_unslash($_POST['step_id'] ?? 0));
                $step = $this->MAIN->getChecklists()->toggleTimer($stepId);
                $repairId = (int) ($step['repair_id'] ?? 0);
                $response['step'] = $step;
                $response['totals'] = $repairId ? $this->MAIN->getChecklists()->getTotals($repairId) : null;
                break;

            case 'set_step_time':
                if (!$this->MAIN->getOptions()->isCheckboxActive('allowManualTimeEdit')) {
                    wp_send_json_error(['message' => __('Manual time edit is disabled', 'vollstart-repair-desk')]);
                }
                $stepId = intval(wp_unslash($_POST['step_id'] ?? 0));
                $duration = (float) sanitize_text_field(wp_unslash($_POST['tracked_duration'] ?? 0));
                $db = $this->MAIN->getDB();
                $tbl = $db->getTabelle('repair_checklist_items');
                $db->update('repair_checklist_items', [
                    'tracked_duration' => max(0, $duration),
                    'timer_started_at' => null,
                ], ['id' => $stepId]);
                $step = $db->getRow("SELECT * FROM %i WHERE id = %d", $tbl, $stepId);
                $response['step'] = $step;
                break;

            case 'get_repair_steps':
                $repairId = intval(wp_unslash($_POST['repair_id'] ?? 0));
                $steps = $this->MAIN->getChecklists()->getRepairSteps($repairId);
                // Attach instruction images per step
                foreach ($steps as $group => &$items) {
                    foreach ($items as &$item) {
                        $itemId = (int) ($item['checklist_item_id'] ?? 0);
                        if ($itemId > 0) {
                            $item['instruction_files'] = $this->getFilesWithUrls('checklist_item', $itemId);
                        }
                    }
                }
                $response['steps'] = $steps;
                $response['progress'] = $this->MAIN->getChecklists()->getProgress($repairId);
                $response['totals'] = $this->MAIN->getChecklists()->getTotals($repairId);
                break;

            case 'toggle_step':
                $stepId = intval(wp_unslash($_POST['step_id'] ?? 0));
                $step = $this->MAIN->getChecklists()->toggleStep($stepId);
                $repairId = (int) ($step['repair_id'] ?? 0);
                $response['step'] = $step;
                $response['progress'] = $repairId ? $this->MAIN->getChecklists()->getProgress($repairId) : null;
                if ($repairId) {
                    $trigger = $this->MAIN->getChecklists()->checkTriggers($repairId, $step);
                    if ($trigger) {
                        $response['trigger_status'] = $trigger;
                    }
                }
                break;

            case 'delete_repair_step':
                $stepId = intval(wp_unslash($_POST['step_id'] ?? 0));
                $db = $this->MAIN->getDB();
                $step = $db->getRow("SELECT * FROM %i WHERE id = %d", $db->getTabelle('repair_checklist_items'), $stepId);
                $this->MAIN->getChecklists()->deleteRepairStep($stepId);
                $repairId = (int) ($step['repair_id'] ?? 0);
                $response['progress'] = $repairId ? $this->MAIN->getChecklists()->getProgress($repairId) : null;
                break;

            case 'add_worklist_to_repair':
                $repairId = intval(wp_unslash($_POST['repair_id'] ?? 0));
                $checklistId = intval(wp_unslash($_POST['checklist_id'] ?? 0));
                if ($repairId > 0 && $checklistId > 0) {
                    $cl = $this->MAIN->getChecklists()->getChecklist($checklistId);
                    $items = $this->MAIN->getChecklists()->getItems($checklistId);
                    foreach ($items as $item) {
                        $this->MAIN->getDB()->insert('repair_checklist_items', [
                            'repair_id'         => $repairId,
                            'checklist_id'      => $checklistId,
                            'checklist_item_id' => (int) $item['id'],
                            'group_name'        => $cl ? $cl['name'] : '',
                            'title'             => $item['title'],
                            'instructions'      => $item['instructions'] ?? '',
                            'default_duration'  => (float) ($item['default_duration'] ?? 0),
                            'default_price'     => (float) ($item['default_price'] ?? 0),
                            'sort_order'        => (int) $item['sort_order'],
                            'is_checked'        => 0,
                        ]);
                    }
                }
                $response['steps'] = $this->MAIN->getChecklists()->getRepairSteps($repairId);
                $response['progress'] = $this->MAIN->getChecklists()->getProgress($repairId);
                $response['totals'] = $this->MAIN->getChecklists()->getTotals($repairId);
                break;

            case 'remove_repair_group':
                $repairId = intval(wp_unslash($_POST['repair_id'] ?? 0));
                $groupName = sanitize_text_field(wp_unslash($_POST['group_name'] ?? ''));
                if ($repairId > 0 && $groupName !== '') {
                    $db = $this->MAIN->getDB();
                    $tbl = $db->getTabelle('repair_checklist_items');
                    $db->rawQuery("DELETE FROM %i WHERE repair_id = %d AND group_name = %s", $tbl, $repairId, $groupName);
                }
                $response['steps'] = $this->MAIN->getChecklists()->getRepairSteps($repairId);
                $response['progress'] = $this->MAIN->getChecklists()->getProgress($repairId);
                $response['totals'] = $this->MAIN->getChecklists()->getTotals($repairId);
                break;

            case 'add_repair_step':
                $repairId = intval(wp_unslash($_POST['repair_id'] ?? 0));
                $groupName = sanitize_text_field(wp_unslash($_POST['group_name'] ?? __('General', 'vollstart-repair-desk')));
                $title = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
                $this->MAIN->getChecklists()->addRepairStep($repairId, $groupName, $title);
                $response['steps'] = $this->MAIN->getChecklists()->getRepairSteps($repairId);
                $response['progress'] = $this->MAIN->getChecklists()->getProgress($repairId);
                break;

            case 'get_status_flow':
                $response['statusFlow'] = $this->MAIN->getRepairs()->getStatusFlowConfig();
                break;

            case 'save_status_flow':
                $flow = $this->jsonPost('flow', '[]');
                if ($flow) {
                    $sanitizedFlow = [];
                    foreach ($flow as $item) {
                        if (!is_array($item)) continue;
                        $sanitizedFlow[] = [
                            'slug'   => sanitize_text_field($item['slug'] ?? ''),
                            'active' => !empty($item['active']),
                        ];
                    }
                    $this->MAIN->getOptions()->changeOption(['key' => 'statusFlow', 'value' => wp_json_encode($sanitizedFlow)]);
                    wp_cache_delete('rd_active_statuses', 'vollstart-repair-desk');
                    $response['statuses'] = $this->MAIN->getRepairs()->getActiveStatuses();
                    $response['allStatuses'] = $this->MAIN->getRepairs()->getAllStatuses();
                    $response['statusFlow'] = $this->MAIN->getRepairs()->getStatusFlowConfig();
                }
                break;

            // --- Support ---
            case 'get_support_info':
                $response['support'] = $this->MAIN->getSupport()->getInfo();
                $response['errorLogs'] = $this->MAIN->getDB()->getErrorLogs(50);
                break;

            case 'clear_error_logs':
                $this->MAIN->getDB()->clearErrorLogs();
                $response['errorLogs'] = [];
                break;

            default:
                // A registered add-on can handle unknown actions via this filter
                // (it reads $_POST directly with its own per-field sanitization).
                $addonResponse = apply_filters('vollstart_rd_ajax_action', null, $action);
                if ($addonResponse !== null) {
                    $response = $addonResponse;
                } else {
                    $response['_error'] = 'Unknown action: ' . $action;
                }
        }

        if ($is_multi) {
            $multi_response[$action] = $response;
        }

        } // end foreach $actions

        // Include server time for timer sync
        if (!$is_multi) {
            $response['_serverTime'] = gmdate('Y-m-d H:i:s');
        }
        wp_send_json_success($is_multi ? $multi_response : $response);
    }
    // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash

    // --- File helpers ---

    private function getFilesWithUrls(string $entityType, int $entityId): array {
        $files = $this->MAIN->getFileUpload()->getByEntity($entityType, $entityId);
        foreach ($files as &$f) {
            $f['url'] = $this->MAIN->getFileUpload()->getFileUrl($f);
            $f['is_image'] = strpos($f['mime_type'], 'image/') === 0;
        }
        return $files;
    }

    private function getFilesWithUrlsByToken(string $tempToken): array {
        $files = $this->MAIN->getFileUpload()->getByTempToken($tempToken);
        foreach ($files as &$f) {
            $f['url'] = $this->MAIN->getFileUpload()->getFileUrl($f);
            $f['is_image'] = strpos($f['mime_type'], 'image/') === 0;
        }
        return $files;
    }

    // --- Options History ---

    /**
     * Get options change history. Simple paginated list (no DataTables server-side).
     */
    private function getOptionsHistory(): array {
        global $wpdb;
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('options_history');

        // phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce is verified by the caller (handleAjax)
        $draw   = intval(wp_unslash($_POST['draw'] ?? 1));
        $start  = max(0, intval(wp_unslash($_POST['start'] ?? 0)));
        $length = max(1, min(100, intval(wp_unslash($_POST['length'] ?? 25))));
        $search = sanitize_text_field(wp_unslash($_POST['search']['value'] ?? ($_POST['search'] ?? '')));
        // phpcs:enable WordPress.Security.NonceVerification.Missing

        $total = (int) $db->getVar("SELECT COUNT(*) FROM %i", $tbl);

        $where = '';
        $searchArgs = [];
        if (!empty($search)) {
            $like = '%' . $db->escLike($search) . '%';
            $where = "WHERE h.option_key LIKE %s OR h.old_value LIKE %s OR h.new_value LIKE %s";
            $searchArgs = [$like, $like, $like];
        }

        $filtered = !empty($search)
            ? (int) $db->getVar("SELECT COUNT(*) FROM %i h {$where}", $tbl, ...$searchArgs)
            : $total;

        $queryArgs = array_merge([$tbl], $searchArgs, [$start, $length]);
        $rows = $db->getResults(
            "SELECT h.*, u.display_name AS changed_by_name
             FROM %i h
             LEFT JOIN {$wpdb->users} u ON h.changed_by = u.ID
             {$where}
             ORDER BY h.changed_at DESC
             LIMIT %d, %d",
            ...$queryArgs
        );

        return [
            'draw'            => $draw,
            'recordsTotal'    => $total,
            'recordsFiltered' => $filtered,
            'data'            => $rows,
        ];
    }

    /**
     * Revert an option to a previous value from history.
     */
    private function revertOption(int $historyId): array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('options_history');
        $row = $db->getRow("SELECT * FROM %i WHERE id = %d", $tbl, $historyId);

        if (!$row) {
            return ['error' => __('History entry not found', 'vollstart-repair-desk')];
        }

        $value = $row['old_value'];
        $decoded = json_decode($value, true);
        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
            $value = $decoded;
        }

        $this->MAIN->getOptions()->changeOption(['key' => $row['option_key'], 'value' => $value]);
        return ['reverted' => true, 'option_key' => $row['option_key']];
    }

    // --- Dashboard Widget ---

    public function registerDashboardWidget(): void {
        wp_add_dashboard_widget(
            'vollstart_rd_dashboard',
            __('Repair Desk', 'vollstart-repair-desk'),
            [$this, 'renderDashboardWidget']
        );
    }

    public function renderDashboardWidget(): void {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('repairs');
        $counts = $this->MAIN->getRepairs()->countByStatus();
        $total = array_sum($counts);
        $defs = $this->MAIN->getRepairs()->getAllStatuses();

        // Open repairs (not final)
        $open = 0;
        foreach ($counts as $slug => $cnt) {
            if (!$this->MAIN->getRepairs()->isFinalStatus($slug)) {
                $open += $cnt;
            }
        }

        // Recent 5 repairs
        $recent = $db->getResults("SELECT repair_number, status, created_at FROM %i ORDER BY created_at DESC LIMIT 5", $tbl);
        ?>
        <div style="margin:-12px -12px -5px">
            <div style="display:flex;gap:12px;padding:12px;border-bottom:1px solid #f1f5f9">
                <div style="flex:1;text-align:center">
                    <div style="font-size:28px;font-weight:700;color:#f97316"><?php echo esc_html($open); ?></div>
                    <div style="font-size:12px;color:#64748b"><?php esc_html_e('Open', 'vollstart-repair-desk'); ?></div>
                </div>
                <div style="flex:1;text-align:center">
                    <div style="font-size:28px;font-weight:700;color:#1e293b"><?php echo esc_html($total); ?></div>
                    <div style="font-size:12px;color:#64748b"><?php esc_html_e('Total', 'vollstart-repair-desk'); ?></div>
                </div>
            </div>
            <?php if ($counts): ?>
            <div style="display:flex;gap:6px;padding:10px 12px;flex-wrap:wrap">
                <?php foreach ($counts as $slug => $cnt):
                    if ($cnt <= 0) continue;
                    $color = isset($defs[$slug]) ? $defs[$slug]['color'] : '#94a3b8';
                    $label = isset($defs[$slug]) ? $defs[$slug]['label'] : $slug;
                ?>
                <span style="font-size:11px;padding:2px 8px;border-radius:999px;background:<?php echo esc_attr($color); ?>15;color:<?php echo esc_attr($color); ?>;border:1px solid <?php echo esc_attr($color); ?>30"><?php echo esc_html($label); ?> <?php echo esc_html($cnt); ?></span>
                <?php endforeach; ?>
            </div>
            <?php endif; ?>
            <?php if ($recent): ?>
            <table style="width:100%;border-collapse:collapse">
                <?php foreach ($recent as $r):
                    $color = isset($defs[$r['status']]) ? $defs[$r['status']]['color'] : '#94a3b8';
                    $label = isset($defs[$r['status']]) ? $defs[$r['status']]['label'] : $r['status'];
                ?>
                <tr style="border-top:1px solid #f1f5f9">
                    <td style="padding:6px 12px;font-size:13px;font-weight:600"><?php echo esc_html($r['repair_number']); ?></td>
                    <td style="padding:6px 4px"><span style="font-size:11px;padding:2px 8px;border-radius:999px;background:<?php echo esc_attr($color); ?>15;color:<?php echo esc_attr($color); ?>"><?php echo esc_html($label); ?></span></td>
                    <td style="padding:6px 12px;font-size:12px;color:#94a3b8;text-align:right"><?php echo esc_html(wp_date('M j', strtotime($r['created_at']))); ?></td>
                </tr>
                <?php endforeach; ?>
            </table>
            <?php endif; ?>
            <div style="padding:10px 12px;border-top:1px solid #f1f5f9;text-align:center">
                <a href="<?php echo esc_url(admin_url('admin.php?page=vollstart-repair-desk')); ?>" style="font-size:13px;color:#f97316;text-decoration:none;font-weight:600"><?php esc_html_e('Open Repair Desk', 'vollstart-repair-desk'); ?> &rarr;</a>
            </div>
        </div>
        <?php
    }
}
