<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\ProductInsights;

use Help4BuilderSuite\Contracts\ModuleContract;
use Help4BuilderSuite\Support\FeatureFlags;
use Help4BuilderSuite\Support\RemoteUrlGuard;
use Help4BuilderSuite\Support\Settings;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;

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

final class Module implements ModuleContract
{
    private const CLIENT_OPTION = 'h4bs_product_insights_client';
    private const COLLECTOR_SCHEMA_OPTION = 'h4bs_product_insights_collector_schema';
    private const REWRITE_VERSION_OPTION = 'h4bs_product_insights_rewrite_version';
    private const HEARTBEAT_HOOK = 'h4bs_product_insights_heartbeat';
    private const COLLECTOR_MAINTENANCE_HOOK = 'h4bs_product_insights_collector_maintenance';
    private const QUERY_VAR = 'h4bs_product_download';
    private const CONSENT_VERSION = '1.0';
    private const COLLECTOR_SCHEMA_VERSION = '1.0';
    private const REST_NAMESPACE = 'h4bs/v1';
    private const RETENTION_DAYS = 730;
    private const MAX_ADMIN_ROWS = 250;

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

    public function boot(): void
    {
        add_action('admin_menu', [$this, 'registerAdminPage']);
        add_action('admin_post_h4bs_product_insights_enable', [$this, 'handleEnable']);
        add_action('admin_post_h4bs_product_insights_send', [$this, 'handleSend']);
        add_action('admin_post_h4bs_product_insights_disable', [$this, 'handleDisable']);
        add_action(self::HEARTBEAT_HOOK, [$this, 'runScheduledHeartbeat']);
        add_action(self::COLLECTOR_MAINTENANCE_HOOK, [$this, 'runCollectorMaintenance']);
        add_action('rest_api_init', [$this, 'registerRestRoutes']);
        add_action('init', [$this, 'registerDownloadRoute']);
        add_action('wp_loaded', [$this, 'maybeFlushDownloadRoute']);
        add_action('template_redirect', [$this, 'serveTrackedDownload'], 0);
        add_action('admin_init', [$this, 'addPrivacyPolicyText']);
        add_filter('query_vars', [$this, 'registerQueryVar']);

        $state = $this->clientState();
        if (! empty($state['enabled']) || ! empty($state['revocation_pending'])) {
            $this->scheduleHeartbeat();
        }
        if ($this->isHelp4CollectorHost()) {
            $this->scheduleCollectorMaintenance();
        }
    }

    public function registerAdminPage(): void
    {
        add_submenu_page(
            'h4bs-settings',
            __('Product Insights', 'help4-builder-suite'),
            __('Product Insights', 'help4-builder-suite'),
            'manage_options',
            'h4bs-product-insights',
            [$this, 'renderAdminPage']
        );
    }

    public function registerRestRoutes(): void
    {
        register_rest_route(
            self::REST_NAMESPACE,
            '/product-insights/adoption',
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'collectAdoption'],
                'permission_callback' => [$this, 'canCollect'],
            ]
        );

        register_rest_route(
            self::REST_NAMESPACE,
            '/product-insights/adoption/revoke',
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'revokeAdoption'],
                'permission_callback' => [$this, 'canCollect'],
            ]
        );
    }

    /**
     * @param array<int, string> $queryVars
     * @return array<int, string>
     */
    public function registerQueryVar(array $queryVars): array
    {
        $queryVars[] = self::QUERY_VAR;
        return array_values(array_unique($queryVars));
    }

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

        add_rewrite_rule(
            '^get/(help4-builder-suite|help4-blank)/?$',
            'index.php?' . self::QUERY_VAR . '=$matches[1]',
            'top'
        );
    }

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

        $current = (string) get_option(self::REWRITE_VERSION_OPTION, '');
        if ($current === H4BS_VERSION) {
            return;
        }

        flush_rewrite_rules(false);
        update_option(self::REWRITE_VERSION_OPTION, H4BS_VERSION, false);
    }

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

        $route = sanitize_key((string) get_query_var(self::QUERY_VAR));
        $products = [
            'help4-builder-suite' => [
                'product' => 'builder-suite',
                'target'  => home_url('/help4-builder-suite/latest.zip'),
            ],
            'help4-blank' => [
                'product' => 'blank-theme',
                'target'  => home_url('/help4-blank/latest.zip'),
            ],
        ];
        if (! isset($products[$route])) {
            return;
        }

        nocache_headers();
        header('X-Robots-Tag: noindex, nofollow', true);
        if (! $this->isAutomatedDownloadRequest()) {
            $this->recordDownloadStart((string) $products[$route]['product']);
        }

        wp_safe_redirect((string) $products[$route]['target'], 302, 'Help4 Builder Suite');
        exit;
    }

    public function canCollect(): bool
    {
        return $this->isHelp4CollectorHost();
    }

    public function collectAdoption(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $payload = $this->sanitizeAdoptionPayload($request->get_json_params());
        if (is_wp_error($payload)) {
            return $payload;
        }
        if (! $this->rateLimitAllowsCollection((string) $payload['installation_id'])) {
            return new WP_Error(
                'h4bs_product_insights_rate_limited',
                __('Please wait before sending another product-insights update.', 'help4-builder-suite'),
                ['status' => 429]
            );
        }

        $this->ensureCollectorTables();
        global $wpdb;
        $table = $this->installTable();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Public collector authorization must read the current token hash.
        $existing = $wpdb->get_row(
            $wpdb->prepare('SELECT id, token_hash, first_seen, heartbeat_count FROM %i WHERE installation_id = %s LIMIT 1', $table, $payload['installation_id']),
            ARRAY_A
        );
        $tokenHash = hash('sha256', (string) $payload['installation_token']);
        if (is_array($existing) && ! hash_equals((string) ($existing['token_hash'] ?? ''), $tokenHash)) {
            return new WP_Error(
                'h4bs_product_insights_token_mismatch',
                __('The installation identity could not be verified.', 'help4-builder-suite'),
                ['status' => 403]
            );
        }

        $now = gmdate('Y-m-d H:i:s');
        $row = [
            'installation_id' => (string) $payload['installation_id'],
            'token_hash'      => $tokenHash,
            'site_host'       => (string) $payload['site_host'],
            'site_url'        => (string) $payload['site_url'],
            'plugin_version'  => (string) $payload['plugin_version'],
            'build_hash'      => (string) $payload['build_hash'],
            'wordpress_version' => (string) $payload['wordpress_version'],
            'php_version'     => (string) $payload['php_version'],
            'theme_slug'      => (string) $payload['theme_slug'],
            'theme_version'   => (string) $payload['theme_version'],
            'channel'         => (string) $payload['channel'],
            'environment_type' => (string) $payload['environment_type'],
            'is_multisite'    => ! empty($payload['is_multisite']) ? 1 : 0,
            'enabled_modules' => wp_json_encode($payload['enabled_modules'], JSON_UNESCAPED_SLASHES),
            'consent_version' => self::CONSENT_VERSION,
            'status'          => 'active',
            'last_seen'       => $now,
        ];

        if (is_array($existing)) {
            $row['heartbeat_count'] = ((int) ($existing['heartbeat_count'] ?? 0)) + 1;
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Mutable collector record is updated immediately after consented receipt.
            $stored = $wpdb->update($table, $row, ['id' => (int) $existing['id']]);
        } else {
            $row['first_seen'] = $now;
            $row['heartbeat_count'] = 1;
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Collector table is first-party operational storage.
            $stored = $wpdb->insert($table, $row);
        }
        if ($stored === false) {
            return new WP_Error(
                'h4bs_product_insights_store_failed',
                __('The product-insights record could not be stored.', 'help4-builder-suite'),
                ['status' => 500]
            );
        }

        return new WP_REST_Response(
            [
                'ok'              => true,
                'installation_id' => (string) $payload['installation_id'],
                'stored_at'       => gmdate('c'),
                'retention_days'  => self::RETENTION_DAYS,
            ],
            is_array($existing) ? 200 : 201
        );
    }

    public function revokeAdoption(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $raw = $request->get_json_params();
        $raw = is_array($raw) ? $raw : [];
        $installationId = sanitize_text_field((string) ($raw['installation_id'] ?? ''));
        $token = sanitize_text_field((string) ($raw['installation_token'] ?? ''));
        if (! $this->validInstallationId($installationId) || ! preg_match('/^[a-f0-9]{64}$/', $token)) {
            return new WP_Error(
                'h4bs_product_insights_invalid_revoke',
                __('The revocation request is invalid.', 'help4-builder-suite'),
                ['status' => 400]
            );
        }

        $this->ensureCollectorTables();
        global $wpdb;
        $table = $this->installTable();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Revocation must verify current collector state.
        $existing = $wpdb->get_row(
            $wpdb->prepare('SELECT id, token_hash FROM %i WHERE installation_id = %s LIMIT 1', $table, $installationId),
            ARRAY_A
        );
        if (! is_array($existing)) {
            return new WP_REST_Response(['ok' => true, 'deleted' => false], 200);
        }
        if (! hash_equals((string) ($existing['token_hash'] ?? ''), hash('sha256', $token))) {
            return new WP_Error(
                'h4bs_product_insights_token_mismatch',
                __('The installation identity could not be verified.', 'help4-builder-suite'),
                ['status' => 403]
            );
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Explicit administrator revocation removes the collector record immediately.
        $deleted = $wpdb->delete($table, ['id' => (int) $existing['id']], ['%d']);
        return new WP_REST_Response(['ok' => true, 'deleted' => $deleted !== false], 200);
    }

    public function handleEnable(): void
    {
        $this->authorizeAdminAction();
        check_admin_referer('h4bs_product_insights_enable');
        $raw = is_array($_POST) ? wp_unslash($_POST) : [];
        if (empty($raw['consent']) || ! rest_sanitize_boolean($raw['consent'])) {
            $this->redirectWithNotice('error', __('Confirm the product-insights consent before enabling sharing.', 'help4-builder-suite'));
        }

        $state = $this->clientState();
        if (! $this->validInstallationId((string) ($state['installation_id'] ?? ''))) {
            $state['installation_id'] = wp_generate_uuid4();
        }
        if (! preg_match('/^[a-f0-9]{64}$/', (string) ($state['installation_token'] ?? ''))) {
            $state['installation_token'] = hash('sha256', wp_generate_password(64, true, true) . '|' . wp_rand() . '|' . microtime(true));
        }
        $state['enabled'] = true;
        $state['revocation_pending'] = false;
        $state['consent_version'] = self::CONSENT_VERSION;
        $state['enabled_at'] = gmdate('c');
        $this->saveClientState($state);
        $this->scheduleHeartbeat();

        $result = $this->sendHeartbeat();
        $message = ! empty($result['ok'])
            ? __('Product insights are enabled and the first consented status was received.', 'help4-builder-suite')
            : sprintf(
                /* translators: %s: remote product-insights error. */
                __('Sharing is enabled, but the first status could not be delivered: %s', 'help4-builder-suite'),
                (string) ($result['message'] ?? __('Unknown error', 'help4-builder-suite'))
            );
        $this->redirectWithNotice(! empty($result['ok']) ? 'success' : 'warning', $message);
    }

    public function handleSend(): void
    {
        $this->authorizeAdminAction();
        check_admin_referer('h4bs_product_insights_send');
        $result = $this->sendHeartbeat();
        $this->redirectWithNotice(
            ! empty($result['ok']) ? 'success' : 'error',
            ! empty($result['ok'])
                ? __('The consented product-insights status was sent.', 'help4-builder-suite')
                : (string) ($result['message'] ?? __('The status could not be sent.', 'help4-builder-suite'))
        );
    }

    public function handleDisable(): void
    {
        $this->authorizeAdminAction();
        check_admin_referer('h4bs_product_insights_disable');
        $state = $this->clientState();
        $state['enabled'] = false;
        $result = $this->sendRevocation($state);
        if (! empty($result['ok'])) {
            $state = $this->defaultClientState();
            $message = __('Product insights are off and the remote installation record was deleted.', 'help4-builder-suite');
            $status = 'success';
            wp_clear_scheduled_hook(self::HEARTBEAT_HOOK);
        } else {
            $state['revocation_pending'] = true;
            $state['last_status'] = 'revoke_failed';
            $state['last_message'] = sanitize_text_field((string) ($result['message'] ?? ''));
            $message = __('Sharing is off. Remote deletion is queued for retry because the collector could not be reached.', 'help4-builder-suite');
            $status = 'warning';
            $this->scheduleHeartbeat();
        }
        $this->saveClientState($state);
        $this->redirectWithNotice($status, $message);
    }

    public function runScheduledHeartbeat(): void
    {
        $state = $this->clientState();
        if (! empty($state['revocation_pending'])) {
            $result = $this->sendRevocation($state);
            if (! empty($result['ok'])) {
                $this->saveClientState($this->defaultClientState());
                wp_clear_scheduled_hook(self::HEARTBEAT_HOOK);
            }
            return;
        }
        if (! empty($state['enabled'])) {
            $this->sendHeartbeat();
        }
    }

    public function runCollectorMaintenance(): void
    {
        if (! $this->isHelp4CollectorHost()) {
            return;
        }
        $this->ensureCollectorTables();
        global $wpdb;
        $cutoff = gmdate('Y-m-d H:i:s', time() - (DAY_IN_SECONDS * self::RETENTION_DAYS));
        $installTable = $this->installTable();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Scheduled retention removes expired first-party opted-in installation records.
        $wpdb->query($wpdb->prepare('DELETE FROM %i WHERE last_seen < %s', $installTable, $cutoff));
    }

    public function renderAdminPage(): void
    {
        if (! current_user_can('manage_options')) {
            return;
        }

        $state = $this->clientState();
        $payload = $this->adoptionPayload($state);
        $noticeNonce = isset($_GET['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_GET['_wpnonce'])) : '';
        $validNotice = $noticeNonce !== '' && wp_verify_nonce($noticeNonce, 'h4bs_product_insights_notice');
        $noticeStatus = $validNotice && isset($_GET['h4bs_pi_status']) ? sanitize_key((string) wp_unslash($_GET['h4bs_pi_status'])) : '';
        $noticeMessage = $validNotice && isset($_GET['h4bs_pi_message']) ? sanitize_text_field((string) wp_unslash($_GET['h4bs_pi_message'])) : '';
        ?>
        <div class="wrap h4bs-admin-wrap">
            <h1><?php esc_html_e('Product Insights', 'help4-builder-suite'); ?></h1>
            <p class="h4bs-admin-lead"><?php esc_html_e('Measure first-party download starts and, only with administrator consent, understand which website domains are actively using Builder Suite. This screen never sends page content, users, customer records, form entries, email addresses, cookies, secrets, or raw logs.', 'help4-builder-suite'); ?></p>

            <?php if ($noticeStatus !== '' && $noticeMessage !== '') : ?>
                <div class="notice notice-<?php echo esc_attr(in_array($noticeStatus, ['success', 'warning', 'error'], true) ? $noticeStatus : 'info'); ?> inline"><p><?php echo esc_html($noticeMessage); ?></p></div>
            <?php endif; ?>

            <div class="h4bs-admin-card-grid">
                <article class="h4bs-admin-card"><h2><?php esc_html_e('Sharing', 'help4-builder-suite'); ?></h2><p><strong><?php echo ! empty($state['enabled']) ? esc_html__('On', 'help4-builder-suite') : esc_html__('Off', 'help4-builder-suite'); ?></strong></p><p><?php esc_html_e('Off by default. Enabling requires an administrator action on this screen.', 'help4-builder-suite'); ?></p></article>
                <article class="h4bs-admin-card"><h2><?php esc_html_e('Last result', 'help4-builder-suite'); ?></h2><p><strong><?php echo esc_html((string) ($state['last_status'] ?: __('Never sent', 'help4-builder-suite'))); ?></strong></p><p><?php echo esc_html((string) ($state['last_message'] ?? '')); ?></p></article>
                <article class="h4bs-admin-card"><h2><?php esc_html_e('Retention', 'help4-builder-suite'); ?></h2><p><strong><?php
                    /* translators: %d: product-insights retention duration in months. */
                    printf(esc_html__('%d months', 'help4-builder-suite'), 24);
                ?></strong></p><p><?php esc_html_e('Stale opted-in installation records are removed after 24 months. Revocation requests delete the matching record sooner.', 'help4-builder-suite'); ?></p></article>
            </div>

            <h2><?php esc_html_e('Exact payload preview', 'help4-builder-suite'); ?></h2>
            <p><?php esc_html_e('These are the only fields sent by the weekly adoption heartbeat after consent.', 'help4-builder-suite'); ?></p>
            <div class="h4bs-admin-table-scroll">
                <table class="widefat striped"><tbody>
                    <?php foreach ($this->payloadPreviewRows($payload) as $label => $value) : ?>
                        <tr><th scope="row"><?php echo esc_html($label); ?></th><td><?php echo esc_html($value); ?></td></tr>
                    <?php endforeach; ?>
                </tbody></table>
            </div>

            <?php if (empty($state['enabled'])) : ?>
                <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                    <?php wp_nonce_field('h4bs_product_insights_enable'); ?>
                    <input type="hidden" name="action" value="h4bs_product_insights_enable">
                    <p><label><input type="checkbox" name="consent" value="1" required> <?php esc_html_e('I consent to send only the fields shown above to Help4 WordPress once per week for product adoption statistics.', 'help4-builder-suite'); ?></label></p>
                    <?php submit_button(__('Enable and send first status', 'help4-builder-suite')); ?>
                </form>
            <?php else : ?>
                <div class="h4bs-admin-actions">
                    <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                        <?php wp_nonce_field('h4bs_product_insights_send'); ?>
                        <input type="hidden" name="action" value="h4bs_product_insights_send">
                        <?php submit_button(__('Send status now', 'help4-builder-suite'), 'secondary', 'submit', false); ?>
                    </form>
                    <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return window.confirm('<?php echo esc_js(__('Stop sharing and request deletion of this installation record?', 'help4-builder-suite')); ?>');">
                        <?php wp_nonce_field('h4bs_product_insights_disable'); ?>
                        <input type="hidden" name="action" value="h4bs_product_insights_disable">
                        <?php submit_button(__('Stop sharing and delete record', 'help4-builder-suite'), 'delete', 'submit', false); ?>
                    </form>
                </div>
            <?php endif; ?>

            <p><?php printf(
                /* translators: 1: privacy policy URL, 2: terms URL. */
                wp_kses_post(__('Review the <a href="%1$s" target="_blank" rel="noopener">Help4 privacy policy</a> and <a href="%2$s" target="_blank" rel="noopener">terms</a>.', 'help4-builder-suite')),
                esc_url('https://help4wordpress.com/privacy-policy/'),
                esc_url('https://help4wordpress.com/terms/')
            ); ?></p>

            <?php if ($this->isHelp4CollectorHost()) : ?>
                <?php $this->renderCollectorReport(); ?>
            <?php endif; ?>
        </div>
        <?php
    }

    public function addPrivacyPolicyText(): void
    {
        if (! function_exists('wp_add_privacy_policy_content')) {
            return;
        }
        wp_add_privacy_policy_content(
            __('Help4 Builder Suite Product Insights', 'help4-builder-suite'),
            wp_kses_post(
                '<p>' . __('Suggested text: Help4 Builder Suite product insights are disabled by default. If an administrator opts in, the site sends its public website address, Builder Suite version and build, WordPress and PHP versions, active theme slug/version, enabled Help4 module names, release channel, environment type, and multisite status to Help4 WordPress once per week. It does not send page content, users, customer records, form entries, email addresses, cookies, secrets, or raw logs. Administrators can stop sharing and request deletion from Help4 Builder Suite > Product Insights.', 'help4-builder-suite') . '</p>'
            )
        );
    }

    /**
     * @return array<string, mixed>
     */
    private function sendHeartbeat(): array
    {
        $state = $this->clientState();
        if (empty($state['enabled'])) {
            return ['ok' => false, 'message' => __('Product insights are disabled.', 'help4-builder-suite')];
        }

        $result = $this->postJson($this->collectorEndpoint('adoption'), $this->adoptionPayload($state));
        $state['last_attempt_at'] = gmdate('c');
        $state['last_status'] = ! empty($result['ok']) ? 'sent' : 'failed';
        $state['last_message'] = sanitize_text_field((string) ($result['message'] ?? ''));
        if (! empty($result['ok'])) {
            $state['last_sent_at'] = gmdate('c');
        }
        $this->saveClientState($state);
        return $result;
    }

    /**
     * @param array<string, mixed> $state
     * @return array<string, mixed>
     */
    private function sendRevocation(array $state): array
    {
        $installationId = (string) ($state['installation_id'] ?? '');
        $token = (string) ($state['installation_token'] ?? '');
        if (! $this->validInstallationId($installationId) || ! preg_match('/^[a-f0-9]{64}$/', $token)) {
            return ['ok' => true, 'message' => __('No remote installation identity existed.', 'help4-builder-suite')];
        }
        return $this->postJson(
            $this->collectorEndpoint('adoption/revoke'),
            [
                'installation_id'    => $installationId,
                'installation_token' => $token,
            ]
        );
    }

    /**
     * @return array<string, mixed>
     */
    private function postJson(string $endpoint, array $payload): array
    {
        if ($endpoint === '' || ! RemoteUrlGuard::isAllowed($endpoint)) {
            return ['ok' => false, 'message' => __('The product-insights endpoint is not an allowed public URL.', 'help4-builder-suite')];
        }
        $body = wp_json_encode($payload, JSON_UNESCAPED_SLASHES);
        if (! is_string($body)) {
            return ['ok' => false, 'message' => __('The product-insights payload could not be encoded.', 'help4-builder-suite')];
        }
        $response = wp_remote_post(
            $endpoint,
            [
                'timeout' => 15,
                'redirection' => 1,
                'headers' => [
                    'Accept' => 'application/json',
                    'Content-Type' => 'application/json; charset=utf-8',
                    'User-Agent' => 'Help4 Builder Suite/' . H4BS_VERSION . ' Product Insights',
                ],
                'body' => $body,
            ]
        );
        if (is_wp_error($response)) {
            return ['ok' => false, 'message' => $response->get_error_message()];
        }
        $code = (int) wp_remote_retrieve_response_code($response);
        $decoded = json_decode((string) wp_remote_retrieve_body($response), true);
        $decoded = is_array($decoded) ? $decoded : [];
        if ($code < 200 || $code >= 300 || empty($decoded['ok'])) {
            return [
                'ok' => false,
                'status_code' => $code,
                'message' => sanitize_text_field((string) ($decoded['message'] ?? wp_remote_retrieve_response_message($response))),
            ];
        }
        return ['ok' => true, 'status_code' => $code, 'message' => __('Collector accepted the status.', 'help4-builder-suite')];
    }

    /**
     * @param array<string, mixed> $state
     * @return array<string, mixed>
     */
    private function adoptionPayload(array $state): array
    {
        $theme = wp_get_theme();
        $flags = (new FeatureFlags())->all();
        $siteUrl = trailingslashit(home_url('/'));
        return [
            'schema'             => 'h4bs-product-insights-v1',
            'consent'            => true,
            'consent_version'    => self::CONSENT_VERSION,
            'installation_id'    => (string) ($state['installation_id'] ?? ''),
            'installation_token' => (string) ($state['installation_token'] ?? ''),
            'site_url'           => $siteUrl,
            'site_host'          => strtolower((string) wp_parse_url($siteUrl, PHP_URL_HOST)),
            'plugin_version'     => H4BS_VERSION,
            'build_hash'         => H4BS_BUILD_HASH,
            'wordpress_version'  => (string) get_bloginfo('version'),
            'php_version'        => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
            'theme_slug'         => sanitize_key((string) $theme->get_stylesheet()),
            'theme_version'      => sanitize_text_field((string) $theme->get('Version')),
            'channel'            => $this->releaseChannel(),
            'environment_type'   => function_exists('wp_get_environment_type') ? sanitize_key(wp_get_environment_type()) : 'production',
            'is_multisite'       => is_multisite(),
            'enabled_modules'    => array_values(array_keys(array_filter($flags))),
            'sent_at'            => gmdate('c'),
        ];
    }

    /**
     * @param mixed $raw
     * @return array<string, mixed>|WP_Error
     */
    private function sanitizeAdoptionPayload($raw): array|WP_Error
    {
        $raw = is_array($raw) ? $raw : [];
        $installationId = sanitize_text_field((string) ($raw['installation_id'] ?? ''));
        $token = sanitize_text_field((string) ($raw['installation_token'] ?? ''));
        $siteUrl = esc_url_raw((string) ($raw['site_url'] ?? ''), ['http', 'https']);
        $siteHost = strtolower((string) wp_parse_url($siteUrl, PHP_URL_HOST));
        if (
            (string) ($raw['schema'] ?? '') !== 'h4bs-product-insights-v1'
            ||
            empty($raw['consent'])
            || (string) ($raw['consent_version'] ?? '') !== self::CONSENT_VERSION
            || ! $this->validInstallationId($installationId)
            || ! preg_match('/^[a-f0-9]{64}$/', $token)
            || $siteUrl === ''
            || $siteHost === ''
            || ! wp_http_validate_url($siteUrl)
            || ! RemoteUrlGuard::isAllowed($siteUrl)
        ) {
            return new WP_Error(
                'h4bs_product_insights_invalid_payload',
                __('The product-insights payload is invalid or missing consent.', 'help4-builder-suite'),
                ['status' => 400]
            );
        }

        $modules = is_array($raw['enabled_modules'] ?? null) ? $raw['enabled_modules'] : [];
        $modules = array_slice(array_values(array_unique(array_filter(array_map('sanitize_key', $modules)))), 0, 80);
        $channels = ['stable', 'beta', 'development', 'wordpress-org', 'unknown'];
        $channel = sanitize_key((string) ($raw['channel'] ?? 'unknown'));
        if (! in_array($channel, $channels, true)) {
            $channel = 'unknown';
        }

        return [
            'installation_id'    => $installationId,
            'installation_token' => $token,
            'site_url'           => substr(trailingslashit($siteUrl), 0, 255),
            'site_host'          => substr(sanitize_text_field($siteHost), 0, 190),
            'plugin_version'     => substr(sanitize_text_field((string) ($raw['plugin_version'] ?? '')), 0, 32),
            'build_hash'         => substr(preg_replace('/[^a-f0-9]/', '', strtolower((string) ($raw['build_hash'] ?? ''))) ?: '', 0, 64),
            'wordpress_version'  => substr(sanitize_text_field((string) ($raw['wordpress_version'] ?? '')), 0, 32),
            'php_version'        => substr(sanitize_text_field((string) ($raw['php_version'] ?? '')), 0, 32),
            'theme_slug'         => substr(sanitize_key((string) ($raw['theme_slug'] ?? '')), 0, 190),
            'theme_version'      => substr(sanitize_text_field((string) ($raw['theme_version'] ?? '')), 0, 32),
            'channel'            => $channel,
            'environment_type'   => substr(sanitize_key((string) ($raw['environment_type'] ?? 'production')), 0, 32),
            'is_multisite'       => ! empty($raw['is_multisite']) && rest_sanitize_boolean($raw['is_multisite']),
            'enabled_modules'    => $modules,
        ];
    }

    private function recordDownloadStart(string $product): void
    {
        $this->ensureCollectorTables();
        global $wpdb;
        $table = $this->downloadTable();
        $product = in_array($product, ['builder-suite', 'blank-theme'], true) ? $product : 'unknown';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public download attribution is read-only, bounded, and must work before authentication.
        $source = isset($_GET['source']) ? substr(sanitize_key((string) wp_unslash($_GET['source'])), 0, 80) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public download attribution is read-only, bounded, and must work before authentication.
        $campaign = isset($_GET['campaign']) ? substr(sanitize_key((string) wp_unslash($_GET['campaign'])), 0, 120) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public download attribution is read-only, bounded, and must work before authentication.
        $version = isset($_GET['version']) ? substr(sanitize_text_field((string) wp_unslash($_GET['version'])), 0, 32) : '';
        $referer = wp_get_referer();
        $sourceHost = is_string($referer) ? strtolower((string) wp_parse_url($referer, PHP_URL_HOST)) : '';
        $sourceHost = substr(sanitize_text_field($sourceHost), 0, 190);
        $date = gmdate('Y-m-d');
        $bucketKey = hash('sha256', implode('|', [$date, $product, $version, $sourceHost, $source, $campaign]));
        $now = gmdate('Y-m-d H:i:s');

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic first-party aggregate avoids storing visitor-level records or IP addresses.
        $wpdb->query(
            $wpdb->prepare(
                'INSERT INTO %i (bucket_key, stat_date, product, release_version, source_host, source, campaign, download_count, last_seen) VALUES (%s, %s, %s, %s, %s, %s, %s, 1, %s) ON DUPLICATE KEY UPDATE download_count = download_count + 1, last_seen = VALUES(last_seen)',
                $table,
                $bucketKey,
                $date,
                $product,
                $version,
                $sourceHost,
                $source,
                $campaign,
                $now
            )
        );
    }

    private function renderCollectorReport(): void
    {
        $this->ensureCollectorTables();
        global $wpdb;
        $installTable = $this->installTable();
        $downloadTable = $this->downloadTable();
        $activeSince = gmdate('Y-m-d H:i:s', time() - (DAY_IN_SECONDS * 30));
        $downloadSince = gmdate('Y-m-d', time() - (DAY_IN_SECONDS * 30));
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Admin report intentionally reads current first-party aggregate state.
        $activeInstalls = (int) $wpdb->get_var($wpdb->prepare('SELECT COUNT(*) FROM %i WHERE status = %s AND last_seen >= %s', $installTable, 'active', $activeSince));
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Admin report intentionally reads current first-party aggregate state.
        $totalDomains = (int) $wpdb->get_var($wpdb->prepare('SELECT COUNT(DISTINCT site_host) FROM %i WHERE status = %s', $installTable, 'active'));
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Admin report intentionally reads current first-party aggregate state.
        $downloadStarts = (int) $wpdb->get_var($wpdb->prepare('SELECT COALESCE(SUM(download_count), 0) FROM %i WHERE stat_date >= %s', $downloadTable, $downloadSince));
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Bounded admin table intentionally reads current collector records.
        $installs = $wpdb->get_results($wpdb->prepare('SELECT site_host, site_url, plugin_version, wordpress_version, php_version, channel, environment_type, first_seen, last_seen, heartbeat_count FROM %i ORDER BY last_seen DESC LIMIT %d', $installTable, self::MAX_ADMIN_ROWS), ARRAY_A);
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Bounded admin table intentionally reads aggregate download buckets.
        $downloads = $wpdb->get_results($wpdb->prepare('SELECT stat_date, product, release_version, source_host, source, campaign, download_count FROM %i ORDER BY stat_date DESC, download_count DESC LIMIT %d', $downloadTable, self::MAX_ADMIN_ROWS), ARRAY_A);
        ?>
        <hr>
        <h2><?php esc_html_e('Help4 first-party collector report', 'help4-builder-suite'); ?></h2>
        <p><?php esc_html_e('Installation rows represent administrators who explicitly opted in. Download values are initiated download redirects, not proof that the ZIP completed or was installed.', 'help4-builder-suite'); ?></p>
        <div class="h4bs-admin-card-grid">
            <article class="h4bs-admin-card"><h3><?php esc_html_e('Active installations', 'help4-builder-suite'); ?></h3><p><strong><?php echo esc_html(number_format_i18n($activeInstalls)); ?></strong></p><p><?php esc_html_e('Seen in the last 30 days', 'help4-builder-suite'); ?></p></article>
            <article class="h4bs-admin-card"><h3><?php esc_html_e('Consented domains', 'help4-builder-suite'); ?></h3><p><strong><?php echo esc_html(number_format_i18n($totalDomains)); ?></strong></p><p><?php esc_html_e('Distinct active website hosts', 'help4-builder-suite'); ?></p></article>
            <article class="h4bs-admin-card"><h3><?php esc_html_e('Download starts', 'help4-builder-suite'); ?></h3><p><strong><?php echo esc_html(number_format_i18n($downloadStarts)); ?></strong></p><p><?php esc_html_e('Last 30 days', 'help4-builder-suite'); ?></p></article>
        </div>
        <h3><?php esc_html_e('Consented installation domains', 'help4-builder-suite'); ?></h3>
        <div class="h4bs-admin-table-scroll"><table class="widefat striped"><thead><tr><th><?php esc_html_e('Domain', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Builder', 'help4-builder-suite'); ?></th><th><?php esc_html_e('WordPress / PHP', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Channel', 'help4-builder-suite'); ?></th><th><?php esc_html_e('First / last seen', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Checks', 'help4-builder-suite'); ?></th></tr></thead><tbody>
        <?php if (! is_array($installs) || $installs === []) : ?><tr><td colspan="6"><?php esc_html_e('No administrators have opted in yet.', 'help4-builder-suite'); ?></td></tr><?php else : foreach ($installs as $row) : ?>
            <tr><th scope="row"><a href="<?php echo esc_url((string) ($row['site_url'] ?? '')); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html((string) ($row['site_host'] ?? '')); ?></a></th><td><?php echo esc_html((string) ($row['plugin_version'] ?? '')); ?></td><td><?php echo esc_html((string) ($row['wordpress_version'] ?? '') . ' / ' . (string) ($row['php_version'] ?? '')); ?></td><td><?php echo esc_html((string) ($row['channel'] ?? '') . ' · ' . (string) ($row['environment_type'] ?? '')); ?></td><td><?php echo esc_html((string) ($row['first_seen'] ?? '') . ' / ' . (string) ($row['last_seen'] ?? '')); ?></td><td><?php echo esc_html(number_format_i18n((int) ($row['heartbeat_count'] ?? 0))); ?></td></tr>
        <?php endforeach; endif; ?>
        </tbody></table></div>
        <h3><?php esc_html_e('Download-start aggregates', 'help4-builder-suite'); ?></h3>
        <div class="h4bs-admin-table-scroll"><table class="widefat striped"><thead><tr><th><?php esc_html_e('Date', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Product', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Version', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Referrer', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Source / campaign', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Starts', 'help4-builder-suite'); ?></th></tr></thead><tbody>
        <?php if (! is_array($downloads) || $downloads === []) : ?><tr><td colspan="6"><?php esc_html_e('No tracked download starts yet.', 'help4-builder-suite'); ?></td></tr><?php else : foreach ($downloads as $row) : ?>
            <tr><td><?php echo esc_html((string) ($row['stat_date'] ?? '')); ?></td><td><?php echo esc_html((string) ($row['product'] ?? '')); ?></td><td><?php echo esc_html((string) ($row['release_version'] ?? '')); ?></td><td><?php echo esc_html((string) ($row['source_host'] ?? __('Direct', 'help4-builder-suite'))); ?></td><td><?php echo esc_html(trim((string) ($row['source'] ?? '') . ' / ' . (string) ($row['campaign'] ?? ''), ' /')); ?></td><td><?php echo esc_html(number_format_i18n((int) ($row['download_count'] ?? 0))); ?></td></tr>
        <?php endforeach; endif; ?>
        </tbody></table></div>
        <?php
    }

    private function ensureCollectorTables(): void
    {
        if (! $this->isHelp4CollectorHost()) {
            return;
        }
        if (
            (string) get_option(self::COLLECTOR_SCHEMA_OPTION, '') === self::COLLECTOR_SCHEMA_VERSION
            && $this->collectorTablesExist()
        ) {
            return;
        }

        global $wpdb;
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        $charset = $wpdb->get_charset_collate();
        $installTable = $this->installTable();
        $downloadTable = $this->downloadTable();
        dbDelta(
            "CREATE TABLE {$installTable} (
                id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
                installation_id VARCHAR(64) NOT NULL,
                token_hash CHAR(64) NOT NULL,
                site_host VARCHAR(190) NOT NULL,
                site_url VARCHAR(255) NOT NULL,
                plugin_version VARCHAR(32) NOT NULL DEFAULT '',
                build_hash CHAR(64) NOT NULL DEFAULT '',
                wordpress_version VARCHAR(32) NOT NULL DEFAULT '',
                php_version VARCHAR(32) NOT NULL DEFAULT '',
                theme_slug VARCHAR(190) NOT NULL DEFAULT '',
                theme_version VARCHAR(32) NOT NULL DEFAULT '',
                channel VARCHAR(32) NOT NULL DEFAULT 'unknown',
                environment_type VARCHAR(32) NOT NULL DEFAULT 'production',
                is_multisite TINYINT(1) NOT NULL DEFAULT 0,
                enabled_modules TEXT NULL,
                consent_version VARCHAR(16) NOT NULL DEFAULT '',
                status VARCHAR(20) NOT NULL DEFAULT 'active',
                first_seen DATETIME NOT NULL,
                last_seen DATETIME NOT NULL,
                heartbeat_count BIGINT UNSIGNED NOT NULL DEFAULT 1,
                PRIMARY KEY (id),
                UNIQUE KEY installation_id (installation_id),
                KEY site_host (site_host),
                KEY plugin_version (plugin_version),
                KEY last_seen (last_seen),
                KEY status_last_seen (status, last_seen)
            ) {$charset};"
        );
        dbDelta(
            "CREATE TABLE {$downloadTable} (
                id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
                bucket_key CHAR(64) NOT NULL,
                stat_date DATE NOT NULL,
                product VARCHAR(64) NOT NULL,
                release_version VARCHAR(32) NOT NULL DEFAULT '',
                source_host VARCHAR(190) NOT NULL DEFAULT '',
                source VARCHAR(80) NOT NULL DEFAULT '',
                campaign VARCHAR(120) NOT NULL DEFAULT '',
                download_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
                last_seen DATETIME NOT NULL,
                PRIMARY KEY (id),
                UNIQUE KEY bucket_key (bucket_key),
                KEY stat_date (stat_date),
                KEY product_date (product, stat_date)
            ) {$charset};"
        );
        update_option(self::COLLECTOR_SCHEMA_OPTION, self::COLLECTOR_SCHEMA_VERSION, false);

    }

    /**
     * @return array<string, mixed>
     */
    private function clientState(): array
    {
        $saved = get_option(self::CLIENT_OPTION, []);
        $saved = is_array($saved) ? $saved : [];
        return array_merge($this->defaultClientState(), $saved);
    }

    /**
     * @return array<string, mixed>
     */
    private function defaultClientState(): array
    {
        return [
            'enabled' => false,
            'revocation_pending' => false,
            'consent_version' => '',
            'installation_id' => '',
            'installation_token' => '',
            'enabled_at' => '',
            'last_attempt_at' => '',
            'last_sent_at' => '',
            'last_status' => '',
            'last_message' => '',
        ];
    }

    /**
     * @param array<string, mixed> $state
     */
    private function saveClientState(array $state): void
    {
        update_option(self::CLIENT_OPTION, array_merge($this->defaultClientState(), $state), false);
    }

    private function scheduleHeartbeat(): void
    {
        if (! wp_next_scheduled(self::HEARTBEAT_HOOK)) {
            wp_schedule_event(time() + HOUR_IN_SECONDS, 'weekly', self::HEARTBEAT_HOOK);
        }
    }

    private function scheduleCollectorMaintenance(): void
    {
        if (! wp_next_scheduled(self::COLLECTOR_MAINTENANCE_HOOK)) {
            wp_schedule_event(time() + DAY_IN_SECONDS, 'weekly', self::COLLECTOR_MAINTENANCE_HOOK);
        }
    }

    private function authorizeAdminAction(): void
    {
        if (! current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to manage product insights.', 'help4-builder-suite'), 403);
        }
    }

    private function redirectWithNotice(string $status, string $message): void
    {
        wp_safe_redirect(
            add_query_arg(
                [
                    'page' => 'h4bs-product-insights',
                    'h4bs_pi_status' => sanitize_key($status),
                    'h4bs_pi_message' => sanitize_text_field($message),
                    '_wpnonce' => wp_create_nonce('h4bs_product_insights_notice'),
                ],
                admin_url('admin.php')
            )
        );
        exit;
    }

    private function collectorEndpoint(string $path): string
    {
        $default = 'https://help4wordpress.com/wp-json/h4bs/v1/product-insights/' . ltrim($path, '/');
        $filtered = apply_filters('h4bs_product_insights_endpoint', $default, $path);
        return esc_url_raw(is_string($filtered) ? $filtered : '', ['http', 'https']);
    }

    private function releaseChannel(): string
    {
        $host = strtolower((string) wp_parse_url((string) Settings::get('update_manifest_url', ''), PHP_URL_HOST));
        if ($host === 'md.help4wordpress.com') {
            return 'beta';
        }
        if (function_exists('wp_get_environment_type') && wp_get_environment_type() === 'development') {
            return 'development';
        }
        return 'stable';
    }

    private function collectorTablesExist(): bool
    {
        global $wpdb;
        $installTable = $this->installTable();
        $downloadTable = $this->downloadTable();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Clone-safe schema check avoids trusting a copied option when collector tables are absent.
        $installExists = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($installTable)));
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Clone-safe schema check avoids trusting a copied option when collector tables are absent.
        $downloadExists = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($downloadTable)));
        return $installExists === $installTable && $downloadExists === $downloadTable;
    }

    private function isHelp4CollectorHost(): bool
    {
        $host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        return in_array($host, ['help4wordpress.com', 'www.help4wordpress.com', 'md.help4wordpress.com'], true);
    }

    private function validInstallationId(string $installationId): bool
    {
        return (bool) preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i', $installationId);
    }

    private function rateLimitAllowsCollection(string $installationId): bool
    {
        $key = 'h4bs_pi_collect_' . substr(hash('sha256', $installationId), 0, 40);
        if (get_transient($key)) {
            return false;
        }
        set_transient($key, '1', 10);
        return true;
    }

    private function isAutomatedDownloadRequest(): bool
    {
        $method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper(sanitize_key((string) wp_unslash($_SERVER['REQUEST_METHOD']))) : 'GET';
        if ($method !== 'GET') {
            return true;
        }
        $purpose = '';
        foreach (['HTTP_SEC_PURPOSE', 'HTTP_PURPOSE', 'HTTP_X_MOZ'] as $key) {
            if (isset($_SERVER[$key])) {
                $purpose .= ' ' . sanitize_text_field((string) wp_unslash($_SERVER[$key]));
            }
        }
        $agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower(sanitize_text_field((string) wp_unslash($_SERVER['HTTP_USER_AGENT']))) : '';
        return str_contains(strtolower($purpose), 'prefetch')
            || (bool) preg_match('/bot|crawler|spider|preview|scanner|headless|monitoring/', $agent);
    }

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

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

    /**
     * @param array<string, mixed> $payload
     * @return array<string, string>
     */
    private function payloadPreviewRows(array $payload): array
    {
        return [
            __('Website URL', 'help4-builder-suite') => (string) ($payload['site_url'] ?? ''),
            __('Website domain', 'help4-builder-suite') => (string) ($payload['site_host'] ?? ''),
            __('Builder version / build', 'help4-builder-suite') => (string) ($payload['plugin_version'] ?? '') . ' / ' . substr((string) ($payload['build_hash'] ?? ''), 0, 12),
            __('WordPress / PHP', 'help4-builder-suite') => (string) ($payload['wordpress_version'] ?? '') . ' / ' . (string) ($payload['php_version'] ?? ''),
            __('Theme', 'help4-builder-suite') => (string) ($payload['theme_slug'] ?? '') . ' ' . (string) ($payload['theme_version'] ?? ''),
            __('Channel / environment', 'help4-builder-suite') => (string) ($payload['channel'] ?? '') . ' / ' . (string) ($payload['environment_type'] ?? ''),
            __('Multisite', 'help4-builder-suite') => ! empty($payload['is_multisite']) ? __('Yes', 'help4-builder-suite') : __('No', 'help4-builder-suite'),
            __('Enabled Help4 modules', 'help4-builder-suite') => implode(', ', is_array($payload['enabled_modules'] ?? null) ? $payload['enabled_modules'] : []),
            __('Installation identity', 'help4-builder-suite') => (string) ($payload['installation_id'] ?? '') !== ''
                ? (string) $payload['installation_id']
                : __('Created only after consent', 'help4-builder-suite'),
        ];
    }
}
