<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\Forms;

if (! defined('ABSPATH')) {
    exit;
}

// This workspace reads and updates dedicated Forms tables with fixed query fragments.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.WP.AlternativeFunctions.file_system_operations_fclose

final class PrivacyWorkspace
{
    private const OPTION = 'h4bs_form_privacy_draft_policy';
    private const EXPORT_MAX_ROWS = 10000;
    private const EXPORT_MAX_BYTES = 67108864;
    private const EXPORT_MIN_FREE_BYTES = 134217728;
    private bool $formsQueryFailed = false;
    private bool $holdEntryQueryFailed = false;
    private bool $holdsQueryFailed = false;
    private bool $auditQueryFailed = false;

    public function boot(): void
    {
        (new TimestampReadiness())->boot();
        add_action('admin_menu', [$this, 'menu']);
        add_action('admin_post_h4bs_save_form_privacy_policy', [$this, 'savePolicy']);
        add_action('admin_post_h4bs_update_form_privacy_hold', [$this, 'updateHold']);
        add_action('admin_post_h4bs_export_form_privacy_preview', [$this, 'exportPreview']);
    }

    public function menu(): void
    {
        add_submenu_page(
            'h4bs-settings',
            __('Privacy & Retention', 'help4-builder-suite'),
            __('Privacy & Retention', 'help4-builder-suite'),
            'access_help4_form_privacy',
            'h4bs-form-privacy',
            [$this, 'render']
        );
    }

    public function render(): void
    {
        if (! current_user_can('access_help4_form_privacy')) {
            wp_die(esc_html__('You cannot access form privacy planning.', 'help4-builder-suite'), '', ['response' => 403]);
        }

        $canManagePolicy = current_user_can('manage_help4_form_privacy');
        $canManageHolds = current_user_can('manage_help4_form_privacy_holds');
        $canExport = current_user_can('export_help4_form_privacy');
        $forms = $this->availableForms();
        $policy = $this->policy();
        $preview = $this->preview($policy);
        $selectedEntryId = $canManageHolds ? absint(wp_unslash($_GET['hold_entry_id'] ?? 0)) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only editor selection.
        $selectedEntry = $selectedEntryId > 0 ? $this->entryForHold($selectedEntryId) : null;
        $activeHolds = $this->activeHolds();
        $audit = $this->recentAudit();
        $auditRecent = array_slice($audit, 0, 5);
        $auditOlder = array_slice($audit, 5);
        $notice = sanitize_key((string) wp_unslash($_GET['privacy_notice'] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only result notice.
        ?>
        <div class="wrap h4bs-admin-wrap h4bs-form-privacy" data-h4bs-form-privacy>
            <h1><?php esc_html_e('Form Entries Privacy & Retention', 'help4-builder-suite'); ?></h1>
            <p><?php if (current_user_can('edit_pages')) : ?><a class="button" href="<?php echo esc_url(admin_url('admin.php?page=h4bs-settings')); ?>"><?php esc_html_e('Work Home', 'help4-builder-suite'); ?></a><?php endif; ?><?php if (current_user_can('view_help4_form_entries')) : ?> <a class="button" href="<?php echo esc_url(admin_url('admin.php?page=h4bs-form-entries')); ?>"><?php esc_html_e('Form Entries', 'help4-builder-suite'); ?></a><?php endif; ?></p>
            <?php if ($notice === 'policy-saved') : ?><div class="notice notice-success inline"><p><?php esc_html_e('Draft policy saved. The preview was rebuilt from current stored messages.', 'help4-builder-suite'); ?></p></div><?php endif; ?>
            <?php if ($notice === 'hold-saved') : ?><div class="notice notice-success inline"><p><?php esc_html_e('Privacy hold updated. Earlier previews are now stale and must not be reused.', 'help4-builder-suite'); ?></p></div><?php endif; ?>
            <?php if ($this->formsQueryFailed) : ?><div class="notice notice-error inline" role="alert"><p><?php esc_html_e('Current forms could not be loaded. Draft policy saving is unavailable until the database can prove the form scope.', 'help4-builder-suite'); ?></p></div><?php endif; ?>
            <div class="notice notice-warning inline"><p><strong><?php esc_html_e('Planning and export only.', 'help4-builder-suite'); ?></strong> <?php esc_html_e('Help4 will not automatically delete, anonymize, or schedule changes. Archive remains ordinary staff filing, not privacy erasure. This tool does not determine legal compliance.', 'help4-builder-suite'); ?></p></div>
            <?php (new TimestampReadiness())->renderPanel($policy); ?>

            <section class="h4bs-privacy-panel" aria-labelledby="h4bs-privacy-policy-title">
                <h2 id="h4bs-privacy-policy-title"><?php esc_html_e('1. Define a draft site policy', 'help4-builder-suite'); ?></h2>
                <p><?php esc_html_e('Use the policy approved by the site owner or their legal adviser. Saving this draft changes no stored message.', 'help4-builder-suite'); ?></p>
                <?php if ($canManagePolicy) : ?>
                <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" data-h4bs-privacy-policy-form>
                    <input type="hidden" name="action" value="h4bs_save_form_privacy_policy">
                    <input type="hidden" name="policy_revision" value="<?php echo esc_attr($policy['revision']); ?>">
                    <input type="hidden" name="request_id" value="<?php echo esc_attr(wp_generate_uuid4()); ?>">
                    <?php wp_nonce_field('h4bs_save_form_privacy_policy'); ?>
                    <label class="h4bs-privacy-age"><?php esc_html_e('Review messages older than', 'help4-builder-suite'); ?> <input type="number" min="1" max="3650" name="age_days" value="<?php echo esc_attr((string) $policy['age_days']); ?>" required> <?php esc_html_e('days', 'help4-builder-suite'); ?></label>
                    <fieldset><legend><?php esc_html_e('Forms included in this draft', 'help4-builder-suite'); ?></legend><p class="description"><?php esc_html_e('Choose at least one form. Unchecked forms stay outside this preview and export.', 'help4-builder-suite'); ?></p><div class="h4bs-privacy-choices"><?php foreach ($forms as $form) : ?><label><input type="checkbox" name="form_ids[]" value="<?php echo esc_attr((string) (int) $form['id']); ?>" <?php checked(in_array((int) $form['id'], $policy['form_ids'], true)); ?>> <?php echo esc_html((string) $form['name']); ?></label><?php endforeach; ?></div></fieldset>
                    <fieldset><legend><?php esc_html_e('Exclude staff statuses', 'help4-builder-suite'); ?></legend><p class="description"><?php esc_html_e('Messages with checked statuses remain outside the eligible set.', 'help4-builder-suite'); ?></p><div class="h4bs-privacy-choices"><?php foreach ($this->statusLabels() as $key => $label) : ?><label><input type="checkbox" name="excluded_statuses[]" value="<?php echo esc_attr($key); ?>" <?php checked(in_array($key, $policy['excluded_statuses'], true)); ?>> <?php echo esc_html($label); ?></label><?php endforeach; ?></div></fieldset>
                    <button class="button button-primary" type="submit" <?php disabled($this->formsQueryFailed); ?>><?php esc_html_e('Save draft and refresh preview', 'help4-builder-suite'); ?></button>
                </form>
                <?php else : ?>
                    <p><strong><?php esc_html_e('Read-only policy view.', 'help4-builder-suite'); ?></strong> <?php esc_html_e('Your role can review the current exact preview but cannot change the site-defined policy.', 'help4-builder-suite'); ?></p>
                    <dl class="h4bs-privacy-policy-readonly"><div><dt><?php esc_html_e('Age threshold', 'help4-builder-suite'); ?></dt><dd>
                        <?php /* translators: %d: number of days in the draft retention threshold. */ ?>
                        <?php printf(esc_html__('%d days', 'help4-builder-suite'), (int) $policy['age_days']); ?>
                    </dd></div><div><dt><?php esc_html_e('Included forms', 'help4-builder-suite'); ?></dt><dd><?php echo esc_html($this->formScopeLabel($forms, $policy['form_ids'])); ?></dd></div><div><dt><?php esc_html_e('Excluded statuses', 'help4-builder-suite'); ?></dt><dd><?php echo esc_html($this->statusScopeLabel($policy['excluded_statuses'])); ?></dd></div></dl>
                <?php endif; ?>
            </section>

            <section class="h4bs-privacy-panel" aria-labelledby="h4bs-privacy-preview-title">
                <h2 id="h4bs-privacy-preview-title"><?php esc_html_e('2. Review the exact impact', 'help4-builder-suite'); ?></h2>
                <?php if (! $preview['valid']) : ?>
                    <div class="notice notice-error inline" role="alert"><p><strong><?php esc_html_e('Exact preview unavailable.', 'help4-builder-suite'); ?></strong> <?php echo esc_html((string) $preview['error']); ?></p></div>
                <?php else : ?>
                    <div class="h4bs-privacy-summary"><p><strong><?php echo esc_html((string) $preview['eligible']); ?></strong> <?php esc_html_e('stored messages are eligible', 'help4-builder-suite'); ?></p><p><strong><?php echo esc_html((string) $preview['held']); ?></strong> <?php esc_html_e('otherwise-eligible messages are protected by a hold', 'help4-builder-suite'); ?></p></div>
                    <p><strong><?php esc_html_e('Frozen cutoff:', 'help4-builder-suite'); ?></strong> <?php echo esc_html((string) $preview['cutoff_label']); ?></p>
                    <details><summary><?php esc_html_e('Show verification fingerprint', 'help4-builder-suite'); ?></summary><p><code class="h4bs-privacy-fingerprint"><?php echo esc_html((string) $preview['fingerprint']); ?></code></p><p class="description"><?php esc_html_e('Any policy, hold, status, form scope, or message revision change makes this fingerprint stale.', 'help4-builder-suite'); ?></p></details>
                    <p class="description"><?php esc_html_e('The bounded sample contains operational identifiers only—never names, email addresses, phone numbers, or answers.', 'help4-builder-suite'); ?></p>
                    <?php if ($preview['sample']) : ?><ol class="h4bs-privacy-sample"><?php foreach ($preview['sample'] as $row) : ?><li><span>
                        <?php /* translators: 1: message ID, 2: form ID, 3: creation date, 4: staff status. */ ?>
                        <?php printf(esc_html__('Message #%1$d · Form #%2$d · %3$s · %4$s', 'help4-builder-suite'), (int) $row['id'], (int) $row['form_id'], esc_html((string) $row['created_at']), esc_html((string) $row['staff_status'])); ?>
                    </span><?php if ($canManageHolds) : ?> <a class="button" href="<?php echo esc_url(admin_url('admin.php?page=h4bs-form-privacy&hold_entry_id=' . (int) $row['id'] . '#h4bs-hold-editor')); ?>"><?php esc_html_e('Review hold', 'help4-builder-suite'); ?></a><?php endif; ?></li><?php endforeach; ?></ol><?php endif; ?>
                    <?php if ($canExport && (int) $preview['eligible'] > 0 && (int) $preview['eligible'] <= self::EXPORT_MAX_ROWS) : ?><form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"><input type="hidden" name="action" value="h4bs_export_form_privacy_preview"><input type="hidden" name="fingerprint" value="<?php echo esc_attr((string) $preview['fingerprint']); ?>"><?php wp_nonce_field('h4bs_export_form_privacy_preview'); ?><button class="button" type="submit"><?php esc_html_e('Export this exact eligible set', 'help4-builder-suite'); ?></button><p class="description"><?php esc_html_e('Export contains the stored personal data for this exact set, is authorized separately, and is refused if the preview changed.', 'help4-builder-suite'); ?></p></form><?php elseif ($canExport && (int) $preview['eligible'] > self::EXPORT_MAX_ROWS) : ?><div class="notice notice-warning inline"><p>
                        <?php /* translators: %s: maximum number of messages allowed in one export. */ ?>
                        <?php printf(esc_html__('This set has more than %s messages. Narrow the draft policy before exporting; Help4 will not create an oversized or partial file.', 'help4-builder-suite'), esc_html(number_format_i18n(self::EXPORT_MAX_ROWS))); ?>
                    </p></div><?php elseif (! $canExport) : ?><p class="description"><?php esc_html_e('Exact-set export is restricted to separately authorized staff.', 'help4-builder-suite'); ?></p><?php endif; ?>
                <?php endif; ?>
            </section>

            <section class="h4bs-privacy-panel" id="h4bs-hold-editor" aria-labelledby="h4bs-privacy-holds-title">
                <h2 id="h4bs-privacy-holds-title"><?php esc_html_e('3. Protect exceptions with a hold', 'help4-builder-suite'); ?></h2>
                <p><?php esc_html_e('A hold keeps an active dispute, registration, audition, or other exception outside every eligible preview. Holds do not archive or erase anything.', 'help4-builder-suite'); ?></p>
                <?php if ($canManageHolds) : ?><form method="get" class="h4bs-privacy-find"><input type="hidden" name="page" value="h4bs-form-privacy"><label><?php esc_html_e('Message ID', 'help4-builder-suite'); ?> <input type="number" min="1" name="hold_entry_id" value="<?php echo $selectedEntryId > 0 ? esc_attr((string) $selectedEntryId) : ''; ?>" required></label><button class="button" type="submit"><?php esc_html_e('Find message', 'help4-builder-suite'); ?></button></form><?php else : ?><p class="description"><?php esc_html_e('Your role can review hold totals but cannot place or release holds.', 'help4-builder-suite'); ?></p><?php endif; ?>
                <?php if ($canManageHolds && $selectedEntryId > 0 && ! is_array($selectedEntry)) : ?><div class="notice notice-error inline" role="alert"><p><?php echo $this->holdEntryQueryFailed ? esc_html__('The message could not be read. No hold was changed.', 'help4-builder-suite') : esc_html__('That stored message was not found. No hold was changed.', 'help4-builder-suite'); ?></p></div><?php endif; ?>
                <?php if ($canManageHolds && is_array($selectedEntry)) : $held = ! empty($selectedEntry['privacy_hold_at']); ?>
                    <div class="h4bs-privacy-hold-editor">
                        <?php /* translators: %d: stored message ID. */ ?>
                        <h3><?php printf(esc_html__('Message #%d', 'help4-builder-suite'), (int) $selectedEntry['id']); ?></h3>
                        <?php /* translators: 1: form ID, 2: creation date, 3: staff status. */ ?>
                        <p><?php printf(esc_html__('Form #%1$d · created %2$s · status %3$s', 'help4-builder-suite'), (int) $selectedEntry['form_id'], esc_html((string) $selectedEntry['created_at']), esc_html((string) $selectedEntry['staff_status'])); ?></p>
                        <p><strong><?php echo $held ? esc_html__('Hold active', 'help4-builder-suite') : esc_html__('No hold', 'help4-builder-suite'); ?></strong><?php if ($held) : ?> · <?php echo esc_html((string) $selectedEntry['privacy_hold_at']); ?><?php endif; ?></p>
                        <?php if ($held && (string) $selectedEntry['privacy_hold_reason'] !== '') : ?><p><?php echo esc_html((string) $selectedEntry['privacy_hold_reason']); ?></p><?php endif; ?>
                        <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" data-h4bs-privacy-hold-form>
                            <input type="hidden" name="action" value="h4bs_update_form_privacy_hold"><input type="hidden" name="hold_action" value="<?php echo $held ? 'release' : 'hold'; ?>"><input type="hidden" name="entry_id" value="<?php echo esc_attr((string) (int) $selectedEntry['id']); ?>"><input type="hidden" name="hold_revision" value="<?php echo esc_attr($this->holdRevision($selectedEntry)); ?>"><input type="hidden" name="request_id" value="<?php echo esc_attr(wp_generate_uuid4()); ?>"><?php wp_nonce_field('h4bs_update_form_privacy_hold'); ?>
                            <label><?php echo $held ? esc_html__('Reason for releasing this hold', 'help4-builder-suite') : esc_html__('Reason for this hold', 'help4-builder-suite'); ?><textarea name="reason" rows="3" maxlength="500" required></textarea><span class="description"><?php esc_html_e('Use a short operational reason. Do not copy personal answers into this field.', 'help4-builder-suite'); ?></span></label>
                            <button class="button <?php echo $held ? '' : 'button-primary'; ?>" type="submit"><?php echo $held ? esc_html__('Release hold', 'help4-builder-suite') : esc_html__('Place hold', 'help4-builder-suite'); ?></button>
                        </form>
                    </div>
                <?php endif; ?>
                <?php if ($this->holdsQueryFailed) : ?><div class="notice notice-error inline" role="alert"><p><?php esc_html_e('Active holds could not be loaded. Do not rely on an empty hold list.', 'help4-builder-suite'); ?></p></div><?php elseif ($activeHolds) : ?><h3><?php esc_html_e('Recent active holds', 'help4-builder-suite'); ?></h3><ul class="h4bs-privacy-active-holds"><?php foreach ($activeHolds as $hold) : ?><li><span>
                    <?php /* translators: 1: message ID, 2: form ID, 3: date the privacy hold was placed. */ ?>
                    <?php printf(esc_html__('Message #%1$d · Form #%2$d · held %3$s', 'help4-builder-suite'), (int) $hold['id'], (int) $hold['form_id'], esc_html((string) $hold['privacy_hold_at'])); ?>
                </span><?php if ($canManageHolds) : ?> <a class="button" href="<?php echo esc_url(admin_url('admin.php?page=h4bs-form-privacy&hold_entry_id=' . (int) $hold['id'] . '#h4bs-hold-editor')); ?>"><?php esc_html_e('Review', 'help4-builder-suite'); ?></a><?php endif; ?></li><?php endforeach; ?></ul><?php endif; ?>
            </section>

            <section class="h4bs-privacy-panel" aria-labelledby="h4bs-privacy-audit-title">
                <h2 id="h4bs-privacy-audit-title"><?php esc_html_e('Recent privacy planning history', 'help4-builder-suite'); ?></h2>
                <p class="description"><?php esc_html_e('This non-PII history records hold changes and prepared exact exports with the responsible staff account. It never stores visitor names, message answers, email addresses, phone numbers, or hold reason text.', 'help4-builder-suite'); ?></p>
                <?php if ($this->auditQueryFailed) : ?>
                    <div class="notice notice-error inline" role="alert"><p><?php esc_html_e('Privacy history could not be loaded. Do not treat this as an empty audit history.', 'help4-builder-suite'); ?></p></div>
                <?php elseif ($auditRecent) : ?>
                    <ul class="h4bs-privacy-audit"><?php foreach ($auditRecent as $row) : ?><li><?php echo esc_html($this->auditSummary($row)); ?></li><?php endforeach; ?></ul>
                    <?php if ($auditOlder) : ?>
                        <?php /* translators: %d: number of older privacy history items. */ ?>
                        <details class="h4bs-privacy-audit-older"><summary><?php printf(esc_html__('Show %d older history items', 'help4-builder-suite'), count($auditOlder)); ?></summary><ul class="h4bs-privacy-audit"><?php foreach ($auditOlder as $row) : ?><li><?php echo esc_html($this->auditSummary($row)); ?></li><?php endforeach; ?></ul></details>
                    <?php endif; ?>
                <?php else : ?>
                    <p><?php esc_html_e('No hold or exact-export history has been recorded yet.', 'help4-builder-suite'); ?></p>
                <?php endif; ?>
            </section>

            <details class="h4bs-privacy-guide"><summary><?php esc_html_e('How to use this workspace safely', 'help4-builder-suite'); ?></summary><ol><li><?php esc_html_e('Confirm the site-defined policy with the owner or their adviser.', 'help4-builder-suite'); ?></li><li><?php esc_html_e('Save the form scope, age, and excluded statuses.', 'help4-builder-suite'); ?></li><li><?php esc_html_e('Place holds on active exceptions before relying on the preview.', 'help4-builder-suite'); ?></li><li><?php esc_html_e('Review the exact count and non-PII sample, then export the same fingerprint if authorized.', 'help4-builder-suite'); ?></li><li><?php esc_html_e('Do not treat Archive as erasure. This release performs no deletion or anonymization.', 'help4-builder-suite'); ?></li></ol></details>
        </div>
        <?php
    }

    public function savePolicy(): void
    {
        $this->requireCapability('manage_help4_form_privacy');
        check_admin_referer('h4bs_save_form_privacy_policy');

        $requestId = $this->requestId();
        $allowedIds = array_map(static fn (array $row): int => (int) $row['id'], $this->availableForms());
        if ($this->formsQueryFailed) {
            wp_die(esc_html__('Current forms could not be loaded, so the requested scope cannot be verified. Nothing was saved.', 'help4-builder-suite'), esc_html__('Draft policy unavailable', 'help4-builder-suite'), ['response' => 500]);
        }
        $submittedIds = is_array($_POST['form_ids'] ?? null) ? array_values(array_unique(array_filter(array_map('absint', wp_unslash($_POST['form_ids']))))) : [];
        if ($submittedIds === [] || array_diff($submittedIds, $allowedIds) !== []) {
            wp_die(esc_html__('Choose only current, available forms. Nothing was saved.', 'help4-builder-suite'), esc_html__('Draft policy scope rejected', 'help4-builder-suite'), ['response' => 400]);
        }
        sort($submittedIds, SORT_NUMERIC);
        $excluded = is_array($_POST['excluded_statuses'] ?? null) ? array_values(array_intersect(array_keys($this->statusLabels()), array_map('sanitize_key', wp_unslash($_POST['excluded_statuses'])))) : [];
        sort($excluded, SORT_STRING);
        $ageDays = min(3650, max(1, absint(wp_unslash($_POST['age_days'] ?? 365))));
        $submittedRevision = sanitize_text_field((string) wp_unslash($_POST['policy_revision'] ?? ''));
        $payloadHash = hash('sha256', (string) wp_json_encode([$submittedRevision, $ageDays, $submittedIds, $excluded]));
        wp_cache_delete(self::OPTION, 'options');
        $rawCurrent = get_option(self::OPTION, null);
        $current = $this->policyFromRaw($rawCurrent);
        if ($current['last_request_id'] === $requestId) {
            if (hash_equals($current['last_payload_hash'], $payloadHash)) {
                $this->redirect('policy-saved');
            }
            wp_die(esc_html__('That request identifier was already used for different policy data.', 'help4-builder-suite'), esc_html__('Draft policy request rejected', 'help4-builder-suite'), ['response' => 409]);
        }
        if ($submittedRevision === '' || ! hash_equals($current['revision'], $submittedRevision)) {
            wp_die(esc_html__('A newer draft policy exists. Nothing was overwritten; reload and review it.', 'help4-builder-suite'), esc_html__('Draft policy changed', 'help4-builder-suite'), ['response' => 409]);
        }

        $sameDraft = (string) $current['cutoff'] !== '' && $ageDays === (int) $current['age_days'] && $submittedIds === $current['form_ids'] && $excluded === $current['excluded_statuses'];
        $cutoff = $sameDraft
            ? (string) $current['cutoff']
            : (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->modify('-' . $ageDays . ' days')->format('Y-m-d H:i:s');
        $value = ['age_days' => $ageDays, 'form_ids' => $submittedIds, 'excluded_statuses' => $excluded, 'cutoff' => $cutoff, 'revision_token' => wp_generate_uuid4(), 'updated_at' => current_time('mysql'), 'updated_by' => get_current_user_id(), 'last_request_id' => $requestId, 'last_payload_hash' => $payloadHash];
        global $wpdb;
        $wpdb->last_error = '';
        $saved = $rawCurrent === null
            ? add_option(self::OPTION, $value, '', false)
            : $wpdb->query($wpdb->prepare('UPDATE %i SET option_value = %s WHERE option_name = %s AND option_value = %s', $wpdb->options, maybe_serialize($value), self::OPTION, maybe_serialize($rawCurrent))) === 1;
        wp_cache_delete(self::OPTION, 'options');
        if ($saved) {
            $this->redirect('policy-saved');
        }
        if ($wpdb->last_error !== '') {
            wp_die(esc_html__('The draft policy could not be saved. Reload before trying again.', 'help4-builder-suite'), esc_html__('Save failed', 'help4-builder-suite'), ['response' => 500]);
        }
        $latest = $this->policy();
        if ($latest['last_request_id'] === $requestId && hash_equals($latest['last_payload_hash'], $payloadHash)) {
            $this->redirect('policy-saved');
        }
        wp_die(esc_html__('A newer draft policy was saved first. Nothing was overwritten; reload and review it.', 'help4-builder-suite'), esc_html__('Draft policy changed', 'help4-builder-suite'), ['response' => 409]);
    }

    public function updateHold(): void
    {
        $this->requireCapability('manage_help4_form_privacy_holds');
        check_admin_referer('h4bs_update_form_privacy_hold');
        $requestId = $this->requestId();
        $entryId = absint(wp_unslash($_POST['entry_id'] ?? 0));
        $action = sanitize_key((string) wp_unslash($_POST['hold_action'] ?? ''));
        $reason = sanitize_textarea_field((string) wp_unslash($_POST['reason'] ?? ''));
        $submittedRevision = sanitize_text_field((string) wp_unslash($_POST['hold_revision'] ?? ''));
        if ($entryId < 1 || ! in_array($action, ['hold', 'release'], true) || mb_strlen(trim($reason)) < 3) {
            wp_die(esc_html__('Choose a valid message and provide a short operational reason. Nothing was changed.', 'help4-builder-suite'), esc_html__('Hold request incomplete', 'help4-builder-suite'), ['response' => 400]);
        }

        global $wpdb;
        $payloadHash = hash_hmac('sha256', (string) wp_json_encode([$entryId, $action, $reason, $submittedRevision]), wp_salt('auth'));
        $wpdb->last_error = '';
        $existingAudit = $wpdb->get_row($wpdb->prepare('SELECT payload_hash FROM %i WHERE request_id = %s LIMIT 1', $this->auditTable(), $requestId), ARRAY_A);
        if ($wpdb->last_error !== '') {
            wp_die(esc_html__('Hold replay safety could not be checked. Nothing was changed.', 'help4-builder-suite'), esc_html__('Hold history unavailable', 'help4-builder-suite'), ['response' => 500]);
        }
        if (is_array($existingAudit)) {
            if (hash_equals((string) $existingAudit['payload_hash'], $payloadHash)) {
                $this->redirect('hold-saved', ['hold_entry_id' => $entryId]);
            }
            wp_die(esc_html__('That request identifier was already used for a different hold change.', 'help4-builder-suite'), esc_html__('Hold request rejected', 'help4-builder-suite'), ['response' => 409]);
        }
        $entry = $this->entryForHold($entryId);
        if (! is_array($entry)) {
            wp_die(esc_html__('That stored message no longer exists. Nothing was changed.', 'help4-builder-suite'), esc_html__('Message unavailable', 'help4-builder-suite'), ['response' => 404]);
        }
        $currentRevision = $this->holdRevision($entry);
        if ($submittedRevision === '' || ! hash_equals($currentRevision, $submittedRevision)) {
            wp_die(esc_html__('Someone changed this hold. Nothing was overwritten; reload and review the latest state.', 'help4-builder-suite'), esc_html__('Hold changed', 'help4-builder-suite'), ['response' => 409]);
        }
        $isHeld = ! empty($entry['privacy_hold_at']);
        if (($action === 'hold' && $isHeld) || ($action === 'release' && ! $isHeld)) {
            wp_die(esc_html__('The requested hold state is already different. Reload and review it.', 'help4-builder-suite'), esc_html__('Hold state changed', 'help4-builder-suite'), ['response' => 409]);
        }

        $newRevision = hash('sha256', (string) wp_json_encode([$entryId, $action, $requestId, microtime(true)]));
        $holdAt = $action === 'hold' ? current_time('mysql') : null;
        $holdReason = $action === 'hold' ? mb_substr($reason, 0, 500) : '';
        $holdBy = $action === 'hold' ? get_current_user_id() : 0;
        if ($wpdb->query('START TRANSACTION') === false) {
            wp_die(esc_html__('A safe database transaction could not be started. Nothing was changed.', 'help4-builder-suite'), esc_html__('Hold save unavailable', 'help4-builder-suite'), ['response' => 500]);
        }
        $updated = $action === 'hold'
            ? $wpdb->query($wpdb->prepare('UPDATE %i SET privacy_hold_at = %s, privacy_hold_reason = %s, privacy_hold_by = %d, privacy_hold_revision = %s WHERE id = %d AND privacy_hold_revision = %s', $this->entriesTable(), $holdAt, $holdReason, $holdBy, $newRevision, $entryId, (string) ($entry['privacy_hold_revision'] ?? '')))
            : $wpdb->query($wpdb->prepare('UPDATE %i SET privacy_hold_at = NULL, privacy_hold_reason = %s, privacy_hold_by = %d, privacy_hold_revision = %s WHERE id = %d AND privacy_hold_revision = %s', $this->entriesTable(), '', 0, $newRevision, $entryId, (string) ($entry['privacy_hold_revision'] ?? '')));
        if ($updated !== 1) {
            $wpdb->query('ROLLBACK');
            wp_die(esc_html__('The hold changed before it could be saved. Nothing was overwritten.', 'help4-builder-suite'), esc_html__('Hold save conflict', 'help4-builder-suite'), ['response' => 409]);
        }
        $inserted = $wpdb->insert($this->auditTable(), ['request_id' => $requestId, 'action' => $action === 'hold' ? 'hold_placed' : 'hold_released', 'entry_id' => $entryId, 'user_id' => get_current_user_id(), 'payload_hash' => $payloadHash, 'previous_revision' => $currentRevision, 'new_revision' => $newRevision, 'reason_hash' => hash_hmac('sha256', $reason, wp_salt('auth')), 'created_at' => current_time('mysql')], ['%s', '%s', '%d', '%d', '%s', '%s', '%s', '%s', '%s']);
        if ($inserted !== 1) {
            $wpdb->query('ROLLBACK');
            wp_die(esc_html__('The hold could not be recorded safely. The entire change was rolled back.', 'help4-builder-suite'), esc_html__('Hold save failed', 'help4-builder-suite'), ['response' => 500]);
        }
        if ($wpdb->query('COMMIT') === false) {
            $wpdb->query('ROLLBACK');
            $persistedRevision = (string) $wpdb->get_var($wpdb->prepare('SELECT privacy_hold_revision FROM %i WHERE id = %d', $this->entriesTable(), $entryId));
            $persistedPayload = (string) $wpdb->get_var($wpdb->prepare('SELECT payload_hash FROM %i WHERE request_id = %s', $this->auditTable(), $requestId));
            if (hash_equals($newRevision, $persistedRevision) && hash_equals($payloadHash, $persistedPayload)) {
                $this->redirect('hold-saved', ['hold_entry_id' => $entryId]);
            }
            wp_die(esc_html__('The database could not confirm the hold commit. Nothing is reported as saved; reload and review the message before retrying.', 'help4-builder-suite'), esc_html__('Hold commit unconfirmed', 'help4-builder-suite'), ['response' => 500]);
        }
        $this->redirect('hold-saved', ['hold_entry_id' => $entryId]);
    }

    public function exportPreview(): void
    {
        $this->requireCapability('export_help4_form_privacy');
        check_admin_referer('h4bs_export_form_privacy_preview');
        $submitted = sanitize_text_field((string) wp_unslash($_POST['fingerprint'] ?? ''));
        $policy = $this->policy();
        $before = $this->preview($policy);
        if (! $before['valid'] || $submitted === '' || ! hash_equals((string) $before['fingerprint'], $submitted)) {
            wp_die(esc_html__('The exact preview changed. Nothing was exported; return and refresh it.', 'help4-builder-suite'), esc_html__('Privacy export is stale', 'help4-builder-suite'), ['response' => 409]);
        }
        if ((int) $before['eligible'] < 1 || (int) $before['eligible'] > self::EXPORT_MAX_ROWS) {
            wp_die(esc_html__('The exact set is empty or too large for a safe staff download. Narrow the policy and refresh the preview.', 'help4-builder-suite'), esc_html__('Privacy export size rejected', 'help4-builder-suite'), ['response' => 413]);
        }
        $freeBytes = disk_free_space(sys_get_temp_dir());
        if ($freeBytes === false || $freeBytes < self::EXPORT_MIN_FREE_BYTES) {
            wp_die(esc_html__('The server cannot prove enough private temporary space for this export. Nothing was prepared.', 'help4-builder-suite'), esc_html__('Privacy export storage unavailable', 'help4-builder-suite'), ['response' => 507]);
        }
        $temporary = tmpfile();
        if ($temporary === false) {
            wp_die(esc_html__('A private temporary export file could not be created.', 'help4-builder-suite'), esc_html__('Privacy export unavailable', 'help4-builder-suite'), ['response' => 500]);
        }
        if (fputcsv($temporary, ['ID', 'Form ID', 'Created', 'Staff status', 'Archived at', 'Name', 'Email', 'Phone', 'Subject', 'Source URL', 'Stored payload JSON']) === false) {
            fclose($temporary);
            wp_die(esc_html__('The private export file could not be written. Nothing was prepared.', 'help4-builder-suite'), esc_html__('Privacy export write failed', 'help4-builder-suite'), ['response' => 500]);
        }
        [$where, $values] = $this->eligibleWhere($policy, false);
        global $wpdb;
        $lastId = 0;
        $written = 0;
        do {
            $wpdb->last_error = '';
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter -- eligibleWhere() returns only fixed SQL fragments plus generated placeholders; every value is passed to prepare().
            $batch = $wpdb->get_results($wpdb->prepare('SELECT id, form_id, created_at, staff_status, archived_at, name, email, phone, subject, source_url, payload FROM %i WHERE ' . $where . ' AND id > %d ORDER BY id ASC LIMIT %d', ...array_merge([$this->entriesTable()], $values, [$lastId, 500])), ARRAY_A);
            if ($wpdb->last_error !== '' || ! is_array($batch)) {
                fclose($temporary);
                wp_die(esc_html__('Stored messages changed or could not be read. Nothing was exported.', 'help4-builder-suite'), esc_html__('Privacy export unavailable', 'help4-builder-suite'), ['response' => 500]);
            }
            foreach ($batch as $row) {
                $lastId = (int) $row['id'];
                ++$written;
                $csvWritten = fputcsv($temporary, [$lastId, (int) $row['form_id'], (string) $row['created_at'], (string) $row['staff_status'], (string) $row['archived_at'], $this->csvCell((string) $row['name']), $this->csvCell((string) $row['email']), $this->csvCell((string) $row['phone']), $this->csvCell((string) $row['subject']), $this->csvCell((string) $row['source_url']), $this->csvCell((string) $row['payload'])]);
                $position = ftell($temporary);
                if ($csvWritten === false || $position === false || $position > self::EXPORT_MAX_BYTES) {
                    fclose($temporary);
                    wp_die(esc_html__('The exact export exceeded its safe file bound or could not be written. Nothing was downloaded.', 'help4-builder-suite'), esc_html__('Privacy export write failed', 'help4-builder-suite'), ['response' => 507]);
                }
            }
        } while (count($batch) === 500);
        $after = $this->preview($policy);
        if (! $after['valid'] || $written !== (int) $before['eligible'] || ! hash_equals((string) $before['fingerprint'], (string) $after['fingerprint'])) {
            fclose($temporary);
            wp_die(esc_html__('Stored messages changed while the export was prepared. Nothing was downloaded; refresh the preview.', 'help4-builder-suite'), esc_html__('Privacy export changed', 'help4-builder-suite'), ['response' => 409]);
        }
        if (! $this->recordExportAudit((string) $before['fingerprint'], $written, $policy)) {
            fclose($temporary);
            wp_die(esc_html__('The exact export could not be added to the privacy history. Nothing was downloaded.', 'help4-builder-suite'), esc_html__('Privacy export audit failed', 'help4-builder-suite'), ['response' => 500]);
        }
        $contentLength = ftell($temporary);
        if ($contentLength === false || ! rewind($temporary)) {
            fclose($temporary);
            wp_die(esc_html__('The prepared export could not be verified for download.', 'help4-builder-suite'), esc_html__('Privacy export unavailable', 'help4-builder-suite'), ['response' => 500]);
        }
        nocache_headers();
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="help4-form-privacy-preview-' . gmdate('Ymd-His') . '.csv"');
        header('Content-Length: ' . (string) $contentLength);
        if (fpassthru($temporary) === false) {
            fclose($temporary);
            exit;
        }
        fclose($temporary);
        exit;
    }

    /** @return array<int, array{id:int,name:string}> */
    private function availableForms(): array
    {
        global $wpdb;
        $wpdb->last_error = '';
        $rows = $wpdb->get_results($wpdb->prepare('SELECT id, name FROM %i WHERE status <> %s ORDER BY name ASC, id ASC', $this->formsTable(), 'archived'), ARRAY_A);
        $this->formsQueryFailed = $wpdb->last_error !== '' || ! is_array($rows);
        return $this->formsQueryFailed ? [] : $rows;
    }

    /** @return array<string, mixed> */
    private function policy(): array
    {
        return $this->policyFromRaw(get_option(self::OPTION, []));
    }

    /** @param mixed $raw @return array<string,mixed> */
    private function policyFromRaw($raw): array
    {
        $raw = is_array($raw) ? $raw : [];
        $ageDays = min(3650, max(1, absint($raw['age_days'] ?? 365)));
        $cutoff = sanitize_text_field((string) ($raw['cutoff'] ?? ''));
        if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $cutoff) !== 1) {
            $cutoff = '';
        }
        $formIds = array_values(array_unique(array_filter(array_map('absint', (array) ($raw['form_ids'] ?? [])))));
        sort($formIds, SORT_NUMERIC);
        $excluded = array_values(array_intersect(array_keys($this->statusLabels()), (array) ($raw['excluded_statuses'] ?? [])));
        sort($excluded, SORT_STRING);
        $revisionToken = sanitize_text_field((string) ($raw['revision_token'] ?? ''));
        $base = ['age_days' => $ageDays, 'form_ids' => $formIds, 'excluded_statuses' => $excluded, 'cutoff' => $cutoff];
        return $base + ['revision' => hash('sha256', (string) wp_json_encode([$base, $revisionToken])), 'revision_token' => $revisionToken, 'last_request_id' => sanitize_text_field((string) ($raw['last_request_id'] ?? '')), 'last_payload_hash' => sanitize_text_field((string) ($raw['last_payload_hash'] ?? ''))];
    }

    /** @return array{valid:bool,error:string,eligible:int,held:int,cutoff_label:string,sample:array<int,array<string,mixed>>,fingerprint:string} */
    private function preview(array $policy): array
    {
        if ($policy['form_ids'] === []) {
            return $this->invalidPreview(__('Choose and save at least one current form before relying on a preview.', 'help4-builder-suite'));
        }
        if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', (string) $policy['cutoff']) !== 1) {
            return $this->invalidPreview(__('Save the draft once to freeze a verifiable UTC cutoff.', 'help4-builder-suite'));
        }
        global $wpdb;
        [$where, $values] = $this->eligibleWhere($policy, false);
        [$heldWhere, $heldValues] = $this->eligibleWhere($policy, true);
        $wpdb->last_error = '';
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- eligibleWhere() returns fixed SQL fragments and placeholders only.
        $eligibleValue = $wpdb->get_var($wpdb->prepare('SELECT COUNT(*) FROM %i WHERE ' . $where, ...array_merge([$this->entriesTable()], $values)));
        if ($wpdb->last_error !== '' || $eligibleValue === null) {
            return $this->invalidPreview(__('Stored messages could not be counted. Refresh after the database is available.', 'help4-builder-suite'));
        }
        $eligible = max(0, (int) $eligibleValue);
        $wpdb->last_error = '';
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- eligibleWhere() returns fixed SQL fragments and placeholders only.
        $heldValue = $wpdb->get_var($wpdb->prepare('SELECT COUNT(*) FROM %i WHERE ' . $heldWhere, ...array_merge([$this->entriesTable()], $heldValues)));
        if ($wpdb->last_error !== '' || $heldValue === null) {
            return $this->invalidPreview(__('Privacy holds could not be counted. No exact preview is available.', 'help4-builder-suite'));
        }
        $held = max(0, (int) $heldValue);
        $wpdb->last_error = '';
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter -- eligibleWhere() returns fixed SQL fragments and placeholders only.
        $sample = $wpdb->get_results($wpdb->prepare('SELECT id, form_id, created_at, staff_status FROM %i WHERE ' . $where . ' ORDER BY created_at ASC, id ASC LIMIT %d', ...array_merge([$this->entriesTable()], $values, [20])), ARRAY_A);
        if ($wpdb->last_error !== '' || ! is_array($sample)) {
            return $this->invalidPreview(__('The non-personal sample could not be read. No exact preview is available.', 'help4-builder-suite'));
        }
        $digest = hash_init('sha256');
        $lastId = 0;
        $digestCount = 0;
        do {
            $wpdb->last_error = '';
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter -- eligibleWhere() returns fixed SQL fragments and placeholders only.
            $batch = $wpdb->get_results($wpdb->prepare('SELECT id, operations_revision, privacy_hold_revision FROM %i WHERE ' . $where . ' AND id > %d ORDER BY id ASC LIMIT %d', ...array_merge([$this->entriesTable()], $values, [$lastId, 500])), ARRAY_A);
            if ($wpdb->last_error !== '' || ! is_array($batch)) {
                return $this->invalidPreview(__('Stored messages changed or could not be read while fingerprinting. Refresh before relying on this preview.', 'help4-builder-suite'));
            }
            foreach ($batch as $row) {
                $lastId = (int) $row['id'];
                ++$digestCount;
                hash_update($digest, $lastId . ':' . (string) $row['operations_revision'] . ':' . (string) $row['privacy_hold_revision'] . "\n");
            }
        } while (count($batch) === 500);
        if ($digestCount !== $eligible) {
            return $this->invalidPreview(__('Data changed while previewing. Refresh before relying on this preview.', 'help4-builder-suite'));
        }
        $fingerprint = hash('sha256', (string) wp_json_encode([$policy['revision'], $policy['cutoff'], $eligible, $digestCount, hash_final($digest)]));
        $cutoffLocal = (new \DateTimeImmutable((string) $policy['cutoff'], new \DateTimeZone('UTC')))->setTimezone(wp_timezone());
        return ['valid' => true, 'error' => '', 'eligible' => $eligible, 'held' => $held, 'cutoff_label' => $cutoffLocal->format('Y-m-d H:i:s T'), 'sample' => $sample, 'fingerprint' => $fingerprint];
    }

    /** @return array{0:string,1:array<int,mixed>} */
    private function eligibleWhere(array $policy, bool $held): array
    {
        $formPlaceholders = implode(',', array_fill(0, count($policy['form_ids']), '%d'));
        $where = 'created_at < %s AND form_id IN (' . $formPlaceholders . ') AND privacy_hold_at IS ' . ($held ? 'NOT NULL' : 'NULL');
        $values = array_merge([(string) $policy['cutoff']], $policy['form_ids']);
        if ($policy['excluded_statuses']) {
            $where .= ' AND staff_status NOT IN (' . implode(',', array_fill(0, count($policy['excluded_statuses']), '%s')) . ')';
            array_push($values, ...$policy['excluded_statuses']);
        }
        return [$where, $values];
    }

    /** @return array{valid:false,error:string,eligible:0,held:0,cutoff_label:string,sample:array<int,mixed>,fingerprint:string} */
    private function invalidPreview(string $error): array
    {
        return ['valid' => false, 'error' => $error, 'eligible' => 0, 'held' => 0, 'cutoff_label' => '', 'sample' => [], 'fingerprint' => ''];
    }

    /** @return array<string,mixed>|null */
    private function entryForHold(int $entryId): ?array
    {
        global $wpdb;
        $wpdb->last_error = '';
        $row = $wpdb->get_row($wpdb->prepare('SELECT id, form_id, created_at, staff_status, privacy_hold_at, privacy_hold_reason, privacy_hold_by, privacy_hold_revision FROM %i WHERE id = %d LIMIT 1', $this->entriesTable(), $entryId), ARRAY_A);
        $this->holdEntryQueryFailed = $wpdb->last_error !== '';
        return ! $this->holdEntryQueryFailed && is_array($row) ? $row : null;
    }

    /** @return array<int,array<string,mixed>> */
    private function activeHolds(): array
    {
        global $wpdb;
        $wpdb->last_error = '';
        $rows = $wpdb->get_results($wpdb->prepare('SELECT id, form_id, privacy_hold_at FROM %i WHERE privacy_hold_at IS NOT NULL ORDER BY privacy_hold_at DESC, id DESC LIMIT %d', $this->entriesTable(), 20), ARRAY_A);
        $this->holdsQueryFailed = $wpdb->last_error !== '' || ! is_array($rows);
        return $this->holdsQueryFailed ? [] : $rows;
    }

    /** @return array<int,array<string,mixed>> */
    private function recentAudit(): array
    {
        global $wpdb;
        $wpdb->last_error = '';
        $rows = $wpdb->get_results($wpdb->prepare('SELECT action, entry_id, user_id, item_count, cutoff_at, policy_revision, fingerprint, created_at FROM %i ORDER BY id DESC LIMIT %d', $this->auditTable(), 20), ARRAY_A);
        $this->auditQueryFailed = $wpdb->last_error !== '' || ! is_array($rows);
        return $this->auditQueryFailed ? [] : $rows;
    }

    private function recordExportAudit(string $fingerprint, int $count, array $policy): bool
    {
        global $wpdb;
        $requestId = wp_generate_uuid4();
        $payloadHash = hash('sha256', $fingerprint . ':' . $count);
        return $wpdb->insert($this->auditTable(), ['request_id' => $requestId, 'action' => 'exact_export_prepared', 'entry_id' => 0, 'user_id' => get_current_user_id(), 'payload_hash' => $payloadHash, 'previous_revision' => '', 'new_revision' => $fingerprint, 'reason_hash' => '', 'item_count' => $count, 'cutoff_at' => (string) $policy['cutoff'], 'policy_revision' => (string) $policy['revision'], 'fingerprint' => $fingerprint, 'created_at' => current_time('mysql')], ['%s', '%s', '%d', '%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s']) === 1;
    }

    /** @param array<string,mixed> $entry */
    private function holdRevision(array $entry): string
    {
        $stored = (string) ($entry['privacy_hold_revision'] ?? '');
        return $stored !== '' ? $stored : hash('sha256', (string) wp_json_encode([(int) ($entry['id'] ?? 0), (string) ($entry['privacy_hold_at'] ?? ''), (string) ($entry['privacy_hold_reason'] ?? ''), (int) ($entry['privacy_hold_by'] ?? 0)]));
    }

    /** @return array<string,string> */
    private function statusLabels(): array
    {
        return ['new' => __('New', 'help4-builder-suite'), 'in_progress' => __('In progress', 'help4-builder-suite'), 'waiting' => __('Waiting for reply', 'help4-builder-suite'), 'resolved' => __('Resolved', 'help4-builder-suite')];
    }

    /** @param array<int,array{id:int,name:string}> $forms @param array<int,int> $selectedIds */
    private function formScopeLabel(array $forms, array $selectedIds): string
    {
        $names = [];
        foreach ($forms as $form) {
            if (in_array((int) $form['id'], $selectedIds, true)) {
                $names[] = (string) $form['name'];
            }
        }
        return $names ? implode(', ', $names) : __('No current forms selected', 'help4-builder-suite');
    }

    /** @param array<int,string> $excluded */
    private function statusScopeLabel(array $excluded): string
    {
        $labels = $this->statusLabels();
        $selected = array_map(static fn (string $status): string => (string) ($labels[$status] ?? $status), $excluded);
        return $selected ? implode(', ', $selected) : __('None', 'help4-builder-suite');
    }

    /** @param array<string,mixed> $row */
    private function auditSummary(array $row): string
    {
        $user = get_userdata((int) ($row['user_id'] ?? 0));
        /* translators: %d: WordPress staff user ID. */
        $actor = $user ? (string) $user->display_name : sprintf(__('Staff user #%d', 'help4-builder-suite'), (int) ($row['user_id'] ?? 0));
        $action = (string) ($row['action'] ?? '');
        if ($action === 'exact_export_prepared') {
            $fingerprint = substr((string) ($row['fingerprint'] ?? ''), 0, 12);
            /* translators: 1: message count, 2: UTC cutoff, 3: short fingerprint, 4: staff account, 5: audit date. */
            return sprintf(__('Exact export prepared · %1$d messages · cutoff %2$s UTC · fingerprint %3$s… · %4$s · %5$s', 'help4-builder-suite'), (int) ($row['item_count'] ?? 0), (string) ($row['cutoff_at'] ?? ''), $fingerprint, $actor, (string) ($row['created_at'] ?? ''));
        }
        $label = $action === 'hold_placed' ? __('Hold placed', 'help4-builder-suite') : ($action === 'hold_released' ? __('Hold released', 'help4-builder-suite') : ucwords(str_replace('_', ' ', $action)));
        /* translators: 1: audit action, 2: message ID, 3: staff account, 4: audit date. */
        return sprintf(__('%1$s · message #%2$d · %3$s · %4$s', 'help4-builder-suite'), $label, (int) ($row['entry_id'] ?? 0), $actor, (string) ($row['created_at'] ?? ''));
    }

    private function csvCell(string $value): string
    {
        return preg_match('/^[=+\-@\t\r]/', ltrim($value, ' ')) === 1 ? "'" . $value : $value;
    }

    private function requestId(): string
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Every registered POST handler verifies its action-specific nonce before calling this helper.
        $requestId = sanitize_text_field((string) wp_unslash($_POST['request_id'] ?? ''));
        if (preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i', $requestId) !== 1) {
            wp_die(esc_html__('The request identifier is invalid. Nothing was changed.', 'help4-builder-suite'), esc_html__('Request rejected', 'help4-builder-suite'), ['response' => 400]);
        }
        return $requestId;
    }

    private function requireCapability(string $capability): void
    {
        if (! current_user_can($capability)) {
            wp_die(esc_html__('Access denied.', 'help4-builder-suite'), '', ['response' => 403]);
        }
    }

    /** @param array<string,int|string> $extra */
    private function redirect(string $notice, array $extra = []): never
    {
        wp_safe_redirect(add_query_arg(array_merge(['page' => 'h4bs-form-privacy', 'privacy_notice' => $notice], $extra), admin_url('admin.php')));
        exit;
    }

    private function formsTable(): string
    {
        global $wpdb;
        return $wpdb->prefix . 'h4_forms';
    }

    private function entriesTable(): string
    {
        global $wpdb;
        return $wpdb->prefix . 'h4_form_entries';
    }

    private function auditTable(): string
    {
        global $wpdb;
        return $wpdb->prefix . 'h4_form_privacy_audit';
    }
}
