<?php

namespace GriffinForms\Includes\Api;

use GriffinForms\Config;
use GriffinForms\Includes\Pipelines\Accounts\ActionStateStore;
use GriffinForms\Includes\Pipelines\Accounts\AdminActionService;
use GriffinForms\Includes\Packages\PackageAdmission;
use GriffinForms\Includes\Packages\PackageEntitlement;
use GriffinForms\Includes\Packages\PackageStateResolver;
use GriffinForms\Log;
use GriffinForms\Settings;
use GriffinForms\Includes\Security\Capabilities;
use GriffinForms\Admin\Sql\App as AdminAppSql;
use GriffinForms\Admin\Sql\Format as AdminSqlFormat;
use GriffinForms\Admin\Sql\Submissions as AdminSubmissionsSql;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * Companion-app-ready submissions REST surface.
 */
class SubmissionsRest
{
    public const NAMESPACE = 'griffinforms/v1';
    private const DEVICE_OPTION_PREFIX = 'api_device_registry_user_';
    private const DEVICE_LIMIT_PER_USER = 5;
    private const DEVICE_LIMIT_TOTAL = 50;

    private Config $config;
    private Settings $settings;
    private Log $log;
    private PackageAdmission $package_admission;
    private PackageEntitlement $package_entitlement;
    private PackageStateResolver $package_state_resolver;
    private ?string $request_id = null;
    private array $audit_once = [];
    private array $authorization_once = [];
    private ?int $device_total_active_cache = null;

    public function __construct()
    {
        $this->config = Config::getInstance();
        $this->settings = Settings::getInstance();
        $this->log = Log::getInstance();
        $this->package_admission = PackageAdmission::getInstance();
        $this->package_entitlement = PackageEntitlement::getInstance();
        $this->package_state_resolver = PackageStateResolver::getInstance();
        add_action('rest_api_init', [$this, 'registerRoutes']);
    }

    public function registerRoutes(): void
    {
        register_rest_route(self::NAMESPACE, '/folders/tree', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getFoldersTree'],
            'permission_callback' => [$this, 'permissionViewSubmissions'],
        ]);

        register_rest_route(self::NAMESPACE, '/folders/(?P<folder_id>\d+)/icon/binary', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getFolderIconBinary'],
            'permission_callback' => [$this, 'permissionViewSubmissions'],
        ]);

        register_rest_route(self::NAMESPACE, '/folders/rename', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'renameFolder'],
            'permission_callback' => [$this, 'permissionRenameFolder'],
        ]);

        register_rest_route(self::NAMESPACE, '/folders/create', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'createFolder'],
            'permission_callback' => [$this, 'permissionCreateFolder'],
        ]);

        register_rest_route(self::NAMESPACE, '/forms', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getForms'],
            'permission_callback' => [$this, 'permissionViewFormsCatalog'],
        ]);

        register_rest_route(self::NAMESPACE, '/forms/(?P<form_id>\d+)/structure', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getFormStructure'],
            'permission_callback' => [$this, 'permissionViewFormsCatalog'],
        ]);

        register_rest_route(self::NAMESPACE, '/devices', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getDevices'],
            'permission_callback' => [$this, 'permissionViewDevices'],
        ]);

        register_rest_route(self::NAMESPACE, '/devices/register', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'registerDevice'],
            'permission_callback' => [$this, 'permissionManageDevices'],
        ]);

        register_rest_route(self::NAMESPACE, '/devices/unregister', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'unregisterDevice'],
            'permission_callback' => [$this, 'permissionManageDevices'],
        ]);

        register_rest_route(self::NAMESPACE, '/devices/(?P<device_id>[A-Za-z0-9._:-]+)', [
            'methods' => WP_REST_Server::EDITABLE,
            'callback' => [$this, 'patchDevice'],
            'permission_callback' => [$this, 'permissionManageDevices'],
        ]);

        register_rest_route(self::NAMESPACE, '/forms/move-folder', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'moveFormFolder'],
            'permission_callback' => [$this, 'permissionMoveFormFolder'],
        ]);

        register_rest_route(self::NAMESPACE, '/forms/rename', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'renameForm'],
            'permission_callback' => [$this, 'permissionRenameForm'],
        ]);

        register_rest_route(self::NAMESPACE, '/submissions', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getSubmissions'],
            'permission_callback' => [$this, 'permissionViewSubmissions'],
        ]);

        register_rest_route(self::NAMESPACE, '/submissions/(?P<submission_id>\d+)', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getSubmissionSummary'],
            'permission_callback' => [$this, 'permissionViewSubmissions'],
        ]);

        register_rest_route(self::NAMESPACE, '/submissions/(?P<submission_id>\d+)/detail', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getSubmissionDetail'],
            'permission_callback' => [$this, 'permissionViewSubmissionDetail'],
        ]);

        register_rest_route(self::NAMESPACE, '/logs/recent', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getRecentLogs'],
            'permission_callback' => [$this, 'permissionViewSubmissionDetail'],
        ]);

        register_rest_route(self::NAMESPACE, '/submissions/(?P<submission_id>\d+)/pdf', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getSubmissionPdf'],
            'permission_callback' => [$this, 'permissionExportPdf'],
        ]);

        register_rest_route(self::NAMESPACE, '/submissions/(?P<submission_id>\d+)/attachments', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getSubmissionAttachments'],
            'permission_callback' => [$this, 'permissionViewSubmissionDetail'],
        ]);

        register_rest_route(self::NAMESPACE, '/submissions/(?P<submission_id>\d+)/attachments/(?P<attachment_id>\d+)/download', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'downloadSubmissionAttachment'],
            'permission_callback' => [$this, 'permissionViewSubmissionDetail'],
        ]);

        register_rest_route(self::NAMESPACE, '/files/(?P<file_id>\d+)/binary', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getFileBinary'],
            'permission_callback' => [$this, 'permissionViewSubmissionDetail'],
        ]);

        register_rest_route(self::NAMESPACE, '/files/(?P<file_id>\d+)/thumbnail', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getFileThumbnail'],
            'permission_callback' => [$this, 'permissionViewSubmissionDetail'],
        ]);

        register_rest_route(self::NAMESPACE, '/submissions/(?P<submission_id>\d+)/share-link', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'createShareLink'],
            'permission_callback' => [$this, 'permissionShareLink'],
        ]);

        register_rest_route(self::NAMESPACE, '/submissions/bulk-delete', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'bulkDeleteSubmissions'],
            'permission_callback' => [$this, 'permissionBulkDelete'],
        ]);

        register_rest_route(self::NAMESPACE, '/submissions/read-state', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'setSubmissionReadState'],
            'permission_callback' => [$this, 'permissionManageSubmissionReadState'],
        ]);

        register_rest_route(self::NAMESPACE, '/packages/pro/status', [
            'methods' => WP_REST_Server::READABLE,
            'callback' => [$this, 'getProPackageStatus'],
            'permission_callback' => [$this, 'permissionViewProPackageStatus'],
        ]);

        register_rest_route(self::NAMESPACE, '/packages/pro/test-state', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'setProPackageTestState'],
            'permission_callback' => [$this, 'permissionViewProPackageStatus'],
        ]);

        register_rest_route(self::NAMESPACE, '/packages/pro/test-modal', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'triggerProActivationSuccessModal'],
            'permission_callback' => [$this, 'permissionViewProPackageStatus'],
        ]);

        register_rest_route(self::NAMESPACE, '/packages/pro/test-modal/clear', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'clearProActivationSuccessModal'],
            'permission_callback' => [$this, 'permissionViewProPackageStatus'],
        ]);
    }

    public function permissionViewSubmissions(WP_REST_Request $request): bool|WP_Error
    {
        return $this->authorize(Capabilities::CAP_VIEW_SUBMISSIONS, $request);
    }

    /**
     * Gutenberg editor needs form list/structure without companion device binding.
     * Keep capability/rate-limit checks, but skip active-device enforcement here.
     */
    public function permissionViewFormsCatalog(WP_REST_Request $request): bool|WP_Error
    {
        return $this->authorize(Capabilities::CAP_VIEW_SUBMISSIONS, $request, false);
    }

    public function permissionViewSubmissionDetail(WP_REST_Request $request): bool|WP_Error
    {
        $token = sanitize_text_field((string) $request->get_param('share_token'));
        if ($token !== '' && $this->isValidShareToken($token, (int) $request['submission_id'])) {
            return true;
        }
        return $this->authorize(Capabilities::CAP_VIEW_SUBMISSION_DETAIL, $request);
    }

    public function permissionExportPdf(WP_REST_Request $request): bool|WP_Error
    {
        return $this->authorize(Capabilities::CAP_EXPORT_SUBMISSION_PDF, $request);
    }

    public function permissionShareLink(WP_REST_Request $request): bool|WP_Error
    {
        return $this->authorize(Capabilities::CAP_SHARE_SUBMISSION_LINK, $request);
    }

    public function permissionBulkDelete(WP_REST_Request $request): bool|WP_Error
    {
        return $this->authorize(Capabilities::CAP_BULK_DELETE_SUBMISSIONS, $request);
    }

    public function permissionMoveFormFolder(WP_REST_Request $request): bool|WP_Error
    {
        return $this->authorize(Capabilities::CAP_MOVE_FORM_FOLDER, $request);
    }

    public function permissionRenameFolder(WP_REST_Request $request): bool|WP_Error
    {
        return $this->authorize(Capabilities::CAP_RENAME_FOLDER, $request);
    }

    public function permissionCreateFolder(WP_REST_Request $request): bool|WP_Error
    {
        return $this->authorize(Capabilities::CAP_CREATE_FOLDER, $request);
    }

    public function permissionRenameForm(WP_REST_Request $request): bool|WP_Error
    {
        return $this->authorize(Capabilities::CAP_RENAME_FORM, $request);
    }

    public function permissionManageSubmissionReadState(WP_REST_Request $request): bool|WP_Error
    {
        return $this->authorize(Capabilities::CAP_MANAGE_SUBMISSION_READ_STATE, $request);
    }

    public function permissionViewDevices(): bool|WP_Error
    {
        return $this->authorize(Capabilities::CAP_VIEW_DEVICES, null, false);
    }

    public function permissionManageDevices(): bool|WP_Error
    {
        return $this->authorize(Capabilities::CAP_MANAGE_DEVICES, null, false);
    }

    public function permissionViewProPackageStatus(): bool|WP_Error
    {
        if (!Capabilities::isApiAvailable()) {
            return new WP_Error(
                'griffinforms_api_disabled',
                __('API temporarily disabled by administrator.', 'griffinforms-form-builder'),
                ['status' => 503]
            );
        }

        if (!is_user_logged_in()) {
            return new WP_Error(
                'griffinforms_api_unauthenticated',
                __('Authentication required.', 'griffinforms-form-builder'),
                ['status' => 401]
            );
        }

        if (!current_user_can('manage_options')) {
            return new WP_Error(
                'griffinforms_api_forbidden',
                __('You are not allowed to perform this action.', 'griffinforms-form-builder'),
                ['status' => 403]
            );
        }

        return true;
    }

    private function authorize(string $capability, ?WP_REST_Request $request = null, bool $enforce_device = true): bool|WP_Error
    {
        $authorization_key = $capability . '|' . $this->requestMethod() . '|' . $this->requestUriPath();
        if (isset($this->authorization_once[$authorization_key])) {
            return true;
        }

        if (!Capabilities::isApiAvailable()) {
            $this->logGuardrailAuditOnce('api_access_denied', 'denied', [
                'request_id' => $this->requestId(),
                'target_type' => 'endpoint',
                'meta' => $this->buildEndpointMeta([
                    'status' => 503,
                    'code' => 'griffinforms_api_disabled',
                    'capability' => $capability,
                ]),
            ], 'api_access_denied|503|' . $capability);
            return new WP_Error(
                'griffinforms_api_disabled',
                __('API temporarily disabled by administrator.', 'griffinforms-form-builder'),
                ['status' => 503]
            );
        }

        if (!is_user_logged_in()) {
            $this->logGuardrailAuditOnce('api_access_denied', 'denied', [
                'request_id' => $this->requestId(),
                'target_type' => 'endpoint',
                'meta' => $this->buildEndpointMeta([
                    'status' => 401,
                    'code' => 'griffinforms_api_unauthenticated',
                    'capability' => $capability,
                ]),
            ], 'api_access_denied|401|' . $capability);
            return new WP_Error(
                'griffinforms_api_unauthenticated',
                __('Authentication required.', 'griffinforms-form-builder'),
                ['status' => 401]
            );
        }

        if (!current_user_can($capability)) {
            $this->logGuardrailAuditOnce('api_access_denied', 'denied', [
                'request_id' => $this->requestId(),
                'target_type' => 'endpoint',
                'meta' => $this->buildEndpointMeta([
                    'status' => 403,
                    'code' => 'griffinforms_api_forbidden',
                    'capability' => $capability,
                ]),
            ], 'api_access_denied|403|' . $capability);
            return new WP_Error(
                'griffinforms_api_forbidden',
                __('You are not allowed to perform this action.', 'griffinforms-form-builder'),
                ['status' => 403]
            );
        }

        $rate = $this->enforceRateLimit();
        if ($rate instanceof WP_Error) {
            return $rate;
        }

        if ($enforce_device) {
            $device = $this->enforceActiveDevice($request, $capability);
            if ($device instanceof WP_Error) {
                return $device;
            }
        }

        $this->authorization_once[$authorization_key] = true;
        return true;
    }

    private function ensurePackageAvailable(string $package_id, bool $require_entitlement = false): bool|WP_Error
    {
        if ($this->package_admission->canAdmitPackage($package_id, $require_entitlement)) {
            return true;
        }

        $blocking_reason_key = (string) $this->package_admission->getBlockingReasonKey($package_id, $require_entitlement);
        $package_state = $this->package_admission->getPackageState($package_id);
        $status = 409;
        $code = 'griffinforms_package_unavailable';
        $message = __('This package is currently unavailable.', 'griffinforms-form-builder');

        if ($blocking_reason_key === 'package_not_installed') {
            $status = 404;
            $code = 'griffinforms_package_not_installed';
            $message = __('The required package is not installed.', 'griffinforms-form-builder');
        } elseif ($blocking_reason_key === 'package_inactive') {
            $status = 409;
            $code = 'griffinforms_package_inactive';
            $message = __('The required package is installed but inactive.', 'griffinforms-form-builder');
        } elseif ($blocking_reason_key === 'package_unapproved') {
            $status = 403;
            $code = 'griffinforms_package_unapproved';
            $message = __('The required package is not approved for GriffinForms participation.', 'griffinforms-form-builder');
        } elseif (in_array($blocking_reason_key, ['incompatible_core_too_old', 'incompatible_package_too_old', 'missing_required_core'], true)) {
            $status = 409;
            $code = 'griffinforms_package_incompatible';
            $message = __('The required package is incompatible with this GriffinForms installation.', 'griffinforms-form-builder');
        } elseif ($require_entitlement) {
            $status = 402;
            $code = 'griffinforms_package_not_entitled';
            $message = __('The required package is not currently entitled for this action.', 'griffinforms-form-builder');
        }

        $this->log->add(
            'warning',
            'package',
            0,
            get_current_user_id(),
            sprintf(
                'Package [%s] request blocked because %s.',
                $package_id,
                $blocking_reason_key !== '' ? $blocking_reason_key : 'package admission failed'
            ),
            'package_provider'
        );

        return new WP_Error($code, $message, [
            'status' => $status,
            'package_id' => $package_id,
            'blocking_reason_key' => $blocking_reason_key,
            'package_state' => [
                'approval_status' => (string) ($package_state['approval_status'] ?? ''),
                'compatibility_state' => (string) ($package_state['compatibility_state'] ?? ''),
                'entitlement_state' => (string) ($package_state['entitlement_state'] ?? ''),
            ],
        ]);
    }

    private function enforceActiveDevice(?WP_REST_Request $request, string $capability): bool|WP_Error
    {
        $user_id = get_current_user_id();
        if ($user_id <= 0) {
            return true;
        }

        $device_id = '';
        if ($request instanceof WP_REST_Request) {
            $resolved_device = $this->resolveRequestDeviceId($request);
            if ($resolved_device instanceof WP_Error) {
                $status = 400;
                $error_data = $resolved_device->get_error_data();
                if (is_array($error_data) && isset($error_data['status'])) {
                    $status = (int) $error_data['status'];
                }
                $this->logGuardrailAuditOnce('api_access_denied', 'denied', [
                    'request_id' => $this->requestId(),
                    'target_type' => 'endpoint',
                    'meta' => $this->buildEndpointMeta([
                        'status' => $status,
                        'code' => $resolved_device->get_error_code(),
                        'capability' => $capability,
                    ]),
                ], 'api_access_denied|' . $resolved_device->get_error_code() . '|' . $capability);
                return $resolved_device;
            }
            $device_id = $resolved_device;
        }

        if ($device_id === '') {
            $this->logGuardrailAuditOnce('api_access_denied', 'denied', [
                'request_id' => $this->requestId(),
                'target_type' => 'endpoint',
                'meta' => $this->buildEndpointMeta([
                    'status' => 401,
                    'code' => 'griffinforms_device_required',
                    'capability' => $capability,
                ]),
            ], 'api_access_denied|device_required|' . $capability);
            return new WP_Error(
                'griffinforms_device_required',
                __('A registered device is required.', 'griffinforms-form-builder'),
                ['status' => 401]
            );
        }

        $registry = $this->getDeviceRegistryForUser($user_id);
        $index = $this->findDeviceRegistryIndex($registry, $device_id);
        if ($index < 0) {
            $this->logGuardrailAuditOnce('api_access_denied', 'denied', [
                'request_id' => $this->requestId(),
                'target_type' => 'endpoint',
                'meta' => $this->buildEndpointMeta([
                    'status' => 403,
                    'code' => 'griffinforms_device_not_authorized',
                    'capability' => $capability,
                    'device_id' => $device_id,
                ]),
            ], 'api_access_denied|device_not_authorized|' . $capability . '|' . $device_id);
            return new WP_Error(
                'griffinforms_device_not_authorized',
                __('This device is not authorized. Remove and re-add the account to continue.', 'griffinforms-form-builder'),
                ['status' => 403]
            );
        }

        $record = $this->normalizeDeviceRecord($registry[$index]);
        $token_state = (string) ($record['token_state'] ?? '');
        $deleted_at = (string) ($record['deleted_at'] ?? '');
        if ($token_state !== 'active' || $deleted_at !== '') {
            $this->logGuardrailAuditOnce('api_access_denied', 'denied', [
                'request_id' => $this->requestId(),
                'target_type' => 'endpoint',
                'meta' => $this->buildEndpointMeta([
                    'status' => 403,
                    'code' => 'griffinforms_device_not_authorized',
                    'capability' => $capability,
                    'device_id' => $device_id,
                    'token_state' => $token_state,
                    'deleted_at' => $deleted_at !== '' ? $deleted_at : null,
                ]),
            ], 'api_access_denied|device_not_active|' . $capability . '|' . $device_id);
            return new WP_Error(
                'griffinforms_device_not_authorized',
                __('This device is no longer authorized. Remove and re-add the account to continue.', 'griffinforms-form-builder'),
                ['status' => 403]
            );
        }

        return true;
    }

    private function resolveRequestDeviceId(WP_REST_Request $request): string|WP_Error
    {
        $header_raw = trim((string) $request->get_header('x-griffinforms-device-id'));
        if ($header_raw !== '') {
            $header = $this->sanitizeDeviceId($header_raw);
            if ($header === '') {
                return new WP_Error(
                    'griffinforms_invalid_device_id',
                    __('Invalid X-GriffinForms-Device-ID header.', 'griffinforms-form-builder'),
                    ['status' => 400]
                );
            }
            return $header;
        }

        $param_raw = trim((string) $request->get_param('device_id'));
        if ($param_raw !== '') {
            $allow_param_fallback = (bool) apply_filters('griffinforms/api_allow_device_id_param_fallback', false, $request);
            if (!$allow_param_fallback) {
                return new WP_Error(
                    'griffinforms_device_header_required',
                    __('X-GriffinForms-Device-ID header is required.', 'griffinforms-form-builder'),
                    ['status' => 400]
                );
            }

            $param = $this->sanitizeDeviceId($param_raw);
            if ($param === '') {
                return new WP_Error(
                    'griffinforms_invalid_device_id',
                    __('Invalid device_id parameter.', 'griffinforms-form-builder'),
                    ['status' => 400]
                );
            }
            return $param;
        }

        return '';
    }

    private function enforceRateLimit(): bool|WP_Error
    {
        $user_id = get_current_user_id();
        if ($user_id <= 0) {
            return true;
        }

        $now = time();
        $minute_key = 'gf_api_rl_m_' . md5((string) $user_id);
        $burst_key = 'gf_api_rl_b_' . md5((string) $user_id);

        $minute_bucket = get_transient($minute_key);
        $burst_bucket = get_transient($burst_key);

        if (!is_array($minute_bucket)) {
            $minute_bucket = [];
        }
        if (!is_array($burst_bucket)) {
            $burst_bucket = [];
        }

        $minute_bucket = array_values(array_filter($minute_bucket, static fn($ts) => ($now - (int) $ts) < 60));
        $burst_bucket = array_values(array_filter($burst_bucket, static fn($ts) => ($now - (int) $ts) < 10));

        if (count($minute_bucket) >= 120 || count($burst_bucket) >= 20) {
            $this->logGuardrailAuditOnce('api_rate_limited', 'rate_limited', [
                'request_id' => $this->requestId(),
                'target_type' => 'endpoint',
                'meta' => $this->buildEndpointMeta([
                    'status' => 429,
                    'code' => 'griffinforms_api_rate_limited',
                    'minute_count' => count($minute_bucket),
                    'burst_count' => count($burst_bucket),
                ]),
            ], 'api_rate_limited|429|user:' . $user_id);
            return new WP_Error(
                'griffinforms_api_rate_limited',
                __('Too many requests. Please retry shortly.', 'griffinforms-form-builder'),
                ['status' => 429]
            );
        }

        $minute_bucket[] = $now;
        $burst_bucket[] = $now;

        set_transient($minute_key, $minute_bucket, 60);
        set_transient($burst_key, $burst_bucket, 10);

        return true;
    }

    public function getFoldersTree(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $sort_by = sanitize_key((string) $request->get_param('sort_by'));
        $sort_dir = strtolower(sanitize_text_field((string) $request->get_param('sort_dir')));
        if ($sort_by === '') {
            $sort_by = 'form_count';
        }
        if ($sort_dir === '') {
            $sort_dir = 'desc';
        }

        if (!in_array($sort_by, ['form_count', 'last_edit_date'], true)) {
            return new WP_Error('griffinforms_invalid_sort_by', __('Invalid sort_by parameter.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        if (!in_array($sort_dir, ['asc', 'desc'], true)) {
            return new WP_Error('griffinforms_invalid_sort_dir', __('Invalid sort_dir parameter.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $folder_table = $this->config->getTable('folder');
        $form_table = $this->config->getTable('form');

        $rows = $wpdb->get_results(
            "SELECT f.id, f.name, f.heading, f.description, COUNT(fr.id) AS form_count, MAX(fr.last_edited) AS last_edit_date
             FROM {$folder_table} f
             LEFT JOIN {$form_table} fr ON fr.folder = f.id
             GROUP BY f.id
             ORDER BY " . ($sort_by === 'last_edit_date' ? 'last_edit_date' : 'form_count') . ' ' . ($sort_dir === 'asc' ? 'ASC' : 'DESC') . ', f.name ASC',
            ARRAY_A
        );

        $forms = $wpdb->get_results(
            "SELECT id, name, heading, description, folder, last_edited FROM {$form_table} ORDER BY name ASC",
            ARRAY_A
        );

        $forms_by_folder = [];
        foreach ((array) $forms as $form) {
            $folder_id = (int) ($form['folder'] ?? 0);
            $forms_by_folder[$folder_id][] = [
                'id' => (int) $form['id'],
                'name' => sanitize_text_field((string) $form['name']),
                'heading' => sanitize_text_field((string) ($form['heading'] ?? '')),
                'description' => sanitize_textarea_field((string) ($form['description'] ?? '')),
                'last_edit_date' => (string) ($form['last_edited'] ?? ''),
            ];
        }

        $payload = [];
        foreach ((array) $rows as $row) {
            $folder_id = (int) $row['id'];
            $payload[] = [
                'id' => $folder_id,
                'name' => sanitize_text_field((string) $row['name']),
                'heading' => sanitize_text_field((string) ($row['heading'] ?? '')),
                'description' => sanitize_textarea_field((string) ($row['description'] ?? '')),
                'form_count' => (int) $row['form_count'],
                'last_edit_date' => (string) ($row['last_edit_date'] ?? ''),
                'forms' => $forms_by_folder[$folder_id] ?? [],
            ];
        }

        if (!empty($forms_by_folder[0])) {
            $payload[] = [
                'id' => 0,
                'name' => __('Uncategorized', 'griffinforms-form-builder'),
                'heading' => '',
                'description' => '',
                'form_count' => count($forms_by_folder[0]),
                'last_edit_date' => '',
                'forms' => $forms_by_folder[0],
            ];
        }

        Capabilities::logAudit('folders_tree_viewed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'folder_collection',
            'meta' => [
                'sort_by' => $sort_by,
                'sort_dir' => $sort_dir,
                'result_count' => count($payload),
            ],
        ]));

        return $this->success($payload, [
            'sort_by' => $sort_by,
            'sort_dir' => $sort_dir,
            'request_id' => $this->requestId(),
        ]);
    }

    public function getProPackageStatus(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $availability = $this->ensurePackageAvailable('pro', false);
        if ($availability instanceof WP_Error) {
            return $availability;
        }

        $state = $this->package_admission->getPackageState('pro');

        return new WP_REST_Response([
            'package' => [
                'package_id' => 'pro',
                'label' => (string) ($state['package_label_internal'] ?? 'Pro'),
                'version' => $state['package_version'] ?? null,
                'is_present' => !empty($state['is_present']),
                'is_active' => !empty($state['is_active']),
                'approval_status' => (string) ($state['approval_status'] ?? ''),
                'compatibility_state' => (string) ($state['compatibility_state'] ?? ''),
                'entitlement_state' => (string) ($state['entitlement_state'] ?? ''),
                'verification_state' => (string) ($state['verification_state'] ?? ''),
                'license_key_present' => !empty($state['license_key_present']),
                'last_verified_at' => (string) ($state['last_verified_at'] ?? ''),
                'last_successful_verification_at' => (string) ($state['last_successful_verification_at'] ?? ''),
                'last_failed_verification_at' => (string) ($state['last_failed_verification_at'] ?? ''),
                'next_retry_at' => (string) ($state['next_retry_at'] ?? ''),
                'grace_started_at' => (string) ($state['grace_started_at'] ?? ''),
                'grace_expires_at' => (string) ($state['grace_expires_at'] ?? ''),
                'license_expires_at' => (string) ($state['license_expires_at'] ?? ''),
                'verification_failure_count' => max(0, absint($state['verification_failure_count'] ?? 0)),
                'integrity_state' => (string) ($state['integrity_state'] ?? ''),
                'update_state' => (string) ($state['update_state'] ?? ''),
                'recommended_user_action' => (string) ($state['recommended_user_action'] ?? ''),
                'warning_layer_key' => (string) ($state['warning_layer_key'] ?? ''),
                'blocking_reason_key' => (string) ($state['blocking_reason_key'] ?? ''),
            ],
        ], 200);
    }

    public function setProPackageTestState(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $package_state = $this->package_admission->getPackageState('pro');
        if (empty($package_state['is_present']) || empty($package_state['is_active'])) {
            return new WP_Error(
                'griffinforms_package_test_state_unavailable',
                __('Pro test-state mutations require the Pro package to be installed and active.', 'griffinforms-form-builder'),
                ['status' => 409]
            );
        }

        $target_state = sanitize_key((string) $request->get_param('state'));
        if (!in_array($target_state, ['clear', 'key_entered', 'verified_active', 'verified_inactive_or_expired', 'unverified_connection_issue', 'grace_period'], true)) {
            return new WP_Error(
                'griffinforms_invalid_package_test_state',
                __('Invalid Pro test state.', 'griffinforms-form-builder'),
                ['status' => 400]
            );
        }

        $result = $this->applyProPackageTestState($target_state, $request);
        if ($result instanceof WP_Error) {
            return $result;
        }

        $state = $this->package_state_resolver->refreshState();
        $pro_state = is_array($state['packages']['pro'] ?? null) ? $state['packages']['pro'] : [];

        return new WP_REST_Response([
            'message' => __('Pro test state updated.', 'griffinforms-form-builder'),
            'package' => $pro_state,
        ], 200);
    }

    public function triggerProActivationSuccessModal(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $package_state = $this->package_admission->getPackageState('pro');
        if (empty($package_state['is_present']) || empty($package_state['is_active'])) {
            return new WP_Error(
                'griffinforms_package_test_modal_unavailable',
                __('Pro modal testing requires the Pro package to be installed and active.', 'griffinforms-form-builder'),
                ['status' => 409]
            );
        }

        if (($package_state['compatibility_state'] ?? '') !== 'compatible') {
            return new WP_Error(
                'griffinforms_package_test_modal_incompatible',
                __('Pro modal testing requires a compatible Core and Pro package state.', 'griffinforms-form-builder'),
                ['status' => 409]
            );
        }

        \GriffinForms\Admin\Menu::storeProActivationSuccessRecord([
            'source' => sanitize_key((string) $request->get_param('source')) ?: 'owner_test_route',
            'updated_at' => gmdate('c'),
            'updated_by' => get_current_user_id(),
        ]);

        $pending_record = get_user_meta(get_current_user_id(), \GriffinForms\Admin\Menu::PRO_ACTIVATION_SUCCESS_META_KEY, true);

        $this->log->add(
            'info',
            'license',
            0,
            get_current_user_id(),
            'Package [pro] activation success modal queued through Core test route.',
            'package_diagnostics'
        );

        return new WP_REST_Response([
            'message' => __('Pro activation success modal queued for the current user.', 'griffinforms-form-builder'),
            'pending' => !empty($pending_record),
            'next_page_url' => admin_url('admin.php?page=' . $this->config::PREFIX . '-forms'),
            'record' => is_array($pending_record) ? $pending_record : [],
        ], 200);
    }

    public function clearProActivationSuccessModal(WP_REST_Request $request): WP_REST_Response
    {
        \GriffinForms\Admin\Menu::clearProActivationSuccessRecord(get_current_user_id());

        $pending_record = get_user_meta(get_current_user_id(), \GriffinForms\Admin\Menu::PRO_ACTIVATION_SUCCESS_META_KEY, true);

        $this->log->add(
            'info',
            'license',
            0,
            get_current_user_id(),
            'Package [pro] activation success modal cleared through Core test route.',
            'package_diagnostics'
        );

        return new WP_REST_Response([
            'message' => __('Pro activation success modal cleared for the current user.', 'griffinforms-form-builder'),
            'pending' => !empty($pending_record),
        ], 200);
    }

    private function applyProPackageTestState(string $target_state, WP_REST_Request $request): bool|WP_Error
    {
        $result = false;

        if ($target_state === 'clear') {
            $result = $this->package_entitlement->clearPackageRecord('pro');
            $this->log->add('info', 'license', 0, get_current_user_id(), 'Package [pro] test entitlement state cleared.', 'license_state');
        } elseif ($target_state === 'key_entered') {
            $result = $this->package_entitlement->markKeyEntered('pro');
            $this->log->add('info', 'license', 0, get_current_user_id(), 'Package [pro] test state set to key_entered.', 'license_activation');
        } elseif ($target_state === 'verified_active') {
            $license_expires_at = $this->sanitizeOptionalIsoLikeTimestamp((string) $request->get_param('license_expires_at'));
            $result = $this->package_entitlement->markVerifiedActive('pro', [
                'license_expires_at' => $license_expires_at,
            ]);
            $this->log->add('success', 'license', 0, get_current_user_id(), 'Package [pro] test state set to verified_active.', 'license_activation');
        } elseif ($target_state === 'verified_inactive_or_expired') {
            $license_expires_at = $this->sanitizeOptionalIsoLikeTimestamp((string) $request->get_param('license_expires_at'));
            $result = $this->package_entitlement->markVerifiedInactiveOrExpired('pro', [
                'license_expires_at' => $license_expires_at,
                'license_key_present' => true,
            ]);
            $this->log->add('warning', 'license', 0, get_current_user_id(), 'Package [pro] test state set to verified_inactive_or_expired.', 'license_state');
        } elseif ($target_state === 'unverified_connection_issue') {
            $next_retry_at = $this->sanitizeOptionalIsoLikeTimestamp((string) $request->get_param('next_retry_at'));
            $result = $this->package_entitlement->markConnectionIssue('pro', [
                'license_key_present' => true,
                'next_retry_at' => $next_retry_at,
            ]);
            $this->log->add('warning', 'license', 0, get_current_user_id(), 'Package [pro] test state set to unverified_connection_issue.', 'license_verification');
        } elseif ($target_state === 'grace_period') {
            $next_retry_at = $this->sanitizeOptionalIsoLikeTimestamp((string) $request->get_param('next_retry_at'));
            $grace_expires_at = $this->sanitizeOptionalIsoLikeTimestamp((string) $request->get_param('grace_expires_at'));
            if ($grace_expires_at === '') {
                $grace_expires_at = gmdate('c', time() + DAY_IN_SECONDS);
            }

            $result = $this->package_entitlement->markConnectionIssue('pro', [
                'license_key_present' => true,
                'next_retry_at' => $next_retry_at,
                'grace_expires_at' => $grace_expires_at,
            ]);
            $this->log->add('warning', 'license', 0, get_current_user_id(), 'Package [pro] test state set to grace_period.', 'license_grace');
        }

        if (!$result) {
            return new WP_Error(
                'griffinforms_package_test_state_failed',
                __('Unable to update the Pro test state.', 'griffinforms-form-builder'),
                ['status' => 500]
            );
        }

        return true;
    }

    private function sanitizeOptionalIsoLikeTimestamp(string $value): string
    {
        $value = trim($value);
        if ($value === '') {
            return '';
        }

        $timestamp = strtotime($value);
        return $timestamp !== false ? gmdate('c', $timestamp) : '';
    }

    public function getFolderIconBinary(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $folder_id = absint($request['folder_id']);
        if ($folder_id <= 0) {
            return new WP_Error('griffinforms_invalid_folder', __('Invalid folder.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $folder_table = $this->config->getTable('folder');
        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT id, icon FROM %i WHERE id = %d LIMIT 1", $folder_table, $folder_id),
            ARRAY_A
        );
        if (!is_array($row)) {
            return new WP_Error('griffinforms_folder_not_found', __('Folder not found.', 'griffinforms-form-builder'), ['status' => 404]);
        }

        $icon_key = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) ($row['icon'] ?? ''));
        if ($icon_key === '') {
            $icon_key = '1';
        }

        $base_dir = trailingslashit($this->config->getRootPath() . 'admin/images/folder');
        $candidate = $base_dir . $icon_key . '.png';
        if (!file_exists($candidate)) {
            $candidate = $base_dir . 'default.png';
        }
        if (!file_exists($candidate)) {
            return new WP_Error('griffinforms_folder_icon_not_found', __('Folder icon not found.', 'griffinforms-form-builder'), ['status' => 404]);
        }

        $binary = @file_get_contents($candidate);
        if (!is_string($binary) || $binary === '') {
            return new WP_Error('griffinforms_folder_icon_read_failed', __('Unable to read folder icon.', 'griffinforms-form-builder'), ['status' => 500]);
        }

        Capabilities::logAudit('folder_icon_viewed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'folder',
            'target_id' => $folder_id,
            'meta' => [
                'icon_key' => $icon_key,
                'filename' => basename($candidate),
            ],
        ]));

        return $this->success([
            'folder_id' => $folder_id,
            'icon_key' => $icon_key,
            'filename' => sanitize_file_name((string) basename($candidate)),
            'mime' => 'image/png',
            'size_bytes' => strlen($binary),
            'content_base64' => base64_encode($binary),
            'encoding' => 'base64',
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function getForms(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $page = max(1, absint($request->get_param('page')));
        $raw_per_page = $request->get_param('per_page');
        $per_page = 50;
        if ($raw_per_page !== null && $raw_per_page !== '') {
            $per_page = max(1, absint($raw_per_page));
        }
        if (is_numeric($raw_per_page) && (int) $raw_per_page > 200) {
            return new WP_Error('griffinforms_per_page_limit', __('per_page cannot be greater than 200.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        $offset = ($page - 1) * $per_page;

        $search = sanitize_text_field((string) $request->get_param('search'));
        if ($search !== '' && mb_strlen($search) < 3) {
            return new WP_Error(
                'griffinforms_search_min_chars',
                __('Please enter at least 3 characters for search.', 'griffinforms-form-builder'),
                ['status' => 400]
            );
        }

        $folder_id = $request->get_param('folder_id');
        $folder_id = $folder_id !== null ? (int) $folder_id : null;

        $sort_by = sanitize_key((string) $request->get_param('sort_by'));
        $sort_dir = strtolower(sanitize_text_field((string) $request->get_param('sort_dir')));

        $sort_by_map = [
            'name' => 'f.name',
            'last_edit_date' => 'f.last_edited',
            'submission_count' => 'submission_count',
        ];

        if ($sort_by === '') {
            $sort_by = 'name';
        }
        if (!isset($sort_by_map[$sort_by])) {
            return new WP_Error('griffinforms_invalid_sort_by', __('Invalid sort_by parameter.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        if ($sort_dir === '') {
            $sort_dir = 'desc';
        }
        if (!in_array($sort_dir, ['asc', 'desc'], true)) {
            return new WP_Error('griffinforms_invalid_sort_dir', __('Invalid sort_dir parameter.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        $sort_dir = strtoupper($sort_dir);

        $form_table = $this->config->getTable('form');
        $sub_table = $this->config->getTable('submission');

        $where = ['1=1'];
        $params = [];

        if ($folder_id !== null) {
            $where[] = 'f.folder = %d';
            $params[] = $folder_id;
        }

        if ($search !== '') {
            $like = '%' . $wpdb->esc_like($search) . '%';
            $where[] = '(f.name LIKE %s OR f.heading LIKE %s OR f.description LIKE %s)';
            $params[] = $like;
            $params[] = $like;
            $params[] = $like;
        }

        $where_sql = implode(' AND ', $where);

        $count_sql = "SELECT COUNT(*) FROM {$form_table} f WHERE {$where_sql}";
        $total = (int) $wpdb->get_var(empty($params) ? $count_sql : $wpdb->prepare($count_sql, ...$params));

        $sql = "SELECT f.id, f.name, f.heading, f.description, f.folder, f.last_edited,
                       COUNT(s.id) AS submission_count,
                       SUM(CASE WHEN s.is_read = 0 THEN 1 ELSE 0 END) AS unread_submission_count
                FROM {$form_table} f
                LEFT JOIN {$sub_table} s ON s.form_id = f.id
                WHERE {$where_sql}
                GROUP BY f.id
                ORDER BY {$sort_by_map[$sort_by]} {$sort_dir}, f.id DESC
                LIMIT %d OFFSET %d";

        $query_params = array_merge($params, [$per_page, $offset]);
        $rows = $wpdb->get_results($wpdb->prepare($sql, ...$query_params), ARRAY_A);

        $data = array_map(static function ($row) {
            return [
                'id' => (int) $row['id'],
                'name' => sanitize_text_field((string) $row['name']),
                'heading' => sanitize_text_field((string) ($row['heading'] ?? '')),
                'description' => sanitize_textarea_field((string) ($row['description'] ?? '')),
                'folder_id' => (int) ($row['folder'] ?? 0),
                'last_edit_date' => (string) ($row['last_edited'] ?? ''),
                'submission_count' => (int) ($row['submission_count'] ?? 0),
                'unread_submission_count' => (int) ($row['unread_submission_count'] ?? 0),
            ];
        }, (array) $rows);

        Capabilities::logAudit('forms_list_viewed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'form_collection',
            'meta' => [
                'page' => $page,
                'per_page' => $per_page,
                'search' => $search,
                'folder_id' => $folder_id,
                'sort_by' => $sort_by,
                'sort_dir' => strtolower($sort_dir),
                'result_count' => count($data),
            ],
        ]));

        return $this->success($data, [
            'page' => $page,
            'per_page' => $per_page,
            'total' => $total,
            'has_more' => ($offset + count($data)) < $total,
            'sort_by' => $sort_by,
            'sort_dir' => strtolower($sort_dir),
            'search' => $search,
            'folder_id' => $folder_id,
            'request_id' => $this->requestId(),
        ]);
    }

    public function getDevices(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $user_id = get_current_user_id();
        if ($user_id <= 0) {
            return $this->apiError('devices_list_view_failed', 'griffinforms_api_unauthenticated', __('Authentication required.', 'griffinforms-form-builder'), 401);
        }

        $include_inactive = $this->parseBoolParam($request->get_param('include_inactive'));
        $registry = $this->getDeviceRegistryForUser($user_id);
        if (!$include_inactive) {
            $registry = array_values(array_filter($registry, static fn($item) => empty($item['deleted_at'])));
        }

        Capabilities::logAudit('devices_list_viewed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'device_collection',
            'meta' => [
                'result_count' => count($registry),
                'include_inactive' => $include_inactive ? 1 : 0,
            ],
        ]));

        return $this->success(array_map([$this, 'mapDeviceResponseRecord'], $registry), [
            'request_id' => $this->requestId(),
            'include_inactive' => $include_inactive,
            'count' => count($registry),
            'limit_per_user' => $this->deviceLimitPerUser(),
            'limit_total' => $this->deviceLimitTotal(),
        ]);
    }

    public function registerDevice(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $user_id = get_current_user_id();
        if ($user_id <= 0) {
            return $this->apiError('device_register_failed', 'griffinforms_api_unauthenticated', __('Authentication required.', 'griffinforms-form-builder'), 401);
        }

        $device_id = $this->sanitizeDeviceId((string) $request->get_param('device_id'));
        if ($device_id === '') {
            return $this->apiError('device_register_failed', 'griffinforms_invalid_device_id', __('device_id is required and must be <= 191 chars.', 'griffinforms-form-builder'), 400);
        }

        $platform = $this->normalizeDevicePlatform((string) $request->get_param('platform'));
        if ($platform === '') {
            return $this->apiError('device_register_failed', 'griffinforms_invalid_platform', __('Invalid platform.', 'griffinforms-form-builder'), 400);
        }

        $app_version = sanitize_text_field((string) $request->get_param('app_version'));
        if (mb_strlen($app_version) > 50) {
            return $this->apiError('device_register_failed', 'griffinforms_invalid_app_version', __('app_version cannot exceed 50 characters.', 'griffinforms-form-builder'), 400);
        }

        $push_token = sanitize_text_field((string) $request->get_param('push_token'));
        if (mb_strlen($push_token) > 255) {
            return $this->apiError('device_register_failed', 'griffinforms_invalid_push_token', __('push_token cannot exceed 255 characters.', 'griffinforms-form-builder'), 400);
        }

        $labels = $this->normalizeDeviceLabels($request->get_param('labels'));
        if ($labels instanceof WP_Error) {
            return $labels;
        }

        $token_state = $this->normalizeDeviceTokenState((string) $request->get_param('token_state'));
        if ($token_state === '') {
            $token_state = 'active';
        }

        $last_seen = $this->normalizeDeviceDateTime($request->get_param('last_seen'));
        if ($last_seen === false) {
            return $this->apiError('device_register_failed', 'griffinforms_invalid_last_seen', __('Invalid last_seen value.', 'griffinforms-form-builder'), 400);
        }
        if ($last_seen === null) {
            $last_seen = current_time('mysql', true);
        }

        $registry = $this->getDeviceRegistryForUser($user_id);
        $index = $this->findDeviceRegistryIndex($registry, $device_id);
        $active_count = $this->countActiveDevices($registry);
        $is_reactivation = ($index >= 0 && !empty($registry[$index]['deleted_at']));
        $per_user_limit = $this->deviceLimitPerUser();
        $total_limit = $this->deviceLimitTotal();

        if ($token_state === 'active' && ($index < 0 || $is_reactivation) && $active_count >= $per_user_limit) {
            return $this->apiError(
                'device_register_failed',
                'griffinforms_device_limit_reached',
                sprintf(
                    /* translators: %d is the maximum devices allowed per user. */
                    __('You can register up to %d active devices.', 'griffinforms-form-builder'),
                    $per_user_limit
                ),
                409,
                ['target_type' => 'device', 'meta' => ['device_id' => $device_id, 'limit' => $per_user_limit]]
            );
        }

        if ($token_state === 'active' && ($index < 0 || $is_reactivation) && $this->countTotalActiveDevices() >= $total_limit) {
            return $this->apiError(
                'device_register_failed',
                'griffinforms_device_global_limit_reached',
                sprintf(
                    /* translators: %d is the global maximum number of active devices. */
                    __('Device capacity reached (%d active devices).', 'griffinforms-form-builder'),
                    $total_limit
                ),
                409,
                ['target_type' => 'device', 'meta' => ['device_id' => $device_id, 'limit_total' => $total_limit]]
            );
        }

        $now = current_time('mysql', true);
        $record = [
            'device_id' => $device_id,
            'platform' => $platform,
            'app_version' => $app_version,
            'push_token' => $push_token,
            'token_state' => $token_state,
            'labels' => $labels,
            'last_seen' => $last_seen,
            'last_registration_ip' => $this->sanitizeIp((string) ($_SERVER['REMOTE_ADDR'] ?? '')),
            'last_user_agent' => $this->sanitizeUserAgent((string) ($_SERVER['HTTP_USER_AGENT'] ?? '')),
            'deleted_at' => $token_state === 'active' ? null : $now,
            'updated_at' => $now,
            'created_at' => $now,
        ];

        $operation = 'created';
        if ($index >= 0) {
            $record['created_at'] = (string) ($registry[$index]['created_at'] ?? $now);
            $registry[$index] = array_merge($registry[$index], $record);
            $operation = $is_reactivation ? 'reactivated' : 'updated';
        } else {
            $registry[] = $record;
        }

        $this->sortDeviceRegistry($registry);
        if (!$this->saveDeviceRegistryForUser($user_id, $registry)) {
            return $this->apiError('device_register_failed', 'griffinforms_device_register_failed', __('Unable to register device.', 'griffinforms-form-builder'), 500, [
                'target_type' => 'device',
                'meta' => ['device_id' => $device_id],
            ]);
        }

        $saved_index = $this->findDeviceRegistryIndex($registry, $device_id);
        $saved_record = $saved_index >= 0 ? $registry[$saved_index] : $record;

        Capabilities::logAudit('device_registered', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'device',
            'target_id' => 0,
            'meta' => [
                'device_id' => $device_id,
                'operation' => $operation,
                'token_state' => $token_state,
            ],
        ]));

        return $this->success([
            'operation' => $operation,
            'device' => $this->mapDeviceResponseRecord($saved_record),
            'limit_per_user' => $this->deviceLimitPerUser(),
            'limit_total' => $this->deviceLimitTotal(),
            'active_count' => $this->countActiveDevices($registry),
            'active_count_total' => $this->countTotalActiveDevices(),
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function unregisterDevice(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $user_id = get_current_user_id();
        if ($user_id <= 0) {
            return $this->apiError('device_unregister_failed', 'griffinforms_api_unauthenticated', __('Authentication required.', 'griffinforms-form-builder'), 401);
        }

        $device_id = $this->sanitizeDeviceId((string) $request->get_param('device_id'));
        if ($device_id === '') {
            return $this->apiError('device_unregister_failed', 'griffinforms_invalid_device_id', __('device_id is required and must be <= 191 chars.', 'griffinforms-form-builder'), 400);
        }

        $purge = $this->parseBoolParam($request->get_param('purge'));
        $registry = $this->getDeviceRegistryForUser($user_id);
        $index = $this->findDeviceRegistryIndex($registry, $device_id);
        if ($index < 0) {
            return $this->apiError('device_unregister_failed', 'griffinforms_device_not_found', __('Device not found.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'device',
                'meta' => ['device_id' => $device_id],
            ]);
        }

        if ($purge) {
            array_splice($registry, $index, 1);
        } else {
            $now = current_time('mysql', true);
            $registry[$index]['token_state'] = 'unregistered';
            $registry[$index]['deleted_at'] = $now;
            $registry[$index]['updated_at'] = $now;
        }

        $this->sortDeviceRegistry($registry);
        if (!$this->saveDeviceRegistryForUser($user_id, $registry)) {
            return $this->apiError('device_unregister_failed', 'griffinforms_device_unregister_failed', __('Unable to unregister device.', 'griffinforms-form-builder'), 500, [
                'target_type' => 'device',
                'meta' => ['device_id' => $device_id, 'purge' => $purge ? 1 : 0],
            ]);
        }

        Capabilities::logAudit('device_unregistered', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'device',
            'target_id' => 0,
            'meta' => [
                'device_id' => $device_id,
                'purge' => $purge ? 1 : 0,
            ],
        ]));

        return $this->success([
            'device_id' => $device_id,
            'unregistered' => true,
            'purged' => $purge,
            'active_count' => $this->countActiveDevices($registry),
            'limit_per_user' => $this->deviceLimitPerUser(),
            'limit_total' => $this->deviceLimitTotal(),
            'active_count_total' => $this->countTotalActiveDevices(),
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function patchDevice(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $user_id = get_current_user_id();
        if ($user_id <= 0) {
            return $this->apiError('device_patch_failed', 'griffinforms_api_unauthenticated', __('Authentication required.', 'griffinforms-form-builder'), 401);
        }

        $device_id = $this->sanitizeDeviceId((string) $request->get_param('device_id'));
        if ($device_id === '') {
            return $this->apiError('device_patch_failed', 'griffinforms_invalid_device_id', __('Invalid device_id.', 'griffinforms-form-builder'), 400);
        }

        $registry = $this->getDeviceRegistryForUser($user_id);
        $index = $this->findDeviceRegistryIndex($registry, $device_id);
        if ($index < 0) {
            return $this->apiError('device_patch_failed', 'griffinforms_device_not_found', __('Device not found.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'device',
                'meta' => ['device_id' => $device_id],
            ]);
        }

        $record = $registry[$index];
        $changes = 0;

        if ($request->has_param('platform')) {
            $platform = $this->normalizeDevicePlatform((string) $request->get_param('platform'));
            if ($platform === '') {
                return $this->apiError('device_patch_failed', 'griffinforms_invalid_platform', __('Invalid platform.', 'griffinforms-form-builder'), 400);
            }
            $record['platform'] = $platform;
            $changes++;
        }

        if ($request->has_param('app_version')) {
            $app_version = sanitize_text_field((string) $request->get_param('app_version'));
            if (mb_strlen($app_version) > 50) {
                return $this->apiError('device_patch_failed', 'griffinforms_invalid_app_version', __('app_version cannot exceed 50 characters.', 'griffinforms-form-builder'), 400);
            }
            $record['app_version'] = $app_version;
            $changes++;
        }

        if ($request->has_param('push_token')) {
            $push_token = sanitize_text_field((string) $request->get_param('push_token'));
            if (mb_strlen($push_token) > 255) {
                return $this->apiError('device_patch_failed', 'griffinforms_invalid_push_token', __('push_token cannot exceed 255 characters.', 'griffinforms-form-builder'), 400);
            }
            $record['push_token'] = $push_token;
            $changes++;
        }

        if ($request->has_param('token_state')) {
            $token_state = $this->normalizeDeviceTokenState((string) $request->get_param('token_state'));
            if ($token_state === '') {
                return $this->apiError('device_patch_failed', 'griffinforms_invalid_token_state', __('Invalid token_state.', 'griffinforms-form-builder'), 400);
            }

            $active_count = $this->countActiveDevices($registry);
            $is_reactivation = !empty($record['deleted_at']) && $token_state === 'active';
            $per_user_limit = $this->deviceLimitPerUser();
            $total_limit = $this->deviceLimitTotal();
            if ($is_reactivation && $active_count >= $per_user_limit) {
                return $this->apiError(
                    'device_patch_failed',
                    'griffinforms_device_limit_reached',
                    sprintf(
                        /* translators: %d is the maximum devices allowed per user. */
                        __('You can register up to %d active devices.', 'griffinforms-form-builder'),
                        $per_user_limit
                    ),
                    409,
                    ['target_type' => 'device', 'meta' => ['device_id' => $device_id, 'limit' => $per_user_limit]]
                );
            }
            if ($is_reactivation && $this->countTotalActiveDevices() >= $total_limit) {
                return $this->apiError(
                    'device_patch_failed',
                    'griffinforms_device_global_limit_reached',
                    sprintf(
                        /* translators: %d is the global maximum number of active devices. */
                        __('Device capacity reached (%d active devices).', 'griffinforms-form-builder'),
                        $total_limit
                    ),
                    409,
                    ['target_type' => 'device', 'meta' => ['device_id' => $device_id, 'limit_total' => $total_limit]]
                );
            }

            $record['token_state'] = $token_state;
            if ($token_state === 'active') {
                $record['deleted_at'] = null;
            } elseif ($token_state === 'unregistered') {
                $record['deleted_at'] = current_time('mysql', true);
            }
            $changes++;
        }

        if ($request->has_param('labels')) {
            $labels = $this->normalizeDeviceLabels($request->get_param('labels'));
            if ($labels instanceof WP_Error) {
                return $labels;
            }
            $record['labels'] = $labels;
            $changes++;
        }

        if ($request->has_param('last_seen')) {
            $last_seen = $this->normalizeDeviceDateTime($request->get_param('last_seen'));
            if ($last_seen === false) {
                return $this->apiError('device_patch_failed', 'griffinforms_invalid_last_seen', __('Invalid last_seen value.', 'griffinforms-form-builder'), 400);
            }
            $record['last_seen'] = $last_seen;
            $changes++;
        }

        if ($changes <= 0) {
            return $this->apiError('device_patch_failed', 'griffinforms_invalid_payload', __('At least one updatable field is required.', 'griffinforms-form-builder'), 400, [
                'target_type' => 'device',
                'meta' => ['device_id' => $device_id],
            ]);
        }

        $record['updated_at'] = current_time('mysql', true);
        $registry[$index] = $record;
        $this->sortDeviceRegistry($registry);

        if (!$this->saveDeviceRegistryForUser($user_id, $registry)) {
            return $this->apiError('device_patch_failed', 'griffinforms_device_update_failed', __('Unable to update device.', 'griffinforms-form-builder'), 500, [
                'target_type' => 'device',
                'meta' => ['device_id' => $device_id],
            ]);
        }

        Capabilities::logAudit('device_updated', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'device',
            'target_id' => 0,
            'meta' => [
                'device_id' => $device_id,
                'field_count' => $changes,
            ],
        ]));

        return $this->success([
            'device' => $this->mapDeviceResponseRecord($record),
            'active_count' => $this->countActiveDevices($registry),
            'limit_per_user' => $this->deviceLimitPerUser(),
            'limit_total' => $this->deviceLimitTotal(),
            'active_count_total' => $this->countTotalActiveDevices(),
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function moveFormFolder(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $form_id = absint($request->get_param('form_id'));
        $new_folder_id = $request->get_param('new_folder_id');
        if ($form_id <= 0 || $new_folder_id === null || $new_folder_id === '') {
            return new WP_Error('griffinforms_invalid_payload', __('form_id and new_folder_id are required.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        $new_folder_id = (int) $new_folder_id;
        if ($new_folder_id < 0) {
            return new WP_Error('griffinforms_invalid_folder', __('new_folder_id must be 0 or a valid folder ID.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $form_table = $this->config->getTable('form');
        $folder_table = $this->config->getTable('folder');

        $form_row = $wpdb->get_row(
            $wpdb->prepare("SELECT id, folder FROM %i WHERE id = %d LIMIT 1", $form_table, $form_id),
            ARRAY_A
        );
        if (!is_array($form_row)) {
            return new WP_Error('griffinforms_form_not_found', __('Form not found.', 'griffinforms-form-builder'), ['status' => 404]);
        }

        if ($new_folder_id > 0) {
            $folder_exists = (int) $wpdb->get_var(
                $wpdb->prepare("SELECT COUNT(*) FROM %i WHERE id = %d", $folder_table, $new_folder_id)
            );
            if ($folder_exists <= 0) {
                return new WP_Error('griffinforms_folder_not_found', __('Target folder not found.', 'griffinforms-form-builder'), ['status' => 404]);
            }
        }

        $old_folder_id = absint($form_row['folder'] ?? 0);
        if ($old_folder_id === $new_folder_id) {
            return $this->success([
                'form_id' => $form_id,
                'old_folder_id' => $old_folder_id,
                'new_folder_id' => $new_folder_id,
                'updated' => false,
            ], [
                'request_id' => $this->requestId(),
            ]);
        }

        $updated = $wpdb->update(
            $form_table,
            ['folder' => $new_folder_id],
            ['id' => $form_id],
            ['%d'],
            ['%d']
        );

        if ($updated === false) {
            return new WP_Error('griffinforms_folder_update_failed', __('Unable to update form folder.', 'griffinforms-form-builder'), ['status' => 500]);
        }

        Capabilities::logAudit('form_folder_changed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'form',
            'target_id' => $form_id,
            'meta' => [
                'old_folder_id' => $old_folder_id,
                'new_folder_id' => $new_folder_id,
            ],
        ]));

        return $this->success([
            'form_id' => $form_id,
            'old_folder_id' => $old_folder_id,
            'new_folder_id' => $new_folder_id,
            'updated' => true,
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function renameFolder(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $folder_id = absint($request->get_param('folder_id'));
        $name = sanitize_text_field((string) $request->get_param('name'));

        if ($folder_id <= 0 || $name === '') {
            return new WP_Error('griffinforms_invalid_payload', __('folder_id and name are required.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        if (mb_strlen($name) > 255) {
            return new WP_Error('griffinforms_invalid_folder_name', __('Folder name is too long.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $folder_table = $this->config->getTable('folder');
        $folder_row = $wpdb->get_row(
            $wpdb->prepare("SELECT id, name FROM %i WHERE id = %d LIMIT 1", $folder_table, $folder_id),
            ARRAY_A
        );
        if (!is_array($folder_row)) {
            return new WP_Error('griffinforms_folder_not_found', __('Folder not found.', 'griffinforms-form-builder'), ['status' => 404]);
        }

        $old_name = sanitize_text_field((string) ($folder_row['name'] ?? ''));
        if ($old_name === $name) {
            return $this->success([
                'folder_id' => $folder_id,
                'old_name' => $old_name,
                'new_name' => $name,
                'updated' => false,
            ], [
                'request_id' => $this->requestId(),
            ]);
        }

        $update_data = [
            'name' => $name,
            'editor' => get_current_user_id(),
            'edited_on' => current_time('mysql'),
        ];
        $updated = $wpdb->update(
            $folder_table,
            $update_data,
            ['id' => $folder_id],
            ['%s', '%d', '%s'],
            ['%d']
        );
        if ($updated === false) {
            return new WP_Error('griffinforms_folder_rename_failed', __('Unable to rename folder.', 'griffinforms-form-builder'), ['status' => 500]);
        }

        Capabilities::logAudit('folder_renamed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'folder',
            'target_id' => $folder_id,
            'meta' => [
                'old_name' => $old_name,
                'new_name' => $name,
            ],
        ]));

        return $this->success([
            'folder_id' => $folder_id,
            'old_name' => $old_name,
            'new_name' => $name,
            'updated' => true,
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function createFolder(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $name = sanitize_text_field((string) $request->get_param('name'));
        $heading = sanitize_text_field((string) $request->get_param('heading'));
        $description = sanitize_textarea_field((string) $request->get_param('description'));
        $icon_raw = sanitize_text_field((string) $request->get_param('icon'));

        if ($name === '') {
            return new WP_Error('griffinforms_invalid_payload', __('name is required.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        if (mb_strlen($name) > 255) {
            return new WP_Error('griffinforms_invalid_folder_name', __('Folder name is too long.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        if ($heading !== '' && mb_strlen($heading) > 1000) {
            return new WP_Error('griffinforms_invalid_folder_heading', __('Folder heading is too long.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        if ($description !== '' && mb_strlen($description) > 2000) {
            return new WP_Error('griffinforms_invalid_folder_description', __('Folder description is too long.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $icon_key = preg_replace('/[^a-zA-Z0-9_-]/', '', $icon_raw);
        if ($icon_key === '') {
            $icon_key = '1';
        }
        if (!ctype_digit($icon_key)) {
            $icon_key = '1';
        }

        $icon_dir = trailingslashit($this->config->getRootPath() . 'admin/images/folder');
        $icon_path = $icon_dir . $icon_key . '.png';
        if (!file_exists($icon_path)) {
            return new WP_Error('griffinforms_invalid_folder_icon', __('Invalid folder icon.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $folder_table = $this->config->getTable('folder');
        $author = (string) get_current_user_id();
        $created = current_time('mysql', true);

        $inserted = $wpdb->insert(
            $folder_table,
            [
                'name' => $name,
                'heading' => $heading,
                'description' => $description,
                'icon' => (int) $icon_key,
                'author' => $author,
                'created' => $created,
                'last_editor' => $author,
                'last_edited' => $created,
            ],
            ['%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s']
        );
        if ($inserted === false) {
            return new WP_Error('griffinforms_folder_create_failed', __('Unable to create folder.', 'griffinforms-form-builder'), ['status' => 500]);
        }

        $folder_id = (int) $wpdb->insert_id;

        Capabilities::logAudit('folder_created', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'folder',
            'target_id' => $folder_id,
            'meta' => [
                'name' => $name,
                'heading' => $heading,
                'icon' => $icon_key,
            ],
        ]));

        return $this->success([
            'folder_id' => $folder_id,
            'name' => $name,
            'heading' => $heading,
            'description' => $description,
            'icon' => (int) $icon_key,
            'author' => $author,
            'created' => $created,
            'last_editor' => $author,
            'last_edited' => $created,
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function renameForm(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $form_id = absint($request->get_param('form_id'));
        $name = sanitize_text_field((string) $request->get_param('name'));

        if ($form_id <= 0 || $name === '') {
            return new WP_Error('griffinforms_invalid_payload', __('form_id and name are required.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        if (mb_strlen($name) > 255) {
            return new WP_Error('griffinforms_invalid_form_name', __('Form name is too long.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $form_table = $this->config->getTable('form');
        $form_row = $wpdb->get_row(
            $wpdb->prepare("SELECT id, name FROM %i WHERE id = %d LIMIT 1", $form_table, $form_id),
            ARRAY_A
        );
        if (!is_array($form_row)) {
            return new WP_Error('griffinforms_form_not_found', __('Form not found.', 'griffinforms-form-builder'), ['status' => 404]);
        }

        $old_name = sanitize_text_field((string) ($form_row['name'] ?? ''));
        if ($old_name === $name) {
            return $this->success([
                'form_id' => $form_id,
                'old_name' => $old_name,
                'new_name' => $name,
                'updated' => false,
            ], [
                'request_id' => $this->requestId(),
            ]);
        }

        $update_data = [
            'name' => $name,
            'last_editor' => get_current_user_id(),
            'last_edited' => current_time('mysql'),
        ];
        $updated = $wpdb->update(
            $form_table,
            $update_data,
            ['id' => $form_id],
            ['%s', '%d', '%s'],
            ['%d']
        );
        if ($updated === false) {
            $db_error = sanitize_text_field((string) $wpdb->last_error);
            $message = __('Unable to rename form.', 'griffinforms-form-builder');
            if ($db_error !== '') {
                $message .= ' ' . sprintf(
                    /* translators: %s is a database error detail. */
                    __('Database error: %s', 'griffinforms-form-builder'),
                    $db_error
                );
            }
            return new WP_Error('griffinforms_form_rename_failed', $message, ['status' => 500]);
        }

        Capabilities::logAudit('form_renamed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'form',
            'target_id' => $form_id,
            'meta' => [
                'old_name' => $old_name,
                'new_name' => $name,
            ],
        ]));

        return $this->success([
            'form_id' => $form_id,
            'old_name' => $old_name,
            'new_name' => $name,
            'updated' => true,
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function getFormStructure(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $form_id = absint($request['form_id']);
        if ($form_id <= 0) {
            return $this->apiError('form_structure_view_failed', 'griffinforms_invalid_form', __('Invalid form.', 'griffinforms-form-builder'), 400, [
                'target_type' => 'form',
                'target_id' => $form_id,
            ]);
        }

        $structure = AdminAppSql::getInstance()->getFormStructure($form_id);
        if (!is_array($structure)) {
            return $this->apiError('form_structure_view_failed', 'griffinforms_form_not_found', __('Form not found.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'form',
                'target_id' => $form_id,
            ]);
        }

        $payload = [
            'id' => (int) ($structure['id'] ?? 0),
            'name' => sanitize_text_field((string) ($structure['name'] ?? '')),
            'heading' => sanitize_text_field((string) ($structure['heading'] ?? '')),
            'description' => sanitize_textarea_field((string) ($structure['description'] ?? '')),
            'submit_label' => sanitize_text_field((string) ($structure['submit_label'] ?? '')),
            'theme_css' => '',
            'theme_fonts' => [],
            'form_style_css' => '',
            'page_style_css' => '',
            'row_style_css' => '',
            'pages' => [],
        ];

        foreach ((array) ($structure['pages'] ?? []) as $page) {
            $page_payload = [
                'id' => (int) ($page['id'] ?? 0),
                'name' => sanitize_text_field((string) ($page['name'] ?? '')),
                'heading' => sanitize_text_field((string) ($page['heading'] ?? '')),
                'description' => sanitize_textarea_field((string) ($page['description'] ?? '')),
                'rows' => [],
            ];

            foreach ((array) ($page['rows'] ?? []) as $row) {
                $row_payload = [
                    'id' => (int) ($row['id'] ?? 0),
                    'name' => sanitize_text_field((string) ($row['name'] ?? '')),
                    'heading' => sanitize_text_field((string) ($row['heading'] ?? '')),
                    'description' => sanitize_textarea_field((string) ($row['description'] ?? '')),
                    'columns' => [],
                ];

                foreach ((array) ($row['columns'] ?? []) as $column) {
                    $column_payload = [
                        'id' => (int) ($column['id'] ?? 0),
                        'name' => sanitize_text_field((string) ($column['name'] ?? '')),
                        'width' => (int) ($column['width'] ?? 12),
                        'fields' => [],
                    ];

                    foreach ((array) ($column['fields'] ?? []) as $field) {
                        $column_payload['fields'][] = [
                            'id' => (int) ($field['id'] ?? 0),
                            'label' => sanitize_text_field((string) ($field['heading'] ?? '')),
                            'description' => sanitize_textarea_field((string) ($field['description'] ?? '')),
                        ];
                    }

                    $row_payload['columns'][] = $column_payload;
                }

                $page_payload['rows'][] = $row_payload;
            }

            $payload['pages'][] = $page_payload;
        }

        $this->attachFormThemePreviewData($payload, $form_id);

        Capabilities::logAudit('form_structure_viewed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'form',
            'target_id' => $form_id,
        ]));

        return $this->success($payload, [
            'request_id' => $this->requestId(),
        ]);
    }

    private function attachFormThemePreviewData(array &$payload, int $form_id): void
    {
        $form_item = new \GriffinForms\Items\Form();
        if (!$form_item->loadItem($form_id)) {
            return;
        }

        if ($payload['submit_label'] === '') {
            $payload['submit_label'] = sanitize_text_field((string) $form_item->getProp('submit_label'));
        }

        $theme_id = absint($form_item->getProp('form_theme'));
        if ($theme_id < 1) {
            return;
        }

        $form_theme = new \GriffinForms\Items\FormTheme();
        if (!$form_theme->loadItem($theme_id)) {
            return;
        }

        $payload['form_style_css'] = (string) $form_theme->getFormStyleCss();
        $payload['page_style_css'] = (string) $form_theme->getPageStyleCss();
        $payload['row_style_css'] = (string) $form_theme->getRowStyleCss();
        $payload['theme_fonts'] = $this->getThemeFonts($form_theme);
        $payload['theme_css'] = $this->buildThemeCss($form_theme);
    }

    private function getThemeFonts(\GriffinForms\Items\FormTheme $form_theme): array
    {
        $fonts = $form_theme->getExternalFontFamilies();
        if (!is_array($fonts)) {
            return [];
        }

        $fonts = array_map('trim', $fonts);
        $fonts = array_values(array_filter($fonts));
        return array_map('sanitize_text_field', $fonts);
    }

    private function buildThemeCss(\GriffinForms\Items\FormTheme $form_theme): string
    {
        $rules = [];

        $form_css = $form_theme->getFormStyleCss();
        if ($form_css !== '') {
            $rules[] = '.griffinforms-block-preview { ' . $form_css . ' }';
        }

        $page_css = $form_theme->getPageStyleCss();
        if ($page_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-page { ' . $page_css . ' }';
        }

        $row_css = $form_theme->getRowStyleCss();
        if ($row_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-row { ' . $row_css . ' }';
        }

        $heading_css = $form_theme->getFormTitleCss();
        if ($heading_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-heading { ' . $heading_css . ' }';
        }

        $desc_css = $form_theme->getFormDescriptionCss();
        if ($desc_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-desc { ' . $desc_css . ' }';
        }

        $page_heading_css = $form_theme->getPageHeadingCss();
        if ($page_heading_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-page-title { ' . $page_heading_css . ' }';
        }

        $page_desc_css = $form_theme->getPageDescriptionCss();
        if ($page_desc_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-page-desc { ' . $page_desc_css . ' }';
        }

        $row_heading_css = $form_theme->getRowHeadingCss();
        if ($row_heading_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-row-heading { ' . $row_heading_css . ' }';
        }

        $row_desc_css = $form_theme->getRowDescriptionCss();
        if ($row_desc_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-row-desc { ' . $row_desc_css . ' }';
        }

        $label_css = $form_theme->getFieldLabelCss();
        if ($label_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-label { ' . $label_css . ' }';
        }

        $field_desc_css = $form_theme->getFieldDescriptionCss();
        if ($field_desc_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-description { ' . $field_desc_css . ' }';
        }

        $style_types = ['text_input', 'textarea', 'select_input', 'checkbox_radio'];
        foreach ($style_types as $style_type) {
            $selector = '.griffinforms-block-preview [data-gf-style-group="block-preview"][data-gf-style-type="' . $style_type . '"]';

            $default_css = $form_theme->getFieldStyleCss($style_type, 'default');
            if ($default_css !== '') {
                $rules[] = $selector . ' { ' . $default_css . ' }';
            }

            $hover_css = $form_theme->getFieldStyleCss($style_type, 'hover');
            if ($hover_css !== '') {
                $rules[] = $selector . ':hover { ' . $hover_css . ' }';
            }

            $focus_css = $form_theme->getFieldStyleCss($style_type, 'focus');
            if ($focus_css !== '') {
                $rules[] = $selector . ':focus, ' . $selector . ':focus-visible { ' . $focus_css . ' }';
            }
        }

        $prev_css = $form_theme->getPreviousButtonCss('default');
        if ($prev_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-nav-prev { ' . $prev_css . ' }';
        }
        $prev_hover = $form_theme->getPreviousButtonCss('hover');
        if ($prev_hover !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-nav-prev:hover { ' . $prev_hover . ' }';
        }
        $prev_active = $form_theme->getPreviousButtonCss('active');
        if ($prev_active !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-nav-prev:active { ' . $prev_active . ' }';
        }

        $next_css = $form_theme->getNextButtonCss('default');
        if ($next_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-nav-next { ' . $next_css . ' }';
        }
        $next_hover = $form_theme->getNextButtonCss('hover');
        if ($next_hover !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-nav-next:hover { ' . $next_hover . ' }';
        }
        $next_active = $form_theme->getNextButtonCss('active');
        if ($next_active !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-nav-next:active { ' . $next_active . ' }';
        }

        $submit_css = $form_theme->getSubmitButtonCss('default');
        if ($submit_css !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-nav-submit { ' . $submit_css . ' }';
        }
        $submit_hover = $form_theme->getSubmitButtonCss('hover');
        if ($submit_hover !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-nav-submit:hover { ' . $submit_hover . ' }';
        }
        $submit_active = $form_theme->getSubmitButtonCss('active');
        if ($submit_active !== '') {
            $rules[] = '.griffinforms-block-preview .gf-bp-nav-submit:active { ' . $submit_active . ' }';
        }

        $label_color = $form_theme->getLabelColor();
        if (!empty($label_color)) {
            $rules[] = '.griffinforms-block-preview .gf-bp-options span:last-child { color: ' . $label_color . ' !important; }';
            $rules[] = '.griffinforms-block-preview .gf-bp-terms-subtext { color: ' . $label_color . ' !important; }';
            $rules[] = '.griffinforms-block-preview .gf-bp-terms-link a { color: ' . $label_color . ' !important; }';
            $rules[] = '.griffinforms-block-preview .gf-bp-terms-link span { color: ' . $label_color . ' !important; }';
        }

        if (empty($rules)) {
            return '';
        }

        return \GriffinForms\Items\FormTheme::prepareStyleTagContent(implode(' ', $rules));
    }

    public function getSubmissions(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $page = max(1, absint($request->get_param('page')));
        $raw_per_page = $request->get_param('per_page');
        $per_page = 50;
        if ($raw_per_page !== null && $raw_per_page !== '') {
            $per_page = max(1, absint($raw_per_page));
        }
        if (is_numeric($raw_per_page) && (int) $raw_per_page > 200) {
            return new WP_Error('griffinforms_per_page_limit', __('per_page cannot be greater than 200.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        $offset = ($page - 1) * $per_page;

        $search = sanitize_text_field((string) $request->get_param('search'));
        if ($search !== '' && mb_strlen($search) < 3) {
            return new WP_Error('griffinforms_search_min_chars', __('Please enter at least 3 characters for search.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $folder_id = $request->get_param('folder_id');
        $folder_id = $folder_id !== null && $folder_id !== '' ? (int) $folder_id : null;
        $form_id = $request->get_param('form_id');
        $form_id = $form_id !== null && $form_id !== '' ? (int) $form_id : null;
        if ($folder_id !== null && $form_id !== null) {
            return new WP_Error('griffinforms_invalid_filter_combination', __('Use either form_id or folder_id, not both.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $date_range = sanitize_key((string) $request->get_param('date_range'));
        $read_filter = sanitize_key((string) $request->get_param('read_filter'));
        $payment_filter = sanitize_key((string) $request->get_param('payment_filter'));
        $attachment_filter = sanitize_key((string) $request->get_param('attachment_filter'));
        $sort_by = sanitize_key((string) $request->get_param('sort_by'));
        $sort_dir = strtolower(sanitize_text_field((string) $request->get_param('sort_dir')));

        $sort_by_map = [
            'submission_name' => 's.id',
            'attachment_count' => 'attachment_count',
            'attachment_size' => 'attachment_size',
            'submission_status' => 's.status',
            'submission_date_time' => 's.created',
            'submitter_name' => 'u.display_name',
            'form_name' => 'f.name',
        ];
        if ($sort_by === '') {
            $sort_by = 'submission_date_time';
        }
        if (!isset($sort_by_map[$sort_by])) {
            return new WP_Error('griffinforms_invalid_sort_by', __('Invalid sort_by parameter.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        if ($sort_dir === '') {
            $sort_dir = 'desc';
        }
        if (!in_array($sort_dir, ['asc', 'desc'], true)) {
            return new WP_Error('griffinforms_invalid_sort_dir', __('Invalid sort_dir parameter.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        $sort_dir = strtoupper($sort_dir);

        $sub_table = $this->config->getTable('submission');
        $form_table = $this->config->getTable('form');
        $files_table = $this->config->getTable('files');
        $users_table = $wpdb->users;

        $where = ['1=1'];
        $params = [];

        if ($form_id !== null && $form_id > 0) {
            $where[] = 's.form_id = %d';
            $params[] = $form_id;
        } elseif ($folder_id !== null) {
            $where[] = 'f.folder = %d';
            $params[] = $folder_id;
        }

        if ($search !== '') {
            $like = '%' . $wpdb->esc_like($search) . '%';
            $where[] = '(s.submission LIKE %s OR f.name LIKE %s OR f.heading LIKE %s OR f.description LIKE %s)';
            array_push($params, $like, $like, $like, $like);
        }

        $allowed_date_ranges = ['', 'today', 'last_2_days', 'this_week', 'this_month', 'last_15_days'];
        if (!in_array($date_range, $allowed_date_ranges, true)) {
            return new WP_Error('griffinforms_invalid_date_range', __('Invalid date_range parameter.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $allowed_read_filters = ['', 'all', 'unread'];
        if (!in_array($read_filter, $allowed_read_filters, true)) {
            return new WP_Error('griffinforms_invalid_read_filter', __('Invalid read_filter parameter.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        if ($read_filter === '') {
            $read_filter = 'all';
        }

        $allowed_payment_filters = ['', 'all', 'payment_incomplete'];
        if (!in_array($payment_filter, $allowed_payment_filters, true)) {
            return new WP_Error('griffinforms_invalid_payment_filter', __('Invalid payment_filter parameter.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        if ($payment_filter === '') {
            $payment_filter = 'all';
        }

        $allowed_attachment_filters = ['', 'all', 'has_attachment'];
        if (!in_array($attachment_filter, $allowed_attachment_filters, true)) {
            return new WP_Error('griffinforms_invalid_attachment_filter', __('Invalid attachment_filter parameter.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        if ($attachment_filter === '') {
            $attachment_filter = 'all';
        }

        if ($read_filter === 'unread') {
            $where[] = 's.is_read = %d';
            $params[] = 0;
        }

        if ($payment_filter === 'payment_incomplete') {
            $where[] = "(LOWER(TRIM(COALESCE(s.payment_status, 'pending'))) <> %s)";
            $params[] = 'paid';
        }

        if ($attachment_filter === 'has_attachment') {
            $where[] = "EXISTS (
                SELECT 1 FROM {$files_table} af
                WHERE af.submission_id = s.id AND af.status = %s
            )";
            $params[] = 'attached';
        }

        [$date_from, $date_to] = $this->resolveDateRange($date_range);
        if ($date_from !== null) {
            $where[] = 's.created >= %s';
            $params[] = $date_from;
        }
        if ($date_to !== null) {
            $where[] = 's.created <= %s';
            $params[] = $date_to;
        }

        $where_sql = implode(' AND ', $where);

        $count_sql = "SELECT COUNT(*)
                      FROM {$sub_table} s
                      LEFT JOIN {$form_table} f ON f.id = s.form_id
                      WHERE {$where_sql}";
        $total = (int) $wpdb->get_var(empty($params) ? $count_sql : $wpdb->prepare($count_sql, ...$params));

        $sql = "SELECT s.id, s.form_id, s.status, s.is_read, s.created, s.author, s.submission_meta,
                       f.name AS form_name,
                       COUNT(fl.id) AS attachment_count,
                       COALESCE(SUM(fl.size_bytes), 0) AS attachment_size,
                       u.display_name AS submitter_name,
                       u.user_email AS submitter_email
                FROM {$sub_table} s
                LEFT JOIN {$form_table} f ON f.id = s.form_id
                LEFT JOIN {$files_table} fl ON fl.submission_id = s.id AND fl.status = 'attached'
                LEFT JOIN {$users_table} u ON u.ID = CAST(s.author AS UNSIGNED)
                WHERE {$where_sql}
                GROUP BY s.id
                ORDER BY {$sort_by_map[$sort_by]} {$sort_dir}, s.id DESC
                LIMIT %d OFFSET %d";

        $query_params = array_merge($params, [$per_page, $offset]);
        $rows = $wpdb->get_results($wpdb->prepare($sql, ...$query_params), ARRAY_A);

        $sql_format = AdminSqlFormat::getInstance();

        $data = [];
        foreach ((array) $rows as $row) {
            $submission_meta = $this->decodeArray($row['submission_meta'] ?? '');
            $compliance_profile = '';
            if (is_array($submission_meta) && !empty($submission_meta['compliance']['profile'])) {
                $compliance_profile = sanitize_text_field((string) $submission_meta['compliance']['profile']);
            }

            $author_id = absint($row['author'] ?? 0);
            $submitter_email = sanitize_email((string) ($row['submitter_email'] ?? ''));

            $data[] = [
                'id' => (int) $row['id'],
                'submission_name' => $sql_format->getSubmissionDisplayName((int) $row['id']),
                'submission_attachment_count' => (int) ($row['attachment_count'] ?? 0),
                'submission_attachment_size' => (int) ($row['attachment_size'] ?? 0),
                'submission_compliance_profile' => $compliance_profile,
                'submission_status' => (int) ($row['status'] ?? 0) === 1 ? 'complete' : 'incomplete',
                'submission_read_state' => ((int) ($row['is_read'] ?? 0) === 1) ? 'read' : 'unread',
                'submission_date_time' => (string) ($row['created'] ?? ''),
                'submitter_name' => sanitize_text_field((string) ($row['submitter_name'] ?? '')),
                'submitter_avatar_url' => $this->buildSubmitterAvatarUrl($author_id, $submitter_email),
                'form_id' => (int) ($row['form_id'] ?? 0),
                'form_name' => sanitize_text_field((string) ($row['form_name'] ?? '')),
            ];
        }

        Capabilities::logAudit('submissions_list_viewed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'submission_collection',
            'meta' => [
                'page' => $page,
                'per_page' => $per_page,
                'search' => $search,
                'date_range' => $date_range,
                'read_filter' => $read_filter,
                'payment_filter' => $payment_filter,
                'attachment_filter' => $attachment_filter,
                'form_id' => $form_id,
                'folder_id' => $folder_id,
                'sort_by' => $sort_by,
                'sort_dir' => strtolower($sort_dir),
                'result_count' => count($data),
            ],
        ]));

        return $this->success($data, [
            'page' => $page,
            'per_page' => $per_page,
            'total' => $total,
            'has_more' => ($offset + count($data)) < $total,
            'sort_by' => $sort_by,
            'sort_dir' => strtolower($sort_dir),
            'search' => $search,
            'date_range' => $date_range,
            'read_filter' => $read_filter,
            'payment_filter' => $payment_filter,
            'attachment_filter' => $attachment_filter,
            'form_id' => $form_id,
            'folder_id' => $folder_id,
            'request_id' => $this->requestId(),
        ]);
    }

    public function getSubmissionSummary(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $submission_id = absint($request['submission_id']);
        if ($submission_id <= 0) {
            return $this->apiError('submission_summary_view_failed', 'griffinforms_invalid_submission', __('Invalid submission.', 'griffinforms-form-builder'), 400, [
                'target_type' => 'submission',
                'target_id' => $submission_id,
            ]);
        }

        $data = $this->fetchSubmissionSummaryById($submission_id);
        if ($data === null) {
            return $this->apiError('submission_summary_view_failed', 'griffinforms_submission_not_found', __('Submission not found.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'submission',
                'target_id' => $submission_id,
            ]);
        }

        $this->markSubmissionAsRead($submission_id);
        $data['submission_read_state'] = 'read';

        Capabilities::logAudit('submission_summary_viewed', 'success', $this->withEndpointMeta([
            'submission_id' => $submission_id,
            'target_type' => 'submission',
            'target_id' => $submission_id,
            'request_id' => $this->requestId(),
        ]));

        return $this->success($data, [
            'request_id' => $this->requestId(),
        ]);
    }

    public function getSubmissionDetail(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $submission_id = absint($request['submission_id']);
        if ($submission_id <= 0) {
            return $this->apiError('submission_detail_view_failed', 'griffinforms_invalid_submission', __('Invalid submission.', 'griffinforms-form-builder'), 400, [
                'target_type' => 'submission',
                'target_id' => $submission_id,
            ]);
        }

        $sub_table = $this->config->getTable('submission');
        $form_table = $this->config->getTable('form');
        $log_table = $this->config->getTable('log');
        $users_table = $wpdb->users;

        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$sub_table} WHERE id = %d LIMIT 1", $submission_id),
            ARRAY_A
        );

        if (!is_array($row)) {
            return $this->apiError('submission_detail_view_failed', 'griffinforms_submission_not_found', __('Submission not found.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'submission',
                'target_id' => $submission_id,
            ]);
        }

        $this->markSubmissionAsRead($submission_id);

        $form_id = (int) ($row['form_id'] ?? 0);
        $author_id = absint($row['author'] ?? 0);
        $form_row = $wpdb->get_row(
            $wpdb->prepare("SELECT id, name, heading, description, compliance_profile, compliance_mode, last_edited FROM {$form_table} WHERE id = %d LIMIT 1", $form_id),
            ARRAY_A
        );
        $author_row = [];
        if ($author_id > 0) {
            $author_row = $wpdb->get_row(
                $wpdb->prepare("SELECT display_name, user_email FROM {$users_table} WHERE ID = %d LIMIT 1", $author_id),
                ARRAY_A
            );
        }

        $submission_payload = $this->decodeArray($row['submission'] ?? '');
        $submission_meta = $this->decodeArray($row['submission_meta'] ?? '');
        $payment_meta = $this->decodeArray($row['payment_meta'] ?? '');

        $event_timeline = is_array($submission_meta) && isset($submission_meta['events']) && is_array($submission_meta['events'])
            ? $submission_meta['events']
            : [];

        $logs = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id, log_type, item_type, item_id, user_id, message, category, date_time
                 FROM {$log_table}
                 WHERE item_type = %s AND item_id = %d
                 ORDER BY id DESC
                 LIMIT 100",
                'submission',
                $submission_id
            ),
            ARRAY_A
        );

        $data = [
            'id' => $submission_id,
            'submission_status' => (int) ($row['status'] ?? 0) === 1 ? 'complete' : 'incomplete',
            'submission_read_state' => 'read',
            'submission_date_time' => (string) ($row['created'] ?? ''),
            'submitter_name' => sanitize_text_field((string) ($author_row['display_name'] ?? '')),
            'submitter_email' => sanitize_email((string) ($author_row['user_email'] ?? '')),
            'submitter_avatar_url' => $this->buildSubmitterAvatarUrl(
                $author_id,
                sanitize_email((string) ($author_row['user_email'] ?? ''))
            ),
            'submission_payload' => $submission_payload,
            'accounts' => $this->buildSubmissionAccountsResponse($submission_id),
            'submission_events_timeline' => $event_timeline,
            'logs_timeline' => array_values((array) $logs),
            'parent_form_data' => [
                'id' => (int) ($form_row['id'] ?? 0),
                'name' => sanitize_text_field((string) ($form_row['name'] ?? '')),
                'heading' => sanitize_text_field((string) ($form_row['heading'] ?? '')),
                'description' => sanitize_textarea_field((string) ($form_row['description'] ?? '')),
                'compliance_profile' => sanitize_text_field((string) ($form_row['compliance_profile'] ?? '')),
                'compliance_mode' => (int) ($form_row['compliance_mode'] ?? 0),
                'last_edit_date' => (string) ($form_row['last_edited'] ?? ''),
            ],
            'payment_log' => is_array($payment_meta) ? $payment_meta : [],
        ];

        Capabilities::logAudit('submission_detail_viewed', 'success', $this->withEndpointMeta([
            'submission_id' => $submission_id,
            'target_type' => 'submission',
            'target_id' => $submission_id,
            'request_id' => $this->requestId(),
        ]));

        return $this->success($data, [
            'request_id' => $this->requestId(),
        ]);
    }

    private function buildSubmissionAccountsResponse(int $submission_id): array
    {
        if ($submission_id <= 0) {
            return [];
        }

        $state_store = new ActionStateStore();
        $state = $state_store->getAccountsState($submission_id);
        if (empty($state) || empty($state['setups']) || !is_array($state['setups'])) {
            return [];
        }

        $view_model = (new AdminActionService())->getSubmissionAccountsViewModel($submission_id);
        if (empty($view_model) || !is_array($view_model)) {
            return [];
        }

        return [
            'submission_id' => $submission_id,
            'integration_id' => sanitize_key((string) ($view_model['integration_id'] ?? '')),
            'contract_phase' => sanitize_key((string) ($view_model['contract_phase'] ?? '')),
            'updated_at' => (string) ($view_model['updated_at'] ?? ''),
            'summary' => $this->sanitizeAccountsSummary($state['summary'] ?? []),
            'setups' => array_values(is_array($view_model['setups'] ?? null) ? $view_model['setups'] : []),
        ];
    }

    private function sanitizeAccountsSummary($summary): array
    {
        if (!is_array($summary)) {
            return [];
        }

        $counts = isset($summary['counts']) && is_array($summary['counts']) ? $summary['counts'] : [];
        $count_buckets = [
            'setups' => absint($counts['setups'] ?? 0),
            'targets' => absint($counts['targets'] ?? 0),
            'created_users' => absint($counts['created_users'] ?? 0),
            'updated_users' => absint($counts['updated_users'] ?? 0),
            'accepted_noop_users' => absint($counts['accepted_noop_users'] ?? 0),
            'all_users' => absint($counts['all_users'] ?? 0),
            'by_result' => [],
            'by_status' => [],
        ];

        foreach ((array) ($counts['by_result'] ?? []) as $key => $value) {
            $sanitized_key = sanitize_key((string) $key);
            if ($sanitized_key === '') {
                continue;
            }
            $count_buckets['by_result'][$sanitized_key] = absint($value);
        }

        foreach ((array) ($counts['by_status'] ?? []) as $key => $value) {
            $sanitized_key = sanitize_key((string) $key);
            if ($sanitized_key === '') {
                continue;
            }
            $count_buckets['by_status'][$sanitized_key] = absint($value);
        }

        return [
            'created_user_ids' => array_values(array_map('absint', array_filter((array) ($summary['created_user_ids'] ?? [])))),
            'updated_user_ids' => array_values(array_map('absint', array_filter((array) ($summary['updated_user_ids'] ?? [])))),
            'accepted_noop_user_ids' => array_values(array_map('absint', array_filter((array) ($summary['accepted_noop_user_ids'] ?? [])))),
            'all_user_ids' => array_values(array_map('absint', array_filter((array) ($summary['all_user_ids'] ?? [])))),
            'counts' => $count_buckets,
        ];
    }

    public function setSubmissionReadState(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $submission_id = absint($request->get_param('submission_id'));
        $raw_state = $request->get_param('read_state');
        if ($submission_id <= 0 || $raw_state === null) {
            return new WP_Error('griffinforms_invalid_payload', __('submission_id and read_state are required.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $state = is_string($raw_state) ? strtolower(trim($raw_state)) : $raw_state;
        $is_read = null;
        if ($state === 'read' || $state === '1' || $state === 1 || $state === true) {
            $is_read = 1;
        } elseif ($state === 'unread' || $state === '0' || $state === 0 || $state === false) {
            $is_read = 0;
        }

        if ($is_read === null) {
            return new WP_Error('griffinforms_invalid_read_state', __('read_state must be read/unread (or 1/0).', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $sub_table = $this->config->getTable('submission');
        $existing = $wpdb->get_row(
            $wpdb->prepare("SELECT id, is_read FROM %i WHERE id = %d LIMIT 1", $sub_table, $submission_id),
            ARRAY_A
        );
        if (!is_array($existing)) {
            return new WP_Error('griffinforms_submission_not_found', __('Submission not found.', 'griffinforms-form-builder'), ['status' => 404]);
        }

        $old_state = (int) ($existing['is_read'] ?? 0);
        if ($old_state === $is_read) {
            return $this->success([
                'submission_id' => $submission_id,
                'old_read_state' => $old_state === 1 ? 'read' : 'unread',
                'new_read_state' => $is_read === 1 ? 'read' : 'unread',
                'updated' => false,
            ], [
                'request_id' => $this->requestId(),
            ]);
        }

        $updated = $wpdb->update(
            $sub_table,
            ['is_read' => $is_read],
            ['id' => $submission_id],
            ['%d'],
            ['%d']
        );
        if ($updated === false) {
            return new WP_Error('griffinforms_read_state_update_failed', __('Unable to update submission read state.', 'griffinforms-form-builder'), ['status' => 500]);
        }

        Capabilities::logAudit('submission_read_state_changed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'submission',
            'target_id' => $submission_id,
            'meta' => [
                'old_read_state' => $old_state === 1 ? 'read' : 'unread',
                'new_read_state' => $is_read === 1 ? 'read' : 'unread',
            ],
        ]));

        return $this->success([
            'submission_id' => $submission_id,
            'old_read_state' => $old_state === 1 ? 'read' : 'unread',
            'new_read_state' => $is_read === 1 ? 'read' : 'unread',
            'updated' => true,
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function getRecentLogs(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $count = absint($request->get_param('count'));
        if ($count <= 0) {
            $count = 20;
        }
        if ($count > 200) {
            return new WP_Error('griffinforms_count_limit', __('count cannot be greater than 200.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $item_type = sanitize_key((string) $request->get_param('item_type'));
        $item_id = absint($request->get_param('item_id'));
        if ($item_type === '' || $item_id <= 0) {
            return new WP_Error('griffinforms_invalid_log_filter', __('item_type and item_id are required.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $log_table = $this->config->getTable('log');
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id, log_type, category, item_type, item_id, user_id, message, date_time
                 FROM %i
                 WHERE item_type = %s AND item_id = %d
                 ORDER BY id DESC
                 LIMIT %d",
                $log_table,
                $item_type,
                $item_id,
                $count
            ),
            ARRAY_A
        );

        $data = array_values(array_map(static function ($row) {
            return [
                'id' => absint($row['id'] ?? 0),
                'log_type' => sanitize_key((string) ($row['log_type'] ?? '')),
                'category' => sanitize_text_field((string) ($row['category'] ?? '')),
                'item_type' => sanitize_key((string) ($row['item_type'] ?? '')),
                'item_id' => absint($row['item_id'] ?? 0),
                'user_id' => absint($row['user_id'] ?? 0),
                'message' => sanitize_text_field((string) ($row['message'] ?? '')),
                'date_time' => (string) ($row['date_time'] ?? ''),
            ];
        }, (array) $rows));

        Capabilities::logAudit('logs_recent_viewed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => $item_type . '_logs',
            'target_id' => $item_id,
            'meta' => [
                'count' => $count,
                'result_count' => count($data),
            ],
        ]));

        return $this->success($data, [
            'item_type' => $item_type,
            'item_id' => $item_id,
            'count' => $count,
            'request_id' => $this->requestId(),
        ]);
    }

    public function getSubmissionPdf(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $submission_id = absint($request['submission_id']);
        if ($submission_id <= 0) {
            return new WP_Error('griffinforms_invalid_submission', __('Invalid submission.', 'griffinforms-form-builder'), ['status' => 400]);
        }

        $detail_request = new WP_REST_Request('GET', self::NAMESPACE . '/submissions/' . $submission_id . '/detail');
        $detail_request->set_param('submission_id', $submission_id);
        $detail = $this->getSubmissionDetail($detail_request);
        if ($detail instanceof WP_Error) {
            return $detail;
        }

        $detail_data = $detail->get_data()['data'] ?? [];
        $lines = [
            'GriffinForms Submission Export',
            'Submission ID: ' . $submission_id,
            'Status: ' . ($detail_data['submission_status'] ?? ''),
            'Date: ' . ($detail_data['submission_date_time'] ?? ''),
            'Form: ' . ($detail_data['parent_form_data']['name'] ?? ''),
        ];

        $pdf_binary = $this->buildSimplePdf($lines);
        $filename = 'submission-' . $submission_id . '.pdf';

        Capabilities::logAudit('submission_pdf_exported', 'success', $this->withEndpointMeta([
            'submission_id' => $submission_id,
            'target_type' => 'submission',
            'target_id' => $submission_id,
            'request_id' => $this->requestId(),
        ]));

        return $this->success([
            'submission_id' => $submission_id,
            'filename' => $filename,
            'mime' => 'application/pdf',
            'content_base64' => base64_encode($pdf_binary),
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function getSubmissionAttachments(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $submission_id = absint($request['submission_id']);
        if ($submission_id <= 0) {
            return $this->apiError('submission_attachments_view_failed', 'griffinforms_invalid_submission', __('Invalid submission.', 'griffinforms-form-builder'), 400, [
                'target_type' => 'submission',
                'target_id' => $submission_id,
            ]);
        }
        if (!$this->submissionExists($submission_id)) {
            return $this->apiError('submission_attachments_view_failed', 'griffinforms_submission_not_found', __('Submission not found.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'submission',
                'target_id' => $submission_id,
            ]);
        }

        $files_table = $this->config->getTable('files');
        $sub_table = $this->config->getTable('submission');
        $uploaded_by_sql = $this->tableHasColumn($files_table, 'uploaded_by')
            ? ', uploaded_by'
            : ', 0 AS uploaded_by';

        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id, submission_id, status, path, url, mime, size_bytes, created_at{$uploaded_by_sql}, token
                 FROM %i
                 WHERE submission_id = %d AND status = %s
                 ORDER BY id DESC",
                $files_table,
                $submission_id,
                'attached'
            ),
            ARRAY_A
        );

        if ($rows === null && !empty($wpdb->last_error)) {
            return $this->apiError('submission_attachments_view_failed', 'griffinforms_db_query_failed', __('Unable to load submission attachments.', 'griffinforms-form-builder'), 500, [
                'target_type' => 'submission',
                'target_id' => $submission_id,
                'meta' => [
                    'db_error' => sanitize_text_field((string) $wpdb->last_error),
                ],
            ]);
        }

        $attachment_rows_by_id = [];
        foreach ((array) $rows as $row) {
            $file_id = absint($row['id'] ?? 0);
            if ($file_id <= 0) {
                continue;
            }
            $attachment_rows_by_id[$file_id] = $row;
        }

        $submission_payload_raw = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT submission FROM {$sub_table} WHERE id = %d LIMIT 1",
                $submission_id
            )
        );

        $payload_tokens = $this->extractFileTokensFromSubmissionPayload(
            $this->decodeArray($submission_payload_raw)
        );

        if (!empty($payload_tokens)) {
            if (count($payload_tokens) > 200) {
                return $this->apiError(
                    'submission_attachments_view_failed',
                    'griffinforms_attachment_token_limit',
                    __('Too many attachment tokens in submission payload.', 'griffinforms-form-builder'),
                    400,
                    [
                        'target_type' => 'submission',
                        'target_id' => $submission_id,
                        'meta' => [
                            'token_count' => count($payload_tokens),
                        ],
                    ]
                );
            }
            $token_placeholders = implode(', ', array_fill(0, count($payload_tokens), '%s'));
            $token_sql = "SELECT id, submission_id, status, path, url, mime, size_bytes, created_at{$uploaded_by_sql}, token
                          FROM {$files_table}
                          WHERE token IN ({$token_placeholders}) AND status = %s
                          ORDER BY id DESC";

            $token_query_params = array_merge($payload_tokens, ['attached']);
            $token_rows = $wpdb->get_results(
                $wpdb->prepare($token_sql, ...$token_query_params),
                ARRAY_A
            );

            if ($token_rows === null && !empty($wpdb->last_error)) {
                return $this->apiError('submission_attachments_view_failed', 'griffinforms_db_query_failed', __('Unable to resolve token-linked attachments.', 'griffinforms-form-builder'), 500, [
                    'target_type' => 'submission',
                    'target_id' => $submission_id,
                    'meta' => [
                        'db_error' => sanitize_text_field((string) $wpdb->last_error),
                        'token_count' => count($payload_tokens),
                    ],
                ]);
            }

            foreach ((array) $token_rows as $token_row) {
                $file_id = absint($token_row['id'] ?? 0);
                if ($file_id <= 0) {
                    continue;
                }
                $attachment_rows_by_id[$file_id] = $token_row;
            }
        }

        usort($attachment_rows_by_id, static function ($left, $right): int {
            return absint($right['id'] ?? 0) <=> absint($left['id'] ?? 0);
        });

        $data = [];
        foreach ($attachment_rows_by_id as $row) {
            $data[] = $this->mapAttachmentResponseRow($row, $submission_id);
        }

        Capabilities::logAudit('submission_attachments_viewed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'submission',
            'target_id' => $submission_id,
            'meta' => [
                'result_count' => count($data),
                'token_fallback_used' => !empty($payload_tokens),
            ],
        ]));

        return $this->success($data, [
            'submission_id' => $submission_id,
            'request_id' => $this->requestId(),
        ]);
    }

    public function downloadSubmissionAttachment(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $submission_id = absint($request['submission_id']);
        $attachment_id = absint($request['attachment_id']);
        if ($submission_id <= 0 || $attachment_id <= 0) {
            return $this->apiError('submission_attachment_download_failed', 'griffinforms_invalid_payload', __('submission_id and attachment_id are required.', 'griffinforms-form-builder'), 400, [
                'target_type' => 'file',
                'target_id' => $attachment_id,
                'meta' => [
                    'submission_id' => $submission_id,
                ],
            ]);
        }
        if (!$this->submissionExists($submission_id)) {
            return $this->apiError('submission_attachment_download_failed', 'griffinforms_submission_not_found', __('Submission not found.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'submission',
                'target_id' => $submission_id,
                'meta' => [
                    'attachment_id' => $attachment_id,
                ],
            ]);
        }

        $files_table = $this->config->getTable('files');
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT id, submission_id, status, path, url, mime, size_bytes
                 FROM %i
                 WHERE id = %d AND submission_id = %d
                 LIMIT 1",
                $files_table,
                $attachment_id,
                $submission_id
            ),
            ARRAY_A
        );

        if (!is_array($row)) {
            return $this->apiError('submission_attachment_download_failed', 'griffinforms_file_not_found', __('Attachment not found.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'file',
                'target_id' => $attachment_id,
                'meta' => [
                    'submission_id' => $submission_id,
                ],
            ]);
        }

        $status = sanitize_key((string) ($row['status'] ?? ''));
        if ($status !== 'attached') {
            return $this->apiError('submission_attachment_download_failed', 'griffinforms_file_not_available', __('Attachment is not available for download.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'file',
                'target_id' => $attachment_id,
                'meta' => [
                    'submission_id' => $submission_id,
                    'file_status' => $status,
                ],
            ]);
        }

        $path = (string) ($row['path'] ?? '');
        $url = (string) ($row['url'] ?? '');
        $binary = '';

        if ($path !== '' && file_exists($path)) {
            $contents = @file_get_contents($path);
            if (is_string($contents)) {
                $binary = $contents;
            }
        } elseif ($url !== '') {
            if (!$this->isTrustedAttachmentUrl($url)) {
                return $this->apiError('submission_attachment_download_failed', 'griffinforms_file_source_untrusted', __('Attachment URL is not trusted for download.', 'griffinforms-form-builder'), 422, [
                    'target_type' => 'file',
                    'target_id' => $attachment_id,
                    'meta' => [
                        'submission_id' => $submission_id,
                    ],
                ]);
            }
            $response = wp_safe_remote_get($url, [
                'timeout' => 20,
                'redirection' => 3,
            ]);
            if (!is_wp_error($response) && (int) wp_remote_retrieve_response_code($response) === 200) {
                $body = wp_remote_retrieve_body($response);
                if (is_string($body)) {
                    $binary = $body;
                }
            }
        }

        if ($binary === '') {
            return $this->apiError('submission_attachment_download_failed', 'griffinforms_file_read_failed', __('Unable to read file binary.', 'griffinforms-form-builder'), 500, [
                'target_type' => 'file',
                'target_id' => $attachment_id,
                'meta' => [
                    'submission_id' => $submission_id,
                ],
            ]);
        }

        $mime = sanitize_text_field((string) ($row['mime'] ?? ''));
        if ($mime === '') {
            $mime = 'application/octet-stream';
        }

        Capabilities::logAudit('submission_attachment_downloaded', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'file',
            'target_id' => $attachment_id,
            'meta' => [
                'submission_id' => $submission_id,
                'mime' => $mime,
                'size_bytes' => strlen($binary),
            ],
        ]));

        return $this->success([
            'id' => $attachment_id,
            'submission_id' => $submission_id,
            'filename' => $this->resolveAttachmentFilename($path, $url, $attachment_id),
            'mime' => $mime,
            'size_bytes' => absint($row['size_bytes'] ?? 0),
            'content_base64' => base64_encode($binary),
            'encoding' => 'base64',
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function getFileBinary(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $file_id = absint($request['file_id']);
        if ($file_id <= 0) {
            return $this->apiError('file_binary_view_failed', 'griffinforms_invalid_file', __('Invalid file.', 'griffinforms-form-builder'), 400, [
                'target_type' => 'file',
                'target_id' => $file_id,
            ]);
        }

        $files_table = $this->config->getTable('files');
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT id, submission_id, status, path, url, mime, size_bytes
                 FROM %i
                 WHERE id = %d
                 LIMIT 1",
                $files_table,
                $file_id
            ),
            ARRAY_A
        );

        if (!is_array($row)) {
            return $this->apiError('file_binary_view_failed', 'griffinforms_file_not_found', __('File not found.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'file',
                'target_id' => $file_id,
            ]);
        }

        $status = sanitize_key((string) ($row['status'] ?? ''));
        if ($status !== 'attached') {
            return $this->apiError('file_binary_view_failed', 'griffinforms_file_not_available', __('File is not available for download.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'file',
                'target_id' => $file_id,
                'meta' => [
                    'file_status' => $status,
                ],
            ]);
        }

        $path = (string) ($row['path'] ?? '');
        $url = (string) ($row['url'] ?? '');
        $binary = '';

        if ($path !== '' && file_exists($path)) {
            $contents = @file_get_contents($path);
            if (is_string($contents)) {
                $binary = $contents;
            }
        } elseif ($url !== '') {
            if (!$this->isTrustedAttachmentUrl($url)) {
                return $this->apiError('file_binary_view_failed', 'griffinforms_file_source_untrusted', __('Attachment URL is not trusted for download.', 'griffinforms-form-builder'), 422, [
                    'target_type' => 'file',
                    'target_id' => $file_id,
                ]);
            }
            $response = wp_safe_remote_get($url, [
                'timeout' => 20,
                'redirection' => 3,
            ]);
            if (!is_wp_error($response) && (int) wp_remote_retrieve_response_code($response) === 200) {
                $body = wp_remote_retrieve_body($response);
                if (is_string($body)) {
                    $binary = $body;
                }
            }
        }

        if ($binary === '') {
            return $this->apiError('file_binary_view_failed', 'griffinforms_file_read_failed', __('Unable to read file binary.', 'griffinforms-form-builder'), 500, [
                'target_type' => 'file',
                'target_id' => $file_id,
            ]);
        }

        $filename = $this->resolveAttachmentFilename($path, $url, $file_id);
        $mime = sanitize_text_field((string) ($row['mime'] ?? ''));
        if ($mime === '') {
            $mime = 'application/octet-stream';
        }

        Capabilities::logAudit('file_binary_viewed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'file',
            'target_id' => $file_id,
            'meta' => [
                'submission_id' => absint($row['submission_id'] ?? 0),
                'mime' => $mime,
                'size_bytes' => strlen($binary),
            ],
        ]));

        return $this->success([
            'id' => $file_id,
            'submission_id' => absint($row['submission_id'] ?? 0),
            'filename' => $filename,
            'mime' => $mime,
            'size_bytes' => absint($row['size_bytes'] ?? 0),
            'content_base64' => base64_encode($binary),
            'encoding' => 'base64',
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function getFileThumbnail(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $file_id = absint($request['file_id']);
        if ($file_id <= 0) {
            return $this->apiError('file_thumbnail_view_failed', 'griffinforms_invalid_file', __('Invalid file.', 'griffinforms-form-builder'), 400, [
                'target_type' => 'file',
                'target_id' => $file_id,
            ]);
        }

        $raw_max_dim = $request->get_param('max_dim');
        $max_dim = absint($raw_max_dim);
        if ($raw_max_dim === null || $raw_max_dim === '') {
            $max_dim = 256;
        }
        if ($max_dim < 32 || $max_dim > 1024) {
            return $this->apiError('file_thumbnail_view_failed', 'griffinforms_invalid_max_dim', __('max_dim must be between 32 and 1024.', 'griffinforms-form-builder'), 400, [
                'target_type' => 'file',
                'target_id' => $file_id,
                'meta' => [
                    'max_dim' => $max_dim,
                ],
            ]);
        }

        $files_table = $this->config->getTable('files');
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT id, submission_id, status, path, mime
                 FROM %i
                 WHERE id = %d
                 LIMIT 1",
                $files_table,
                $file_id
            ),
            ARRAY_A
        );

        if (!is_array($row)) {
            return $this->apiError('file_thumbnail_view_failed', 'griffinforms_file_not_found', __('File not found.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'file',
                'target_id' => $file_id,
            ]);
        }

        $status = sanitize_key((string) ($row['status'] ?? ''));
        if ($status !== 'attached') {
            return $this->apiError('file_thumbnail_view_failed', 'griffinforms_file_not_available', __('File is not available for thumbnail.', 'griffinforms-form-builder'), 404, [
                'target_type' => 'file',
                'target_id' => $file_id,
                'meta' => [
                    'file_status' => $status,
                ],
            ]);
        }

        $mime = sanitize_text_field((string) ($row['mime'] ?? ''));
        if (strpos($mime, 'image/') !== 0) {
            return $this->apiError('file_thumbnail_view_failed', 'griffinforms_thumbnail_unavailable', __('Thumbnail is available only for image attachments.', 'griffinforms-form-builder'), 422, [
                'target_type' => 'file',
                'target_id' => $file_id,
                'meta' => [
                    'mime' => $mime,
                ],
            ]);
        }

        $path = (string) ($row['path'] ?? '');
        if ($path === '' || !file_exists($path)) {
            return $this->apiError('file_thumbnail_view_failed', 'griffinforms_thumbnail_unavailable', __('Image file path is unavailable for thumbnail generation.', 'griffinforms-form-builder'), 422, [
                'target_type' => 'file',
                'target_id' => $file_id,
            ]);
        }

        if (!function_exists('wp_get_image_editor')) {
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }

        $editor = wp_get_image_editor($path);
        if (is_wp_error($editor)) {
            return $this->apiError('file_thumbnail_view_failed', 'griffinforms_thumbnail_generation_failed', __('Unable to initialize image editor.', 'griffinforms-form-builder'), 500, [
                'target_type' => 'file',
                'target_id' => $file_id,
            ]);
        }

        $resize = $editor->resize($max_dim, $max_dim, false);
        if (is_wp_error($resize)) {
            return $this->apiError('file_thumbnail_view_failed', 'griffinforms_thumbnail_generation_failed', __('Unable to resize image.', 'griffinforms-form-builder'), 500, [
                'target_type' => 'file',
                'target_id' => $file_id,
                'meta' => [
                    'max_dim' => $max_dim,
                ],
            ]);
        }

        $saved = $editor->save();
        if (!is_array($saved) || empty($saved['path'])) {
            return $this->apiError('file_thumbnail_view_failed', 'griffinforms_thumbnail_generation_failed', __('Unable to generate thumbnail.', 'griffinforms-form-builder'), 500, [
                'target_type' => 'file',
                'target_id' => $file_id,
            ]);
        }

        $thumb_path = (string) $saved['path'];
        $thumb_mime = sanitize_text_field((string) ($saved['mime-type'] ?? $mime));
        $thumb_binary = @file_get_contents($thumb_path);
        if (!is_string($thumb_binary) || $thumb_binary === '') {
            return $this->apiError('file_thumbnail_view_failed', 'griffinforms_thumbnail_generation_failed', __('Unable to read thumbnail binary.', 'griffinforms-form-builder'), 500, [
                'target_type' => 'file',
                'target_id' => $file_id,
            ]);
        }

        // Cleanup generated derivative thumbnail if it is not the original file path.
        if ($thumb_path !== $path && file_exists($thumb_path)) {
            @unlink($thumb_path);
        }

        Capabilities::logAudit('file_thumbnail_viewed', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'file',
            'target_id' => $file_id,
            'meta' => [
                'submission_id' => absint($row['submission_id'] ?? 0),
                'max_dim' => $max_dim,
                'mime' => $thumb_mime,
                'size_bytes' => strlen($thumb_binary),
            ],
        ]));

        return $this->success([
            'id' => $file_id,
            'submission_id' => absint($row['submission_id'] ?? 0),
            'mime' => $thumb_mime,
            'max_dim' => $max_dim,
            'size_bytes' => strlen($thumb_binary),
            'content_base64' => base64_encode($thumb_binary),
            'encoding' => 'base64',
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function createShareLink(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $submission_id = absint($request['submission_id']);
        if ($submission_id <= 0) {
            return new WP_Error('griffinforms_invalid_submission', __('Invalid submission.', 'griffinforms-form-builder'), ['status' => 400]);
        }
        if (!$this->submissionExists($submission_id)) {
            return new WP_Error('griffinforms_submission_not_found', __('Submission not found.', 'griffinforms-form-builder'), ['status' => 404]);
        }

        $expires_in_hours = absint($request->get_param('expires_in_hours'));
        if ($expires_in_hours <= 0) {
            $expires_in_hours = 24;
        }
        $expires_in_hours = min(168, $expires_in_hours);

        $token = wp_generate_password(40, false, false);
        $expires_at = time() + ($expires_in_hours * HOUR_IN_SECONDS);
        $key = $this->shareTokenKey($token);

        $existing_active = get_transient($this->shareActiveTokenKey($submission_id));
        if (is_string($existing_active) && $existing_active !== '') {
            delete_transient($this->shareTokenKey($existing_active));
        }

        set_transient($key, [
            'submission_id' => $submission_id,
            'issued_by' => get_current_user_id(),
            'expires_at' => $expires_at,
        ], $expires_in_hours * HOUR_IN_SECONDS);
        set_transient($this->shareActiveTokenKey($submission_id), $token, $expires_in_hours * HOUR_IN_SECONDS);

        $url = add_query_arg(
            ['share_token' => $token],
            rest_url(self::NAMESPACE . '/submissions/' . $submission_id . '/detail')
        );

        Capabilities::logAudit('submission_share_link_created', 'success', $this->withEndpointMeta([
            'submission_id' => $submission_id,
            'target_type' => 'submission',
            'target_id' => $submission_id,
            'request_id' => $this->requestId(),
            'expires_at' => gmdate('c', $expires_at),
        ]));

        return $this->success([
            'submission_id' => $submission_id,
            'url' => esc_url_raw($url),
            'expires_at' => gmdate('c', $expires_at),
            'expires_in_hours' => $expires_in_hours,
        ], [
            'request_id' => $this->requestId(),
        ]);
    }

    public function bulkDeleteSubmissions(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        global $wpdb;

        $idempotency_key = trim((string) $request->get_header('X-Idempotency-Key'));
        if ($idempotency_key === '') {
            return $this->apiError(
                'submissions_bulk_delete_failed',
                'griffinforms_idempotency_required',
                __('X-Idempotency-Key header is required.', 'griffinforms-form-builder'),
                400,
                ['target_type' => 'submission_collection']
            );
        }

        if (mb_strlen($idempotency_key) > 128 || !preg_match('/^[A-Za-z0-9._:-]+$/', $idempotency_key)) {
            return $this->apiError(
                'submissions_bulk_delete_failed',
                'griffinforms_invalid_idempotency_key',
                __('X-Idempotency-Key must be <= 128 characters and contain only letters, numbers, dot, underscore, colon, or hyphen.', 'griffinforms-form-builder'),
                400,
                ['target_type' => 'submission_collection']
            );
        }

        $idem_cache_key = 'gf_api_idem_' . md5(get_current_user_id() . '|' . $idempotency_key);
        $cached = get_transient($idem_cache_key);
        if (is_array($cached)) {
            Capabilities::logAudit('submissions_bulk_delete_idempotency_replayed', 'success', $this->withEndpointMeta([
                'request_id' => $this->requestId(),
                'target_type' => 'submission_collection',
                'meta' => [
                    'idempotency_key' => $idempotency_key,
                ],
            ]));
            return $this->success($cached['data'] ?? [], $cached['meta'] ?? []);
        }

        $ids = $request->get_param('submission_ids');
        if (!is_array($ids)) {
            return $this->apiError(
                'submissions_bulk_delete_failed',
                'griffinforms_invalid_payload',
                __('submission_ids must be an array.', 'griffinforms-form-builder'),
                400,
                ['target_type' => 'submission_collection']
            );
        }

        $ids = array_values(array_unique(array_filter(array_map('absint', $ids))));
        if (empty($ids)) {
            return $this->apiError(
                'submissions_bulk_delete_failed',
                'griffinforms_invalid_payload',
                __('No valid submission IDs provided.', 'griffinforms-form-builder'),
                400,
                ['target_type' => 'submission_collection']
            );
        }

        if (count($ids) > 100) {
            return $this->apiError(
                'submissions_bulk_delete_failed',
                'griffinforms_batch_limit',
                __('A maximum of 100 submission IDs is allowed per request.', 'griffinforms-form-builder'),
                400,
                ['target_type' => 'submission_collection']
            );
        }

        Capabilities::logAudit('submissions_bulk_delete_requested', 'success', $this->withEndpointMeta([
            'request_id' => $this->requestId(),
            'target_type' => 'submission_collection',
            'target_ids' => $ids,
            'meta' => [
                'idempotency_key' => $idempotency_key,
            ],
        ]));

        $sub_table = $this->config->getTable('submission');
        $sub_sql = AdminSubmissionsSql::getInstance();

        $deleted = [];
        $failed = [];
        $skipped = [];

        foreach ($ids as $submission_id) {
            $exists = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$sub_table} WHERE id = %d", $submission_id));
            if ($exists === 0) {
                $skipped[] = $submission_id;
                continue;
            }

            $sub_sql->deleteAttachmentsBySubmissionId($submission_id);
            \GriffinForms\Includes\Pipelines\SubmissionHistoryManager::getInstance()->deleteRevisionsBySubmissionId($submission_id);
            $result = $wpdb->delete($sub_table, ['id' => $submission_id], ['%d']);
            if ($result !== false && (int) $result > 0) {
                $this->log->deleteByItem('submission', $submission_id);
                $deleted[] = $submission_id;
                continue;
            }

            $failed[] = [
                'submission_id' => $submission_id,
                'error' => $wpdb->last_error,
            ];
        }

        $outcome = !empty($failed) ? (!empty($deleted) ? 'partial' : 'failed') : 'success';
        $event_name = 'submissions_bulk_delete_completed';
        if ($outcome === 'partial') {
            $event_name = 'submissions_bulk_delete_partially_failed';
        } elseif ($outcome === 'failed') {
            $event_name = 'submissions_bulk_delete_failed';
        }
        Capabilities::logAudit($event_name, $outcome, $this->withEndpointMeta([
            'target_ids' => $ids,
            'target_type' => 'submission_collection',
            'deleted' => $deleted,
            'skipped' => $skipped,
            'failed_count' => count($failed),
            'request_id' => $this->requestId(),
        ]));

        $data = [
            'requested_count' => count($ids),
            'deleted_count' => count($deleted),
            'skipped_count' => count($skipped),
            'failed_count' => count($failed),
            'deleted' => $deleted,
            'skipped' => $skipped,
            'failed' => $failed,
        ];

        $meta = [
            'request_id' => $this->requestId(),
            'idempotency_key' => $idempotency_key,
        ];

        set_transient($idem_cache_key, ['data' => $data, 'meta' => $meta], 10 * MINUTE_IN_SECONDS);

        return $this->success($data, $meta);
    }

    private function resolveDateRange(string $range): array
    {
        if ($range === '') {
            return [null, null];
        }

        $now = current_time('timestamp', true);
        $start = null;
        $end = gmdate('Y-m-d H:i:s', $now);

        switch ($range) {
            case 'today':
                $start = gmdate('Y-m-d 00:00:00', $now);
                break;
            case 'last_2_days':
                $start = gmdate('Y-m-d H:i:s', $now - 2 * DAY_IN_SECONDS);
                break;
            case 'this_week':
                $weekday = (int) gmdate('N', $now);
                $start_ts = strtotime('-' . ($weekday - 1) . ' days', $now);
                $start = gmdate('Y-m-d 00:00:00', $start_ts);
                break;
            case 'this_month':
                $start = gmdate('Y-m-01 00:00:00', $now);
                break;
            case 'last_15_days':
                $start = gmdate('Y-m-d H:i:s', $now - 15 * DAY_IN_SECONDS);
                break;
            default:
                return [null, null];
        }

        return [$start, $end];
    }

    private function decodeArray($value): array
    {
        $decoded = maybe_unserialize($value);
        return is_array($decoded) ? $decoded : [];
    }

    private function extractFileTokensFromSubmissionPayload(array $payload): array
    {
        $tokens = [];
        $this->collectFileTokensFromValue($payload, $tokens);
        return array_values(array_unique($tokens));
    }

    private function collectFileTokensFromValue($value, array &$tokens): void
    {
        if (is_array($value)) {
            foreach ($value as $child) {
                $this->collectFileTokensFromValue($child, $tokens);
            }
            return;
        }

        if (!is_string($value) || $value === '') {
            return;
        }

        $parts = array_map('trim', explode(',', $value));
        foreach ($parts as $part) {
            if ($part === '') {
                continue;
            }
            if (preg_match('/^[a-f0-9]{40}$/i', $part)) {
                $tokens[] = strtolower($part);
            }
        }
    }

    private function mapAttachmentResponseRow(array $row, int $submission_id): array
    {
        $file_id = absint($row['id'] ?? 0);
        $path = (string) ($row['path'] ?? '');
        $url = (string) ($row['url'] ?? '');

        return [
            'id' => $file_id,
            'submission_id' => absint($row['submission_id'] ?? 0),
            'filename' => $this->resolveAttachmentFilename($path, $url, $file_id),
            'mime' => sanitize_text_field((string) ($row['mime'] ?? 'application/octet-stream')),
            'size_bytes' => absint($row['size_bytes'] ?? 0),
            'created_at' => (string) ($row['created_at'] ?? ''),
            'uploaded_by' => absint($row['uploaded_by'] ?? 0),
            'download_url' => esc_url_raw(
                rest_url(self::NAMESPACE . '/submissions/' . $submission_id . '/attachments/' . $file_id . '/download')
            ),
        ];
    }

    private function resolveAttachmentFilename(string $path, string $url, int $file_id): string
    {
        if ($path !== '') {
            $name = wp_basename($path);
            if ($name !== '') {
                return sanitize_file_name($name);
            }
        }

        if ($url !== '') {
            $parsed = wp_parse_url($url);
            $candidate = isset($parsed['path']) ? wp_basename((string) $parsed['path']) : '';
            if ($candidate !== '') {
                return sanitize_file_name($candidate);
            }
        }

        return 'file-' . $file_id;
    }

    private function parseBoolParam($value): bool
    {
        if (is_bool($value)) {
            return $value;
        }
        $value = strtolower(trim((string) $value));
        return in_array($value, ['1', 'true', 'yes', 'on'], true);
    }

    private function deviceLimitPerUser(): int
    {
        $limit = (int) apply_filters('griffinforms/device_limit_per_user', self::DEVICE_LIMIT_PER_USER);
        return max(1, $limit);
    }

    private function deviceLimitTotal(): int
    {
        $limit = (int) apply_filters('griffinforms/device_limit_total', self::DEVICE_LIMIT_TOTAL);
        return max(1, $limit);
    }

    private function countTotalActiveDevices(): int
    {
        if ($this->device_total_active_cache !== null) {
            return $this->device_total_active_cache;
        }

        global $wpdb;
        $table = $this->config->getTable('options');
        if (!$table) {
            $this->device_total_active_cache = 0;
            return 0;
        }

        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT option_name FROM %i WHERE option_name LIKE %s",
                $table,
                self::DEVICE_OPTION_PREFIX . '%'
            ),
            ARRAY_A
        );

        $count = 0;
        foreach ((array) $rows as $row) {
            $option_name = sanitize_text_field((string) ($row['option_name'] ?? ''));
            if ($option_name === '' || strpos($option_name, self::DEVICE_OPTION_PREFIX) !== 0) {
                continue;
            }
            $registry = $this->settings->getOption($option_name, []);
            if (!is_array($registry)) {
                continue;
            }
            foreach ($registry as $record) {
                $normalized = $this->normalizeDeviceRecord($record);
                if (!empty($normalized['device_id']) && empty($normalized['deleted_at'])) {
                    $count++;
                }
            }
        }

        $this->device_total_active_cache = max(0, $count);
        return $this->device_total_active_cache;
    }

    private function sanitizeDeviceId(string $device_id): string
    {
        $device_id = trim($device_id);
        if ($device_id === '' || mb_strlen($device_id) > 191) {
            return '';
        }
        if (!preg_match('/^[A-Za-z0-9._:-]+$/', $device_id)) {
            return '';
        }
        return $device_id;
    }

    private function normalizeDevicePlatform(string $platform): string
    {
        $platform = sanitize_key($platform);
        if ($platform === '') {
            return '';
        }
        $allowed = ['macos', 'ios', 'android', 'windows', 'linux', 'web', 'unknown'];
        return in_array($platform, $allowed, true) ? $platform : '';
    }

    private function normalizeDeviceTokenState(string $token_state): string
    {
        $token_state = sanitize_key($token_state);
        if ($token_state === '') {
            return '';
        }
        $allowed = ['active', 'invalid', 'expired', 'unregistered', 'disabled'];
        return in_array($token_state, $allowed, true) ? $token_state : '';
    }

    private function normalizeDeviceLabels($labels): array|WP_Error
    {
        if ($labels === null || $labels === '') {
            return [];
        }

        if (is_string($labels)) {
            $decoded = json_decode($labels, true);
            if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
                $labels = $decoded;
            } else {
                $labels = array_map('trim', explode(',', $labels));
            }
        }

        if (!is_array($labels)) {
            return $this->apiError('device_payload_invalid', 'griffinforms_invalid_labels', __('labels must be an array of strings.', 'griffinforms-form-builder'), 400);
        }

        $normalized = [];
        foreach ($labels as $label) {
            if (!is_scalar($label)) {
                continue;
            }
            $label = sanitize_text_field((string) $label);
            if ($label === '') {
                continue;
            }
            if (mb_strlen($label) > 64) {
                return $this->apiError('device_payload_invalid', 'griffinforms_invalid_labels', __('Each label must be <= 64 characters.', 'griffinforms-form-builder'), 400);
            }
            $normalized[] = $label;
        }

        $normalized = array_values(array_unique($normalized));
        if (count($normalized) > 20) {
            return $this->apiError('device_payload_invalid', 'griffinforms_invalid_labels', __('A maximum of 20 labels is allowed.', 'griffinforms-form-builder'), 400);
        }

        return $normalized;
    }

    private function normalizeDeviceDateTime($value): string|false|null
    {
        if ($value === null || $value === '') {
            return null;
        }
        $ts = strtotime((string) $value);
        if ($ts === false) {
            return false;
        }
        return gmdate('Y-m-d H:i:s', $ts);
    }

    private function deviceRegistryOptionName(int $user_id): string
    {
        return self::DEVICE_OPTION_PREFIX . max(1, $user_id);
    }

    private function getDeviceRegistryForUser(int $user_id): array
    {
        $user_id = absint($user_id);
        if ($user_id <= 0) {
            return [];
        }
        $option_name = $this->deviceRegistryOptionName($user_id);
        $stored = $this->settings->getOption($option_name, []);
        if (is_object($stored)) {
            $stored = (array) $stored;
        }
        if (!is_array($stored)) {
            return [];
        }

        $registry = [];
        foreach ($stored as $record) {
            $normalized = $this->normalizeDeviceRecord($record);
            if (!empty($normalized['device_id'])) {
                $registry[] = $normalized;
            }
        }
        $this->sortDeviceRegistry($registry);
        return $registry;
    }

    private function saveDeviceRegistryForUser(int $user_id, array $registry): bool
    {
        $user_id = absint($user_id);
        if ($user_id <= 0) {
            return false;
        }
        $option_name = $this->deviceRegistryOptionName($user_id);
        if (empty($registry)) {
            $deleted = $this->settings->deleteOption($option_name) !== false;
            $this->device_total_active_cache = null;
            return $deleted;
        }
        $result = $this->settings->updateOption($option_name, array_values($registry), 'capabilitymatrix');
        $this->device_total_active_cache = null;
        return $result !== false;
    }

    private function findDeviceRegistryIndex(array $registry, string $device_id): int
    {
        foreach ($registry as $index => $record) {
            if ((string) ($record['device_id'] ?? '') === $device_id) {
                return (int) $index;
            }
        }
        return -1;
    }

    private function countActiveDevices(array $registry): int
    {
        $count = 0;
        foreach ($registry as $record) {
            if (empty($record['deleted_at'])) {
                $count++;
            }
        }
        return $count;
    }

    private function sortDeviceRegistry(array &$registry): void
    {
        usort($registry, static function ($a, $b) {
            $a_updated = strtotime((string) ($a['updated_at'] ?? '1970-01-01 00:00:00')) ?: 0;
            $b_updated = strtotime((string) ($b['updated_at'] ?? '1970-01-01 00:00:00')) ?: 0;
            return $b_updated <=> $a_updated;
        });
    }

    private function normalizeDeviceRecord($record): array
    {
        $record = is_array($record) ? $record : [];
        $device_id = $this->sanitizeDeviceId((string) ($record['device_id'] ?? ''));
        $platform = $this->normalizeDevicePlatform((string) ($record['platform'] ?? 'unknown'));
        if ($platform === '') {
            $platform = 'unknown';
        }

        $token_state = $this->normalizeDeviceTokenState((string) ($record['token_state'] ?? 'active'));
        if ($token_state === '') {
            $token_state = 'active';
        }

        $labels = [];
        if (isset($record['labels']) && is_array($record['labels'])) {
            foreach ($record['labels'] as $label) {
                $label = sanitize_text_field((string) $label);
                if ($label !== '') {
                    $labels[] = $label;
                }
            }
            $labels = array_values(array_unique($labels));
        }

        return [
            'device_id' => $device_id,
            'platform' => $platform,
            'app_version' => sanitize_text_field((string) ($record['app_version'] ?? '')),
            'push_token' => sanitize_text_field((string) ($record['push_token'] ?? '')),
            'token_state' => $token_state,
            'labels' => $labels,
            'last_seen' => sanitize_text_field((string) ($record['last_seen'] ?? '')),
            'last_registration_ip' => $this->sanitizeIp((string) ($record['last_registration_ip'] ?? '')),
            'last_user_agent' => $this->sanitizeUserAgent((string) ($record['last_user_agent'] ?? '')),
            'deleted_at' => sanitize_text_field((string) ($record['deleted_at'] ?? '')),
            'created_at' => sanitize_text_field((string) ($record['created_at'] ?? current_time('mysql', true))),
            'updated_at' => sanitize_text_field((string) ($record['updated_at'] ?? current_time('mysql', true))),
        ];
    }

    private function mapDeviceResponseRecord(array $record): array
    {
        $normalized = $this->normalizeDeviceRecord($record);
        return [
            'id' => substr(hash('sha256', (string) $normalized['device_id']), 0, 16),
            'device_id' => $normalized['device_id'],
            'platform' => $normalized['platform'],
            'app_version' => $normalized['app_version'],
            'push_token' => $normalized['push_token'],
            'token_state' => $normalized['token_state'],
            'labels' => $normalized['labels'],
            'last_seen' => $normalized['last_seen'],
            'is_active' => empty($normalized['deleted_at']),
            'deleted_at' => $normalized['deleted_at'] !== '' ? $normalized['deleted_at'] : null,
            'created_at' => $normalized['created_at'],
            'updated_at' => $normalized['updated_at'],
        ];
    }

    private function sanitizeIp(string $ip): string
    {
        $ip = sanitize_text_field($ip);
        return mb_strlen($ip) > 45 ? substr($ip, 0, 45) : $ip;
    }

    private function sanitizeUserAgent(string $user_agent): string
    {
        $user_agent = sanitize_textarea_field($user_agent);
        return mb_strlen($user_agent) > 2000 ? substr($user_agent, 0, 2000) : $user_agent;
    }

    private function buildSubmitterAvatarUrl(int $user_id, string $email = ''): string
    {
        $avatar_target = $user_id > 0 ? $user_id : $email;
        if ($avatar_target === '' || $avatar_target === 0) {
            return '';
        }

        $avatar_url = get_avatar_url($avatar_target, ['size' => 64]);
        return is_string($avatar_url) ? esc_url_raw($avatar_url) : '';
    }

    private function isValidShareToken(string $token, int $submission_id): bool
    {
        if ($token === '' || $submission_id <= 0) {
            return false;
        }

        $active = get_transient($this->shareActiveTokenKey($submission_id));
        if (!is_string($active) || !hash_equals($active, $token)) {
            return false;
        }

        $data = get_transient($this->shareTokenKey($token));
        if (!is_array($data)) {
            return false;
        }

        if ((int) ($data['submission_id'] ?? 0) !== $submission_id) {
            return false;
        }

        $expires_at = (int) ($data['expires_at'] ?? 0);
        if ($expires_at <= 0 || $expires_at < time()) {
            return false;
        }

        return true;
    }

    private function shareTokenKey(string $token): string
    {
        return 'gf_api_sharelink_' . md5($token);
    }

    private function shareActiveTokenKey(int $submission_id): string
    {
        return 'gf_api_sharelink_active_' . absint($submission_id);
    }

    private function requestId(): string
    {
        if ($this->request_id === null) {
            $this->request_id = substr(hash('sha256', (string) microtime(true) . '|' . wp_rand()), 0, 16);
        }
        return $this->request_id;
    }

    private function logGuardrailAuditOnce(string $action, string $outcome, array $context, string $dedupe_suffix): void
    {
        // Ignore preflight noise; real request path is logged on the subsequent method.
        if ($this->requestMethod() === 'OPTIONS') {
            return;
        }

        $key = $this->requestId() . '|' . $dedupe_suffix . '|' . $this->requestMethod() . '|' . $this->requestUriPath();
        if (isset($this->audit_once[$key])) {
            return;
        }

        $this->audit_once[$key] = true;
        Capabilities::logAudit($action, $outcome, $context);
    }

    private function buildEndpointMeta(array $meta): array
    {
        $meta['request_method'] = $this->requestMethod();
        $meta['request_uri'] = $this->requestUriPath();
        return $meta;
    }

    private function withEndpointMeta(array $context): array
    {
        $meta = [];
        if (isset($context['meta']) && is_array($context['meta'])) {
            $meta = $context['meta'];
        }
        $context['meta'] = $this->buildEndpointMeta($meta);
        return $context;
    }

    private function requestMethod(): string
    {
        $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
        return $method !== '' ? $method : 'GET';
    }

    private function requestUriPath(): string
    {
        $uri = (string) ($_SERVER['REQUEST_URI'] ?? '');
        if ($uri === '') {
            return '';
        }
        $path = (string) parse_url($uri, PHP_URL_PATH);
        return sanitize_text_field($path);
    }

    private function isTrustedAttachmentUrl(string $url): bool
    {
        $url = esc_url_raw($url);
        if ($url === '' || !wp_http_validate_url($url)) {
            return false;
        }

        $candidate_host = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
        if ($candidate_host === '') {
            return false;
        }

        $trusted_hosts = [];
        $site_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        if ($site_host !== '') {
            $trusted_hosts[$site_host] = true;
        }

        $uploads = wp_get_upload_dir();
        $uploads_baseurl = is_array($uploads) ? (string) ($uploads['baseurl'] ?? '') : '';
        $uploads_host = strtolower((string) wp_parse_url($uploads_baseurl, PHP_URL_HOST));
        if ($uploads_host !== '') {
            $trusted_hosts[$uploads_host] = true;
        }

        return isset($trusted_hosts[$candidate_host]);
    }

    private function apiError(
        string $action,
        string $code,
        string $message,
        int $status,
        array $context = []
    ): WP_Error {
        $outcome = 'failed';
        if ($status === 401 || $status === 403) {
            $outcome = 'denied';
        } elseif ($status === 429) {
            $outcome = 'rate_limited';
        }

        $meta = isset($context['meta']) && is_array($context['meta']) ? $context['meta'] : [];
        $meta['status'] = $status;
        $meta['code'] = $code;
        $context['meta'] = $meta;
        $context['request_id'] = $this->requestId();

        Capabilities::logAudit($action, $outcome, $this->withEndpointMeta($context));

        return new WP_Error($code, $message, ['status' => $status]);
    }

    private function success($data, array $meta = []): WP_REST_Response
    {
        return new WP_REST_Response([
            'success' => true,
            'data' => $data,
            'meta' => $meta,
        ]);
    }

    /**
     * Build a very small single-page PDF document from plain lines.
     */
    private function buildSimplePdf(array $lines): string
    {
        $y = 780;
        $content = "BT\n/F1 12 Tf\n50 {$y} Td\n";
        foreach ($lines as $index => $line) {
            $escaped = str_replace(['\\', '(', ')'], ['\\\\', '\\(', '\\)'], (string) $line);
            if ($index === 0) {
                $content .= "({$escaped}) Tj\n";
            } else {
                $content .= "T* ({$escaped}) Tj\n";
            }
        }
        $content .= "ET\n";

        $objects = [];
        $objects[] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
        $objects[] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
        $objects[] = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n";
        $objects[] = "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n";
        $objects[] = "5 0 obj\n<< /Length " . strlen($content) . " >>\nstream\n{$content}endstream\nendobj\n";

        $pdf = "%PDF-1.4\n";
        $offsets = [0];
        foreach ($objects as $obj) {
            $offsets[] = strlen($pdf);
            $pdf .= $obj;
        }

        $xref_pos = strlen($pdf);
        $pdf .= "xref\n0 " . (count($objects) + 1) . "\n";
        $pdf .= "0000000000 65535 f \n";
        for ($i = 1; $i <= count($objects); $i++) {
            $pdf .= sprintf('%010d 00000 n ' . "\n", $offsets[$i]);
        }
        $pdf .= "trailer\n<< /Size " . (count($objects) + 1) . " /Root 1 0 R >>\nstartxref\n{$xref_pos}\n%%EOF";

        return $pdf;
    }

    private function submissionExists(int $submission_id): bool
    {
        global $wpdb;

        if ($submission_id <= 0) {
            return false;
        }

        $sub_table = $this->config->getTable('submission');
        $exists = (int) $wpdb->get_var(
            $wpdb->prepare("SELECT COUNT(*) FROM {$sub_table} WHERE id = %d", $submission_id)
        );
        return $exists > 0;
    }

    private function tableHasColumn(string $table, string $column): bool
    {
        global $wpdb;

        if ($table === '' || $column === '') {
            return false;
        }

        $result = $wpdb->get_var(
            $wpdb->prepare(
                "SHOW COLUMNS FROM {$table} LIKE %s",
                $column
            )
        );

        return !empty($result);
    }

    private function markSubmissionAsRead(int $submission_id): void
    {
        global $wpdb;

        if ($submission_id <= 0) {
            return;
        }

        $sub_table = $this->config->getTable('submission');
        $wpdb->query(
            $wpdb->prepare(
                "UPDATE %i SET is_read = 1 WHERE id = %d AND is_read <> 1",
                $sub_table,
                $submission_id
            )
        );
    }

    private function fetchSubmissionSummaryById(int $submission_id): ?array
    {
        global $wpdb;

        if ($submission_id <= 0) {
            return null;
        }

        $sub_table = $this->config->getTable('submission');
        $form_table = $this->config->getTable('form');
        $files_table = $this->config->getTable('files');
        $users_table = $wpdb->users;

        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT s.id, s.form_id, s.status, s.is_read, s.created, s.author, s.submission_meta,
                        f.name AS form_name,
                        COUNT(fl.id) AS attachment_count,
                        COALESCE(SUM(fl.size_bytes), 0) AS attachment_size,
                        u.display_name AS submitter_name,
                        u.user_email AS submitter_email
                 FROM {$sub_table} s
                 LEFT JOIN {$form_table} f ON f.id = s.form_id
                 LEFT JOIN {$files_table} fl ON fl.submission_id = s.id AND fl.status = 'attached'
                 LEFT JOIN {$users_table} u ON u.ID = CAST(s.author AS UNSIGNED)
                 WHERE s.id = %d
                 GROUP BY s.id
                 LIMIT 1",
                $submission_id
            ),
            ARRAY_A
        );

        if (!is_array($row)) {
            return null;
        }

        $submission_meta = $this->decodeArray($row['submission_meta'] ?? '');
        $compliance_profile = '';
        if (is_array($submission_meta) && !empty($submission_meta['compliance']['profile'])) {
            $compliance_profile = sanitize_text_field((string) $submission_meta['compliance']['profile']);
        }

        $sql_format = AdminSqlFormat::getInstance();
        return [
            'id' => (int) $row['id'],
            'submission_name' => $sql_format->getSubmissionDisplayName((int) $row['id']),
            'submission_attachment_count' => (int) ($row['attachment_count'] ?? 0),
            'submission_attachment_size' => (int) ($row['attachment_size'] ?? 0),
            'submission_compliance_profile' => $compliance_profile,
            'submission_status' => (int) ($row['status'] ?? 0) === 1 ? 'complete' : 'incomplete',
            'submission_read_state' => ((int) ($row['is_read'] ?? 0) === 1) ? 'read' : 'unread',
            'submission_date_time' => (string) ($row['created'] ?? ''),
            'submitter_name' => sanitize_text_field((string) ($row['submitter_name'] ?? '')),
            'submitter_avatar_url' => $this->buildSubmitterAvatarUrl(
                absint($row['author'] ?? 0),
                sanitize_email((string) ($row['submitter_email'] ?? ''))
            ),
            'form_id' => (int) ($row['form_id'] ?? 0),
            'form_name' => sanitize_text_field((string) ($row['form_name'] ?? '')),
        ];
    }
}
