<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\EmailMarketing;

use Help4BuilderSuite\Contracts\ModuleContract;

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

// Dedicated tables must reflect current consent, suppression, queue, and engagement state.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching

final class Module implements ModuleContract
{
    private const SETTINGS = 'h4bs_email_marketing_settings';
    private const USAGE = 'h4bs_email_marketing_usage';
    private const CRON = 'h4bs_email_marketing_queue';
    private const BATCH = 25;
    private const SNAPSHOT_BATCH = 500;
    private const MAX_ATTEMPTS = 3;
    private const LOCK = 'h4bs_email_marketing_queue_lock';

    private ?TokenService $tokens = null;
    private ?MessageCompiler $compiler = null;
    private string $lastMailError = '';

    public function key(): string
    {
        return 'email_marketing';
    }

    public function boot(): void
    {
        add_filter('cron_schedules', [$this, 'cronSchedules']);
        add_action(self::CRON, [$this, 'processQueue']);
        add_action('init', [$this, 'ensureSchedule']);
        add_action('admin_init', [$this, 'addPrivacyPolicyContent']);
        add_action('wp_enqueue_scripts', [$this, 'enqueueSignupAssets']);
        add_action('admin_menu', [$this, 'adminMenu']);
        add_action('admin_post_h4bs_email_settings', [$this, 'saveSettings']);
        add_action('admin_post_h4bs_email_campaign', [$this, 'saveCampaign']);
        add_action('admin_post_h4bs_email_queue', [$this, 'queueCampaign']);
        add_action('admin_post_h4bs_email_campaign_status', [$this, 'campaignStatus']);
        add_action('admin_post_h4bs_email_segment', [$this, 'saveSegment']);
        add_action('admin_post_h4bs_email_template', [$this, 'saveTemplate']);
        add_action('admin_post_h4bs_email_automation', [$this, 'saveAutomation']);
        add_action('admin_post_h4bs_email_suppression', [$this, 'saveSuppression']);
        add_action('admin_post_h4bs_email_signup', [$this, 'signup']);
        add_action('admin_post_nopriv_h4bs_email_signup', [$this, 'signup']);
        add_action('admin_post_h4bs_email_confirm', [$this, 'confirm']);
        add_action('admin_post_nopriv_h4bs_email_confirm', [$this, 'confirm']);
        add_action('admin_post_h4bs_email_unsubscribe', [$this, 'unsubscribe']);
        add_action('admin_post_nopriv_h4bs_email_unsubscribe', [$this, 'unsubscribe']);
        add_action('admin_post_h4bs_email_open', [$this, 'trackOpen']);
        add_action('admin_post_nopriv_h4bs_email_open', [$this, 'trackOpen']);
        add_action('admin_post_h4bs_email_click', [$this, 'trackClick']);
        add_action('admin_post_nopriv_h4bs_email_click', [$this, 'trackClick']);
        add_shortcode('h4_email_signup', [$this, 'signupShortcode']);
        add_filter('wp_privacy_personal_data_exporters', [$this, 'privacyExporters']);
        add_filter('wp_privacy_personal_data_erasers', [$this, 'privacyErasers']);
        add_filter('h4bs_newsletter_subscriber_payload', [$this, 'filterNewsletterSubscriberPayload'], 10, 3);
        add_action('h4bs_newsletter_subscriber_saved', [$this, 'newsletterSubscriberSaved'], 10, 4);
        add_action('h4bs_newsletter_status_changed', [$this, 'newsletterStatusChanged'], 10, 2);
        add_action('wp_mail_failed', [$this, 'mailFailed']);
    }

    public function enqueueSignupAssets(): void
    {
        $post = get_post();
        if (! $post instanceof \WP_Post || ! has_shortcode((string) $post->post_content, 'h4_email_signup')) {
            return;
        }

        wp_enqueue_style('dashicons');
        wp_enqueue_style(
            'h4bs-frontend',
            H4BS_URL . 'assets/css/frontend.css',
            ['dashicons'],
            H4BS_VERSION
        );
    }

    public function addPrivacyPolicyContent(): void
    {
        if (! function_exists('wp_add_privacy_policy_content')) {
            return;
        }

        $content = '<p>' . esc_html__('Suggested text:', 'help4-builder-suite') . '</p>';
        $content .= '<p>' . esc_html__('When you choose to subscribe, we store the email address and any name you provide, the form source, tags, consent method and time, confirmation status, and campaign or automation delivery state. We use this information to send the email updates you requested, prevent duplicate or suppressed sends, and honor unsubscribe requests.', 'help4-builder-suite') . '</p>';
        $content .= '<p>' . esc_html__('If the site owner enables engagement reporting, signed first-party links and a small first-party image request may record estimated opens and link clicks. Known automated scanner and prefetch activity is classified separately. Raw subscriber email addresses and IP addresses are not written to engagement events.', 'help4-builder-suite') . '</p>';
        $content .= '<p>' . esc_html__('The configured WordPress mail transport receives each recipient address and message when mail is sent. An external SMTP or email-delivery provider may therefore process that information under the site owner\'s agreement with that provider. Unsubscribed addresses remain on a suppression list; privacy erasure replaces the address with a one-way suppression hash so future sends remain blocked.', 'help4-builder-suite') . '</p>';

        wp_add_privacy_policy_content(
            __('Help4 Email Marketing', 'help4-builder-suite'),
            wp_kses_post(wpautop($content, false))
        );
    }

    /** @param array<string,array<string,mixed>> $schedules @return array<string,array<string,mixed>> */
    public function cronSchedules(array $schedules): array
    {
        $schedules['h4bs_email_fifteen_minutes'] = ['interval' => 15 * MINUTE_IN_SECONDS, 'display' => 'Every 15 minutes'];
        return $schedules;
    }

    public function ensureSchedule(): void
    {
        if (! wp_next_scheduled(self::CRON)) {
            wp_schedule_event(time() + 300, 'h4bs_email_fifteen_minutes', self::CRON);
        }
    }

    public function adminMenu(): void
    {
        add_submenu_page('h4bs-settings', __('Email Marketing', 'help4-builder-suite'), __('Email Marketing', 'help4-builder-suite'), 'manage_options', 'h4bs-email-marketing', [$this, 'renderAdmin']);
    }

    /** @return array<string,mixed> */
    private function settings(): array
    {
        $saved = get_option(self::SETTINGS, []);
        return wp_parse_args(is_array($saved) ? $saved : [], [
            'from_name' => get_bloginfo('name'),
            'from_email' => get_option('admin_email'),
            'reply_to' => get_option('admin_email'),
            'postal_address' => '',
            'privacy_url' => get_privacy_policy_url(),
            'daily_limit' => 100,
            'tracking_enabled' => 0,
            'sender_verified' => 0,
            'authentication_verified' => 0,
            'double_opt_in' => 1,
            'consent_label' => __('I want to receive email updates and can unsubscribe at any time. The site may measure email opens and link clicks when engagement reporting is enabled.', 'help4-builder-suite'),
            'confirmation_subject' => __('Confirm your subscription', 'help4-builder-suite'),
            'confirmation_body' => __('Please confirm that you want to receive email from {{site_name}}.', 'help4-builder-suite'),
        ]);
    }

    public function renderAdmin(): void
    {
        $this->requireAdmin();
        global $wpdb;
        $settings = $this->settings();
        $campaigns = (array) $wpdb->get_results($wpdb->prepare(
            "SELECT c.*,COUNT(r.id) recipients,SUM(CASE WHEN r.status='accepted' THEN 1 ELSE 0 END) accepted,SUM(CASE WHEN r.status='failed' THEN 1 ELSE 0 END) failures,SUM(CASE WHEN r.open_count>0 THEN 1 ELSE 0 END) unique_opens,SUM(CASE WHEN r.click_count>0 THEN 1 ELSE 0 END) unique_clicks,SUM(r.open_count) total_opens,SUM(r.click_count) total_clicks FROM %i c LEFT JOIN %i r ON r.campaign_id=c.id GROUP BY c.id ORDER BY c.id DESC LIMIT 100",
            $this->table('campaigns'), $this->table('recipients')
        ), ARRAY_A);
        $segments = (array) $wpdb->get_results($wpdb->prepare('SELECT * FROM %i ORDER BY name', $this->table('segments')), ARRAY_A);
        $templates = (array) $wpdb->get_results($wpdb->prepare('SELECT * FROM %i ORDER BY name', $this->table('templates')), ARRAY_A);
        $automations = (array) $wpdb->get_results($wpdb->prepare('SELECT * FROM %i ORDER BY id DESC LIMIT 100', $this->table('automations')), ARRAY_A);
        $suppressions = (array) $wpdb->get_results($wpdb->prepare('SELECT * FROM %i ORDER BY id DESC LIMIT 100', $this->table('suppressions')), ARRAY_A);
        $suppressionCount = (int) $wpdb->get_var($wpdb->prepare('SELECT COUNT(*) FROM %i', $this->table('suppressions')));
        $subscriberCounts = (array) $wpdb->get_results($wpdb->prepare('SELECT status,COUNT(*) total FROM %i GROUP BY status', $this->subscribers()), ARRAY_A);
        $events = (array) $wpdb->get_results($wpdb->prepare('SELECT campaign_id,event_type,is_automated,COUNT(*) total FROM %i GROUP BY campaign_id,event_type,is_automated', $this->table('events')), ARRAY_A);
        $eventMap = [];
        foreach ($events as $event) {
            $key = ! empty($event['is_automated']) ? 'automated' : (string) $event['event_type'];
            $eventMap[(int) $event['campaign_id']][$key] = ($eventMap[(int) $event['campaign_id']][$key] ?? 0) + (int) $event['total'];
        }
        $counts = [];
        foreach ($subscriberCounts as $count) {
            $counts[sanitize_key((string) ($count['status'] ?? 'unknown'))] = (int) ($count['total'] ?? 0);
        }
        $readiness = $this->readiness();
        $usage = $this->usage();
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice selector.
        $notice = sanitize_key((string) wp_unslash($_GET['h4_email_notice'] ?? ''));
        $notices = [
            'settings_saved' => __('Sender settings saved.', 'help4-builder-suite'),
            'settings_invalid' => __('Enter valid From and Reply-To addresses.', 'help4-builder-suite'),
            'campaign_saved' => __('Campaign draft saved. Review it, then queue it.', 'help4-builder-suite'),
            'campaign_queued' => __('Campaign recipient snapshot is being prepared.', 'help4-builder-suite'),
            'campaign_updated' => __('Campaign status updated.', 'help4-builder-suite'),
            'campaign_not_ready' => __('Complete every sending-readiness gate before queueing.', 'help4-builder-suite'),
            'campaign_not_queueable' => __('Only an unsent draft, failed, or cancelled campaign can build a new recipient snapshot.', 'help4-builder-suite'),
            'campaign_invalid' => __('Campaign name, subject, and HTML content are required.', 'help4-builder-suite'),
            'segment_saved' => __('Segment saved.', 'help4-builder-suite'),
            'template_saved' => __('Template saved.', 'help4-builder-suite'),
            'automation_saved' => __('Automation saved.', 'help4-builder-suite'),
            'automation_steps_required' => __('Add at least one complete automation step.', 'help4-builder-suite'),
            'suppression_saved' => __('Address added to the global suppression list.', 'help4-builder-suite'),
            'suppression_removed' => __('Suppression removed. The contact is not automatically resubscribed.', 'help4-builder-suite'),
        ];
        ?>
        <div class="wrap h4bs-admin-wrap">
            <h1><?php esc_html_e('Email Marketing', 'help4-builder-suite'); ?></h1>
            <p><?php esc_html_e('Self-hosted audiences, campaigns, templates, segments, drip automations, consent, suppression, and engagement reporting. Business discovery and website research are intentionally separate.', 'help4-builder-suite'); ?></p>
            <?php if (isset($notices[$notice])) : ?><div class="notice notice-success is-dismissible"><p><?php echo esc_html($notices[$notice]); ?></p></div><?php endif; ?>

            <h2><?php esc_html_e('Audience and sending readiness', 'help4-builder-suite'); ?></h2>
            <p>
                <?php
                /* translators: 1: subscribed contacts, 2: pending contacts, 3: suppressed contacts, 4: today's attempts, 5: daily cap. */
                echo esc_html(sprintf(__('Subscribed: %1$d | Pending confirmation: %2$d | Suppressions: %3$d | Today: %4$d of %5$d attempts', 'help4-builder-suite'), (int) ($counts['subscribed'] ?? 0), (int) ($counts['pending'] ?? 0), $suppressionCount, $usage['attempted'], (int) $settings['daily_limit']));
                ?>
            </p>
            <?php if ($readiness === []) : ?>
                <div class="notice notice-success inline"><p><?php esc_html_e('Campaign sending gates are complete.', 'help4-builder-suite'); ?></p></div>
            <?php else : ?>
                <div class="notice notice-warning inline"><p><strong><?php esc_html_e('Sending remains locked:', 'help4-builder-suite'); ?></strong> <?php echo esc_html(implode(' ', $readiness)); ?></p></div>
            <?php endif; ?>

            <h2><?php esc_html_e('Campaign reports', 'help4-builder-suite'); ?></h2>
            <div class="h4bs-admin-table-scroll h4bs-admin-table-scroll--campaigns">
            <table class="widefat striped"><thead><tr><th><?php esc_html_e('Campaign', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Status', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Audience', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Accepted', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Unique opens', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Unique clicks', 'help4-builder-suite'); ?></th><th><?php esc_html_e('CTOR', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Failures', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Actions', 'help4-builder-suite'); ?></th></tr></thead><tbody>
            <?php foreach ($campaigns as $campaign) : ?>
                <?php
                $campaignId = (int) ($campaign['id'] ?? 0);
                $status = (string) ($campaign['status'] ?? 'draft');
                $accepted = (int) ($campaign['accepted'] ?? 0);
                $opens = (int) ($campaign['unique_opens'] ?? 0);
                $clicks = (int) ($campaign['unique_clicks'] ?? 0);
                $summary = $eventMap[$campaignId] ?? [];
                $queueUrl = wp_nonce_url(add_query_arg(['action' => 'h4bs_email_queue', 'campaign_id' => $campaignId], admin_url('admin-post.php')), 'h4bs_email_queue_' . $campaignId);
                $statusBase = add_query_arg(['action' => 'h4bs_email_campaign_status', 'campaign_id' => $campaignId], admin_url('admin-post.php'));
                ?>
                <tr>
                    <td><strong><?php echo esc_html((string) ($campaign['name'] ?? '')); ?></strong><br><small><?php echo esc_html((string) ($campaign['scheduled_at'] ?? '')); ?></small></td>
                    <td><?php echo esc_html($status); ?></td>
                    <td><?php echo esc_html((string) ((int) ($campaign['recipient_count'] ?? $campaign['recipients'] ?? 0))); ?></td>
                    <td><?php echo esc_html((string) $accepted); ?></td>
                    <td><?php echo esc_html(sprintf('%d (%.1f%%)', $opens, $this->rate($opens, $accepted))); ?></td>
                    <td><?php echo esc_html(sprintf('%d (%.1f%%)', $clicks, $this->rate($clicks, $accepted))); ?></td>
                    <td><?php echo esc_html(sprintf('%.1f%%', $this->rate($clicks, $opens))); ?></td>
                    <td><?php echo esc_html((string) ((int) ($campaign['failures'] ?? 0))); ?><br><small><?php
                        /* translators: %d: Number of automated scanner or prefetch events excluded from human engagement. */
                        echo esc_html(sprintf(__('Automated activity filtered: %d', 'help4-builder-suite'), (int) ($summary['automated'] ?? 0)));
                    ?></small></td>
                    <td>
                        <?php if (in_array($status, ['draft', 'failed', 'cancelled'], true)) : ?><a class="button button-small" href="<?php echo esc_url($queueUrl); ?>"><?php esc_html_e('Queue', 'help4-builder-suite'); ?></a><?php endif; ?>
                        <?php if (in_array($status, ['preparing', 'queued'], true)) : ?><a class="button button-small" href="<?php echo esc_url(wp_nonce_url(add_query_arg('operation', 'pause', $statusBase), 'h4bs_email_campaign_status_' . $campaignId)); ?>"><?php esc_html_e('Pause', 'help4-builder-suite'); ?></a><?php endif; ?>
                        <?php if ($status === 'paused') : ?><a class="button button-small" href="<?php echo esc_url(wp_nonce_url(add_query_arg('operation', 'resume', $statusBase), 'h4bs_email_campaign_status_' . $campaignId)); ?>"><?php esc_html_e('Resume', 'help4-builder-suite'); ?></a><?php endif; ?>
                        <?php if (in_array($status, ['preparing', 'queued', 'paused'], true)) : ?><a class="button button-small" href="<?php echo esc_url(wp_nonce_url(add_query_arg('operation', 'cancel', $statusBase), 'h4bs_email_campaign_status_' . $campaignId)); ?>"><?php esc_html_e('Cancel', 'help4-builder-suite'); ?></a><?php endif; ?>
                    </td>
                </tr>
            <?php endforeach; ?>
            <?php if ($campaigns === []) : ?><tr><td colspan="9"><?php esc_html_e('No campaigns yet.', 'help4-builder-suite'); ?></td></tr><?php endif; ?>
            </tbody></table>
            </div>
            <p class="description"><?php esc_html_e('Accepted means the configured WordPress mail transport accepted the message, not guaranteed inbox delivery. Opens are estimates; unique clicks are the stronger engagement signal.', 'help4-builder-suite'); ?></p>

            <div class="h4bs-admin-grid">
                <section>
                    <h2><?php esc_html_e('Create campaign draft', 'help4-builder-suite'); ?></h2>
                    <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                        <?php wp_nonce_field('h4bs_email_campaign'); ?><input type="hidden" name="action" value="h4bs_email_campaign">
                        <p><label><?php esc_html_e('Internal name', 'help4-builder-suite'); ?><br><input class="large-text" name="name" required></label></p>
                        <p><label><?php esc_html_e('Audience', 'help4-builder-suite'); ?><br><select name="segment_id"><option value="0"><?php esc_html_e('All subscribed contacts', 'help4-builder-suite'); ?></option><?php foreach ($segments as $segment) : ?><option value="<?php echo esc_attr((string) $segment['id']); ?>"><?php echo esc_html((string) $segment['name']); ?></option><?php endforeach; ?></select></label></p>
                        <p><label><?php esc_html_e('Start from template', 'help4-builder-suite'); ?><br><select name="template_id"><option value="0"><?php esc_html_e('None', 'help4-builder-suite'); ?></option><?php foreach ($templates as $template) : ?><option value="<?php echo esc_attr((string) $template['id']); ?>"><?php echo esc_html((string) $template['name']); ?></option><?php endforeach; ?></select></label></p>
                        <p><label><?php esc_html_e('Subject', 'help4-builder-suite'); ?><br><input class="large-text" name="subject"></label></p>
                        <p><label><?php esc_html_e('Preview text', 'help4-builder-suite'); ?><br><input class="large-text" name="preview_text"></label></p>
                        <p><label><?php esc_html_e('Message HTML', 'help4-builder-suite'); ?><br><textarea class="large-text code" rows="10" name="content_html"></textarea></label></p>
                        <p><label><?php esc_html_e('Plain-text fallback', 'help4-builder-suite'); ?><br><textarea class="large-text" rows="5" name="content_text"></textarea></label></p>
                        <p><label><?php esc_html_e('Schedule in UTC', 'help4-builder-suite'); ?> <input type="datetime-local" name="scheduled_at"></label></p>
                        <p><label><input type="checkbox" name="tracking_enabled" value="1" <?php checked(! empty($settings['tracking_enabled'])); ?>> <?php esc_html_e('Measure human opens and clicks; classify known scanner activity separately', 'help4-builder-suite'); ?></label></p>
                        <?php submit_button(__('Save draft', 'help4-builder-suite'), 'primary', 'submit', false); ?>
                    </form>
                </section>
                <section>
                    <h2><?php esc_html_e('Dynamic segment', 'help4-builder-suite'); ?></h2>
                    <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                        <?php wp_nonce_field('h4bs_email_segment'); ?><input type="hidden" name="action" value="h4bs_email_segment">
                        <p><label><?php esc_html_e('Name', 'help4-builder-suite'); ?><br><input class="regular-text" name="name" required></label></p>
                        <p><label><?php esc_html_e('Match', 'help4-builder-suite'); ?> <select name="match_type"><option value="all"><?php esc_html_e('all rules', 'help4-builder-suite'); ?></option><option value="any"><?php esc_html_e('any rule', 'help4-builder-suite'); ?></option></select></label></p>
                        <p><label><?php esc_html_e('Tag contains', 'help4-builder-suite'); ?><br><input class="regular-text" name="tag"></label></p>
                        <p><label><?php esc_html_e('Source contains', 'help4-builder-suite'); ?><br><input class="regular-text" name="source"></label></p>
                        <p><label><?php esc_html_e('Email domain equals', 'help4-builder-suite'); ?><br><input class="regular-text" name="email_domain" placeholder="example.com"></label></p>
                        <?php submit_button(__('Save segment', 'help4-builder-suite'), 'secondary', 'submit', false); ?>
                    </form>
                    <h2><?php esc_html_e('Reusable template', 'help4-builder-suite'); ?></h2>
                    <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                        <?php wp_nonce_field('h4bs_email_template'); ?><input type="hidden" name="action" value="h4bs_email_template">
                        <p><label><?php esc_html_e('Name', 'help4-builder-suite'); ?><br><input class="regular-text" name="name" required></label></p>
                        <p><label><?php esc_html_e('Default subject', 'help4-builder-suite'); ?><br><input class="large-text" name="subject"></label></p>
                        <p><label><?php esc_html_e('HTML', 'help4-builder-suite'); ?><br><textarea class="large-text code" rows="6" name="content_html" required></textarea></label></p>
                        <?php submit_button(__('Save template', 'help4-builder-suite'), 'secondary', 'submit', false); ?>
                    </form>
                </section>
            </div>

            <h2><?php esc_html_e('Welcome and drip automation', 'help4-builder-suite'); ?></h2>
            <p><?php esc_html_e('Active subscriber-added automations enroll only confirmed, non-suppressed contacts. Each delay is measured after the previous accepted step.', 'help4-builder-suite'); ?></p>
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                <?php wp_nonce_field('h4bs_email_automation'); ?><input type="hidden" name="action" value="h4bs_email_automation">
                <p><label><?php esc_html_e('Automation name', 'help4-builder-suite'); ?> <input class="regular-text" name="name" required></label> <label><?php esc_html_e('Status', 'help4-builder-suite'); ?> <select name="status"><option value="draft"><?php esc_html_e('Draft', 'help4-builder-suite'); ?></option><option value="active"><?php esc_html_e('Active', 'help4-builder-suite'); ?></option><option value="paused"><?php esc_html_e('Paused', 'help4-builder-suite'); ?></option></select></label> <label><?php esc_html_e('Segment', 'help4-builder-suite'); ?> <select name="segment_id"><option value="0"><?php esc_html_e('All confirmed contacts', 'help4-builder-suite'); ?></option><?php foreach ($segments as $segment) : ?><option value="<?php echo esc_attr((string) $segment['id']); ?>"><?php echo esc_html((string) $segment['name']); ?></option><?php endforeach; ?></select></label></p>
                <div class="h4bs-admin-table-scroll h4bs-admin-table-scroll--automation">
                <table class="widefat striped"><thead><tr><th><?php esc_html_e('Delay hours', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Subject', 'help4-builder-suite'); ?></th><th><?php esc_html_e('HTML message', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Plain text', 'help4-builder-suite'); ?></th></tr></thead><tbody>
                    <?php for ($step = 0; $step < 3; ++$step) : ?><tr><td><input type="number" min="0" max="8760" name="step_delay[]" value="<?php echo esc_attr($step === 0 ? '0' : '48'); ?>"></td><td><input class="large-text" name="step_subject[]"></td><td><textarea class="large-text code" rows="4" name="step_html[]"></textarea></td><td><textarea class="large-text" rows="4" name="step_text[]"></textarea></td></tr><?php endfor; ?>
                </tbody></table>
                </div>
                <?php submit_button(__('Save automation', 'help4-builder-suite')); ?>
            </form>
            <?php if ($automations !== []) : ?><p><?php esc_html_e('Existing automations:', 'help4-builder-suite'); ?> <?php echo esc_html(implode(', ', array_map(static fn (array $automation): string => (string) ($automation['name'] ?? '') . ' (' . (string) ($automation['status'] ?? '') . ')', $automations))); ?></p><?php endif; ?>

            <h2><?php esc_html_e('Global suppression list', 'help4-builder-suite'); ?></h2>
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                <?php wp_nonce_field('h4bs_email_suppression'); ?><input type="hidden" name="action" value="h4bs_email_suppression"><input type="hidden" name="operation" value="add">
                <label><?php esc_html_e('Email', 'help4-builder-suite'); ?> <input type="email" name="email" required></label> <label><?php esc_html_e('Reason', 'help4-builder-suite'); ?> <input class="regular-text" name="detail"></label> <?php submit_button(__('Suppress', 'help4-builder-suite'), 'secondary', 'submit', false); ?>
            </form>
            <div class="h4bs-admin-table-scroll h4bs-admin-table-scroll--suppressions">
            <table class="widefat striped"><thead><tr><th><?php esc_html_e('Address', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Reason', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Added', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Action', 'help4-builder-suite'); ?></th></tr></thead><tbody>
            <?php foreach ($suppressions as $suppression) : ?><tr><td><?php echo str_starts_with((string) $suppression['email'], 'sha256:') ? esc_html__('Retained one-way privacy hash', 'help4-builder-suite') : esc_html((string) $suppression['email']); ?></td><td><?php echo esc_html((string) $suppression['reason']); ?></td><td><?php echo esc_html((string) $suppression['created_at']); ?></td><td><form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"><?php wp_nonce_field('h4bs_email_suppression'); ?><input type="hidden" name="action" value="h4bs_email_suppression"><input type="hidden" name="operation" value="remove"><input type="hidden" name="suppression_id" value="<?php echo esc_attr((string) $suppression['id']); ?>"><button class="button button-small"><?php esc_html_e('Remove', 'help4-builder-suite'); ?></button></form></td></tr><?php endforeach; ?>
            <?php if ($suppressions === []) : ?><tr><td colspan="4"><?php esc_html_e('No suppressions.', 'help4-builder-suite'); ?></td></tr><?php endif; ?></tbody></table>
            </div>

            <h2><?php esc_html_e('Sender and compliance settings', 'help4-builder-suite'); ?></h2>
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                <?php wp_nonce_field('h4bs_email_settings'); ?><input type="hidden" name="action" value="h4bs_email_settings">
                <table class="form-table" role="presentation"><tbody>
                    <tr><th><label for="h4bs-email-from-name"><?php esc_html_e('From', 'help4-builder-suite'); ?></label></th><td><input id="h4bs-email-from-name" name="from_name" value="<?php echo esc_attr((string) $settings['from_name']); ?>"> <input type="email" name="from_email" value="<?php echo esc_attr((string) $settings['from_email']); ?>" required></td></tr>
                    <tr><th><label for="h4bs-email-reply"><?php esc_html_e('Reply-To', 'help4-builder-suite'); ?></label></th><td><input id="h4bs-email-reply" type="email" name="reply_to" value="<?php echo esc_attr((string) $settings['reply_to']); ?>" required></td></tr>
                    <tr><th><label for="h4bs-email-address"><?php esc_html_e('Postal address', 'help4-builder-suite'); ?></label></th><td><textarea id="h4bs-email-address" class="large-text" name="postal_address" required><?php echo esc_textarea((string) $settings['postal_address']); ?></textarea></td></tr>
                    <tr><th><label for="h4bs-email-privacy"><?php esc_html_e('Privacy URL', 'help4-builder-suite'); ?></label></th><td><input id="h4bs-email-privacy" class="large-text" type="url" name="privacy_url" value="<?php echo esc_attr((string) $settings['privacy_url']); ?>" required></td></tr>
                    <tr><th><label for="h4bs-email-cap"><?php esc_html_e('Daily attempt cap', 'help4-builder-suite'); ?></label></th><td><input id="h4bs-email-cap" type="number" min="1" max="50000" name="daily_limit" value="<?php echo esc_attr((string) $settings['daily_limit']); ?>"></td></tr>
                    <tr><th><label for="h4bs-email-consent"><?php esc_html_e('Consent label', 'help4-builder-suite'); ?></label></th><td><input id="h4bs-email-consent" class="large-text" name="consent_label" value="<?php echo esc_attr((string) $settings['consent_label']); ?>" required></td></tr>
                    <tr><th><?php esc_html_e('Confirmation email', 'help4-builder-suite'); ?></th><td><input class="large-text" name="confirmation_subject" value="<?php echo esc_attr((string) $settings['confirmation_subject']); ?>" required><br><textarea class="large-text" name="confirmation_body" rows="3" required><?php echo esc_textarea((string) $settings['confirmation_body']); ?></textarea></td></tr>
                    <tr><th><?php esc_html_e('Readiness gates', 'help4-builder-suite'); ?></th><td><label><input type="checkbox" name="sender_verified" value="1" <?php checked($settings['sender_verified']); ?>> <?php esc_html_e('Sending mailbox tested', 'help4-builder-suite'); ?></label><br><label><input type="checkbox" name="authentication_verified" value="1" <?php checked($settings['authentication_verified']); ?>> <?php esc_html_e('SPF, DKIM, and DMARC passed', 'help4-builder-suite'); ?></label><br><label><input type="checkbox" name="tracking_enabled" value="1" <?php checked($settings['tracking_enabled']); ?>> <?php esc_html_e('Local engagement reporting enabled', 'help4-builder-suite'); ?></label></td></tr>
                </tbody></table>
                <?php submit_button(__('Save sender settings', 'help4-builder-suite')); ?>
            </form>
            <h2><?php esc_html_e('Signup form', 'help4-builder-suite'); ?></h2><p><code>[h4_email_signup]</code> <?php esc_html_e('uses required consent, confirmation before activation, and the same global suppression list as Forms -> Newsletter.', 'help4-builder-suite'); ?></p>
        </div>
        <?php
    }

    public function saveSettings(): void
    {
        $this->requireAdmin();
        check_admin_referer('h4bs_email_settings');

        $fromEmail = sanitize_email((string) wp_unslash($_POST['from_email'] ?? ''));
        $replyTo = sanitize_email((string) wp_unslash($_POST['reply_to'] ?? ''));
        if ($fromEmail === '' || $replyTo === '') {
            $this->adminRedirect('settings_invalid');
        }

        update_option(self::SETTINGS, [
            'from_name' => sanitize_text_field((string) wp_unslash($_POST['from_name'] ?? '')),
            'from_email' => $fromEmail,
            'reply_to' => $replyTo,
            'postal_address' => sanitize_textarea_field((string) wp_unslash($_POST['postal_address'] ?? '')),
            'privacy_url' => esc_url_raw((string) wp_unslash($_POST['privacy_url'] ?? '')),
            'daily_limit' => max(1, min(50000, absint(wp_unslash($_POST['daily_limit'] ?? 100)))),
            'tracking_enabled' => ! empty($_POST['tracking_enabled']) ? 1 : 0,
            'sender_verified' => ! empty($_POST['sender_verified']) ? 1 : 0,
            'authentication_verified' => ! empty($_POST['authentication_verified']) ? 1 : 0,
            'double_opt_in' => 1,
            'consent_label' => sanitize_text_field((string) wp_unslash($_POST['consent_label'] ?? '')),
            'confirmation_subject' => sanitize_text_field((string) wp_unslash($_POST['confirmation_subject'] ?? '')),
            'confirmation_body' => sanitize_textarea_field((string) wp_unslash($_POST['confirmation_body'] ?? '')),
        ], false);

        $this->adminRedirect('settings_saved');
    }

    public function saveTemplate(): void
    {
        $this->requireAdmin();
        check_admin_referer('h4bs_email_template');

        $name = sanitize_text_field((string) wp_unslash($_POST['name'] ?? ''));
        $content = wp_kses_post((string) wp_unslash($_POST['content_html'] ?? ''));
        if ($name === '' || $content === '') {
            $this->adminRedirect('template_invalid');
        }

        global $wpdb;
        $wpdb->insert($this->table('templates'), [
            'name' => $name,
            'subject' => sanitize_text_field((string) wp_unslash($_POST['subject'] ?? '')),
            'preview_text' => sanitize_text_field((string) wp_unslash($_POST['preview_text'] ?? '')),
            'content_html' => $content,
            'content_text' => sanitize_textarea_field((string) wp_unslash($_POST['content_text'] ?? '')),
            'status' => 'active',
        ], ['%s', '%s', '%s', '%s', '%s', '%s']);

        $this->adminRedirect('template_saved');
    }

    public function saveSegment(): void
    {
        $this->requireAdmin();
        check_admin_referer('h4bs_email_segment');

        $name = sanitize_text_field((string) wp_unslash($_POST['name'] ?? ''));
        if ($name === '') {
            $this->adminRedirect('segment_invalid');
        }

        $rules = [];
        $tag = sanitize_text_field((string) wp_unslash($_POST['tag'] ?? ''));
        $source = sanitize_text_field((string) wp_unslash($_POST['source'] ?? ''));
        $emailDomain = strtolower(sanitize_text_field((string) wp_unslash($_POST['email_domain'] ?? '')));
        if ($tag !== '') {
            $rules[] = ['field' => 'tags', 'operator' => 'contains', 'value' => $tag];
        }
        if ($source !== '') {
            $rules[] = ['field' => 'source', 'operator' => 'contains', 'value' => $source];
        }
        if ($emailDomain !== '') {
            $rules[] = ['field' => 'email_domain', 'operator' => 'equals', 'value' => ltrim($emailDomain, '@')];
        }

        global $wpdb;
        $matchType = sanitize_key((string) wp_unslash($_POST['match_type'] ?? 'all'));
        $wpdb->insert($this->table('segments'), [
            'name' => $name,
            'match_type' => in_array($matchType, ['all', 'any'], true) ? $matchType : 'all',
            'rules_json' => wp_json_encode($rules),
            'status' => 'active',
        ], ['%s', '%s', '%s', '%s']);

        $this->adminRedirect('segment_saved');
    }

    public function saveCampaign(): void
    {
        $this->requireAdmin();
        check_admin_referer('h4bs_email_campaign');

        $name = sanitize_text_field((string) wp_unslash($_POST['name'] ?? ''));
        $subject = sanitize_text_field((string) wp_unslash($_POST['subject'] ?? ''));
        $previewText = sanitize_text_field((string) wp_unslash($_POST['preview_text'] ?? ''));
        $contentHtml = wp_kses_post((string) wp_unslash($_POST['content_html'] ?? ''));
        $contentText = sanitize_textarea_field((string) wp_unslash($_POST['content_text'] ?? ''));
        $templateId = absint(wp_unslash($_POST['template_id'] ?? 0));

        if ($templateId > 0) {
            global $wpdb;
            $template = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE id = %d AND status = %s', $this->table('templates'), $templateId, 'active'), ARRAY_A);
            if (is_array($template)) {
                $subject = $subject !== '' ? $subject : sanitize_text_field((string) ($template['subject'] ?? ''));
                $previewText = $previewText !== '' ? $previewText : sanitize_text_field((string) ($template['preview_text'] ?? ''));
                $contentHtml = $contentHtml !== '' ? $contentHtml : wp_kses_post((string) ($template['content_html'] ?? ''));
                $contentText = $contentText !== '' ? $contentText : sanitize_textarea_field((string) ($template['content_text'] ?? ''));
            }
        }

        if ($name === '' || $subject === '' || $contentHtml === '') {
            $this->adminRedirect('campaign_invalid');
        }

        $scheduledAt = $this->normalizeUtcDate(sanitize_text_field((string) wp_unslash($_POST['scheduled_at'] ?? '')));
        global $wpdb;
        $wpdb->insert($this->table('campaigns'), [
            'name' => $name,
            'segment_id' => absint(wp_unslash($_POST['segment_id'] ?? 0)) ?: null,
            'subject' => $subject,
            'preview_text' => $previewText,
            'content_html' => $contentHtml,
            'content_text' => $contentText,
            'status' => 'draft',
            'tracking_enabled' => ! empty($_POST['tracking_enabled']) ? 1 : 0,
            'scheduled_at' => $scheduledAt,
            'created_by' => get_current_user_id(),
        ], ['%s', '%d', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%d']);

        $this->adminRedirect('campaign_saved');
    }

    public function queueCampaign(): void
    {
        $this->requireAdmin();
        $campaignId = absint(wp_unslash($_REQUEST['campaign_id'] ?? 0));
        check_admin_referer('h4bs_email_queue_' . $campaignId);
        if ($campaignId <= 0) {
            $this->adminRedirect('campaign_invalid');
        }

        $readiness = $this->readiness();
        if ($readiness !== []) {
            $this->adminRedirect('campaign_not_ready');
        }

        global $wpdb;
        $campaign = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE id = %d', $this->table('campaigns'), $campaignId), ARRAY_A);
        if (! is_array($campaign) || ! in_array((string) ($campaign['status'] ?? ''), ['draft', 'failed', 'cancelled'], true)) {
            $this->adminRedirect('campaign_not_queueable');
        }

        $wpdb->delete($this->table('recipients'), ['campaign_id' => $campaignId], ['%d']);
        $wpdb->update($this->table('campaigns'), [
            'status' => 'preparing',
            'snapshot_cursor' => 0,
            'snapshot_complete' => 0,
            'recipient_count' => 0,
            'queued_at' => current_time('mysql', true),
            'pause_reason' => '',
            'started_at' => null,
            'completed_at' => null,
        ], ['id' => $campaignId], ['%s', '%d', '%d', '%d', '%s', '%s', '%s', '%s'], ['%d']);
        wp_schedule_single_event(time() + 5, self::CRON);

        $this->adminRedirect('campaign_queued');
    }

    public function campaignStatus(): void
    {
        $this->requireAdmin();
        $campaignId = absint(wp_unslash($_REQUEST['campaign_id'] ?? 0));
        $operation = sanitize_key((string) wp_unslash($_REQUEST['operation'] ?? ''));
        check_admin_referer('h4bs_email_campaign_status_' . $campaignId);

        global $wpdb;
        if ($operation === 'pause') {
            $wpdb->update($this->table('campaigns'), ['status' => 'paused', 'pause_reason' => 'manual'], ['id' => $campaignId], ['%s', '%s'], ['%d']);
            $wpdb->query($wpdb->prepare("UPDATE %i SET status = 'paused' WHERE campaign_id = %d AND status IN ('queued','retry')", $this->table('recipients'), $campaignId));
        } elseif ($operation === 'resume') {
            $snapshotComplete = (int) $wpdb->get_var($wpdb->prepare('SELECT snapshot_complete FROM %i WHERE id = %d', $this->table('campaigns'), $campaignId));
            $wpdb->update($this->table('campaigns'), ['status' => $snapshotComplete === 1 ? 'queued' : 'preparing', 'pause_reason' => ''], ['id' => $campaignId], ['%s', '%s'], ['%d']);
            $wpdb->query($wpdb->prepare("UPDATE %i SET status = 'queued' WHERE campaign_id = %d AND status = 'paused'", $this->table('recipients'), $campaignId));
            wp_schedule_single_event(time() + 5, self::CRON);
        } elseif ($operation === 'cancel') {
            $wpdb->update($this->table('campaigns'), ['status' => 'cancelled', 'completed_at' => current_time('mysql', true)], ['id' => $campaignId], ['%s', '%s'], ['%d']);
            $wpdb->query($wpdb->prepare("UPDATE %i SET status = 'cancelled' WHERE campaign_id = %d AND status IN ('queued','retry','paused','sending')", $this->table('recipients'), $campaignId));
        }

        $this->adminRedirect('campaign_updated');
    }

    public function mailFailed(\WP_Error $error): void
    {
        $this->lastMailError = sanitize_text_field($error->get_error_message());
    }

    public function processQueue(): void
    {
        if (! $this->acquireQueueLock()) {
            return;
        }

        try {
            $this->recoverStaleClaims();
            $this->prepareCampaignRecipients();
            $this->processAutomationQueue();
            $this->processCampaignQueue();
            $this->completeFinishedCampaigns();
        } finally {
            delete_option(self::LOCK);
        }

        if ($this->hasPendingWork()) {
            wp_schedule_single_event(time() + 60, self::CRON);
        }
    }

    private function acquireQueueLock(): bool
    {
        $now = time();
        if (add_option(self::LOCK, $now, '', false)) {
            return true;
        }

        $existing = (int) get_option(self::LOCK, 0);
        if ($existing > 0 && ($now - $existing) < 10 * MINUTE_IN_SECONDS) {
            return false;
        }

        delete_option(self::LOCK);
        return add_option(self::LOCK, $now, '', false);
    }

    private function recoverStaleClaims(): void
    {
        global $wpdb;
        $cutoff = gmdate('Y-m-d H:i:s', time() - 20 * MINUTE_IN_SECONDS);
        $wpdb->query($wpdb->prepare(
            "UPDATE %i SET status = 'retry', next_attempt_at = %s WHERE status = 'sending' AND updated_at < %s",
            $this->table('recipients'),
            current_time('mysql', true),
            $cutoff
        ));
        $wpdb->query($wpdb->prepare(
            "UPDATE %i SET status = 'active', next_send_at = %s WHERE status = 'processing' AND updated_at < %s",
            $this->table('enrollments'),
            current_time('mysql', true),
            $cutoff
        ));
    }

    private function prepareCampaignRecipients(): void
    {
        global $wpdb;
        $campaign = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM %i WHERE status = 'preparing' ORDER BY id ASC LIMIT 1",
            $this->table('campaigns')
        ), ARRAY_A);
        if (! is_array($campaign)) {
            return;
        }

        $campaignId = (int) ($campaign['id'] ?? 0);
        $cursor = (int) ($campaign['snapshot_cursor'] ?? 0);
        $subscribers = $wpdb->get_results($wpdb->prepare(
            "SELECT s.* FROM %i s LEFT JOIN %i x ON x.email = s.email WHERE s.status = %s AND s.id > %d AND x.id IS NULL ORDER BY s.id ASC LIMIT %d",
            $this->subscribers(),
            $this->table('suppressions'),
            'subscribed',
            $cursor,
            self::SNAPSHOT_BATCH
        ), ARRAY_A);
        if (! is_array($subscribers)) {
            $subscribers = [];
        }

        $segment = null;
        $segmentId = (int) ($campaign['segment_id'] ?? 0);
        if ($segmentId > 0) {
            $segment = $wpdb->get_row($wpdb->prepare(
                'SELECT * FROM %i WHERE id = %d AND status = %s',
                $this->table('segments'),
                $segmentId,
                'active'
            ), ARRAY_A);
            if (! is_array($segment)) {
                $wpdb->update($this->table('campaigns'), ['status' => 'failed', 'pause_reason' => 'segment_missing'], ['id' => $campaignId], ['%s', '%s'], ['%d']);
                return;
            }
        }

        $inserted = 0;
        $lastId = $cursor;
        foreach ($subscribers as $subscriber) {
            $lastId = (int) ($subscriber['id'] ?? $lastId);
            if (is_array($segment) && ! $this->subscriberMatchesSegment($subscriber, $segment)) {
                continue;
            }
            $result = $wpdb->query($wpdb->prepare(
                "INSERT IGNORE INTO %i (campaign_id,subscriber_id,status,next_attempt_at) VALUES (%d,%d,'queued',%s)",
                $this->table('recipients'),
                $campaignId,
                (int) ($subscriber['id'] ?? 0),
                current_time('mysql', true)
            ));
            if ($result === 1) {
                ++$inserted;
            }
        }

        $wpdb->query($wpdb->prepare(
            'UPDATE %i SET snapshot_cursor = %d, recipient_count = recipient_count + %d WHERE id = %d',
            $this->table('campaigns'),
            $lastId,
            $inserted,
            $campaignId
        ));

        if (count($subscribers) < self::SNAPSHOT_BATCH) {
            $count = (int) $wpdb->get_var($wpdb->prepare('SELECT COUNT(*) FROM %i WHERE campaign_id = %d', $this->table('recipients'), $campaignId));
            $status = $count > 0 ? 'queued' : 'completed';
            $wpdb->update($this->table('campaigns'), [
                'status' => $status,
                'snapshot_complete' => 1,
                'recipient_count' => $count,
                'completed_at' => $count > 0 ? null : current_time('mysql', true),
            ], ['id' => $campaignId], ['%s', '%d', '%d', '%s'], ['%d']);
        }
    }

    private function processCampaignQueue(): void
    {
        $remaining = $this->remainingDailyCapacity();
        if ($remaining <= 0) {
            return;
        }
        if ($this->readiness() !== []) {
            global $wpdb;
            $wpdb->query($wpdb->prepare("UPDATE %i SET status = 'paused', pause_reason = 'readiness_failed' WHERE status = 'queued'", $this->table('campaigns')));
            $wpdb->query($wpdb->prepare("UPDATE %i r INNER JOIN %i c ON c.id = r.campaign_id SET r.status = 'paused' WHERE c.pause_reason = 'readiness_failed' AND r.status IN ('queued','retry')", $this->table('recipients'), $this->table('campaigns')));
            return;
        }

        global $wpdb;
        $limit = min(self::BATCH, $remaining);
        $now = current_time('mysql', true);
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT r.*,c.subject,c.preview_text,c.content_html,c.content_text,c.tracking_enabled,c.status campaign_status,c.scheduled_at,s.email,s.first_name,s.last_name,s.tags,s.source,s.status subscriber_status
             FROM %i r
             INNER JOIN %i c ON c.id = r.campaign_id
             INNER JOIN %i s ON s.id = r.subscriber_id
             LEFT JOIN %i x ON x.email = s.email
             WHERE c.status = 'queued'
               AND (c.scheduled_at IS NULL OR c.scheduled_at <= %s)
               AND r.status IN ('queued','retry')
               AND (r.next_attempt_at IS NULL OR r.next_attempt_at <= %s)
               AND s.status = 'subscribed'
               AND x.id IS NULL
             ORDER BY r.id ASC LIMIT %d",
            $this->table('recipients'),
            $this->table('campaigns'),
            $this->subscribers(),
            $this->table('suppressions'),
            $now,
            $now,
            $limit
        ), ARRAY_A);
        if (! is_array($rows)) {
            return;
        }

        foreach ($rows as $row) {
            if ($this->remainingDailyCapacity() <= 0) {
                break;
            }
            $recipientId = (int) ($row['id'] ?? 0);
            $claimed = $wpdb->query($wpdb->prepare(
                "UPDATE %i SET status = 'sending' WHERE id = %d AND status IN ('queued','retry')",
                $this->table('recipients'),
                $recipientId
            ));
            if ($claimed !== 1) {
                continue;
            }

            $campaign = [
                'id' => (int) ($row['campaign_id'] ?? 0),
                'subject' => (string) ($row['subject'] ?? ''),
                'preview_text' => (string) ($row['preview_text'] ?? ''),
                'content_html' => (string) ($row['content_html'] ?? ''),
                'content_text' => (string) ($row['content_text'] ?? ''),
                'tracking_enabled' => (int) ($row['tracking_enabled'] ?? 0),
            ];
            $subscriber = [
                'id' => (int) ($row['subscriber_id'] ?? 0),
                'email' => (string) ($row['email'] ?? ''),
                'first_name' => (string) ($row['first_name'] ?? ''),
                'last_name' => (string) ($row['last_name'] ?? ''),
            ];
            $message = $this->compiler()->campaign($campaign, $subscriber, $row, $this->settings());
            $this->attemptCampaignDelivery($row, $subscriber, $message);
        }
    }

    /**
     * @param array<string,mixed> $recipient
     * @param array<string,mixed> $subscriber
     * @param array{subject:string,html:string,text:string,headers:array<int,string>,message_id:string} $message
     */
    private function attemptCampaignDelivery(array $recipient, array $subscriber, array $message): void
    {
        global $wpdb;
        $recipientId = (int) ($recipient['id'] ?? 0);
        $attempts = (int) ($recipient['attempts'] ?? 0) + 1;
        $this->incrementUsage();
        $this->lastMailError = '';
        $altBody = (string) $message['text'];
        $altBodySetter = static function ($mailer) use ($altBody): void {
            if (is_object($mailer) && property_exists($mailer, 'AltBody')) {
                $mailer->AltBody = $altBody;
            }
        };
        add_action('phpmailer_init', $altBodySetter, 99);
        $accepted = wp_mail(
            sanitize_email((string) ($subscriber['email'] ?? '')),
            (string) $message['subject'],
            (string) $message['html'],
            $message['headers']
        );
        remove_action('phpmailer_init', $altBodySetter, 99);

        if ($accepted) {
            $wpdb->update($this->table('recipients'), [
                'status' => 'accepted',
                'attempts' => $attempts,
                'accepted_at' => current_time('mysql', true),
                'message_id' => (string) $message['message_id'],
                'last_error' => '',
            ], ['id' => $recipientId], ['%s', '%d', '%s', '%s', '%s'], ['%d']);
            $wpdb->query($wpdb->prepare(
                'UPDATE %i SET started_at = COALESCE(started_at,%s) WHERE id = %d',
                $this->table('campaigns'),
                current_time('mysql', true),
                (int) ($recipient['campaign_id'] ?? 0)
            ));
            $this->recordEvent('accepted', [
                'campaign_id' => (int) ($recipient['campaign_id'] ?? 0),
                'recipient_id' => $recipientId,
                'subscriber_id' => (int) ($recipient['subscriber_id'] ?? 0),
            ], false);
            return;
        }

        $failed = $attempts >= self::MAX_ATTEMPTS;
        $delay = [1 => 15 * MINUTE_IN_SECONDS, 2 => HOUR_IN_SECONDS, 3 => 6 * HOUR_IN_SECONDS][$attempts] ?? 6 * HOUR_IN_SECONDS;
        $wpdb->update($this->table('recipients'), [
            'status' => $failed ? 'failed' : 'retry',
            'attempts' => $attempts,
            'next_attempt_at' => $failed ? null : gmdate('Y-m-d H:i:s', time() + $delay),
            'last_error' => $this->lastMailError !== '' ? $this->lastMailError : __('Mail transport rejected the message.', 'help4-builder-suite'),
        ], ['id' => $recipientId], ['%s', '%d', '%s', '%s'], ['%d']);
    }

    private function completeFinishedCampaigns(): void
    {
        global $wpdb;
        $campaigns = $wpdb->get_results($wpdb->prepare(
            "SELECT id FROM %i WHERE status = 'queued' ORDER BY id ASC LIMIT 50",
            $this->table('campaigns')
        ), ARRAY_A);
        if (! is_array($campaigns)) {
            return;
        }
        foreach ($campaigns as $campaign) {
            $campaignId = (int) ($campaign['id'] ?? 0);
            $pending = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM %i WHERE campaign_id = %d AND status IN ('queued','retry','sending','paused')",
                $this->table('recipients'),
                $campaignId
            ));
            if ($pending === 0) {
                $wpdb->update($this->table('campaigns'), ['status' => 'completed', 'completed_at' => current_time('mysql', true)], ['id' => $campaignId], ['%s', '%s'], ['%d']);
            }
        }
    }

    private function hasPendingWork(): bool
    {
        global $wpdb;
        $campaigns = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM %i c
             WHERE c.status = 'preparing'
                OR (c.status = 'queued' AND (c.scheduled_at IS NULL OR c.scheduled_at <= %s)
                    AND EXISTS (SELECT 1 FROM %i r WHERE r.campaign_id = c.id AND r.status IN ('queued','retry') AND (r.next_attempt_at IS NULL OR r.next_attempt_at <= %s)))",
            $this->table('campaigns'),
            gmdate('Y-m-d H:i:s', time() + 2 * MINUTE_IN_SECONDS),
            $this->table('recipients'),
            gmdate('Y-m-d H:i:s', time() + 2 * MINUTE_IN_SECONDS)
        ));
        $automations = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM %i WHERE status = 'processing' OR (status = 'active' AND next_send_at <= %s)",
            $this->table('enrollments'),
            gmdate('Y-m-d H:i:s', time() + 2 * MINUTE_IN_SECONDS)
        ));
        return $campaigns > 0 || $automations > 0;
    }

    public function saveAutomation(): void
    {
        $this->requireAdmin();
        check_admin_referer('h4bs_email_automation');

        $name = sanitize_text_field((string) wp_unslash($_POST['name'] ?? ''));
        $status = sanitize_key((string) wp_unslash($_POST['status'] ?? 'draft'));
        if (! in_array($status, ['draft', 'active', 'paused'], true)) {
            $status = 'draft';
        }
        if ($name === '') {
            $this->adminRedirect('automation_invalid');
        }

        $delays = isset($_POST['step_delay']) && is_array($_POST['step_delay']) ? wp_unslash($_POST['step_delay']) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each numeric item is bounded below.
        $subjects = isset($_POST['step_subject']) && is_array($_POST['step_subject']) ? wp_unslash($_POST['step_subject']) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each subject is sanitized below.
        $htmlBodies = isset($_POST['step_html']) && is_array($_POST['step_html']) ? wp_unslash($_POST['step_html']) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each body is passed through wp_kses_post below.
        $textBodies = isset($_POST['step_text']) && is_array($_POST['step_text']) ? wp_unslash($_POST['step_text']) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each text body is sanitized below.
        $steps = [];
        for ($index = 0; $index < min(12, max(count($subjects), count($htmlBodies))); ++$index) {
            $subject = sanitize_text_field((string) ($subjects[$index] ?? ''));
            $html = wp_kses_post((string) ($htmlBodies[$index] ?? ''));
            if ($subject === '' || $html === '') {
                continue;
            }
            $steps[] = [
                'delay_hours' => max(0, min(8760, absint($delays[$index] ?? 0))),
                'subject' => $subject,
                'content_html' => $html,
                'content_text' => sanitize_textarea_field((string) ($textBodies[$index] ?? '')),
            ];
        }
        if ($steps === []) {
            $this->adminRedirect('automation_steps_required');
        }

        global $wpdb;
        $wpdb->insert($this->table('automations'), [
            'name' => $name,
            'trigger_type' => 'subscriber_added',
            'segment_id' => absint(wp_unslash($_POST['segment_id'] ?? 0)) ?: null,
            'status' => $status,
        ], ['%s', '%s', '%d', '%s']);
        $automationId = (int) $wpdb->insert_id;
        foreach ($steps as $index => $step) {
            $wpdb->insert($this->table('automation_steps'), [
                'automation_id' => $automationId,
                'step_order' => $index + 1,
                'delay_hours' => $step['delay_hours'],
                'subject' => $step['subject'],
                'content_html' => $step['content_html'],
                'content_text' => $step['content_text'],
            ], ['%d', '%d', '%d', '%s', '%s', '%s']);
        }

        $this->adminRedirect('automation_saved');
    }

    /**
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    public function filterNewsletterSubscriberPayload(array $payload, int $existingId = 0, string $context = 'public'): array
    {
        $email = sanitize_email((string) ($payload['email'] ?? ''));
        if ($email === '') {
            return $payload;
        }

        $payload['ip_address'] = '';
        $payload['source'] = sanitize_text_field((string) ($payload['source'] ?? $context));
        $payload['consent_context'] = $context;
        $payload['request_hash'] = $this->requestHash();
        if ($this->isSuppressed($email)) {
            $payload['status'] = 'unsubscribed';
            return $payload;
        }

        if ($context !== 'admin') {
            if ($existingId > 0) {
                global $wpdb;
                $existingStatus = (string) $wpdb->get_var($wpdb->prepare('SELECT status FROM %i WHERE id = %d', $this->subscribers(), $existingId));
                if ($existingStatus === 'subscribed') {
                    $payload['status'] = 'subscribed';
                    return $payload;
                }
            }
            $payload['status'] = 'pending';
            $payload['consent_method'] = 'double_opt_in';
        }
        return $payload;
    }

    /**
     * @param array<string,mixed> $row
     */
    public function newsletterSubscriberSaved(int $subscriberId, array $row, bool $updated = false, string $context = 'public'): void
    {
        if ($subscriberId <= 0) {
            return;
        }
        $email = sanitize_email((string) ($row['email'] ?? ''));
        if ($email === '' || $this->isSuppressed($email)) {
            return;
        }

        if ($context === 'admin' && (string) ($row['status'] ?? '') === 'subscribed') {
            $this->enrollSubscriber($subscriberId);
            return;
        }
        $this->issueConfirmation($subscriberId, $email);
    }

    public function newsletterStatusChanged(int $subscriberId, string $status): void
    {
        global $wpdb;
        $subscriber = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE id = %d', $this->subscribers(), $subscriberId), ARRAY_A);
        if (! is_array($subscriber)) {
            return;
        }
        $email = sanitize_email((string) ($subscriber['email'] ?? ''));
        if ($status === 'subscribed' && ! $this->isSuppressed($email)) {
            $this->enrollSubscriber($subscriberId);
            return;
        }
        if ($status === 'subscribed') {
            $wpdb->update($this->subscribers(), ['status' => 'unsubscribed'], ['id' => $subscriberId], ['%s'], ['%d']);
            return;
        }
        if (in_array($status, ['unsubscribed', 'bounced'], true)) {
            $this->suppressSubscriber($subscriberId, $email, $status, 'newsletter_admin_status');
        }
    }

    private function enrollSubscriber(int $subscriberId): void
    {
        global $wpdb;
        $subscriber = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE id = %d AND status = %s', $this->subscribers(), $subscriberId, 'subscribed'), ARRAY_A);
        if (! is_array($subscriber) || $this->isSuppressed((string) ($subscriber['email'] ?? ''))) {
            return;
        }
        $automations = $wpdb->get_results($wpdb->prepare("SELECT * FROM %i WHERE status = 'active' AND trigger_type = 'subscriber_added' ORDER BY id ASC", $this->table('automations')), ARRAY_A);
        if (! is_array($automations)) {
            return;
        }
        foreach ($automations as $automation) {
            $segmentId = (int) ($automation['segment_id'] ?? 0);
            if ($segmentId > 0) {
                $segment = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE id = %d AND status = %s', $this->table('segments'), $segmentId, 'active'), ARRAY_A);
                if (! is_array($segment) || ! $this->subscriberMatchesSegment($subscriber, $segment)) {
                    continue;
                }
            }
            $firstStep = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE automation_id = %d ORDER BY step_order ASC LIMIT 1', $this->table('automation_steps'), (int) ($automation['id'] ?? 0)), ARRAY_A);
            if (! is_array($firstStep)) {
                continue;
            }
            $nextSend = gmdate('Y-m-d H:i:s', time() + ((int) ($firstStep['delay_hours'] ?? 0) * HOUR_IN_SECONDS));
            $wpdb->query($wpdb->prepare(
                "INSERT IGNORE INTO %i (automation_id,subscriber_id,next_step,status,next_send_at) VALUES (%d,%d,1,'active',%s)",
                $this->table('enrollments'),
                (int) ($automation['id'] ?? 0),
                $subscriberId,
                $nextSend
            ));
        }
        wp_schedule_single_event(time() + 60, self::CRON);
    }

    private function processAutomationQueue(): void
    {
        $remaining = $this->remainingDailyCapacity();
        if ($remaining <= 0 || $this->readiness() !== []) {
            return;
        }

        global $wpdb;
        $limit = min(self::BATCH, $remaining);
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT e.*,a.name automation_name,a.status automation_status,s.email,s.first_name,s.last_name,s.status subscriber_status
             FROM %i e
             INNER JOIN %i a ON a.id = e.automation_id
             INNER JOIN %i s ON s.id = e.subscriber_id
             LEFT JOIN %i x ON x.email = s.email
             WHERE e.status = 'active' AND e.next_send_at <= %s AND a.status = 'active' AND s.status = 'subscribed' AND x.id IS NULL
             ORDER BY e.next_send_at ASC,e.id ASC LIMIT %d",
            $this->table('enrollments'),
            $this->table('automations'),
            $this->subscribers(),
            $this->table('suppressions'),
            current_time('mysql', true),
            $limit
        ), ARRAY_A);
        if (! is_array($rows)) {
            return;
        }

        foreach ($rows as $row) {
            if ($this->remainingDailyCapacity() <= 0) {
                break;
            }
            $enrollmentId = (int) ($row['id'] ?? 0);
            $claimed = $wpdb->update($this->table('enrollments'), ['status' => 'processing'], ['id' => $enrollmentId, 'status' => 'active'], ['%s'], ['%d', '%s']);
            if ($claimed !== 1) {
                continue;
            }
            $step = $wpdb->get_row($wpdb->prepare(
                'SELECT * FROM %i WHERE automation_id = %d AND step_order = %d',
                $this->table('automation_steps'),
                (int) ($row['automation_id'] ?? 0),
                (int) ($row['next_step'] ?? 1)
            ), ARRAY_A);
            if (! is_array($step)) {
                $wpdb->update($this->table('enrollments'), ['status' => 'completed'], ['id' => $enrollmentId], ['%s'], ['%d']);
                continue;
            }
            $automation = ['id' => (int) ($row['automation_id'] ?? 0), 'name' => (string) ($row['automation_name'] ?? '')];
            $subscriber = [
                'id' => (int) ($row['subscriber_id'] ?? 0),
                'email' => (string) ($row['email'] ?? ''),
                'first_name' => (string) ($row['first_name'] ?? ''),
                'last_name' => (string) ($row['last_name'] ?? ''),
            ];
            $message = $this->compiler()->automation($automation, $step, $row, $subscriber, $this->settings());
            $this->attemptAutomationDelivery($row, $step, $subscriber, $message);
        }
    }

    /**
     * @param array<string,mixed> $enrollment
     * @param array<string,mixed> $step
     * @param array<string,mixed> $subscriber
     * @param array{subject:string,html:string,text:string,headers:array<int,string>,message_id:string} $message
     */
    private function attemptAutomationDelivery(array $enrollment, array $step, array $subscriber, array $message): void
    {
        global $wpdb;
        $enrollmentId = (int) ($enrollment['id'] ?? 0);
        $attempts = (int) ($enrollment['attempts'] ?? 0) + 1;
        $this->incrementUsage();
        $this->lastMailError = '';
        $altBody = (string) $message['text'];
        $altBodySetter = static function ($mailer) use ($altBody): void {
            if (is_object($mailer) && property_exists($mailer, 'AltBody')) {
                $mailer->AltBody = $altBody;
            }
        };
        add_action('phpmailer_init', $altBodySetter, 99);
        $accepted = wp_mail(sanitize_email((string) ($subscriber['email'] ?? '')), (string) $message['subject'], (string) $message['html'], $message['headers']);
        remove_action('phpmailer_init', $altBodySetter, 99);

        if (! $accepted) {
            $failed = $attempts >= self::MAX_ATTEMPTS;
            $delay = $attempts === 1 ? 15 * MINUTE_IN_SECONDS : HOUR_IN_SECONDS;
            $wpdb->update($this->table('enrollments'), [
                'status' => $failed ? 'failed' : 'active',
                'attempts' => $attempts,
                'next_send_at' => $failed ? null : gmdate('Y-m-d H:i:s', time() + $delay),
                'last_error' => $this->lastMailError !== '' ? $this->lastMailError : __('Mail transport rejected the message.', 'help4-builder-suite'),
            ], ['id' => $enrollmentId], ['%s', '%d', '%s', '%s'], ['%d']);
            return;
        }

        $this->recordEvent('accepted', [
            'automation_id' => (int) ($enrollment['automation_id'] ?? 0),
            'enrollment_id' => $enrollmentId,
            'step_id' => (int) ($step['id'] ?? 0),
            'subscriber_id' => (int) ($enrollment['subscriber_id'] ?? 0),
        ], false);
        $nextOrder = (int) ($enrollment['next_step'] ?? 1) + 1;
        $nextStep = $wpdb->get_row($wpdb->prepare(
            'SELECT * FROM %i WHERE automation_id = %d AND step_order = %d',
            $this->table('automation_steps'),
            (int) ($enrollment['automation_id'] ?? 0),
            $nextOrder
        ), ARRAY_A);
        $wpdb->update($this->table('enrollments'), [
            'next_step' => $nextOrder,
            'status' => is_array($nextStep) ? 'active' : 'completed',
            'next_send_at' => is_array($nextStep) ? gmdate('Y-m-d H:i:s', time() + ((int) ($nextStep['delay_hours'] ?? 0) * HOUR_IN_SECONDS)) : null,
            'last_sent_at' => current_time('mysql', true),
            'attempts' => 0,
            'last_error' => '',
        ], ['id' => $enrollmentId], ['%d', '%s', '%s', '%s', '%d', '%s'], ['%d']);
    }

    /**
     * @param array<string,mixed> $subscriber
     * @param array<string,mixed> $segment
     */
    private function subscriberMatchesSegment(array $subscriber, array $segment): bool
    {
        $rules = json_decode((string) ($segment['rules_json'] ?? '[]'), true);
        if (! is_array($rules) || $rules === []) {
            return true;
        }
        $results = [];
        foreach ($rules as $rule) {
            if (! is_array($rule)) {
                continue;
            }
            $field = sanitize_key((string) ($rule['field'] ?? ''));
            $operator = sanitize_key((string) ($rule['operator'] ?? 'equals'));
            $expected = strtolower(trim((string) ($rule['value'] ?? '')));
            $actual = '';
            if ($field === 'email_domain') {
                $actual = strtolower((string) substr(strrchr((string) ($subscriber['email'] ?? ''), '@') ?: '', 1));
            } elseif (in_array($field, ['tags', 'source', 'status'], true)) {
                $actual = strtolower((string) ($subscriber[$field] ?? ''));
            } else {
                continue;
            }
            $results[] = match ($operator) {
                'contains' => $expected !== '' && str_contains($actual, $expected),
                'not_contains' => $expected !== '' && ! str_contains($actual, $expected),
                default => hash_equals($expected, $actual),
            };
        }
        if ($results === []) {
            return true;
        }
        $matched = (string) ($segment['match_type'] ?? 'all') === 'any'
            ? in_array(true, $results, true)
            : ! in_array(false, $results, true);
        return (bool) apply_filters('h4bs_email_segment_match', $matched, $subscriber, $segment, $results);
    }

    /** @return array{date:string,attempted:int} */
    private function usage(): array
    {
        $today = gmdate('Y-m-d');
        $usage = get_option(self::USAGE, []);
        if (! is_array($usage) || (string) ($usage['date'] ?? '') !== $today) {
            return ['date' => $today, 'attempted' => 0];
        }
        return ['date' => $today, 'attempted' => max(0, (int) ($usage['attempted'] ?? 0))];
    }

    private function remainingDailyCapacity(): int
    {
        $usage = $this->usage();
        return max(0, (int) ($this->settings()['daily_limit'] ?? 100) - $usage['attempted']);
    }

    private function incrementUsage(): void
    {
        $usage = $this->usage();
        ++$usage['attempted'];
        update_option(self::USAGE, $usage, false);
    }

    public function signup(): void
    {
        $nonce = sanitize_text_field((string) wp_unslash($_POST['_h4bs_email_nonce'] ?? ''));
        $email = sanitize_email((string) wp_unslash($_POST['email'] ?? ''));
        $consent = ! empty($_POST['marketing_consent']);
        $honeypot = sanitize_text_field((string) wp_unslash($_POST['company_website'] ?? ''));
        $sentAt = absint(wp_unslash($_POST['h4bs_sent_at'] ?? 0));
        $validTiming = $sentAt > 0 && time() >= ($sentAt + 1) && time() <= ($sentAt + DAY_IN_SECONDS);

        if (! wp_verify_nonce($nonce, 'h4bs_email_signup') || $email === '' || ! is_email($email) || ! $consent || $honeypot !== '' || ! $validTiming) {
            $this->publicRedirect('signup_invalid');
        }

        $rateKey = 'h4bs_email_signup_' . substr($this->requestHash(), 0, 32);
        $attempts = (int) get_transient($rateKey);
        if ($attempts >= 5) {
            $this->publicRedirect('signup_received');
        }
        set_transient($rateKey, $attempts + 1, HOUR_IN_SECONDS);

        if ($this->isSuppressed($email)) {
            $this->publicRedirect('signup_received');
        }

        global $wpdb;
        $existing = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE email = %s LIMIT 1', $this->subscribers(), $email), ARRAY_A);
        if (is_array($existing) && (string) ($existing['status'] ?? '') === 'subscribed') {
            $this->publicRedirect('signup_received');
        }

        $row = [
            'email' => $email,
            'first_name' => sanitize_text_field((string) wp_unslash($_POST['first_name'] ?? '')),
            'last_name' => sanitize_text_field((string) wp_unslash($_POST['last_name'] ?? '')),
            'status' => 'pending',
            'tags' => sanitize_text_field((string) wp_unslash($_POST['tags'] ?? 'website-newsletter')),
            'source' => sanitize_text_field((string) wp_unslash($_POST['source'] ?? home_url('/'))),
            'ip_address' => '',
            'payload' => wp_json_encode([
                'consent_method' => 'double_opt_in',
                'request_hash' => $this->requestHash(),
            ]),
            'consent_source' => sanitize_text_field((string) wp_unslash($_POST['source'] ?? home_url('/'))),
            'consent_method' => 'double_opt_in',
            'user_agent_hash' => $this->userAgentHash(),
            'unsubscribed_at' => null,
        ];
        if (is_array($existing)) {
            $subscriberId = (int) ($existing['id'] ?? 0);
            $wpdb->update($this->subscribers(), $row, ['id' => $subscriberId], ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s'], ['%d']);
        } else {
            $wpdb->insert($this->subscribers(), $row, ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);
            $subscriberId = (int) $wpdb->insert_id;
        }

        if ($subscriberId > 0) {
            $this->issueConfirmation($subscriberId, $email);
        }
        $this->publicRedirect('signup_received');
    }

    private function issueConfirmation(int $subscriberId, string $email): void
    {
        global $wpdb;
        $subscriber = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE id = %d', $this->subscribers(), $subscriberId), ARRAY_A);
        if (! is_array($subscriber) || (string) ($subscriber['status'] ?? '') === 'subscribed' || $this->isSuppressed($email)) {
            return;
        }
        $lastSent = strtotime((string) ($subscriber['confirmation_sent_at'] ?? '')) ?: 0;
        if ($lastSent > (time() - 5 * MINUTE_IN_SECONDS)) {
            return;
        }

        try {
            $token = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
        } catch (\Throwable $exception) {
            $token = wp_generate_password(48, false, false);
        }
        $hash = $this->tokens()->sign('confirmation-token', [$subscriberId, strtolower($email), $token]);
        $expires = gmdate('Y-m-d H:i:s', time() + 2 * DAY_IN_SECONDS);
        $wpdb->update($this->subscribers(), [
            'status' => 'pending',
            'confirmation_token_hash' => $hash,
            'confirmation_expires_at' => $expires,
            'confirmation_sent_at' => current_time('mysql', true),
        ], ['id' => $subscriberId], ['%s', '%s', '%s', '%s'], ['%d']);

        $settings = $this->settings();
        $url = add_query_arg([
            'action' => 'h4bs_email_confirm',
            's' => $subscriberId,
            't' => $token,
        ], admin_url('admin-post.php'));
        $subject = sanitize_text_field((string) ($settings['confirmation_subject'] ?? __('Confirm your subscription', 'help4-builder-suite')));
        $body = sanitize_textarea_field((string) ($settings['confirmation_body'] ?? ''));
        $body = strtr($body, ['{{site_name}}' => sanitize_text_field((string) get_bloginfo('name'))]);
        $html = '<div style="font-family:Arial,sans-serif;max-width:620px;margin:0 auto;padding:24px;color:#172033;">'
            . '<h1 style="font-size:26px;line-height:1.2;">' . esc_html($subject) . '</h1>'
            . '<p style="font-size:16px;line-height:1.6;">' . nl2br(esc_html($body)) . '</p>'
            . '<p><a href="' . esc_url($url) . '" style="display:inline-block;padding:12px 20px;background:#0877c9;color:#fff;text-decoration:none;border-radius:4px;">' . esc_html__('Confirm subscription', 'help4-builder-suite') . '</a></p>'
            . '<p style="font-size:12px;color:#52647b;">' . esc_html__('If you did not request this, no action is needed.', 'help4-builder-suite') . '</p></div>';
        $headers = [
            'Content-Type: text/html; charset=UTF-8',
            'From: ' . $this->cleanHeader((string) ($settings['from_name'] ?? get_bloginfo('name'))) . ' <' . sanitize_email((string) ($settings['from_email'] ?? get_option('admin_email'))) . '>',
            'Reply-To: ' . sanitize_email((string) ($settings['reply_to'] ?? get_option('admin_email'))),
        ];
        wp_mail($email, $subject, $html, $headers);
    }

    // Confirmation links carry a short-lived, one-time HMAC token instead of a session nonce.
    // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
    public function confirm(): void
    {
        $subscriberId = absint(wp_unslash($_REQUEST['s'] ?? 0));
        $token = sanitize_text_field((string) wp_unslash($_REQUEST['t'] ?? ''));
        global $wpdb;
        $subscriber = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE id = %d', $this->subscribers(), $subscriberId), ARRAY_A);
        if (! is_array($subscriber)) {
            $this->publicRedirect('confirmation_invalid', home_url('/'));
        }
        $email = sanitize_email((string) ($subscriber['email'] ?? ''));
        $expected = (string) ($subscriber['confirmation_token_hash'] ?? '');
        $actual = $this->tokens()->sign('confirmation-token', [$subscriberId, strtolower($email), $token]);
        $expires = strtotime((string) ($subscriber['confirmation_expires_at'] ?? '')) ?: 0;
        if ($expected === '' || ! hash_equals($expected, $actual) || $expires < time() || $this->isSuppressed($email)) {
            $this->publicRedirect('confirmation_invalid', home_url('/'));
        }

        $wpdb->update($this->subscribers(), [
            'status' => 'subscribed',
            'consent_at' => current_time('mysql', true),
            'confirmation_token_hash' => '',
            'confirmation_expires_at' => null,
            'unsubscribed_at' => null,
        ], ['id' => $subscriberId], ['%s', '%s', '%s', '%s', '%s'], ['%d']);
        $this->enrollSubscriber($subscriberId);
        do_action('h4bs_email_subscriber_confirmed', $subscriberId, $email);
        $this->publicRedirect('confirmed', home_url('/'));
    }
    // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing

    // RFC 8058 one-click requests cannot carry a WordPress session nonce; the subscriber HMAC authorizes this endpoint.
    // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
    public function unsubscribe(): void
    {
        $subscriberId = absint(wp_unslash($_REQUEST['s'] ?? 0));
        $token = sanitize_text_field((string) wp_unslash($_REQUEST['t'] ?? ''));
        global $wpdb;
        $subscriber = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE id = %d', $this->subscribers(), $subscriberId), ARRAY_A);
        if (! is_array($subscriber)) {
            status_header(404);
            wp_die(esc_html__('This unsubscribe link is invalid.', 'help4-builder-suite'));
        }
        $email = sanitize_email((string) ($subscriber['email'] ?? ''));
        if (! $this->tokens()->verify($token, 'unsubscribe', [$subscriberId, strtolower($email)])) {
            status_header(404);
            wp_die(esc_html__('This unsubscribe link is invalid.', 'help4-builder-suite'));
        }

        $method = strtoupper(sanitize_key((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')));
        $oneClick = sanitize_text_field((string) wp_unslash($_POST['List-Unsubscribe'] ?? '')) === 'One-Click';
        if ($method !== 'POST' && ! $oneClick) {
            $action = admin_url('admin-post.php');
            $form = '<form method="post" action="' . esc_url($action) . '"><input type="hidden" name="action" value="h4bs_email_unsubscribe"><input type="hidden" name="s" value="' . esc_attr((string) $subscriberId) . '"><input type="hidden" name="t" value="' . esc_attr($token) . '"><input type="hidden" name="List-Unsubscribe" value="One-Click"><button type="submit">' . esc_html__('Unsubscribe', 'help4-builder-suite') . '</button></form>';
            $allowed = [
                'h1' => [], 'p' => [], 'form' => ['method' => true, 'action' => true],
                'input' => ['type' => true, 'name' => true, 'value' => true],
                'button' => ['type' => true],
            ];
            wp_die(wp_kses('<h1>' . esc_html__('Stop email updates?', 'help4-builder-suite') . '</h1><p>' . esc_html__('Confirm below. This address will be placed on the site-wide suppression list.', 'help4-builder-suite') . '</p>' . $form, $allowed), esc_html__('Email preferences', 'help4-builder-suite'), ['response' => 200]);
        }

        $this->suppressSubscriber($subscriberId, $email, 'unsubscribe', 'recipient_request');
        $campaignId = absint(wp_unslash($_REQUEST['c'] ?? 0));
        $recipientId = absint(wp_unslash($_REQUEST['r'] ?? 0));
        if ($campaignId > 0 && $recipientId > 0) {
            $validRecipient = (int) $wpdb->get_var($wpdb->prepare('SELECT COUNT(*) FROM %i WHERE id = %d AND campaign_id = %d AND subscriber_id = %d', $this->table('recipients'), $recipientId, $campaignId, $subscriberId));
            if ($validRecipient === 1) {
                $this->recordEvent('unsubscribe', ['campaign_id' => $campaignId, 'recipient_id' => $recipientId, 'subscriber_id' => $subscriberId], false);
            }
        }
        wp_die(esc_html__('You are unsubscribed. This address will not receive future marketing messages from this site.', 'help4-builder-suite'), esc_html__('Unsubscribed', 'help4-builder-suite'), ['response' => 200]);
    }
    // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing

    public function saveSuppression(): void
    {
        $this->requireAdmin();
        check_admin_referer('h4bs_email_suppression');
        global $wpdb;
        $operation = sanitize_key((string) wp_unslash($_POST['operation'] ?? 'add'));
        if ($operation === 'remove') {
            $id = absint(wp_unslash($_POST['suppression_id'] ?? 0));
            if ($id > 0) {
                $wpdb->delete($this->table('suppressions'), ['id' => $id], ['%d']);
            }
            $this->adminRedirect('suppression_removed');
        }
        $email = sanitize_email((string) wp_unslash($_POST['email'] ?? ''));
        if ($email === '') {
            $this->adminRedirect('suppression_invalid');
        }
        $subscriberId = (int) $wpdb->get_var($wpdb->prepare('SELECT id FROM %i WHERE email = %s', $this->subscribers(), $email));
        $this->suppressSubscriber($subscriberId, $email, 'manual', sanitize_text_field((string) wp_unslash($_POST['detail'] ?? '')));
        $this->adminRedirect('suppression_saved');
    }

    private function suppressSubscriber(int $subscriberId, string $email, string $reason, string $detail): void
    {
        global $wpdb;
        $email = strtolower(sanitize_email($email));
        if ($email === '') {
            return;
        }
        $wpdb->query($wpdb->prepare(
            'INSERT INTO %i (email,reason,detail) VALUES (%s,%s,%s) ON DUPLICATE KEY UPDATE reason = VALUES(reason), detail = VALUES(detail)',
            $this->table('suppressions'),
            $email,
            sanitize_key($reason),
            sanitize_text_field($detail)
        ));
        if ($subscriberId <= 0) {
            $subscriberId = (int) $wpdb->get_var($wpdb->prepare('SELECT id FROM %i WHERE email = %s', $this->subscribers(), $email));
        }
        if ($subscriberId > 0) {
            $wpdb->update($this->subscribers(), [
                'status' => 'unsubscribed',
                'unsubscribed_at' => current_time('mysql', true),
                'confirmation_token_hash' => '',
                'confirmation_expires_at' => null,
            ], ['id' => $subscriberId], ['%s', '%s', '%s', '%s'], ['%d']);
            $wpdb->query($wpdb->prepare("UPDATE %i SET status = 'suppressed' WHERE subscriber_id = %d AND status IN ('queued','retry','paused','sending')", $this->table('recipients'), $subscriberId));
            $wpdb->query($wpdb->prepare("UPDATE %i SET status = 'cancelled' WHERE subscriber_id = %d AND status IN ('active','processing')", $this->table('enrollments'), $subscriberId));
        }
    }

    private function isSuppressed(string $email): bool
    {
        $email = strtolower(sanitize_email($email));
        if ($email === '') {
            return true;
        }
        global $wpdb;
        $hashed = $this->suppressionHash($email);
        return (int) $wpdb->get_var($wpdb->prepare('SELECT COUNT(*) FROM %i WHERE email IN (%s,%s)', $this->table('suppressions'), $email, $hashed)) > 0;
    }

    private function suppressionHash(string $email): string
    {
        return 'sha256:' . $this->tokens()->sign('suppression-email', [strtolower($email)]);
    }

    // Tracking URLs are per-recipient HMAC capabilities and intentionally work for signed-out mail clients.
    // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
    public function trackOpen(): void
    {
        $context = $this->trackingContext('open');
        if ($context !== null) {
            $automated = $this->isAutomatedRequest();
            $this->recordEvent('open', $context, $automated);
            if (! $automated && (int) ($context['recipient_id'] ?? 0) > 0) {
                global $wpdb;
                $recipientId = (int) $context['recipient_id'];
                $wpdb->query($wpdb->prepare(
                    'UPDATE %i SET open_count = open_count + 1, first_opened_at = COALESCE(first_opened_at,%s), last_opened_at = %s WHERE id = %d',
                    $this->table('recipients'),
                    current_time('mysql', true),
                    current_time('mysql', true),
                    $recipientId
                ));
            }
        }

        nocache_headers();
        header('Content-Type: image/gif');
        header('Content-Length: 43');
        echo base64_decode('R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped,WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Fixed binary transparent GIF response.
        exit;
    }

    public function trackClick(): void
    {
        $encoded = sanitize_text_field((string) wp_unslash($_REQUEST['u'] ?? ''));
        $destination = $this->decodeUrl($encoded);
        if ($destination === '' || ! $this->isHttpUrl($destination)) {
            status_header(404);
            exit;
        }
        $context = $this->trackingContext('click', $destination);
        if ($context === null) {
            status_header(404);
            exit;
        }

        $automated = $this->isAutomatedRequest();
        $context['destination_url'] = $destination;
        $this->recordEvent('click', $context, $automated);
        if (! $automated && (int) ($context['recipient_id'] ?? 0) > 0) {
            global $wpdb;
            $recipientId = (int) $context['recipient_id'];
            $wpdb->query($wpdb->prepare(
                'UPDATE %i SET click_count = click_count + 1, first_clicked_at = COALESCE(first_clicked_at,%s), last_clicked_at = %s WHERE id = %d',
                $this->table('recipients'),
                current_time('mysql', true),
                current_time('mysql', true),
                $recipientId
            ));
        }

        nocache_headers();
        wp_redirect($destination, 302, 'Help4 Builder Suite'); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Destination is signed and limited to HTTP(S).
        exit;
    }

    /**
     * @return array<string,int>|null
     */
    private function trackingContext(string $event, string $destination = ''): ?array
    {
        $kind = sanitize_key((string) wp_unslash($_REQUEST['kind'] ?? 'campaign'));
        $subscriberId = absint(wp_unslash($_REQUEST['s'] ?? 0));
        $token = sanitize_text_field((string) wp_unslash($_REQUEST['t'] ?? ''));
        if ($kind === 'campaign') {
            $campaignId = absint(wp_unslash($_REQUEST['c'] ?? 0));
            $recipientId = absint(wp_unslash($_REQUEST['r'] ?? 0));
            $parts = [$campaignId, $recipientId, $subscriberId];
            if ($event === 'click') {
                $parts[] = $destination;
            }
            if (! $this->tokens()->verify($token, 'campaign-' . $event, $parts)) {
                return null;
            }
            global $wpdb;
            $valid = (int) $wpdb->get_var($wpdb->prepare(
                'SELECT COUNT(*) FROM %i WHERE id = %d AND campaign_id = %d AND subscriber_id = %d',
                $this->table('recipients'),
                $recipientId,
                $campaignId,
                $subscriberId
            ));
            if ($valid !== 1) {
                return null;
            }
            return ['campaign_id' => $campaignId, 'recipient_id' => $recipientId, 'subscriber_id' => $subscriberId, 'automation_id' => 0, 'enrollment_id' => 0, 'step_id' => 0];
        }

        if ($kind !== 'automation') {
            return null;
        }
        $automationId = absint(wp_unslash($_REQUEST['a'] ?? 0));
        $enrollmentId = absint(wp_unslash($_REQUEST['e'] ?? 0));
        $stepId = absint(wp_unslash($_REQUEST['step'] ?? 0));
        $parts = [$automationId, $enrollmentId, $stepId, $subscriberId];
        if ($event === 'click') {
            $parts[] = $destination;
        }
        if (! $this->tokens()->verify($token, 'automation-' . $event, $parts)) {
            return null;
        }
        global $wpdb;
        $valid = (int) $wpdb->get_var($wpdb->prepare(
            'SELECT COUNT(*) FROM %i WHERE id = %d AND automation_id = %d AND subscriber_id = %d',
            $this->table('enrollments'),
            $enrollmentId,
            $automationId,
            $subscriberId
        ));
        if ($valid !== 1) {
            return null;
        }
        return ['campaign_id' => 0, 'recipient_id' => 0, 'subscriber_id' => $subscriberId, 'automation_id' => $automationId, 'enrollment_id' => $enrollmentId, 'step_id' => $stepId];
    }
    // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing

    /**
     * @param array<string,mixed> $context
     */
    private function recordEvent(string $eventType, array $context, bool $automated): void
    {
        if (! in_array($eventType, ['accepted', 'open', 'click', 'unsubscribe'], true)) {
            return;
        }
        $signedDestination = $eventType === 'click' ? esc_url_raw((string) ($context['destination_url'] ?? '')) : '';
        $destination = $signedDestination !== '' ? $this->eventDestination($signedDestination) : '';
        global $wpdb;
        $wpdb->insert($this->table('events'), [
            'campaign_id' => (int) ($context['campaign_id'] ?? 0),
            'recipient_id' => (int) ($context['recipient_id'] ?? 0),
            'subscriber_id' => (int) ($context['subscriber_id'] ?? 0),
            'automation_id' => (int) ($context['automation_id'] ?? 0),
            'enrollment_id' => (int) ($context['enrollment_id'] ?? 0),
            'step_id' => (int) ($context['step_id'] ?? 0),
            'event_type' => $eventType,
            'url_hash' => $signedDestination !== '' ? hash('sha256', $signedDestination) : '',
            'destination_url' => $destination,
            'request_hash' => $this->requestHash(),
            'is_automated' => $automated ? 1 : 0,
        ], ['%d', '%d', '%d', '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%d']);
    }

    private function eventDestination(string $url): string
    {
        $scheme = strtolower((string) wp_parse_url($url, PHP_URL_SCHEME));
        $host = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
        if ($host === '' || ! in_array($scheme, ['http', 'https'], true)) {
            return '';
        }
        $port = absint(wp_parse_url($url, PHP_URL_PORT));
        return $scheme . '://' . $host . ($port > 0 ? ':' . $port : '');
    }

    private function isAutomatedRequest(): bool
    {
        $method = strtoupper(sanitize_key((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')));
        if ($method === 'HEAD') {
            return true;
        }
        $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower(sanitize_text_field((string) wp_unslash($_SERVER['HTTP_USER_AGENT']))) : '';
        $purpose = isset($_SERVER['HTTP_PURPOSE']) ? strtolower(sanitize_text_field((string) wp_unslash($_SERVER['HTTP_PURPOSE']))) : '';
        $secPurpose = isset($_SERVER['HTTP_SEC_PURPOSE']) ? strtolower(sanitize_text_field((string) wp_unslash($_SERVER['HTTP_SEC_PURPOSE']))) : '';
        if (str_contains($purpose, 'prefetch') || str_contains($secPurpose, 'prefetch')) {
            return true;
        }
        $patterns = [
            'googleimageproxy', 'google web preview', 'yahoo! slurp', 'barracuda', 'proofpoint',
            'mimecast', 'safelinks', 'urlscan', 'mailguard', 'trendmicro', 'symantec', 'sophos',
            'spider', 'crawler', 'headless', 'phantomjs', 'bot/', 'facebookexternalhit', 'linkedinbot',
        ];
        foreach ($patterns as $pattern) {
            if ($userAgent !== '' && str_contains($userAgent, $pattern)) {
                return true;
            }
        }
        return (bool) apply_filters('h4bs_email_request_is_automated', false, $userAgent, $method);
    }

    /**
     * @param array<string,mixed> $exporters
     * @return array<string,mixed>
     */
    public function privacyExporters(array $exporters): array
    {
        $exporters['h4bs-email-marketing'] = [
            'exporter_friendly_name' => __('Help4 email marketing', 'help4-builder-suite'),
            'callback' => [$this, 'privacyExport'],
        ];
        return $exporters;
    }

    /**
     * @param array<string,mixed> $erasers
     * @return array<string,mixed>
     */
    public function privacyErasers(array $erasers): array
    {
        $erasers['h4bs-email-marketing'] = [
            'eraser_friendly_name' => __('Help4 email marketing', 'help4-builder-suite'),
            'callback' => [$this, 'privacyErase'],
        ];
        return $erasers;
    }

    /**
     * @return array{data:array<int,array<string,mixed>>,done:bool}
     */
    public function privacyExport(string $emailAddress, int $page = 1): array
    {
        $email = sanitize_email($emailAddress);
        if ($email === '' || $page > 1) {
            return ['data' => [], 'done' => true];
        }
        global $wpdb;
        $subscriber = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE email = %s', $this->subscribers(), $email), ARRAY_A);
        if (! is_array($subscriber)) {
            return ['data' => [], 'done' => true];
        }
        $subscriberId = (int) ($subscriber['id'] ?? 0);
        $recipients = $wpdb->get_results($wpdb->prepare(
            'SELECT r.status,r.accepted_at,r.open_count,r.first_opened_at,r.click_count,r.first_clicked_at,c.name campaign_name FROM %i r INNER JOIN %i c ON c.id = r.campaign_id WHERE r.subscriber_id = %d ORDER BY r.id DESC LIMIT 500',
            $this->table('recipients'),
            $this->table('campaigns'),
            $subscriberId
        ), ARRAY_A);
        $data = [[
            'group_id' => 'h4bs-email-subscriber',
            'group_label' => __('Email subscription', 'help4-builder-suite'),
            'item_id' => 'subscriber-' . $subscriberId,
            'data' => [
                ['name' => __('Email', 'help4-builder-suite'), 'value' => (string) ($subscriber['email'] ?? '')],
                ['name' => __('First name', 'help4-builder-suite'), 'value' => (string) ($subscriber['first_name'] ?? '')],
                ['name' => __('Last name', 'help4-builder-suite'), 'value' => (string) ($subscriber['last_name'] ?? '')],
                ['name' => __('Status', 'help4-builder-suite'), 'value' => (string) ($subscriber['status'] ?? '')],
                ['name' => __('Tags', 'help4-builder-suite'), 'value' => (string) ($subscriber['tags'] ?? '')],
                ['name' => __('Consent time', 'help4-builder-suite'), 'value' => (string) ($subscriber['consent_at'] ?? '')],
                ['name' => __('Consent method', 'help4-builder-suite'), 'value' => (string) ($subscriber['consent_method'] ?? '')],
            ],
        ]];
        foreach (is_array($recipients) ? $recipients : [] as $recipient) {
            $data[] = [
                'group_id' => 'h4bs-email-engagement',
                'group_label' => __('Email campaign history', 'help4-builder-suite'),
                'item_id' => 'campaign-' . sanitize_key((string) ($recipient['campaign_name'] ?? 'campaign')) . '-' . count($data),
                'data' => [
                    ['name' => __('Campaign', 'help4-builder-suite'), 'value' => (string) ($recipient['campaign_name'] ?? '')],
                    ['name' => __('Delivery status', 'help4-builder-suite'), 'value' => (string) ($recipient['status'] ?? '')],
                    ['name' => __('Accepted time', 'help4-builder-suite'), 'value' => (string) ($recipient['accepted_at'] ?? '')],
                    ['name' => __('Open count', 'help4-builder-suite'), 'value' => (string) ((int) ($recipient['open_count'] ?? 0))],
                    ['name' => __('Click count', 'help4-builder-suite'), 'value' => (string) ((int) ($recipient['click_count'] ?? 0))],
                ],
            ];
        }
        return ['data' => $data, 'done' => true];
    }

    /**
     * @return array{items_removed:bool,items_retained:bool,messages:array<int,string>,done:bool}
     */
    public function privacyErase(string $emailAddress, int $page = 1): array
    {
        $email = sanitize_email($emailAddress);
        if ($email === '' || $page > 1) {
            return ['items_removed' => false, 'items_retained' => false, 'messages' => [], 'done' => true];
        }
        global $wpdb;
        $subscriber = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE email = %s', $this->subscribers(), $email), ARRAY_A);
        if (! is_array($subscriber)) {
            return ['items_removed' => false, 'items_retained' => false, 'messages' => [], 'done' => true];
        }
        $subscriberId = (int) ($subscriber['id'] ?? 0);
        $hash = $this->suppressionHash($email);
        $existingSuppression = $wpdb->get_row($wpdb->prepare('SELECT * FROM %i WHERE email = %s', $this->table('suppressions'), $email), ARRAY_A);
        $hashedSuppressionId = (int) $wpdb->get_var($wpdb->prepare('SELECT id FROM %i WHERE email = %s', $this->table('suppressions'), $hash));
        if (is_array($existingSuppression)) {
            if ($hashedSuppressionId > 0) {
                $wpdb->delete($this->table('suppressions'), ['id' => (int) ($existingSuppression['id'] ?? 0)], ['%d']);
            } else {
                $wpdb->update($this->table('suppressions'), ['email' => $hash, 'reason' => 'privacy', 'detail' => 'privacy_erasure_hash'], ['id' => (int) ($existingSuppression['id'] ?? 0)], ['%s', '%s', '%s'], ['%d']);
            }
        } elseif ($hashedSuppressionId === 0) {
            $wpdb->insert($this->table('suppressions'), ['email' => $hash, 'reason' => 'privacy', 'detail' => 'privacy_erasure_hash'], ['%s', '%s', '%s']);
        }
        $wpdb->update($this->subscribers(), [
            'email' => 'erased-' . $subscriberId . '@invalid.example',
            'first_name' => '',
            'last_name' => '',
            'status' => 'erased',
            'tags' => '',
            'source' => '',
            'ip_address' => '',
            'payload' => '',
            'confirmation_token_hash' => '',
            'confirmation_expires_at' => null,
            'consent_source' => '',
            'user_agent_hash' => '',
            'unsubscribed_at' => current_time('mysql', true),
        ], ['id' => $subscriberId], ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s'], ['%d']);
        $wpdb->query($wpdb->prepare("UPDATE %i SET status = 'suppressed' WHERE subscriber_id = %d AND status IN ('queued','retry','paused','sending')", $this->table('recipients'), $subscriberId));
        $wpdb->query($wpdb->prepare("UPDATE %i SET status = 'cancelled' WHERE subscriber_id = %d AND status IN ('active','processing')", $this->table('enrollments'), $subscriberId));
        return [
            'items_removed' => true,
            'items_retained' => true,
            'messages' => [__('Anonymous delivery and engagement aggregates plus a one-way suppression hash were retained to prevent future marketing.', 'help4-builder-suite')],
            'done' => true,
        ];
    }

    /**
     * @param array<string,mixed> $atts
     */
    public function signupShortcode(array $atts = []): string
    {
        $atts = shortcode_atts([
            'tags' => 'website-newsletter',
            'button' => __('Subscribe', 'help4-builder-suite'),
            'show_name' => '1',
            'class' => '',
        ], $atts, 'h4_email_signup');
        $settings = $this->settings();
        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only notice selected after a redirect.
        $notice = sanitize_key((string) wp_unslash($_GET['h4_email_notice'] ?? ''));
        // phpcs:enable WordPress.Security.NonceVerification.Recommended
        $notices = [
            'signup_received' => __('Check your inbox for a confirmation link. If this address is already subscribed or suppressed, no additional message will be sent.', 'help4-builder-suite'),
            'confirmed' => __('Your subscription is confirmed.', 'help4-builder-suite'),
            'confirmation_invalid' => __('That confirmation link is invalid or expired. Submit the form again for a fresh link.', 'help4-builder-suite'),
            'signup_invalid' => __('Please enter a valid email address and confirm that you want email updates.', 'help4-builder-suite'),
        ];
        $requestPath = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_URI'])) : '/';
        $source = home_url($requestPath);
        $class = 'h4bs-form h4bs-email-signup';
        $extra = sanitize_html_class((string) $atts['class']);
        if ($extra !== '') {
            $class .= ' ' . $extra;
        }

        ob_start();
        ?>
        <div class="<?php echo esc_attr($class); ?>">
            <?php if (isset($notices[$notice])) : ?>
                <div class="h4bs-form-notice" role="status"><?php echo esc_html($notices[$notice]); ?></div>
            <?php endif; ?>
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                <input type="hidden" name="action" value="h4bs_email_signup">
                <input type="hidden" name="_h4bs_email_nonce" value="<?php echo esc_attr(wp_create_nonce('h4bs_email_signup')); ?>">
                <input type="hidden" name="h4bs_sent_at" value="<?php echo esc_attr((string) time()); ?>">
                <input type="hidden" name="redirect_to" value="<?php echo esc_attr($source); ?>">
                <input type="hidden" name="source" value="<?php echo esc_attr($source); ?>">
                <input type="hidden" name="tags" value="<?php echo esc_attr(sanitize_text_field((string) $atts['tags'])); ?>">
                <input type="text" name="company_website" value="" tabindex="-1" autocomplete="off" aria-hidden="true" class="h4bs-form-honeypot">
                <div class="h4bs-form-grid">
                    <?php if ((string) $atts['show_name'] !== '0') : ?>
                        <div class="h4bs-form-field h4bs-form-field--50"><label><?php esc_html_e('First name', 'help4-builder-suite'); ?><input type="text" name="first_name" autocomplete="given-name"></label></div>
                        <div class="h4bs-form-field h4bs-form-field--50"><label><?php esc_html_e('Last name', 'help4-builder-suite'); ?><input type="text" name="last_name" autocomplete="family-name"></label></div>
                    <?php endif; ?>
                    <div class="h4bs-form-field h4bs-form-field--100"><label><?php esc_html_e('Email', 'help4-builder-suite'); ?><input type="email" name="email" autocomplete="email" required></label></div>
                    <div class="h4bs-form-field h4bs-form-field--100"><label class="h4bs-checkbox-label"><input type="checkbox" name="marketing_consent" value="1" required><span><?php echo esc_html((string) ($settings['consent_label'] ?? '')); ?></span></label></div>
                </div>
                <div class="h4bs-form-actions"><button type="submit" class="h4bs-form-submit"><?php echo esc_html((string) $atts['button']); ?></button></div>
            </form>
        </div>
        <?php
        return (string) ob_get_clean();
    }

    /** @return array<int,string> */
    private function readiness(): array
    {
        $settings = $this->settings();
        $issues = [];
        if (! is_email((string) ($settings['from_email'] ?? '')) || ! is_email((string) ($settings['reply_to'] ?? ''))) {
            $issues[] = __('A valid From and Reply-To address are required.', 'help4-builder-suite');
        }
        if (trim((string) ($settings['postal_address'] ?? '')) === '') {
            $issues[] = __('A physical postal address is required in every marketing footer.', 'help4-builder-suite');
        }
        if (! $this->isHttpUrl((string) ($settings['privacy_url'] ?? ''))) {
            $issues[] = __('A valid privacy-policy URL is required.', 'help4-builder-suite');
        }
        if (empty($settings['sender_verified'])) {
            $issues[] = __('Confirm that the sending mailbox has been tested.', 'help4-builder-suite');
        }
        if (empty($settings['authentication_verified'])) {
            $issues[] = __('Confirm SPF, DKIM, and DMARC authentication before sending.', 'help4-builder-suite');
        }
        return (array) apply_filters('h4bs_email_campaign_readiness', $issues, $settings);
    }

    private function adminRedirect(string $notice): void
    {
        wp_safe_redirect(add_query_arg(['page' => 'h4bs-email-marketing', 'h4_email_notice' => sanitize_key($notice)], admin_url('admin.php')));
        exit;
    }

    private function publicRedirect(string $notice, string $fallback = ''): void
    {
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Every caller verifies its form nonce or signed public token before redirecting.
        $requested = isset($_POST['redirect_to']) ? esc_url_raw((string) wp_unslash($_POST['redirect_to'])) : '';
        // phpcs:enable WordPress.Security.NonceVerification.Missing
        $fallback = $fallback !== '' ? $fallback : home_url('/');
        $target = wp_validate_redirect($requested, $fallback);
        wp_safe_redirect(add_query_arg('h4_email_notice', sanitize_key($notice), $target));
        exit;
    }

    private function normalizeUtcDate(string $value): ?string
    {
        $value = sanitize_text_field($value);
        if ($value === '') {
            return null;
        }
        try {
            $date = new \DateTimeImmutable($value, new \DateTimeZone('UTC'));
            return $date->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
        } catch (\Throwable $exception) {
            return null;
        }
    }

    private function decodeUrl(string $encoded): string
    {
        if ($encoded === '' || strlen($encoded) > 8192 || ! preg_match('/^[A-Za-z0-9_-]+$/', $encoded)) {
            return '';
        }
        $padding = strlen($encoded) % 4;
        if ($padding > 0) {
            $encoded .= str_repeat('=', 4 - $padding);
        }
        $decoded = base64_decode(strtr($encoded, '-_', '+/'), true);
        return is_string($decoded) ? esc_url_raw($decoded) : '';
    }

    private function isHttpUrl(string $url): bool
    {
        $url = esc_url_raw($url);
        $scheme = strtolower((string) wp_parse_url($url, PHP_URL_SCHEME));
        $host = (string) wp_parse_url($url, PHP_URL_HOST);
        return $host !== '' && in_array($scheme, ['http', 'https'], true);
    }

    private function requestHash(): string
    {
        $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field((string) wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
        $agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
        $language = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_ACCEPT_LANGUAGE'])) : '';
        return $this->tokens()->fingerprint([$ip, $agent, $language]);
    }

    private function userAgentHash(): string
    {
        $agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
        return $agent !== '' ? $this->tokens()->fingerprint([$agent]) : '';
    }

    private function cleanHeader(string $value): string
    {
        return trim((string) preg_replace('/[\r\n]+/', ' ', sanitize_text_field($value)));
    }

    private function tokens(): TokenService
    {
        if (! $this->tokens instanceof TokenService) {
            $this->tokens = new TokenService();
        }
        return $this->tokens;
    }

    private function compiler(): MessageCompiler
    {
        if (! $this->compiler instanceof MessageCompiler) {
            $this->compiler = new MessageCompiler($this->tokens());
        }
        return $this->compiler;
    }

    private function rate(int $part, int $whole): float
    {
        return $whole > 0 ? ($part / $whole) * 100 : 0.0;
    }

    private function table(string $name): string
    {
        global $wpdb;
        return $wpdb->prefix . 'h4_email_' . $name;
    }

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

    private function requireAdmin(): void
    {
        if (! current_user_can('manage_options')) {
            wp_die(
                esc_html__('Insufficient permission.', 'help4-builder-suite'),
                '',
                ['response' => 403]
            );
        }
    }
}
