<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\Multisite;

use Help4BuilderSuite\Contracts\ModuleContract;
use Help4BuilderSuite\Database\Installer;
use Help4BuilderSuite\Modules\SEO\Module as SeoModule;
use Help4BuilderSuite\Support\BusinessProfileDefaults;
use Help4BuilderSuite\Support\NetworkSettings;
use Help4BuilderSuite\Support\Settings;

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

final class Module implements ModuleContract
{
    private const PAGE_SLUG = 'h4bs-network';
    private const NOTICE_TRANSIENT_PREFIX = 'h4bs_network_notice_';
    private const BATCH_SIZE = 10;
    private const HEALTH_JOB_OPTION = 'h4bs_network_seo_health_job';
    private const HEALTH_JOB_HOOK = 'h4bs_multisite_seo_health_batch';
    private const BUILDER_COUNT_TRANSIENT = 'h4bs_network_builder_count';

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

    public function boot(): void
    {
        if (! is_multisite()) {
            return;
        }

        add_action('network_admin_menu', [$this, 'registerNetworkMenu']);
        add_action('network_admin_enqueue_scripts', [$this, 'enqueueNetworkAssets']);
        add_action('admin_post_h4bs_network_save_settings', [$this, 'handleSaveSettings']);
        add_action('admin_post_h4bs_network_bulk_action', [$this, 'handleBulkAction']);
        add_action('wp_initialize_site', [$this, 'initializeNewSite'], 100, 2);
        add_action(NetworkSettings::PROVISION_HOOK, [$this, 'processProvisionBatch']);
        add_action(self::HEALTH_JOB_HOOK, [$this, 'processSeoHealthBatch']);
        add_action('admin_notices', [$this, 'renderSitePolicyNotice']);
        add_action('save_post', [$this, 'invalidateBuilderCount'], 10, 0);
        add_action('deleted_post', [$this, 'invalidateBuilderCount'], 10, 0);
        add_action('h4bs_builder_layout_saved', [$this, 'invalidateBuilderCount'], 10, 0);

        $this->maybeScheduleNetworkUpgrade();
    }

    public function registerNetworkMenu(): void
    {
        add_menu_page(
            __('Help4 Network', 'help4-builder-suite'),
            __('Help4 Network', 'help4-builder-suite'),
            'manage_network_options',
            self::PAGE_SLUG,
            [$this, 'renderNetworkPage'],
            'dashicons-admin-multisite',
            58
        );
    }

    public function enqueueNetworkAssets(string $hook): void
    {
        if (! str_contains($hook, self::PAGE_SLUG)) {
            return;
        }
        wp_register_script('h4bs-network-admin', '', [], H4BS_VERSION, true);
        wp_enqueue_script('h4bs-network-admin');
        wp_add_inline_script(
            'h4bs-network-admin',
            "document.addEventListener('DOMContentLoaded',function(){var toggle=document.querySelector('[data-h4bs-network-check-all]');if(!toggle){return;}toggle.addEventListener('change',function(){document.querySelectorAll('input[name=\"site_ids[]\"]').forEach(function(input){input.checked=toggle.checked;});});});"
        );
    }

    public function renderNetworkPage(): void
    {
        $this->requireNetworkAdministrator();
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only Network Admin pagination.
        $page = max(1, isset($_GET['paged']) ? absint(wp_unslash($_GET['paged'])) : 1);
        $perPage = 25;
        $networkId = get_current_network_id();
        $total = (int) get_sites([
            'network_id' => $networkId,
            'count' => true,
            'spam' => 0,
            'deleted' => 0,
        ]);
        $sites = get_sites([
            'network_id' => $networkId,
            'number' => $perPage,
            'offset' => ($page - 1) * $perPage,
            'orderby' => 'id',
            'order' => 'ASC',
            'spam' => 0,
            'deleted' => 0,
        ]);
        $settings = NetworkSettings::all();
        $state = get_site_option(NetworkSettings::PROVISION_STATE_OPTION, []);
        $state = is_array($state) ? $state : [];
        $healthJob = get_site_option(self::HEALTH_JOB_OPTION, []);
        $healthJob = is_array($healthJob) ? $healthJob : [];
        $notice = get_site_transient(self::NOTICE_TRANSIENT_PREFIX . get_current_user_id());
        if (is_array($notice)) {
            delete_site_transient(self::NOTICE_TRANSIENT_PREFIX . get_current_user_id());
        }
        $provisionedLabel = sprintf(
            /* translators: %s: Help4 Builder Suite version provisioned across the network. */
            __('sites provisioned for %s', 'help4-builder-suite'),
            (string) ($state['version'] ?? H4BS_VERSION)
        );
        ?>
        <div class="wrap h4bs-admin-wrap h4bs-network-admin">
            <h1><?php esc_html_e('Help4 Network Control', 'help4-builder-suite'); ?></h1>
            <p class="h4bs-admin-lead"><?php esc_html_e('Manage Builder readiness, search visibility, sitemap health, and safe SEO defaults across the network without taking ownership away from individual site teams.', 'help4-builder-suite'); ?></p>

            <?php if (is_array($notice)) : ?>
                <div class="notice <?php echo ! empty($notice['error']) ? 'notice-error' : 'notice-success'; ?> is-dismissible"><p><?php echo esc_html((string) ($notice['message'] ?? '')); ?></p></div>
            <?php endif; ?>

            <?php if (is_file(trailingslashit(ABSPATH) . 'robots.txt')) : ?>
                <div class="notice notice-warning"><p><?php esc_html_e('A physical robots.txt exists at the shared WordPress root. Multisite needs virtual per-site robots output, so verify that the physical file does not hide mapped-domain or subdirectory sitemap rules.', 'help4-builder-suite'); ?></p></div>
            <?php endif; ?>

            <div class="h4bs-network-summary" aria-label="<?php esc_attr_e('Network summary', 'help4-builder-suite'); ?>">
                <div><strong><?php echo esc_html((string) $total); ?></strong><span><?php esc_html_e('active sites', 'help4-builder-suite'); ?></span></div>
                <div><strong><?php echo esc_html((string) count((array) ($settings['enforced_keys'] ?? []))); ?></strong><span><?php esc_html_e('enforced SEO defaults', 'help4-builder-suite'); ?></span></div>
                <div><strong><?php echo esc_html((string) ($state['processed'] ?? 0)); ?></strong><span><?php echo esc_html($provisionedLabel); ?></span></div>
                <div><strong><?php echo esc_html(ucfirst((string) ($state['status'] ?? 'ready'))); ?></strong><span><?php esc_html_e('provisioning state', 'help4-builder-suite'); ?></span></div>
            </div>

            <?php if (($healthJob['status'] ?? '') === 'running') : ?>
                <div class="notice notice-info"><p><?php echo esc_html(sprintf(
                    /* translators: 1: completed site count, 2: queued site count */
                    __('Network SEO health scan is running: %1$d of %2$d sites complete.', 'help4-builder-suite'),
                    absint($healthJob['cursor'] ?? 0),
                    count((array) ($healthJob['site_ids'] ?? []))
                )); ?></p></div>
            <?php endif; ?>

            <section class="h4bs-admin-card">
                <h2><?php esc_html_e('Network SEO and onboarding policy', 'help4-builder-suite'); ?></h2>
                <p><?php esc_html_e('Defaults fill new or empty site values. Enforcement is separate and opt-in per field. Verification tokens and IndexNow keys always remain site-specific.', 'help4-builder-suite'); ?></p>
                <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                    <input type="hidden" name="action" value="h4bs_network_save_settings">
                    <?php wp_nonce_field('h4bs_network_save_settings', 'h4bs_network_nonce'); ?>
                    <table class="form-table" role="presentation">
                        <tr>
                            <th scope="row"><?php esc_html_e('New sites', 'help4-builder-suite'); ?></th>
                            <td><label><input type="checkbox" name="h4bs_network[provision_new_sites]" value="1" <?php checked((string) ($settings['provision_new_sites'] ?? '1'), '1'); ?>> <?php esc_html_e('Install per-site tables and apply network defaults when WordPress creates a site', 'help4-builder-suite'); ?></label></td>
                        </tr>
                        <tr>
                            <th scope="row"><label for="h4bs-network-profile"><?php esc_html_e('Default business profile', 'help4-builder-suite'); ?></label></th>
                            <td>
                                <select id="h4bs-network-profile" name="h4bs_network[default_business_profile]">
                                    <?php foreach (BusinessProfileDefaults::catalog() as $slug => $profile) : ?>
                                        <option value="<?php echo esc_attr((string) $slug); ?>" <?php selected((string) ($settings['default_business_profile'] ?? 'business'), (string) $slug); ?>><?php echo esc_html((string) ($profile['label'] ?? $slug)); ?></option>
                                    <?php endforeach; ?>
                                </select>
                            </td>
                        </tr>
                    </table>

                    <div class="h4bs-admin-table-scroll">
                        <table class="widefat striped h4bs-network-policy-table">
                            <thead><tr><th><?php esc_html_e('SEO setting', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Network default', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Enforce', 'help4-builder-suite'); ?></th></tr></thead>
                            <tbody>
                            <?php foreach (NetworkSettings::policyCatalog() as $key => $definition) : ?>
                                <?php $value = $settings['default_values'][$key] ?? ''; ?>
                                <?php
                                $enforceLabel = sprintf(
                                    /* translators: %s: SEO setting label. */
                                    __('Enforce %s', 'help4-builder-suite'),
                                    $definition['label']
                                );
                                ?>
                                <tr>
                                    <th scope="row"><label for="h4bs-network-<?php echo esc_attr($key); ?>"><?php echo esc_html($definition['label']); ?></label><span><?php echo esc_html($definition['description']); ?></span></th>
                                    <td><?php $this->renderPolicyField($key, $definition['type'], $value); ?></td>
                                    <td><input type="checkbox" name="h4bs_network[enforced_keys][]" value="<?php echo esc_attr($key); ?>" aria-label="<?php echo esc_attr($enforceLabel); ?>" <?php checked(in_array($key, (array) ($settings['enforced_keys'] ?? []), true)); ?>></td>
                                </tr>
                            <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                    <?php submit_button(__('Save Network Policy', 'help4-builder-suite')); ?>
                </form>
            </section>

            <section class="h4bs-admin-card">
                <h2><?php esc_html_e('Sites and search readiness', 'help4-builder-suite'); ?></h2>
                <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                    <input type="hidden" name="action" value="h4bs_network_bulk_action">
                    <?php wp_nonce_field('h4bs_network_bulk_action', 'h4bs_network_bulk_nonce'); ?>
                    <div class="tablenav top">
                        <div class="alignleft actions bulkactions">
                            <label class="screen-reader-text" for="h4bs-network-action"><?php esc_html_e('Bulk action', 'help4-builder-suite'); ?></label>
                            <select id="h4bs-network-action" name="network_action">
                                <option value=""><?php esc_html_e('Choose action', 'help4-builder-suite'); ?></option>
                                <option value="provision"><?php esc_html_e('Install or upgrade site data', 'help4-builder-suite'); ?></option>
                                <option value="apply_defaults"><?php esc_html_e('Apply network SEO defaults', 'help4-builder-suite'); ?></option>
                                <option value="run_health"><?php esc_html_e('Run SEO and sitemap health', 'help4-builder-suite'); ?></option>
                                <option value="full_index"><?php esc_html_e('Queue full IndexNow sweep', 'help4-builder-suite'); ?></option>
                            </select>
                            <button class="button" type="submit"><?php esc_html_e('Apply', 'help4-builder-suite'); ?></button>
                            <label class="h4bs-network-overwrite"><input type="checkbox" name="overwrite_defaults" value="1"> <?php esc_html_e('Replace existing values when applying defaults', 'help4-builder-suite'); ?></label>
                        </div>
                    </div>
                    <div class="h4bs-admin-table-scroll">
                        <table class="widefat striped h4bs-network-sites-table">
                            <thead><tr><td class="check-column"><input type="checkbox" data-h4bs-network-check-all aria-label="<?php esc_attr_e('Select all sites on this page', 'help4-builder-suite'); ?>"></td><th><?php esc_html_e('Site', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Indexing', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Builder', 'help4-builder-suite'); ?></th><th><?php esc_html_e('SEO health', 'help4-builder-suite'); ?></th><th><?php esc_html_e('IndexNow', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Provisioning', 'help4-builder-suite'); ?></th></tr></thead>
                            <tbody>
                            <?php foreach ($sites as $site) : ?>
                                <?php if (! $site instanceof \WP_Site) { continue; } ?>
                                <?php $snapshot = $this->siteSnapshot($site); ?>
                                <?php
                                $selectSiteLabel = sprintf(
                                    /* translators: %s: network site name. */
                                    __('Select %s', 'help4-builder-suite'),
                                    (string) $snapshot['name']
                                );
                                ?>
                                <tr>
                                    <th scope="row" class="check-column"><input type="checkbox" name="site_ids[]" value="<?php echo esc_attr((string) $site->blog_id); ?>" aria-label="<?php echo esc_attr($selectSiteLabel); ?>"></th>
                                    <td><strong><a href="<?php echo esc_url((string) $snapshot['home']); ?>" target="_blank" rel="noopener"><?php echo esc_html((string) $snapshot['name']); ?></a></strong><span><?php echo esc_html((string) $snapshot['home']); ?></span><a href="<?php echo esc_url(get_admin_url((int) $site->blog_id, 'admin.php?page=h4bs-seo-control')); ?>"><?php esc_html_e('SEO controls', 'help4-builder-suite'); ?></a></td>
                                    <td><strong class="<?php echo $snapshot['public'] ? 'is-good' : 'is-warning'; ?>"><?php echo esc_html($snapshot['public'] ? __('Public', 'help4-builder-suite') : __('Discouraged', 'help4-builder-suite')); ?></strong><a href="<?php echo esc_url((string) $snapshot['sitemap_url']); ?>" target="_blank" rel="noopener"><?php esc_html_e('Sitemap', 'help4-builder-suite'); ?></a></td>
                                    <td><strong><?php echo esc_html((string) $snapshot['builder_pages']); ?></strong><span><?php esc_html_e('Builder-enabled items', 'help4-builder-suite'); ?></span></td>
                                    <td><strong class="<?php echo (int) $snapshot['seo_score'] >= 80 ? 'is-good' : ((int) $snapshot['seo_score'] > 0 ? 'is-warning' : ''); ?>"><?php echo (int) $snapshot['seo_score'] > 0 ? esc_html((string) $snapshot['seo_score'] . '%') : esc_html__('Not audited', 'help4-builder-suite'); ?></strong><span><?php echo esc_html((string) $snapshot['health_summary']); ?></span></td>
                                    <td><strong><?php echo esc_html((string) $snapshot['indexnow_queue']); ?></strong><span><?php echo esc_html((string) $snapshot['indexnow_status']); ?></span></td>
                                    <td><strong class="<?php echo $snapshot['db_current'] ? 'is-good' : 'is-warning'; ?>"><?php echo esc_html((string) $snapshot['db_version']); ?></strong><span><?php echo esc_html($snapshot['db_current'] ? __('Current', 'help4-builder-suite') : __('Upgrade needed', 'help4-builder-suite')); ?></span></td>
                                </tr>
                            <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                </form>
                <?php
                $totalPages = max(1, (int) ceil($total / $perPage));
                if ($totalPages > 1) {
                    echo '<div class="tablenav"><div class="tablenav-pages">';
                    echo wp_kses_post(paginate_links([
                        'base' => add_query_arg(['page' => self::PAGE_SLUG, 'paged' => '%#%'], network_admin_url('admin.php')),
                        'format' => '',
                        'current' => $page,
                        'total' => $totalPages,
                    ]));
                    echo '</div></div>';
                }
                ?>
            </section>
        </div>
        <?php
    }

    public function handleSaveSettings(): void
    {
        $this->requireNetworkAdministrator();
        check_admin_referer('h4bs_network_save_settings', 'h4bs_network_nonce');
        $raw = isset($_POST['h4bs_network']) && is_array($_POST['h4bs_network'])
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The complete nested payload is allowlisted and sanitized by NetworkSettings::sanitize().
            ? wp_unslash($_POST['h4bs_network'])
            : [];
        $raw['provision_new_sites'] = ! empty($raw['provision_new_sites']) ? '1' : '0';
        $raw['default_values'] = is_array($raw['default_values'] ?? null) ? $raw['default_values'] : [];
        foreach (NetworkSettings::policyCatalog() as $key => $definition) {
            if ($definition['type'] === 'bool') {
                $raw['default_values'][$key] = ! empty($raw['default_values'][$key]) ? '1' : '0';
            }
        }
        $clean = NetworkSettings::sanitize($raw);
        update_site_option(NetworkSettings::OPTION_KEY, $clean);
        $this->setNotice(__('Network SEO and onboarding policy saved.', 'help4-builder-suite'));
        wp_safe_redirect(network_admin_url('admin.php?page=' . self::PAGE_SLUG));
        exit;
    }

    public function handleBulkAction(): void
    {
        $this->requireNetworkAdministrator();
        check_admin_referer('h4bs_network_bulk_action', 'h4bs_network_bulk_nonce');
        $action = sanitize_key((string) wp_unslash($_POST['network_action'] ?? ''));
        $allowed = ['provision', 'apply_defaults', 'run_health', 'full_index'];
        if (! in_array($action, $allowed, true)) {
            $this->setNotice(__('Choose a valid network action.', 'help4-builder-suite'), true);
            wp_safe_redirect(network_admin_url('admin.php?page=' . self::PAGE_SLUG));
            exit;
        }

        $siteIds = array_values(array_unique(array_filter(array_map('absint', (array) wp_unslash($_POST['site_ids'] ?? [])))));
        $siteIds = array_slice($siteIds, 0, 100);
        $networkId = get_current_network_id();
        $siteIds = array_values(array_filter($siteIds, static function (int $siteId) use ($networkId): bool {
            $site = get_site($siteId);
            return $site instanceof \WP_Site && (int) $site->network_id === $networkId;
        }));
        if ($siteIds === []) {
            $this->setNotice(__('Select at least one site before applying a network action.', 'help4-builder-suite'), true);
            wp_safe_redirect(network_admin_url('admin.php?page=' . self::PAGE_SLUG));
            exit;
        }
        $overwrite = ! empty($_POST['overwrite_defaults']);
        if ($action === 'run_health') {
            update_site_option(self::HEALTH_JOB_OPTION, [
                'status' => 'running',
                'site_ids' => $siteIds,
                'cursor' => 0,
                'results' => [],
                'failed' => [],
                'started_at' => gmdate('c'),
                'completed_at' => '',
            ]);
            if (! wp_next_scheduled(self::HEALTH_JOB_HOOK)) {
                wp_schedule_single_event(time() + 2, self::HEALTH_JOB_HOOK);
            }
            $this->setNotice(sprintf(
                /* translators: %d: selected site count */
                __('SEO and sitemap health checks were queued for %d site(s).', 'help4-builder-suite'),
                count($siteIds)
            ));
            wp_safe_redirect(network_admin_url('admin.php?page=' . self::PAGE_SLUG));
            exit;
        }
        $processed = 0;
        $failed = [];
        foreach ($siteIds as $siteId) {
            $site = get_site($siteId);
            try {
                $this->withSite($siteId, function () use ($action, $overwrite): void {
                    if ($action === 'provision') {
                        Installer::installForCurrentSite();
                        return;
                    }
                    if ($action === 'apply_defaults') {
                        NetworkSettings::applyDefaultsToCurrentSite($overwrite, false);
                        return;
                    }
                    $seo = new SeoModule();
                    update_option('h4bs_indexnow_full_index', [
                        'status' => 'running',
                        'page' => 1,
                        'queued' => 0,
                        'started_at' => gmdate('c'),
                        'completed_at' => '',
                    ], false);
                    $seo->processIndexNowFullIndex();
                });
                $processed++;
            } catch (\Throwable $throwable) {
                $failed[$siteId] = sanitize_text_field($throwable->getMessage());
            }
        }

        $message = sprintf(
            /* translators: 1: action name, 2: successful site count, 3: failed site count */
            __('Network action %1$s completed for %2$d site(s); %3$d failed.', 'help4-builder-suite'),
            $action,
            $processed,
            count($failed)
        );
        $this->setNotice($message, $failed !== []);
        do_action('h4bs_network_bulk_action_completed', $action, $siteIds, $processed, $failed);
        wp_safe_redirect(network_admin_url('admin.php?page=' . self::PAGE_SLUG));
        exit;
    }

    /**
     * @param array<string, mixed> $args
     */
    public function initializeNewSite(\WP_Site $newSite, array $args): void
    {
        unset($args);
        $settings = NetworkSettings::all();
        if (! rest_sanitize_boolean($settings['provision_new_sites'] ?? '1')) {
            return;
        }

        $this->withSite((int) $newSite->blog_id, static function (): void {
            Installer::installForCurrentSite();
            $report = NetworkSettings::applyDefaultsToCurrentSite(true, true);
            update_option('h4bs_multisite_initialized', [
                'version' => H4BS_VERSION,
                'initialized_at' => gmdate('c'),
                'network_defaults' => $report['updated'],
                'business_profile' => $report['profile'],
            ], false);
        });
    }

    public function processProvisionBatch(): void
    {
        if (! is_multisite() || ! $this->isNetworkActive()) {
            return;
        }
        $state = get_site_option(NetworkSettings::PROVISION_STATE_OPTION, []);
        $state = is_array($state) ? $state : [];
        if (($state['status'] ?? '') !== 'running') {
            return;
        }

        $offset = max(0, absint($state['offset'] ?? 0));
        $siteIds = get_sites([
            'network_id' => get_current_network_id(),
            'fields' => 'ids',
            'number' => self::BATCH_SIZE,
            'offset' => $offset,
            'orderby' => 'id',
            'order' => 'ASC',
            'spam' => 0,
            'deleted' => 0,
        ]);
        $failed = is_array($state['failed'] ?? null) ? $state['failed'] : [];
        $processed = absint($state['processed'] ?? 0);
        foreach ($siteIds as $siteId) {
            try {
                $this->withSite((int) $siteId, static function (): void {
                    Installer::installForCurrentSite();
                });
                $processed++;
            } catch (\Throwable $throwable) {
                $failed[(int) $siteId] = sanitize_text_field($throwable->getMessage());
            }
        }

        $state['processed'] = $processed;
        $state['failed'] = $failed;
        $state['offset'] = $offset + count($siteIds);
        $state['last_batch_at'] = gmdate('c');
        if (count($siteIds) < self::BATCH_SIZE) {
            $state['status'] = $failed === [] ? 'complete' : 'complete_with_errors';
            $state['completed_at'] = gmdate('c');
            update_site_option('h4bs_network_provision_version', H4BS_VERSION);
        } else {
            wp_schedule_single_event(time() + 10, NetworkSettings::PROVISION_HOOK);
        }
        update_site_option(NetworkSettings::PROVISION_STATE_OPTION, $state);
        do_action('h4bs_multisite_provision_batch_completed', $state, $siteIds);
    }

    public function processSeoHealthBatch(): void
    {
        if (! is_multisite()) {
            return;
        }
        $job = get_site_option(self::HEALTH_JOB_OPTION, []);
        $job = is_array($job) ? $job : [];
        if (($job['status'] ?? '') !== 'running') {
            return;
        }
        $siteIds = array_values(array_filter(array_map('absint', (array) ($job['site_ids'] ?? []))));
        $cursor = absint($job['cursor'] ?? 0);
        if (! isset($siteIds[$cursor])) {
            $job['status'] = empty($job['failed']) ? 'complete' : 'complete_with_errors';
            $job['completed_at'] = gmdate('c');
            update_site_option(self::HEALTH_JOB_OPTION, $job);
            do_action('h4bs_multisite_seo_health_completed', $job);
            return;
        }

        $siteId = (int) $siteIds[$cursor];
        $results = is_array($job['results'] ?? null) ? $job['results'] : [];
        $failed = is_array($job['failed'] ?? null) ? $job['failed'] : [];
        try {
            $report = $this->withSite($siteId, static function (): array {
                $seo = new SeoModule();
                $report = $seo->buildSeoHealthReport();
                update_option('h4bs_seo_health_report', $report, false);
                return $report;
            });
            $results[$siteId] = [
                'checked_at' => (string) ($report['checked_at'] ?? gmdate('c')),
                'scanned' => absint($report['scanned'] ?? 0),
                'sitemaps' => count((array) ($report['sitemaps'] ?? [])),
            ];
        } catch (\Throwable $throwable) {
            $failed[$siteId] = sanitize_text_field($throwable->getMessage());
        }
        $job['cursor'] = $cursor + 1;
        $job['results'] = $results;
        $job['failed'] = $failed;
        $job['last_site_id'] = $siteId;
        $job['last_batch_at'] = gmdate('c');
        if ($job['cursor'] >= count($siteIds)) {
            $job['status'] = $failed === [] ? 'complete' : 'complete_with_errors';
            $job['completed_at'] = gmdate('c');
        } else {
            wp_schedule_single_event(time() + 10, self::HEALTH_JOB_HOOK);
        }
        update_site_option(self::HEALTH_JOB_OPTION, $job);
        do_action('h4bs_multisite_seo_health_site_completed', $siteId, $job);
    }

    public function renderSitePolicyNotice(): void
    {
        if (is_network_admin() || ! current_user_can('manage_options')) {
            return;
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing guard for Help4 admin screens.
        $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
        if (! str_starts_with($page, 'h4bs')) {
            return;
        }
        $keys = NetworkSettings::enforcedKeys();
        if ($keys === []) {
            return;
        }
        $labels = [];
        $catalog = NetworkSettings::policyCatalog();
        foreach ($keys as $key) {
            if (isset($catalog[$key])) {
                $labels[] = (string) $catalog[$key]['label'];
            }
        }
        ?>
        <div class="notice notice-info"><p><?php echo esc_html(sprintf(
            /* translators: %s: comma-separated setting labels */
            __('This site follows network-enforced Help4 SEO values for: %s. Site-specific verification tokens and IndexNow keys remain local.', 'help4-builder-suite'),
            implode(', ', $labels)
        )); ?></p></div>
        <?php
    }

    public function invalidateBuilderCount(): void
    {
        delete_transient(self::BUILDER_COUNT_TRANSIENT);
    }

    private function maybeScheduleNetworkUpgrade(): void
    {
        if (! $this->isNetworkActive()) {
            return;
        }
        $completedVersion = (string) get_site_option('h4bs_network_provision_version', '');
        $state = get_site_option(NetworkSettings::PROVISION_STATE_OPTION, []);
        $state = is_array($state) ? $state : [];
        if ($completedVersion === H4BS_VERSION || (($state['status'] ?? '') === 'running' && ($state['version'] ?? '') === H4BS_VERSION)) {
            return;
        }
        update_site_option(NetworkSettings::PROVISION_STATE_OPTION, [
            'status' => 'running',
            'offset' => 0,
            'processed' => 0,
            'failed' => [],
            'version' => H4BS_VERSION,
            'started_at' => gmdate('c'),
            'completed_at' => '',
        ]);
        if (! wp_next_scheduled(NetworkSettings::PROVISION_HOOK)) {
            wp_schedule_single_event(time() + 5, NetworkSettings::PROVISION_HOOK);
        }
    }

    private function isNetworkActive(): bool
    {
        if (! defined('H4BS_FILE')) {
            return false;
        }
        $networkActive = (array) get_site_option('active_sitewide_plugins', []);
        return isset($networkActive[plugin_basename((string) H4BS_FILE)]);
    }

    /**
     * @return array<string, mixed>
     */
    private function siteSnapshot(\WP_Site $site): array
    {
        return $this->withSite((int) $site->blog_id, static function () use ($site): array {
            $report = get_option('h4bs_seo_health_report', []);
            $report = is_array($report) ? $report : [];
            $missing = is_array($report['missing'] ?? null) ? $report['missing'] : [];
            $scanned = absint($report['scanned'] ?? 0);
            $issues = array_sum(array_map('absint', $missing))
                + absint($report['duplicate_titles'] ?? 0)
                + absint($report['duplicate_descriptions'] ?? 0);
            $possible = max(1, $scanned * 4);
            $score = $scanned > 0 ? max(1, min(100, (int) round(100 - (($issues / $possible) * 100)))) : 0;
            $builderCount = get_transient(self::BUILDER_COUNT_TRANSIENT);
            if ($builderCount === false) {
                $builderQuery = new \WP_Query([
                    'post_type' => array_values(get_post_types([], 'names')),
                    'post_status' => ['publish', 'draft', 'private', 'pending'],
                    'posts_per_page' => 1,
                    'fields' => 'ids',
                    'no_found_rows' => false,
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Bounded Network Admin count, cached per site and invalidated on Builder saves.
                    'meta_key' => '_h4_builder_enabled',
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Exact enabled marker for the cached Builder usage count.
                    'meta_value' => '1',
                ]);
                $builderCount = (int) $builderQuery->found_posts;
                set_transient(self::BUILDER_COUNT_TRANSIENT, $builderCount, HOUR_IN_SECONDS);
            }
            $queue = get_option('h4bs_indexnow_queue', []);
            $queue = is_array($queue) ? $queue : [];
            $last = get_option('h4bs_indexnow_last_result', []);
            $last = is_array($last) ? $last : [];
            $effectiveSettings = Settings::all();
            $indexNowEnabled = rest_sanitize_boolean($effectiveSettings['indexnow_enabled'] ?? '0');
            $indexNowKey = (string) preg_replace('/[^A-Za-z0-9-]/', '', (string) ($effectiveSettings['indexnow_key'] ?? ''));
            $dbVersion = (string) get_option('h4bs_db_version', 'not installed');
            $home = home_url('/');
            $siteName = get_bloginfo('name');
            if ($siteName === '') {
                $siteName = sprintf(
                    /* translators: %d: network site ID. */
                    __('Site %d', 'help4-builder-suite'),
                    (int) $site->blog_id
                );
            }
            $healthSummary = __('Run the network health action', 'help4-builder-suite');
            if ($scanned > 0) {
                $healthSummary = sprintf(
                    /* translators: 1: scanned content count, 2: SEO issue count. */
                    __('%1$d scanned, %2$d issues', 'help4-builder-suite'),
                    $scanned,
                    $issues
                );
            }
            return [
                'name' => $siteName,
                'home' => $home,
                'public' => (string) get_option('blog_public', '1') === '1' && ! (bool) $site->archived,
                'sitemap_url' => home_url('/wp-sitemap.xml'),
                'builder_pages' => (int) $builderCount,
                'seo_score' => $score,
                'health_summary' => $healthSummary,
                'indexnow_queue' => count($queue),
                'indexnow_status' => (string) ($last['outcome'] ?? ($indexNowEnabled
                    ? (strlen($indexNowKey) >= 8 ? __('Ready', 'help4-builder-suite') : __('Key required', 'help4-builder-suite'))
                    : __('Disabled', 'help4-builder-suite'))),
                'db_version' => $dbVersion,
                'db_current' => $dbVersion === H4BS_VERSION,
            ];
        });
    }

    private function renderPolicyField(string $key, string $type, mixed $value): void
    {
        $name = 'h4bs_network[default_values][' . $key . ']';
        $id = 'h4bs-network-' . $key;
        if ($type === 'bool') {
            ?>
            <label><input id="<?php echo esc_attr($id); ?>" type="checkbox" name="<?php echo esc_attr($name); ?>" value="1" <?php checked(rest_sanitize_boolean($value)); ?>> <?php esc_html_e('Enabled by default', 'help4-builder-suite'); ?></label>
            <?php
            return;
        }
        if ($type === 'urls' || $type === 'textarea') {
            $display = $type === 'urls' ? implode("\n", array_map('strval', (array) $value)) : (string) $value;
            ?><textarea id="<?php echo esc_attr($id); ?>" class="large-text code" rows="4" name="<?php echo esc_attr($name); ?>"><?php echo esc_textarea($display); ?></textarea><?php
            return;
        }
        $inputType = $type === 'url' ? 'url' : 'text';
        ?><input id="<?php echo esc_attr($id); ?>" class="regular-text" type="<?php echo esc_attr($inputType); ?>" name="<?php echo esc_attr($name); ?>" value="<?php echo esc_attr((string) $value); ?>"><?php
    }

    /**
     * @template T
     * @param callable(): T $callback
     * @return T
     */
    private function withSite(int $siteId, callable $callback): mixed
    {
        $switched = get_current_blog_id() !== $siteId;
        if ($switched) {
            switch_to_blog($siteId);
        }
        try {
            return $callback();
        } finally {
            if ($switched) {
                restore_current_blog();
            }
        }
    }

    private function requireNetworkAdministrator(): void
    {
        if (! is_multisite() || ! is_super_admin() || ! current_user_can('manage_network_options')) {
            wp_die(esc_html__('You do not have permission to manage Help4 network settings.', 'help4-builder-suite'));
        }
    }

    private function setNotice(string $message, bool $error = false): void
    {
        set_site_transient(
            self::NOTICE_TRANSIENT_PREFIX . get_current_user_id(),
            ['message' => sanitize_text_field($message), 'error' => $error],
            60
        );
    }
}
