<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\SEO;

use Help4BuilderSuite\Support\RobotsContract;

use Help4BuilderSuite\Contracts\ModuleContract;
use Help4BuilderSuite\Support\AdminRequest;
use Help4BuilderSuite\Support\ContactRoutes;
use Help4BuilderSuite\Support\Settings;
use WP_Post;
use WP_User;

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

final class Module implements ModuleContract
{
    private int $headingGuardPostId = 0;
    /** @var array<string, array<int, array{name: string, url: string}>> */
    private array $breadcrumbItemsCache = [];
    private GoogleBusinessAddon $googleBusinessAddon;
    private LocalBusinessAddon $localBusinessAddon;
    private RealEstateSchema $realEstateSchema;
    private DiscoveryStandards $discoveryStandards;
    private CommerceDiscovery $commerceDiscovery;
    private IntentScorer $intentScorer;
    private SeoMigration $seoMigration;
    private LinkHealthCrawler $linkHealthCrawler;
    private PerformingArtsSchema $performingArtsSchema;
    private MeasurementBridge $measurementBridge;
    private OrganicLandingAudit $organicLandingAudit;

    public function __construct()
    {
        $this->googleBusinessAddon = new GoogleBusinessAddon();
        $this->localBusinessAddon = new LocalBusinessAddon();
        $this->realEstateSchema = new RealEstateSchema();
        $this->discoveryStandards = new DiscoveryStandards();
        $this->commerceDiscovery = new CommerceDiscovery();
        $this->intentScorer = new IntentScorer($this->commerceDiscovery);
        $this->seoMigration = new SeoMigration();
        $this->linkHealthCrawler = new LinkHealthCrawler();
        $this->performingArtsSchema = new PerformingArtsSchema();
        $this->measurementBridge = new MeasurementBridge();
        $this->organicLandingAudit = new OrganicLandingAudit();
    }

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

    public function boot(): void
    {
        if (function_exists('wp_robots')) {
            remove_action('wp_head', 'wp_robots', 1);
        }
        remove_action('wp_head', 'rel_canonical');
        add_action('admin_menu', [$this, 'registerAdminPage']);
        add_action('admin_post_h4bs_generate_missing_image_alt', [$this, 'handleGenerateMissingImageAlt']);
        add_action('admin_post_h4bs_indexnow_full_index', [$this, 'handleIndexNowFullIndex']);
        add_action('admin_post_h4bs_run_seo_health', [$this, 'handleRunSeoHealth']);
        add_action('add_meta_boxes', [$this, 'registerMetaBox']);
        add_action('save_post', [$this, 'saveMetaBox'], 10, 2);
        add_filter('pre_get_document_title', [$this, 'filterDocumentTitle'], 999);
        add_filter('document_title_parts', [$this, 'filterDocumentTitleParts'], 999);
        add_filter('wp_robots', [$this, 'filterRobots'], 99);
        add_filter('robots_txt', [$this, 'filterRobotsTxt'], 99, 2);
        add_filter('wp_sitemaps_posts_query_args', [$this, 'filterSitemapPostQueryArgs'], 10, 2);
        add_filter('pre_handle_404', [$this, 'repairSitemapHttpStatus'], 10, 2);
        add_action('init', [$this, 'repairStaticRobotsTxt'], 1);
        add_action('template_redirect', [$this, 'maybeRenderLlmsText'], 0);
        add_action('template_redirect', [$this, 'maybeRenderSeoDiscoveryFiles'], 0);
        add_action('template_redirect', [$this, 'redirectLegacySitemapUrls'], 0);
        add_action('template_redirect', [$this, 'startBuilderHeadingGuard'], 20);
        add_action('wp_head', [$this, 'renderHeadMeta'], 3);
        add_action('transition_post_status', [$this, 'queueIndexNowTransition'], 10, 3);
        add_action('before_delete_post', [$this, 'queueIndexNowDeletion'], 10, 2);
        add_action('h4bs_commerce_product_saved', [$this, 'queueCommerceProductChange'], 10, 3);
        add_action('h4bs_commerce_product_deleted', [$this, 'queueCommerceProductDeletion'], 10, 2);
        add_action('h4bs_indexnow_submit', [$this, 'submitIndexNowQueue']);
        add_action('h4bs_indexnow_full_index_batch', [$this, 'processIndexNowFullIndex']);
        add_filter('the_content_feed', [$this, 'filterFeedContent']);
        add_filter('the_excerpt_rss', [$this, 'filterFeedContent']);
        add_action('show_user_profile', [$this, 'renderAuthorSeoFields']);
        add_action('edit_user_profile', [$this, 'renderAuthorSeoFields']);
        add_action('personal_options_update', [$this, 'saveAuthorSeoFields']);
        add_action('edit_user_profile_update', [$this, 'saveAuthorSeoFields']);
        add_shortcode('h4_breadcrumbs', [$this, 'renderBreadcrumbsShortcode']);
        $this->localBusinessAddon->boot();
        $this->googleBusinessAddon->boot();
        $this->commerceDiscovery->boot();
        $this->seoMigration->boot();
        $this->linkHealthCrawler->boot();
        $this->measurementBridge->boot();
        $this->organicLandingAudit->boot();
    }

    public function registerAdminPage(): void
    {
        add_submenu_page(
            'h4bs-settings',
            __('SEO Control Center', 'help4-builder-suite'),
            __('SEO Control', 'help4-builder-suite'),
            'manage_options',
            'h4bs-seo-control',
            [$this, 'renderAdminPage']
        );
    }

    public function startBuilderHeadingGuard(): void
    {
        if (is_admin() || ! is_singular()) return;
        $postId = (int) get_queried_object_id();
        if ($postId <= 0 || ! $this->builderLayoutOwnsH1($postId)) return;
        $this->headingGuardPostId = $postId;
        ob_start([$this, 'guardBuilderHeadingHtml']);
    }

    public function guardBuilderHeadingHtml(string $html): string
    {
        if (substr_count(strtolower($html), '<h1') < 2) return $html;
        $html = (string) preg_replace_callback('/<header\b[^>]*>.*?<\/header>/is', static function (array $match): string {
            return (string) preg_replace_callback('/<h1\b([^>]*\bclass\s*=\s*(["\'])(?:(?!\2).)*(?:site-title|wp-block-site-title|site-branding)(?:(?!\2).)*\2[^>]*)>(.*?)<\/h1>/is', static fn(array $heading): string => '<p' . $heading[1] . ' data-h4bs-heading-demoted="site-title">' . $heading[3] . '</p>', (string) $match[0], 1);
        }, $html, 1);
        $builderPosition = $this->currentPageBuilderPosition($html);
        if ($builderPosition === false) return $html;
        $prefix = substr($html, 0, $builderPosition);
        $suffix = substr($html, $builderPosition);
        $demotions = 0;
        $prefix = (string) preg_replace_callback('/(<article\b[^>]*>\s*)<h1\b([^>]*)>(.*?)<\/h1>/is', static fn(array $match): string => $match[1] . '<p' . $match[2] . ' data-h4bs-heading-demoted="entry-title">' . $match[3] . '</p>', $prefix, 1, $demotions);
        if ($demotions === 0) {
            $prefix = (string) preg_replace_callback('/(<article\b(?:(?!<\/article>).)*?)<h1\b([^>]*)>(.*?)<\/h1>/is', static fn(array $match): string => $match[1] . '<p' . $match[2] . ' data-h4bs-heading-demoted="entry-title">' . $match[3] . '</p>', $prefix, 1);
        }
        return $prefix . $suffix;
    }

    /** @return int|false */
    private function currentPageBuilderPosition(string $html)
    {
        $postId = $this->headingGuardPostId > 0 ? $this->headingGuardPostId : (int) get_queried_object_id();
        if ($postId > 0) {
            $pattern = '/<div\b(?=[^>]*\bclass\s*=\s*(["\'])[^"\']*\bh4bs-builder-page\b[^"\']*\1)(?=[^>]*\bdata-h4bs-layout-id\s*=\s*(["\'])' . preg_quote((string) $postId, '/') . '\2)[^>]*>/i';
            if (preg_match($pattern, $html, $match, PREG_OFFSET_CAPTURE) === 1) return (int) $match[0][1];
        }
        return stripos($html, 'h4bs-builder-page');
    }

    private function builderLayoutOwnsH1(int $postId): bool
    {
        if ((string) get_post_meta($postId, '_h4_builder_enabled', true) !== '1' || (string) get_post_meta($postId, '_h4_builder_mode', true) !== 'help4') return false;
        $raw = get_post_meta($postId, '_h4_builder_layout', true);
        $layout = is_string($raw) ? json_decode($raw, true) : $raw;
        if (! is_array($layout)) return false;
        foreach ($layout as $block) {
            if (! is_array($block)) continue;
            $settings = is_array($block['settings'] ?? null) ? $block['settings'] : [];
            if (($block['type'] ?? '') === 'heading' && strtolower((string) ($settings['tag'] ?? 'h2')) === 'h1') return true;
            if (($block['type'] ?? '') === 'slider') {
                foreach ((array) ($settings['slides'] ?? []) as $slide) if (is_array($slide) && strtolower((string) ($slide['heading_tag'] ?? '')) === 'h1') return true;
                if ($this->referencedSliderOwnsH1(absint($settings['slider_id'] ?? 0))) return true;
            }
            if (($block['type'] ?? '') === 'html' && $this->htmlBlockOwnsH1((string) ($settings['html'] ?? ''))) return true;
        }
        return false;
    }

    private function referencedSliderOwnsH1(int $sliderId): bool
    {
        if ($sliderId <= 0) return false;
        $slider = get_post($sliderId);
        if (! $slider instanceof \WP_Post || $slider->post_type !== 'h4_slider' || in_array($slider->post_status, ['trash', 'auto-draft'], true)) return false;
        $raw = get_post_meta($sliderId, '_h4_slider_slides', true);
        $slides = is_string($raw) ? json_decode($raw, true) : $raw;
        if (! is_array($slides) || $slides === []) return false;
        foreach ($slides as $slide) {
            if (! is_array($slide)) continue;
            foreach ((array) ($slide['layers'] ?? []) as $layer) {
                if (! is_array($layer)) continue;
                if (strtolower((string) ($layer['type'] ?? '')) === 'heading' && strtolower((string) ($layer['tag'] ?? 'h2')) === 'h1') return true;
            }
        }
        return false;
    }

    private function htmlBlockOwnsH1(string $html): bool
    {
        if ($html === '') return false;
        $withoutExecutableText = (string) preg_replace('/<(script|style|template|noscript)\b[^>]*>.*?<\/\1>/is', '', $html);
        $withoutExecutableText = (string) preg_replace('/<!--.*?-->/s', '', $withoutExecutableText);
        $sanitized = wp_kses_post($withoutExecutableText);
        if (class_exists('WP_HTML_Tag_Processor')) {
            $processor = new \WP_HTML_Tag_Processor($sanitized);
            return $processor->next_tag(['tag_name' => 'H1']);
        }
        return preg_match('/<h1\b[^>]*>/i', $sanitized) === 1;
    }

    public function registerMetaBox(): void
    {
        $types = get_post_types(['public' => true], 'names');
        foreach ($types as $type) {
            if ($type === 'attachment') {
                continue;
            }

            add_meta_box(
                'h4bs-seo-meta',
                __('Help4 SEO', 'help4-builder-suite'),
                [$this, 'renderMetaBox'],
                $type,
                'normal',
                'default'
            );
        }
    }

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

        $rows = $this->seoAuditRows(120);
        $imageRows = $this->imageSeoAuditRows(80);
        $total = count($rows);
        $average = 0;
        $needsWork = 0;
        foreach ($rows as $row) {
            $score = (int) ($row['score'] ?? 0);
            $average += $score;
            if ($score < 80) {
                $needsWork++;
            }
        }
        $average = $total > 0 ? (int) round($average / $total) : 0;
        $missingAlt = 0;
        foreach ($imageRows as $imageRow) {
            if (empty($imageRow['alt'])) {
                $missingAlt++;
            }
        }
        $generatedAlt = AdminRequest::queryAbsInt('h4bs_alt_generated');
        $queuedIndexNow = count($this->normalizedIndexNowQueue());
        $lastIndexNow = get_option('h4bs_indexnow_last_result', []);
        $lastIndexNow = is_array($lastIndexNow) ? $lastIndexNow : [];
        $fullIndex = get_option('h4bs_indexnow_full_index', []);
        $fullIndex = is_array($fullIndex) ? $fullIndex : [];
        $healthReport = get_option('h4bs_seo_health_report', []);
        $healthReport = is_array($healthReport) ? $healthReport : [];
        $fullIndexStarted = AdminRequest::queryAbsInt('h4bs_full_index_started');
        $fullIndexQueued = AdminRequest::queryAbsInt('h4bs_full_index_queued');
        $healthChecked = AdminRequest::queryAbsInt('h4bs_health_checked');
        $discoveryReport = $this->discoveryStandards->coverageReport(Settings::all(), $healthReport);
        $commerceAudit = $this->commerceDiscovery->catalogAudit(500);
        $internalLinkGraph = $this->internalLinkGraph(300);
        $internalLinkSummary = is_array($internalLinkGraph['summary'] ?? null) ? $internalLinkGraph['summary'] : [];
        ?>
        <style>
            .h4bs-seo-control,
            .h4bs-seo-control * {
                box-sizing: border-box;
            }
            .h4bs-seo-control [style*="display:grid"] > *,
            .h4bs-seo-control [style*="display:flex"] > * {
                min-width: 0;
            }
            .h4bs-seo-control table.widefat {
                display: block;
                max-width: 100%;
                overflow-x: auto;
            }
            .h4bs-seo-control a,
            .h4bs-seo-control code,
            .h4bs-seo-control strong {
                overflow-wrap: anywhere;
                word-break: break-word;
            }
            .h4bs-seo-control input,
            .h4bs-seo-control select,
            .h4bs-seo-control textarea,
            .h4bs-seo-control button {
                max-width: 100%;
            }
        </style>
        <div class="wrap h4bs-admin-wrap h4bs-seo-control">
            <h1><?php esc_html_e('Help4 SEO Control Center', 'help4-builder-suite'); ?></h1>
            <p><?php esc_html_e('Audit titles, descriptions, canonical URLs, robots controls, schema, social previews, and content depth from one screen.', 'help4-builder-suite'); ?></p>
            <?php if ($generatedAlt > 0) : ?>
                <?php /* translators: %d: Number of image alt text values generated. */ ?>
                <div class="notice notice-success is-dismissible"><p><?php echo esc_html(sprintf(__('Generated missing alt text for %d image(s).', 'help4-builder-suite'), $generatedAlt)); ?></p></div>
            <?php endif; ?>
            <?php if ($fullIndexStarted > 0) : ?>
                <?php /* translators: %d: Number of URLs queued in the first full-index batch. */ ?>
                <div class="notice notice-success is-dismissible"><p><?php echo esc_html(sprintf(__('Full indexing sweep started; %d URL(s) were queued in the first batch.', 'help4-builder-suite'), $fullIndexQueued)); ?></p></div>
            <?php endif; ?>
            <?php if ($healthChecked > 0) : ?>
                <div class="notice notice-success is-dismissible"><p><?php esc_html_e('Indexing and metadata health checks were refreshed.', 'help4-builder-suite'); ?></p></div>
            <?php endif; ?>

            <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin:1rem 0 1.5rem;">
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                    <strong style="display:block;font-size:1.6rem;"><?php echo esc_html((string) $total); ?></strong>
                    <span><?php esc_html_e('public items scanned', 'help4-builder-suite'); ?></span>
                </div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                    <strong style="display:block;font-size:1.6rem;"><?php echo esc_html((string) $average); ?>/100</strong>
                    <span><?php esc_html_e('average SEO health', 'help4-builder-suite'); ?></span>
                </div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                    <strong style="display:block;font-size:1.6rem;"><?php echo esc_html((string) $needsWork); ?></strong>
                    <span><?php esc_html_e('need attention', 'help4-builder-suite'); ?></span>
                </div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                    <strong style="display:block;font-size:1.6rem;"><?php echo esc_html(home_url('/wp-sitemap.xml')); ?></strong>
                    <span><?php esc_html_e('core sitemap target', 'help4-builder-suite'); ?></span>
                </div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                    <strong style="display:block;font-size:1.6rem;"><?php echo esc_html(home_url('/llms.txt')); ?></strong>
                    <span><?php esc_html_e('AI assistant guide', 'help4-builder-suite'); ?></span>
                </div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                    <strong style="display:block;font-size:1.6rem;"><?php echo esc_html((string) $missingAlt); ?></strong>
                    <span><?php esc_html_e('images missing alt text', 'help4-builder-suite'); ?></span>
                </div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                    <strong style="display:block;font-size:1.6rem;"><?php echo esc_html((string) $queuedIndexNow); ?></strong>
                    <span><?php esc_html_e('URLs waiting for IndexNow', 'help4-builder-suite'); ?></span>
                </div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                    <strong style="display:block;font-size:1.6rem;"><?php echo esc_html((string) ($internalLinkSummary['orphan_candidates'] ?? 0)); ?></strong>
                    <span><?php esc_html_e('content-orphan candidates', 'help4-builder-suite'); ?></span>
                </div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                    <strong style="display:block;font-size:1.6rem;"><?php echo esc_html((string) ($internalLinkSummary['weak_candidates'] ?? 0)); ?></strong>
                    <span><?php esc_html_e('weakly linked items', 'help4-builder-suite'); ?></span>
                </div>
            </div>

            <h2><?php esc_html_e('Indexing and Discovery Health', 'help4-builder-suite'); ?></h2>
            <p><?php esc_html_e('Inspect sitemap responses, metadata coverage, duplicates, schema coverage, and the opt-in IndexNow delivery queue. These checks do not use Google\'s retired sitemap ping or its restricted Indexing API.', 'help4-builder-suite'); ?></p>
            <div style="display:flex;flex-wrap:wrap;gap:.75rem;margin:0 0 1rem;">
                <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                    <?php wp_nonce_field('h4bs_run_seo_health', 'h4bs_seo_health_nonce'); ?>
                    <input type="hidden" name="action" value="h4bs_run_seo_health">
                    <button type="submit" class="button button-primary"><?php esc_html_e('Refresh SEO Health', 'help4-builder-suite'); ?></button>
                </form>
                <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                    <?php wp_nonce_field('h4bs_indexnow_full_index', 'h4bs_indexnow_full_index_nonce'); ?>
                    <input type="hidden" name="action" value="h4bs_indexnow_full_index">
                    <button type="submit" class="button"><?php esc_html_e('Queue Full IndexNow Sweep', 'help4-builder-suite'); ?></button>
                </form>
            </div>
            <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem;margin:1rem 0 1.5rem;">
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                    <strong><?php esc_html_e('Last IndexNow response', 'help4-builder-suite'); ?></strong>
                    <p style="margin:.5rem 0 0;"><?php echo esc_html($this->indexNowResultSummary($lastIndexNow)); ?></p>
                </div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                    <strong><?php esc_html_e('Full-index sweep', 'help4-builder-suite'); ?></strong>
                    <p style="margin:.5rem 0 0;"><?php echo esc_html($this->fullIndexSummary($fullIndex)); ?></p>
                </div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                    <strong><?php esc_html_e('Metadata findings', 'help4-builder-suite'); ?></strong>
                    <p style="margin:.5rem 0 0;"><?php echo esc_html($this->metadataHealthSummary($healthReport)); ?></p>
                </div>
            </div>
            <?php $sitemapRows = is_array($healthReport['sitemaps'] ?? null) ? $healthReport['sitemaps'] : []; ?>
            <?php if ($sitemapRows !== []) : ?>
                <table class="widefat striped" style="margin-bottom:1.5rem;">
                    <thead><tr><th><?php esc_html_e('Sitemap', 'help4-builder-suite'); ?></th><th><?php esc_html_e('HTTP', 'help4-builder-suite'); ?></th><th><?php esc_html_e('URLs detected', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Checked', 'help4-builder-suite'); ?></th></tr></thead>
                    <tbody>
                        <?php foreach ($sitemapRows as $sitemapRow) : ?>
                            <tr>
                                <td><a href="<?php echo esc_url((string) ($sitemapRow['url'] ?? '')); ?>" target="_blank" rel="noopener"><?php echo esc_html((string) ($sitemapRow['label'] ?? $sitemapRow['url'] ?? '')); ?></a></td>
                                <td><strong><?php echo esc_html((string) ($sitemapRow['status'] ?? 0)); ?></strong></td>
                                <td><?php echo esc_html((string) ($sitemapRow['url_count'] ?? 0)); ?></td>
                                <td><?php echo esc_html((string) ($healthReport['checked_at'] ?? '')); ?></td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            <?php endif; ?>
            <?php $schemaCoverage = is_array($healthReport['schema_coverage'] ?? null) ? $healthReport['schema_coverage'] : []; ?>
            <?php if ($schemaCoverage !== []) : ?>
                <h3><?php esc_html_e('Structured Data Coverage by Post Type', 'help4-builder-suite'); ?></h3>
                <table class="widefat striped" style="margin-bottom:1.5rem;">
                    <thead><tr><th><?php esc_html_e('Post type', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Effective schema types', 'help4-builder-suite'); ?></th></tr></thead>
                    <tbody>
                        <?php foreach ($schemaCoverage as $postType => $coverage) : ?>
                            <?php
                            $coverageParts = [];
                            foreach (is_array($coverage) ? $coverage : [] as $schemaName => $schemaCount) {
                                $coverageParts[] = sanitize_text_field((string) $schemaName) . ': ' . absint($schemaCount);
                            }
                            ?>
                            <tr><td><code><?php echo esc_html((string) $postType); ?></code></td><td><?php echo esc_html(implode(', ', $coverageParts)); ?></td></tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            <?php endif; ?>

            <h2><?php esc_html_e('Internal Link Coverage', 'help4-builder-suite'); ?></h2>
            <p><?php esc_html_e('This bounded report counts contextual links in published content and saved Builder layouts. Global navigation may provide another path, so zero-inbound results are review candidates rather than automatic errors.', 'help4-builder-suite'); ?></p>
            <?php if (is_multisite()) : ?>
                <p class="description"><?php esc_html_e('Results are scoped to this site. Use Help4 Network Sites for network-wide indexing and sitemap status, then improve contextual links inside each site separately.', 'help4-builder-suite'); ?></p>
            <?php endif; ?>
            <?php $internalLinkRows = is_array($internalLinkGraph['priority_items'] ?? null) ? $internalLinkGraph['priority_items'] : []; ?>
            <?php if ($internalLinkRows === []) : ?>
                <div class="notice notice-success inline"><p><?php esc_html_e('No content-orphan or weak-link candidates were found in the scanned items.', 'help4-builder-suite'); ?></p></div>
            <?php else : ?>
                <table class="widefat striped" style="margin-bottom:1.5rem;">
                    <thead><tr><th><?php esc_html_e('Content', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Status', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Inbound', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Outbound', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Actions', 'help4-builder-suite'); ?></th></tr></thead>
                    <tbody>
                        <?php foreach ($internalLinkRows as $internalLinkRow) : ?>
                            <?php if (! is_array($internalLinkRow)) { continue; } ?>
                            <tr>
                                <td><strong><?php echo esc_html((string) ($internalLinkRow['title'] ?? '')); ?></strong><br><span class="description"><?php echo esc_html((string) ($internalLinkRow['type'] ?? '')); ?></span></td>
                                <td><?php echo esc_html((string) ($internalLinkRow['status_label'] ?? '')); ?></td>
                                <td><?php echo esc_html((string) ($internalLinkRow['inbound'] ?? 0)); ?></td>
                                <td><?php echo esc_html((string) ($internalLinkRow['outbound'] ?? 0)); ?></td>
                                <td>
                                    <?php if ((string) ($internalLinkRow['edit_url'] ?? '') !== '') : ?><a href="<?php echo esc_url((string) $internalLinkRow['edit_url']); ?>"><?php esc_html_e('Edit', 'help4-builder-suite'); ?></a> · <?php endif; ?>
                                    <a href="<?php echo esc_url((string) ($internalLinkRow['url'] ?? '')); ?>" target="_blank" rel="noopener"><?php esc_html_e('View', 'help4-builder-suite'); ?></a>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            <?php endif; ?>

            <?php $this->seoMigration->renderPanel(); ?>
            <?php $this->linkHealthCrawler->renderPanel(); ?>

            <h2><?php esc_html_e('International Search and Sharing Coverage', 'help4-builder-suite'); ?></h2>
            <p><?php esc_html_e('This report maps the standards Help4 emits to international search engines, social networks, and messaging previews. Verification tokens are optional ownership tools and do not lower content quality scores when omitted.', 'help4-builder-suite'); ?></p>
            <?php foreach ([
                'standards' => __('Discovery standards', 'help4-builder-suite'),
                'search' => __('Search engine reach', 'help4-builder-suite'),
                'sharing' => __('Social and messaging previews', 'help4-builder-suite'),
            ] as $coverageGroup => $coverageTitle) : ?>
                <?php $coverageRows = is_array($discoveryReport[$coverageGroup] ?? null) ? $discoveryReport[$coverageGroup] : []; ?>
                <h3><?php echo esc_html((string) $coverageTitle); ?></h3>
                <table class="widefat striped" style="margin-bottom:1.5rem;">
                    <thead><tr><th><?php esc_html_e('Standard / platform', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Status', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Markets / consumers', 'help4-builder-suite'); ?></th><th><?php esc_html_e('How Help4 covers it', 'help4-builder-suite'); ?></th></tr></thead>
                    <tbody>
                        <?php foreach ($coverageRows as $coverageRow) : ?>
                            <?php if (! is_array($coverageRow)) { continue; } ?>
                            <tr>
                                <td><strong><?php echo esc_html((string) ($coverageRow['label'] ?? '')); ?></strong></td>
                                <td>
                                    <strong><?php echo ! empty($coverageRow['ready']) ? esc_html__('Ready', 'help4-builder-suite') : esc_html__('Needs setup', 'help4-builder-suite'); ?></strong>
                                    <?php if (array_key_exists('verification_configured', $coverageRow) && $coverageRow['verification_configured'] !== null) : ?>
                                        <br><span class="description"><?php echo ! empty($coverageRow['verification_configured']) ? esc_html__('Ownership verified in settings', 'help4-builder-suite') : esc_html__('Optional verification not configured', 'help4-builder-suite'); ?></span>
                                    <?php endif; ?>
                                </td>
                                <td><?php echo esc_html((string) ($coverageRow['reach'] ?? '')); ?></td>
                                <td><?php echo esc_html((string) ($coverageRow['coverage'] ?? '')); ?></td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            <?php endforeach; ?>

            <h2><?php esc_html_e('Commerce Search Readiness', 'help4-builder-suite'); ?></h2>
            <p><?php esc_html_e('This bounded catalog sample checks the data that product search, shopping feeds, social previews, and comparison surfaces need. It reports completeness without claiming that a score guarantees placement.', 'help4-builder-suite'); ?></p>
            <?php $commerceCounts = is_array($commerceAudit['counts'] ?? null) ? $commerceAudit['counts'] : []; ?>
            <?php $commerceCoverage = is_array($commerceAudit['coverage'] ?? null) ? $commerceAudit['coverage'] : []; ?>
            <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin:1rem 0 1.5rem;">
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;"><strong style="display:block;font-size:1.5rem;"><?php echo esc_html((string) ($commerceAudit['score'] ?? 0)); ?>/100</strong><span><?php esc_html_e('catalog discovery score', 'help4-builder-suite'); ?></span></div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;"><strong style="display:block;font-size:1.5rem;"><?php echo esc_html((string) ($commerceCounts['sampled'] ?? 0)); ?></strong><span><?php esc_html_e('active products sampled', 'help4-builder-suite'); ?></span></div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;"><strong style="display:block;font-size:1.5rem;"><?php echo esc_html((string) ($commerceCoverage['feed_eligible'] ?? 0)); ?>%</strong><span><?php esc_html_e('feed-ready products', 'help4-builder-suite'); ?></span></div>
                <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;"><strong style="display:block;font-size:1rem;overflow-wrap:anywhere;"><?php echo ! empty($commerceAudit['feed_enabled']) ? esc_html((string) ($commerceAudit['feed_url'] ?? '')) : esc_html__('Disabled', 'help4-builder-suite'); ?></strong><span><?php esc_html_e('merchant discovery feed', 'help4-builder-suite'); ?></span></div>
            </div>
            <?php if ((int) ($commerceCounts['sampled'] ?? 0) > 0) : ?>
                <table class="widefat striped">
                    <thead><tr><th><?php esc_html_e('Catalog signal', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Coverage', 'help4-builder-suite'); ?></th><th><?php esc_html_e('What complete means', 'help4-builder-suite'); ?></th></tr></thead>
                    <tbody>
                        <?php foreach ([
                            'description' => [__('Descriptions', 'help4-builder-suite'), __('Original, factual product copy.', 'help4-builder-suite')],
                            'image' => [__('Primary images', 'help4-builder-suite'), __('A crawlable representative image.', 'help4-builder-suite')],
                            'landing_url' => [__('Landing URLs', 'help4-builder-suite'), __('A stable product URL showing matching data.', 'help4-builder-suite')],
                            'identifier' => [__('Identifiers', 'help4-builder-suite'), __('GTIN when assigned, otherwise truthful MPN/SKU identity.', 'help4-builder-suite')],
                            'brand' => [__('Brands', 'help4-builder-suite'), __('A product brand or truthful default brand.', 'help4-builder-suite')],
                            'price_currency' => [__('Price and currency', 'help4-builder-suite'), __('A positive price with ISO currency.', 'help4-builder-suite')],
                            'availability' => [__('Availability', 'help4-builder-suite'), __('Current stock, backorder, or preorder state.', 'help4-builder-suite')],
                            'variants' => [__('Variant identity', 'help4-builder-suite'), __('Every variant has a SKU and differentiating attributes.', 'help4-builder-suite')],
                        ] as $coverageKey => $coverageCopy) : ?>
                            <tr><td><strong><?php echo esc_html((string) $coverageCopy[0]); ?></strong></td><td><?php echo esc_html((string) ($commerceCoverage[$coverageKey] ?? 0)); ?>%</td><td><?php echo esc_html((string) $coverageCopy[1]); ?></td></tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            <?php endif; ?>

            <h2><?php esc_html_e('SEO Score Playbook', 'help4-builder-suite'); ?></h2>
            <p><?php esc_html_e('Use this plain-English guide when a page score drops. The audit is local to WordPress and is meant to show the next useful edit, not pretend a score alone guarantees rankings, Analytics setup, or ad approval.', 'help4-builder-suite'); ?></p>
            <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem;margin:1rem 0 1.5rem;">
                <?php foreach ($this->scorePlaybookCards() as $card) : ?>
                    <div style="border:1px solid #dcdcde;background:#fff;border-radius:8px;padding:1rem;">
                        <strong style="display:block;margin-bottom:.35rem;"><?php echo esc_html((string) ($card['title'] ?? '')); ?></strong>
                        <p style="margin:.25rem 0;"><?php echo esc_html((string) ($card['meaning'] ?? '')); ?></p>
                        <p style="margin:.5rem 0 0;"><span class="description"><?php echo esc_html((string) ($card['fix'] ?? '')); ?></span></p>
                    </div>
                <?php endforeach; ?>
            </div>

            <h2><?php esc_html_e('Content SEO Audit', 'help4-builder-suite'); ?></h2>
            <table class="widefat striped">
                <thead>
                    <tr>
                        <th><?php esc_html_e('Content', 'help4-builder-suite'); ?></th>
                        <th><?php esc_html_e('Score', 'help4-builder-suite'); ?></th>
                        <th><?php esc_html_e('Readability', 'help4-builder-suite'); ?></th>
                        <th><?php esc_html_e('Analytics / Ads', 'help4-builder-suite'); ?></th>
                        <th><?php esc_html_e('Intent', 'help4-builder-suite'); ?></th>
                        <th><?php esc_html_e('Schema', 'help4-builder-suite'); ?></th>
                        <th><?php esc_html_e('Robots', 'help4-builder-suite'); ?></th>
                        <th><?php esc_html_e('Checks', 'help4-builder-suite'); ?></th>
                        <th><?php esc_html_e('Updated', 'help4-builder-suite'); ?></th>
                    </tr>
                </thead>
                <tbody>
                    <?php if ($rows === []) : ?>
                        <tr><td colspan="9"><?php esc_html_e('No public content found yet.', 'help4-builder-suite'); ?></td></tr>
                    <?php endif; ?>
                    <?php foreach ($rows as $row) : ?>
                        <?php
                        $editUrl = get_edit_post_link((int) ($row['id'] ?? 0), '');
                        $viewUrl = get_permalink((int) ($row['id'] ?? 0));
                        $warnings = is_array($row['warnings'] ?? null) ? $row['warnings'] : [];
                        ?>
                        <tr>
                            <td>
                                <strong><?php echo esc_html((string) ($row['title'] ?? '')); ?></strong><br>
                                <span><?php echo esc_html((string) ($row['type'] ?? '')); ?></span>
                                <?php if (is_string($editUrl) && $editUrl !== '') : ?>
                                    <br><a href="<?php echo esc_url($editUrl); ?>"><?php esc_html_e('Edit SEO', 'help4-builder-suite'); ?></a>
                                <?php endif; ?>
                                <?php if (is_string($viewUrl) && $viewUrl !== '') : ?>
                                    <span> | </span><a href="<?php echo esc_url($viewUrl); ?>" target="_blank" rel="noopener"><?php esc_html_e('View', 'help4-builder-suite'); ?></a>
                                <?php endif; ?>
                            </td>
                            <td><strong><?php echo esc_html((string) ($row['score'] ?? 0)); ?>/100</strong><br><?php echo esc_html((string) ($row['label'] ?? '')); ?></td>
                            <td>
                                <strong><?php echo esc_html((string) ($row['readability_score'] ?? 0)); ?>/100</strong><br>
                                <?php echo esc_html((string) ($row['readability_label'] ?? '')); ?><br>
                                <span><?php echo esc_html((string) ($row['readability_summary'] ?? '')); ?></span>
                            </td>
                            <td>
                                <strong><?php esc_html_e('GA', 'help4-builder-suite'); ?> <?php echo esc_html((string) ($row['analytics_score'] ?? 0)); ?></strong>
                                <span> / </span>
                                <strong><?php esc_html_e('Ads', 'help4-builder-suite'); ?> <?php echo esc_html((string) ($row['adsense_score'] ?? 0)); ?></strong><br>
                                <span><?php echo esc_html((string) ($row['conversion_goal'] ?? '')); ?></span>
                            </td>
                            <td>
                                <strong><?php echo esc_html((string) ($row['intent_profile_label'] ?? __('General website', 'help4-builder-suite'))); ?></strong><br>
                                <span><?php echo esc_html((string) ($row['intent_score'] ?? 0)); ?>/100</span>
                                <?php if ((string) ($row['keyword'] ?? '') !== '') : ?><br><span><?php echo esc_html((string) $row['keyword']); ?></span><?php endif; ?>
                            </td>
                            <td><?php echo esc_html((string) ($row['schema'] ?? '')); ?></td>
                            <td><?php echo esc_html((string) ($row['robots'] ?? '')); ?></td>
                            <td>
                                <?php if ($warnings === []) : ?>
                                    <?php esc_html_e('Looks solid', 'help4-builder-suite'); ?>
                                <?php else : ?>
                                    <ul style="margin:.25rem 0 0 1rem;list-style:disc;">
                                        <?php foreach (array_slice($warnings, 0, 4) as $warning) : ?>
                                            <li><?php echo esc_html((string) $warning); ?></li>
                                        <?php endforeach; ?>
                                    </ul>
                                <?php endif; ?>
                            </td>
                            <td><?php echo esc_html((string) ($row['modified'] ?? '')); ?></td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>

            <h2 style="margin-top:2rem;"><?php esc_html_e('Image SEO Audit', 'help4-builder-suite'); ?></h2>
            <p><?php esc_html_e('Find media-library images that need alt text before they quietly weaken accessibility and image search relevance.', 'help4-builder-suite'); ?></p>
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="margin:0 0 1rem;">
                <?php wp_nonce_field('h4bs_generate_missing_image_alt', 'h4bs_image_alt_nonce'); ?>
                <input type="hidden" name="action" value="h4bs_generate_missing_image_alt">
                <button type="submit" class="button button-primary"><?php esc_html_e('Generate Missing Alt Text', 'help4-builder-suite'); ?></button>
            </form>
            <table class="widefat striped">
                <thead>
                    <tr>
                        <th><?php esc_html_e('Image', 'help4-builder-suite'); ?></th>
                        <th><?php esc_html_e('Alt Text', 'help4-builder-suite'); ?></th>
                        <th><?php esc_html_e('Attached To', 'help4-builder-suite'); ?></th>
                        <th><?php esc_html_e('Size', 'help4-builder-suite'); ?></th>
                        <th><?php esc_html_e('Action', 'help4-builder-suite'); ?></th>
                    </tr>
                </thead>
                <tbody>
                    <?php if ($imageRows === []) : ?>
                        <tr><td colspan="5"><?php esc_html_e('No image attachments found yet.', 'help4-builder-suite'); ?></td></tr>
                    <?php endif; ?>
                    <?php foreach ($imageRows as $imageRow) : ?>
                        <?php $editMediaUrl = get_edit_post_link((int) ($imageRow['id'] ?? 0), ''); ?>
                        <tr>
                            <td>
                                <?php echo wp_kses_post((string) ($imageRow['thumb'] ?? '')); ?>
                                <br><strong><?php echo esc_html((string) ($imageRow['title'] ?? '')); ?></strong>
                                <br><code><?php echo esc_html((string) ($imageRow['file'] ?? '')); ?></code>
                            </td>
                            <td>
                                <?php if (! empty($imageRow['alt'])) : ?>
                                    <?php echo esc_html((string) $imageRow['alt']); ?>
                                <?php else : ?>
                                    <strong><?php esc_html_e('Missing', 'help4-builder-suite'); ?></strong>
                                <?php endif; ?>
                            </td>
                            <td><?php echo esc_html((string) ($imageRow['parent'] ?? '')); ?></td>
                            <td><?php echo esc_html((string) ($imageRow['size'] ?? '')); ?></td>
                            <td>
                                <?php if (is_string($editMediaUrl) && $editMediaUrl !== '') : ?>
                                    <a class="button button-small" href="<?php echo esc_url($editMediaUrl); ?>"><?php esc_html_e('Edit Media', 'help4-builder-suite'); ?></a>
                                <?php endif; ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>

            <h2><?php esc_html_e('Breadcrumb Shortcode', 'help4-builder-suite'); ?></h2>
            <p><?php esc_html_e('Use [h4_breadcrumbs] in templates or pages to show a crawlable breadcrumb trail. The plugin also emits BreadcrumbList schema on singular content.', 'help4-builder-suite'); ?></p>
        </div>
        <?php
    }

    public function handleGenerateMissingImageAlt(): void
    {
        if (! current_user_can('manage_options')) {
            wp_die(esc_html__('Unauthorized request.', 'help4-builder-suite'));
        }
        check_admin_referer('h4bs_generate_missing_image_alt', 'h4bs_image_alt_nonce');

        $attachments = get_posts([
            'post_type' => 'attachment',
            'post_status' => 'inherit',
            'post_mime_type' => 'image',
            'posts_per_page' => 300,
            'orderby' => 'date',
            'order' => 'DESC',
        ]);

        $updated = 0;
        foreach ($attachments as $attachment) {
            if (! $attachment instanceof WP_Post) {
                continue;
            }
            $existing = trim((string) get_post_meta($attachment->ID, '_wp_attachment_image_alt', true));
            if ($existing !== '') {
                continue;
            }
            $alt = $this->generateAttachmentAltText($attachment);
            if ($alt === '') {
                continue;
            }
            update_post_meta($attachment->ID, '_wp_attachment_image_alt', $alt);
            $updated++;
        }

        wp_safe_redirect(
            add_query_arg(
                [
                    'page' => 'h4bs-seo-control',
                    'h4bs_alt_generated' => $updated,
                ],
                admin_url('admin.php')
            )
        );
        exit;
    }

    public function handleRunSeoHealth(): void
    {
        if (! current_user_can('manage_options')) {
            wp_die(esc_html__('Unauthorized request.', 'help4-builder-suite'));
        }
        check_admin_referer('h4bs_run_seo_health', 'h4bs_seo_health_nonce');

        update_option('h4bs_seo_health_report', $this->buildSeoHealthReport(), false);
        wp_safe_redirect(add_query_arg([
            'page' => 'h4bs-seo-control',
            'h4bs_health_checked' => 1,
        ], admin_url('admin.php')));
        exit;
    }

    /**
     * Preserve HTTP 200 for populated core sitemap pages whose empty main query made
     * WordPress mark the request as a paginated 404 before the sitemap provider runs.
     *
     * @param mixed $preempt
     * @param mixed $wpQuery
     * @return mixed
     */
    public function repairSitemapHttpStatus($preempt, $wpQuery)
    {
        if ($preempt !== false || ! $this->isCoreSitemapEnabled()) {
            return $preempt;
        }

        $providerName = sanitize_key((string) get_query_var('sitemap'));
        $subtype = sanitize_key((string) get_query_var('sitemap-subtype'));
        $page = max(1, absint(get_query_var('paged')));
        if ($providerName === '' || $providerName === 'index' || ! function_exists('wp_sitemaps_get_server')) {
            return $preempt;
        }

        $server = wp_sitemaps_get_server();
        $provider = $server->registry->get_provider($providerName);
        if (! $provider) {
            return $preempt;
        }
        $urls = $provider->get_url_list($page, $subtype);
        if (! is_array($urls) || $urls === []) {
            return $preempt;
        }

        if (is_object($wpQuery)) {
            $wpQuery->is_404 = false;
        }
        status_header(200);
        return true;
    }

    /**
     * @return array<string, mixed>
     */
    public function buildSeoHealthReport(): array
    {
        $sitemapTargets = [
            ['label' => __('Core sitemap index', 'help4-builder-suite'), 'url' => home_url('/wp-sitemap.xml')],
        ];
        if ($this->isCoreSitemapEnabled() && function_exists('wp_sitemaps_get_server')) {
            $server = wp_sitemaps_get_server();
            foreach (array_slice($server->index->get_sitemap_list(), 0, 40) as $entry) {
                $url = is_array($entry) ? $this->sanitizeHttpUrl((string) ($entry['loc'] ?? '')) : '';
                if ($url !== '') {
                    $sitemapTargets[] = ['label' => basename((string) wp_parse_url($url, PHP_URL_PATH)), 'url' => $url];
                }
            }
        }
        $sitemapTargets = apply_filters('h4bs_seo_health_sitemap_targets', $sitemapTargets);
        $sitemapRows = [];
        if (is_array($sitemapTargets)) {
            foreach (array_slice($sitemapTargets, 0, 50) as $target) {
                if (! is_array($target)) {
                    continue;
                }
                $url = $this->sanitizeHttpUrl((string) ($target['url'] ?? ''));
                if ($url === '') {
                    continue;
                }
                $response = wp_safe_remote_get($url, [
                    'timeout' => 10,
                    'redirection' => 2,
                    'limit_response_size' => 1024 * 1024,
                    'user-agent' => 'Help4BuilderSuite-SEO-Health/' . H4BS_VERSION,
                ]);
                $body = is_wp_error($response) ? '' : (string) wp_remote_retrieve_body($response);
                $sitemapRows[] = [
                    'label' => sanitize_text_field((string) ($target['label'] ?? $url)),
                    'url' => $url,
                    'status' => is_wp_error($response) ? 0 : (int) wp_remote_retrieve_response_code($response),
                    'url_count' => substr_count($body, '<loc>'),
                    'error' => is_wp_error($response) ? $response->get_error_message() : '',
                ];
            }
        }

        $postTypes = get_post_types(['public' => true], 'names');
        unset($postTypes['attachment']);
        $posts = get_posts([
            'post_type' => array_values($postTypes),
            'post_status' => 'publish',
            'posts_per_page' => 500,
            'orderby' => 'ID',
            'order' => 'ASC',
            'no_found_rows' => true,
        ]);
        $settings = Settings::all();
        $missing = ['title' => 0, 'description' => 0, 'canonical' => 0, 'social_image' => 0];
        $titles = [];
        $descriptions = [];
        $schemaCoverage = [];
        foreach ($posts as $post) {
            if (! $post instanceof WP_Post) {
                continue;
            }
            $title = trim((string) get_post_meta($post->ID, '_h4_seo_title', true));
            $title = $title !== '' ? $title : trim(get_the_title($post));
            $description = trim((string) get_post_meta($post->ID, '_h4_seo_description', true));
            $description = $description !== '' ? $description : $this->normalizeMetaDescription(wp_strip_all_tags((string) get_the_excerpt($post)));
            $canonical = trim((string) get_post_meta($post->ID, '_h4_seo_canonical', true));
            $canonical = $canonical !== '' ? $this->sanitizeHttpUrl($canonical) : $this->sanitizeHttpUrl((string) get_permalink($post));
            $socialImage = $this->primarySocialImageUrlForPost($post->ID, $settings);
            if ($title === '') {
                $missing['title']++;
            } else {
                $titles[strtolower($title)][] = $post->ID;
            }
            if ($description === '') {
                $missing['description']++;
            } else {
                $descriptions[strtolower($description)][] = $post->ID;
            }
            if ($canonical === '') {
                $missing['canonical']++;
            }
            if ($socialImage === '') {
                $missing['social_image']++;
            }
            $schemaKey = sanitize_key((string) get_post_meta($post->ID, '_h4_seo_schema_type', true));
            $schemaKey = isset($this->schemaTypeLabels()[$schemaKey]) ? $schemaKey : ($post->post_type === 'post' ? 'article' : 'webpage');
            if (! isset($schemaCoverage[$post->post_type])) {
                $schemaCoverage[$post->post_type] = [];
            }
            $schemaName = $this->schemaTypeName($schemaKey);
            $schemaCoverage[$post->post_type][$schemaName] = absint($schemaCoverage[$post->post_type][$schemaName] ?? 0) + 1;
        }
        $duplicateTitles = count(array_filter($titles, static fn (array $ids): bool => count($ids) > 1));
        $duplicateDescriptions = count(array_filter($descriptions, static fn (array $ids): bool => count($ids) > 1));

        $report = [
            'checked_at' => gmdate('c'),
            'scanned' => count($posts),
            'sitemaps' => $sitemapRows,
            'missing' => $missing,
            'duplicate_titles' => $duplicateTitles,
            'duplicate_descriptions' => $duplicateDescriptions,
            'schema_coverage' => $schemaCoverage,
        ];
        $report['international_discovery'] = $this->discoveryStandards->coverageReport($settings, $report);
        return $report;
    }

    public function renderMetaBox(WP_Post $post): void
    {
        wp_nonce_field('h4bs_seo_meta', 'h4bs_seo_meta_nonce');
        ?>
        <style>
            #h4bs-seo-meta .inside,
            #h4bs-seo-meta .inside * {
                box-sizing: border-box;
            }
            #h4bs-seo-meta .inside {
                max-width: 100%;
                width: 100%;
            }
            #h4bs-seo-meta .inside > div,
            #h4bs-seo-meta section,
            #h4bs-seo-meta input,
            #h4bs-seo-meta select,
            #h4bs-seo-meta textarea {
                max-width: 100%;
                min-width: 0;
            }
        </style>
        <?php

        $title             = (string) get_post_meta($post->ID, '_h4_seo_title', true);
        $description       = (string) get_post_meta($post->ID, '_h4_seo_description', true);
        $canonical         = (string) get_post_meta($post->ID, '_h4_seo_canonical', true);
        $focusKeyword      = (string) get_post_meta($post->ID, '_h4_seo_focus_keyword', true);
        $relatedKeywords   = get_post_meta($post->ID, '_h4_seo_related_keywords', true);
        $relatedKeywords   = is_array($relatedKeywords) ? $relatedKeywords : [];
        $socialTitle       = (string) get_post_meta($post->ID, '_h4_seo_social_title', true);
        $socialDescription = (string) get_post_meta($post->ID, '_h4_seo_social_description', true);
        $socialImageUrl    = (string) get_post_meta($post->ID, '_h4_seo_social_image_url', true);
        $socialImageAlt    = (string) get_post_meta($post->ID, '_h4_seo_social_image_alt', true);
        $twitterCreator    = (string) get_post_meta($post->ID, '_h4_seo_twitter_creator', true);
        $hreflangText      = $this->discoveryStandards->formatHreflangAlternates(
            (array) get_post_meta($post->ID, '_h4_seo_hreflang', true)
        );
        $openGraphType     = $this->sanitizeOpenGraphType((string) get_post_meta($post->ID, '_h4_seo_og_type', true));
        $schemaType        = sanitize_key((string) get_post_meta($post->ID, '_h4_seo_schema_type', true));
        $noindex           = (string) get_post_meta($post->ID, '_h4_seo_noindex', true);
        $nofollow          = (string) get_post_meta($post->ID, '_h4_seo_nofollow', true);
        $noarchive         = (string) get_post_meta($post->ID, '_h4_seo_noarchive', true);
        $nosnippet         = (string) get_post_meta($post->ID, '_h4_seo_nosnippet', true);
        $maxImagePreview   = sanitize_key((string) get_post_meta($post->ID, '_h4_seo_max_image_preview', true));
        $ogImageId         = absint(get_post_meta($post->ID, '_h4_seo_og_image_id', true));
        $conversionGoal    = sanitize_key((string) get_post_meta($post->ID, '_h4_seo_conversion_goal', true));
        $analyticsEvent    = sanitize_key((string) get_post_meta($post->ID, '_h4_seo_analytics_event', true));
        $primaryCta        = (string) get_post_meta($post->ID, '_h4_seo_primary_cta', true);
        $faqText           = $this->formatFaqItemsForTextarea((string) get_post_meta($post->ID, '_h4_seo_faq_items', true));
        $realEstate        = $this->realEstateSchema->forPost($post->ID);
        $performingArts    = $this->performingArtsSchema->forPost($post->ID);
        $commerceProductId = absint(get_post_meta($post->ID, '_h4_seo_commerce_product_id', true));
        $analysis          = $this->analyzePost($post);
        $schemaOptions     = $this->schemaTypeLabels();
        $goalOptions       = $this->conversionGoalLabels();
        $intentOptions     = $this->intentScorer->profileCatalog();
        $intentProfile     = sanitize_key((string) get_post_meta($post->ID, '_h4_seo_intent_profile', true));
        $scores            = is_array($analysis['scores'] ?? null) ? $analysis['scores'] : [];
        $metrics           = is_array($analysis['metrics'] ?? null) ? $analysis['metrics'] : [];
        $actionPlan        = array_slice(array_merge(
            is_array($analysis['intent_actions'] ?? null) ? $analysis['intent_actions'] : [],
            $this->scoreActionPlan($scores)
        ), 0, 4);
        $linkSuggestions   = $this->internalLinkSuggestions($post);
        if (! isset($schemaOptions[$schemaType])) {
            $schemaType = $post->post_type === 'post' ? 'article' : 'webpage';
        }
        if (! isset($goalOptions[$conversionGoal])) {
            $conversionGoal = '';
        }
        if (! in_array($maxImagePreview, ['large', 'standard', 'none'], true)) {
            $maxImagePreview = 'large';
        }
        $suggestedAnalyticsEvent = $analyticsEvent !== '' ? $analyticsEvent : $this->suggestAnalyticsEventName($post, $conversionGoal);
        ?>
        <div style="display:grid;gap:1rem;max-width:980px;">
            <section style="border:1px solid #dcdcde;border-radius:8px;padding:1rem;background:#fff;">
                <h3 style="margin:0 0 .5rem;"><?php esc_html_e('SEO Health', 'help4-builder-suite'); ?></h3>
                <p style="margin:.25rem 0 .75rem;">
                    <strong><?php echo esc_html((string) ($analysis['score'] ?? 0)); ?>/100</strong>
                    <?php echo esc_html((string) ($analysis['label'] ?? __('Needs review', 'help4-builder-suite'))); ?>
                </p>
                <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin:.75rem 0;">
                    <?php foreach ([
                        'search' => __('Search', 'help4-builder-suite'),
                        'readability' => __('Readability', 'help4-builder-suite'),
                        'analytics' => __('Analytics-ready', 'help4-builder-suite'),
                        'adsense' => __('AdSense quality', 'help4-builder-suite'),
                        'intent' => (string) ($analysis['intent_profile_label'] ?? __('Intent fit', 'help4-builder-suite')),
                    ] as $scoreKey => $scoreLabel) : ?>
                        <?php $bucketScore = isset($scores[$scoreKey]) ? (int) $scores[$scoreKey] : 0; ?>
                        <div style="border:1px solid #dcdcde;border-radius:8px;padding:.75rem;background:#f6f7f7;">
                            <strong style="display:block;font-size:1.1rem;"><?php echo esc_html((string) $bucketScore); ?>/100</strong>
                            <span><?php echo esc_html((string) $scoreLabel); ?></span><br>
                            <small><?php echo esc_html($this->scoreLabel($bucketScore)); ?></small>
                        </div>
                    <?php endforeach; ?>
                </div>
                <p class="description" style="margin:.25rem 0 .75rem;"><?php echo esc_html((string) ($analysis['intent_profile_description'] ?? '')); ?> <?php esc_html_e('The profile changes formula weights so irrelevant checks do not distort the result. It does not send data to search engines or guarantee rankings, analytics setup, shopping placement, or ad approval.', 'help4-builder-suite'); ?></p>
                <?php $scoreFormula = is_array($analysis['score_formula'] ?? null) ? $analysis['score_formula'] : []; ?>
                <?php if ($scoreFormula !== []) : ?>
                    <p class="description" style="margin:.25rem 0 .75rem;"><strong><?php esc_html_e('Formula:', 'help4-builder-suite'); ?></strong>
                        <?php echo esc_html(implode(' + ', array_map(
                            static fn (string $key, mixed $weight): string => ucfirst($key) . ' ' . (int) $weight . '%',
                            array_keys($scoreFormula),
                            array_values($scoreFormula)
                        ))); ?>
                    </p>
                <?php endif; ?>
                <?php if ($actionPlan !== []) : ?>
                    <div style="border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7;padding:.75rem;margin:.75rem 0;">
                        <h4 style="margin:0 0 .5rem;"><?php esc_html_e('Fix Next', 'help4-builder-suite'); ?></h4>
                        <ol style="margin:.25rem 0 .25rem 1.25rem;">
                            <?php foreach ($actionPlan as $action) : ?>
                                <li>
                                    <strong><?php echo esc_html((string) ($action['title'] ?? '')); ?>:</strong>
                                    <?php echo esc_html((string) ($action['fix'] ?? '')); ?>
                                </li>
                            <?php endforeach; ?>
                        </ol>
                    </div>
                <?php endif; ?>
                <?php if (! empty($analysis['warnings']) && is_array($analysis['warnings'])) : ?>
                    <ul style="margin:.5rem 0 0 1rem;list-style:disc;">
                        <?php foreach ($analysis['warnings'] as $warning) : ?>
                            <li><?php echo esc_html((string) $warning); ?></li>
                        <?php endforeach; ?>
                    </ul>
                <?php else : ?>
                    <p style="margin:0;"><?php esc_html_e('The basics are in place. Keep the page useful, specific, fast, and easy to understand.', 'help4-builder-suite'); ?></p>
                <?php endif; ?>
                <?php $intentChecks = is_array($analysis['intent_checks'] ?? null) ? $analysis['intent_checks'] : []; ?>
                <?php if ($intentChecks !== []) : ?>
                    <details style="margin-top:.75rem;">
                        <summary><strong><?php esc_html_e('View intent-specific checklist', 'help4-builder-suite'); ?></strong></summary>
                        <table class="widefat striped" style="margin-top:.75rem;">
                            <thead><tr><th><?php esc_html_e('Signal', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Points', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Next action', 'help4-builder-suite'); ?></th></tr></thead>
                            <tbody><?php foreach ($intentChecks as $intentCheck) : ?><tr><td><?php echo esc_html((string) ($intentCheck['label'] ?? '')); ?></td><td><?php echo esc_html((string) ($intentCheck['earned'] ?? 0)); ?>/<?php echo esc_html((string) ($intentCheck['possible'] ?? 0)); ?></td><td><?php echo (string) ($intentCheck['status'] ?? '') === 'pass' ? esc_html__('Complete', 'help4-builder-suite') : esc_html((string) ($intentCheck['fix'] ?? '')); ?></td></tr><?php endforeach; ?></tbody>
                        </table>
                    </details>
                <?php endif; ?>
            </section>
            <section style="border:1px solid #dcdcde;border-radius:8px;padding:1rem;background:#fff;">
                <h3 style="margin:0 0 .75rem;"><?php esc_html_e('Internal Link Opportunities', 'help4-builder-suite'); ?></h3>
                <?php if ($linkSuggestions === []) : ?>
                    <p><?php esc_html_e('No strong unlinked matches were found. Keep links useful and contextual instead of forcing them.', 'help4-builder-suite'); ?></p>
                <?php else : ?>
                    <ul style="margin:.25rem 0 0 1.25rem;list-style:disc;">
                        <?php foreach ($linkSuggestions as $suggestion) : ?>
                            <li><a href="<?php echo esc_url((string) $suggestion['url']); ?>" target="_blank" rel="noopener"><?php echo esc_html((string) $suggestion['title']); ?></a> <span class="description"><?php echo esc_html((string) $suggestion['reason']); ?></span></li>
                        <?php endforeach; ?>
                    </ul>
                <?php endif; ?>
            </section>
            <section style="border:1px solid #dcdcde;border-radius:8px;padding:1rem;background:#fff;">
                <h3 style="margin:0 0 .75rem;"><?php esc_html_e('Search Result', 'help4-builder-suite'); ?></h3>
                <p>
                    <label for="h4_seo_title"><strong><?php esc_html_e('SEO Title', 'help4-builder-suite'); ?></strong></label><br>
                    <input id="h4_seo_title" type="text" name="h4_seo_title" class="widefat" value="<?php echo esc_attr($title); ?>" placeholder="<?php echo esc_attr(get_the_title($post)); ?>">
                    <?php /* translators: %d: Current SEO title character count. */ ?>
                    <span class="description"><?php echo esc_html(sprintf(__('Recommended: 35-65 characters. Current: %d.', 'help4-builder-suite'), $this->textLength($title))); ?></span>
                </p>
                <p>
                    <label for="h4_seo_description"><strong><?php esc_html_e('Meta Description', 'help4-builder-suite'); ?></strong></label><br>
                    <textarea id="h4_seo_description" name="h4_seo_description" class="widefat" rows="4"><?php echo esc_textarea($description); ?></textarea>
                    <?php /* translators: %d: Current meta description character count. */ ?>
                    <span class="description"><?php echo esc_html(sprintf(__('Recommended: 120-165 characters. Current: %d.', 'help4-builder-suite'), $this->textLength($description))); ?></span>
                </p>
                <p>
                    <label for="h4_seo_intent_profile"><strong><?php esc_html_e('SEO Page Profile', 'help4-builder-suite'); ?></strong></label><br>
                    <select id="h4_seo_intent_profile" name="h4_seo_intent_profile">
                        <?php /* translators: %s: Automatically detected SEO page profile label. */ ?>
                        <option value=""><?php echo esc_html(sprintf(__('Automatic (%s)', 'help4-builder-suite'), (string) ($analysis['intent_profile_label'] ?? __('General website', 'help4-builder-suite')))); ?></option>
                        <?php foreach ($intentOptions as $intentKey => $intentData) : ?>
                            <option value="<?php echo esc_attr((string) $intentKey); ?>" <?php selected($intentProfile, (string) $intentKey); ?>><?php echo esc_html((string) ($intentData['label'] ?? $intentKey)); ?></option>
                        <?php endforeach; ?>
                    </select>
                    <span class="description"><?php esc_html_e('Automatic uses page schema, Help4 blocks, post type, language alternates, and the selected business profile. Override only when this page serves a different search intent.', 'help4-builder-suite'); ?></span>
                </p>
                <p>
                    <label for="h4_seo_focus_keyword"><strong><?php esc_html_e('Primary Keyword / Intent', 'help4-builder-suite'); ?></strong></label><br>
                    <input id="h4_seo_focus_keyword" type="text" name="h4_seo_focus_keyword" class="widefat" value="<?php echo esc_attr($focusKeyword); ?>" placeholder="<?php esc_attr_e('managed WordPress hosting, local SEO service page, emergency repair...', 'help4-builder-suite'); ?>">
                    <span class="description"><?php esc_html_e('Plain-English search intent used by the audit and content guidance.', 'help4-builder-suite'); ?></span>
                </p>
                <p>
                    <label for="h4_seo_related_keywords"><strong><?php esc_html_e('Related Keyphrases and Terms', 'help4-builder-suite'); ?></strong></label><br>
                    <textarea id="h4_seo_related_keywords" name="h4_seo_related_keywords" class="widefat" rows="4" placeholder="WordPress care plan&#10;managed website support&#10;site maintenance"><?php echo esc_textarea(implode("\n", array_map('strval', $relatedKeywords))); ?></textarea>
                    <span class="description"><?php esc_html_e('One natural synonym, supporting phrase, or closely related topic per line. Help4 checks topical coverage without asking you to repeat the same phrase.', 'help4-builder-suite'); ?></span>
                </p>
                <p>
                    <label for="h4_seo_canonical"><strong><?php esc_html_e('Canonical URL', 'help4-builder-suite'); ?></strong></label><br>
                    <input id="h4_seo_canonical" type="url" name="h4_seo_canonical" class="widefat code" value="<?php echo esc_attr($canonical); ?>">
                    <span class="description"><?php esc_html_e('Leave blank to use the page permalink.', 'help4-builder-suite'); ?></span>
                </p>
            </section>
            <section style="border:1px solid #dcdcde;border-radius:8px;padding:1rem;background:#fff;">
                <h3 style="margin:0 0 .75rem;"><?php esc_html_e('Readability, Analytics, and AdSense Signals', 'help4-builder-suite'); ?></h3>
                <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-bottom:.75rem;">
                    <div style="border:1px solid #dcdcde;border-radius:8px;padding:.75rem;background:#f6f7f7;">
                        <strong><?php echo esc_html((string) ($metrics['word_count'] ?? 0)); ?></strong><br>
                        <span><?php esc_html_e('words', 'help4-builder-suite'); ?></span>
                    </div>
                    <div style="border:1px solid #dcdcde;border-radius:8px;padding:.75rem;background:#f6f7f7;">
                        <strong><?php echo esc_html((string) ($metrics['avg_sentence_words'] ?? 0)); ?></strong><br>
                        <span><?php esc_html_e('avg sentence words', 'help4-builder-suite'); ?></span>
                    </div>
                    <div style="border:1px solid #dcdcde;border-radius:8px;padding:.75rem;background:#f6f7f7;">
                        <strong><?php echo esc_html((string) ($metrics['reading_grade'] ?? 0)); ?></strong><br>
                        <span><?php esc_html_e('reading grade', 'help4-builder-suite'); ?></span>
                    </div>
                    <div style="border:1px solid #dcdcde;border-radius:8px;padding:.75rem;background:#f6f7f7;">
                        <strong><?php echo esc_html((string) ($metrics['reading_ease'] ?? 0)); ?></strong><br>
                        <span><?php esc_html_e('reading ease', 'help4-builder-suite'); ?></span>
                    </div>
                    <div style="border:1px solid #dcdcde;border-radius:8px;padding:.75rem;background:#f6f7f7;">
                        <strong><?php echo esc_html((string) ($metrics['internal_link_count'] ?? 0)); ?></strong><br>
                        <span><?php esc_html_e('internal links', 'help4-builder-suite'); ?></span>
                    </div>
                    <div style="border:1px solid #dcdcde;border-radius:8px;padding:.75rem;background:#f6f7f7;">
                        <strong><?php echo esc_html((string) ($metrics['external_link_count'] ?? 0)); ?></strong><br>
                        <span><?php esc_html_e('external links', 'help4-builder-suite'); ?></span>
                    </div>
                    <div style="border:1px solid #dcdcde;border-radius:8px;padding:.75rem;background:#f6f7f7;">
                        <strong><?php echo esc_html((string) ($metrics['keyphrase_density'] ?? 0)); ?>%</strong><br>
                        <span><?php esc_html_e('primary phrase density', 'help4-builder-suite'); ?></span>
                    </div>
                    <div style="border:1px solid #dcdcde;border-radius:8px;padding:.75rem;background:#f6f7f7;">
                        <strong><?php echo esc_html((string) ($metrics['related_keyphrases_found'] ?? 0)); ?>/<?php echo esc_html((string) ($metrics['related_keyphrases_total'] ?? 0)); ?></strong><br>
                        <span><?php esc_html_e('related terms covered', 'help4-builder-suite'); ?></span>
                    </div>
                </div>
                <p>
                    <label for="h4_seo_conversion_goal"><strong><?php esc_html_e('Primary Conversion Goal', 'help4-builder-suite'); ?></strong></label><br>
                    <select id="h4_seo_conversion_goal" name="h4_seo_conversion_goal">
                        <?php foreach ($goalOptions as $value => $label) : ?>
                            <option value="<?php echo esc_attr((string) $value); ?>" <?php selected($conversionGoal, (string) $value); ?>><?php echo esc_html((string) $label); ?></option>
                        <?php endforeach; ?>
                    </select>
                    <span class="description"><?php esc_html_e('Used to make the page easier to map to Google Analytics key events and funnel reports.', 'help4-builder-suite'); ?></span>
                </p>
                <p>
                    <label for="h4_seo_analytics_event"><strong><?php esc_html_e('Suggested Analytics Event Name', 'help4-builder-suite'); ?></strong></label><br>
                    <input id="h4_seo_analytics_event" type="text" name="h4_seo_analytics_event" class="regular-text code" value="<?php echo esc_attr($analyticsEvent); ?>" placeholder="<?php echo esc_attr($suggestedAnalyticsEvent); ?>">
                    <span class="description"><?php esc_html_e('Optional GA4-friendly event name for the main action. The plugin stores this plan; it does not fire tracking events by itself.', 'help4-builder-suite'); ?></span>
                </p>
                <p>
                    <label for="h4_seo_primary_cta"><strong><?php esc_html_e('Main Call To Action', 'help4-builder-suite'); ?></strong></label><br>
                    <input id="h4_seo_primary_cta" type="text" name="h4_seo_primary_cta" class="widefat" value="<?php echo esc_attr($primaryCta); ?>" placeholder="<?php esc_attr_e('Request a quote, Book a call, Download the plugin, Start checkout...', 'help4-builder-suite'); ?>">
                    <span class="description"><?php esc_html_e('Plain-English action users should take. Helps the audit find pages that are readable but do not convert.', 'help4-builder-suite'); ?></span>
                </p>
                <p class="description"><?php esc_html_e('AdSense quality checks focus on useful depth, readable organization, image context, and low link clutter. Sensitive policy topics still need a human review before ads are enabled.', 'help4-builder-suite'); ?></p>
            </section>
            <section style="border:1px solid #dcdcde;border-radius:8px;padding:1rem;background:#fff;">
                <h3 style="margin:0 0 .75rem;"><?php esc_html_e('Social Sharing', 'help4-builder-suite'); ?></h3>
                <p>
                    <label for="h4_seo_social_title"><strong><?php esc_html_e('Social Title', 'help4-builder-suite'); ?></strong></label><br>
                    <input id="h4_seo_social_title" type="text" name="h4_seo_social_title" class="widefat" value="<?php echo esc_attr($socialTitle); ?>">
                </p>
                <p>
                    <label for="h4_seo_social_description"><strong><?php esc_html_e('Social Description', 'help4-builder-suite'); ?></strong></label><br>
                    <textarea id="h4_seo_social_description" name="h4_seo_social_description" class="widefat" rows="3"><?php echo esc_textarea($socialDescription); ?></textarea>
                </p>
                <p>
                    <label for="h4_seo_og_type"><strong><?php esc_html_e('Open Graph Object Type', 'help4-builder-suite'); ?></strong></label><br>
                    <input id="h4_seo_og_type" type="text" name="h4_seo_og_type" class="regular-text code" list="h4_seo_og_type_options" value="<?php echo esc_attr($openGraphType); ?>" placeholder="website">
                    <datalist id="h4_seo_og_type_options"><option value="website"><option value="article"><option value="product"><option value="profile"><option value="place"></datalist>
                    <span class="description"><?php esc_html_e('Leave blank for an automatic type. Extensions can filter h4bs_open_graph_type for custom post types.', 'help4-builder-suite'); ?></span>
                </p>
                <p data-h4bs-image-profile="social">
                    <label for="h4_seo_social_image_url"><strong><?php esc_html_e('Primary Social Image URL', 'help4-builder-suite'); ?></strong></label><br>
                    <input id="h4_seo_social_image_url" type="url" name="h4_seo_social_image_url" class="widefat code" value="<?php echo esc_attr($socialImageUrl); ?>" placeholder="https://example.com/listing-photo.jpg">
                    <span class="description"><?php esc_html_e('Supports local MLS and custom-post-type image URLs that are not Media Library attachments. This takes priority over the attachment field.', 'help4-builder-suite'); ?></span>
                </p>
                <p data-h4bs-image-profile="social">
                    <label for="h4_seo_og_image_id"><strong><?php esc_html_e('OG Image Attachment ID', 'help4-builder-suite'); ?></strong></label><br>
                    <input id="h4_seo_og_image_id" type="number" name="h4_seo_og_image_id" class="small-text" value="<?php echo esc_attr((string) $ogImageId); ?>">
                    <span class="description"><?php esc_html_e('Use a 1200x630 image when possible. Falls back to featured image, site icon, then the Help4 demo image.', 'help4-builder-suite'); ?></span>
                </p>
                <p>
                    <label for="h4_seo_social_image_alt"><strong><?php esc_html_e('Social Image Alt Text', 'help4-builder-suite'); ?></strong></label><br>
                    <input id="h4_seo_social_image_alt" type="text" name="h4_seo_social_image_alt" class="widefat" value="<?php echo esc_attr($socialImageAlt); ?>">
                    <span class="description"><?php esc_html_e('Describe the image itself for accessible Open Graph and X previews. Media Library alt text and the global default remain available as fallbacks.', 'help4-builder-suite'); ?></span>
                </p>
                <p>
                    <label for="h4_seo_twitter_creator"><strong><?php esc_html_e('X / Twitter Creator Handle', 'help4-builder-suite'); ?></strong></label><br>
                    <input id="h4_seo_twitter_creator" type="text" name="h4_seo_twitter_creator" class="regular-text" value="<?php echo esc_attr($twitterCreator); ?>" placeholder="@creator">
                </p>
            </section>
            <section style="border:1px solid #dcdcde;border-radius:8px;padding:1rem;background:#fff;">
                <h3 style="margin:0 0 .75rem;"><?php esc_html_e('International Versions', 'help4-builder-suite'); ?></h3>
                <p>
                    <label for="h4_seo_hreflang"><strong><?php esc_html_e('hreflang Alternate URLs', 'help4-builder-suite'); ?></strong></label><br>
                    <textarea id="h4_seo_hreflang" name="h4_seo_hreflang" class="widefat code" rows="6" placeholder="en-US=https://example.com/service/&#10;en-AU=https://example.com/au/service/&#10;zh-CN=https://example.com/zh/service/&#10;x-default=https://example.com/service/"><?php echo esc_textarea($hreflangText); ?></textarea>
                    <span class="description"><?php esc_html_e('One language or language-region and absolute URL per line. Help4 validates BCP 47-style tags, adds this page\'s current-language self reference when alternates exist, supports x-default, and emits matching Open Graph locale alternates.', 'help4-builder-suite'); ?></span>
                </p>
            </section>
            <section style="border:1px solid #dcdcde;border-radius:8px;padding:1rem;background:#fff;">
                <h3 style="margin:0 0 .75rem;"><?php esc_html_e('Schema and FAQ', 'help4-builder-suite'); ?></h3>
                <p>
                    <label for="h4_seo_schema_type"><strong><?php esc_html_e('Primary Schema Type', 'help4-builder-suite'); ?></strong></label><br>
                    <select id="h4_seo_schema_type" name="h4_seo_schema_type">
                        <?php foreach ($schemaOptions as $value => $label) : ?>
                            <option value="<?php echo esc_attr((string) $value); ?>" <?php selected($schemaType, (string) $value); ?>><?php echo esc_html((string) $label); ?></option>
                        <?php endforeach; ?>
                    </select>
                </p>
                <p>
                    <label for="h4_seo_commerce_product_id"><strong><?php esc_html_e('Help4 Commerce Product ID', 'help4-builder-suite'); ?></strong></label><br>
                    <input id="h4_seo_commerce_product_id" type="number" min="0" step="1" name="h4_seo_commerce_product_id" value="<?php echo esc_attr((string) $commerceProductId); ?>">
                    <span class="description"><?php esc_html_e('For Product schema, map this page to an active Help4 product so offers, identifiers, price, currency, availability, condition, image, and brand stay synchronized.', 'help4-builder-suite'); ?></span>
                </p>
                <p>
                    <label for="h4_seo_faq_items"><strong><?php esc_html_e('FAQ Schema Items', 'help4-builder-suite'); ?></strong></label><br>
                    <textarea id="h4_seo_faq_items" name="h4_seo_faq_items" class="widefat code" rows="6" placeholder="<?php esc_attr_e('Question | Answer', 'help4-builder-suite'); ?>"><?php echo esc_textarea($faqText); ?></textarea>
                    <span class="description"><?php esc_html_e('One question per line, using "Question | Answer". JSON arrays are also accepted. Keep every answer visible and factual. Google generally limits FAQ rich results to authoritative government and health sites; valid markup does not guarantee a rich result.', 'help4-builder-suite'); ?></span>
                </p>
            </section>
            <section style="border:1px solid #dcdcde;border-radius:8px;padding:1rem;background:#fff;">
                <h3 style="margin:0 0 .75rem;"><?php esc_html_e('Performing Arts Event / Course Details', 'help4-builder-suite'); ?></h3>
                <p class="description"><?php esc_html_e('Use Event only for a real, uniquely addressable public event with accurate date and location. Use Course only for curriculum with lessons or modules and a factual learning outcome. Do not use these fields for promotions, ordinary business hours, or invented credentials.', 'help4-builder-suite'); ?></p>
                <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.75rem;">
                    <p><label><strong><?php esc_html_e('Event start (ISO 8601)', 'help4-builder-suite'); ?></strong><br><input name="h4_seo_performing_arts[event_start]" value="<?php echo esc_attr((string) $performingArts['event_start']); ?>" placeholder="2026-09-12T19:00:00-04:00"></label></p>
                    <p><label><strong><?php esc_html_e('Event end (ISO 8601)', 'help4-builder-suite'); ?></strong><br><input name="h4_seo_performing_arts[event_end]" value="<?php echo esc_attr((string) $performingArts['event_end']); ?>"></label></p>
                    <p><label><strong><?php esc_html_e('Event status', 'help4-builder-suite'); ?></strong><br><select name="h4_seo_performing_arts[event_status]"><?php foreach (['scheduled','cancelled','postponed','rescheduled','moved_online'] as $value) : ?><option value="<?php echo esc_attr($value); ?>" <?php selected((string) $performingArts['event_status'], $value); ?>><?php echo esc_html(ucwords(str_replace('_', ' ', $value))); ?></option><?php endforeach; ?></select></label></p>
                    <p><label><strong><?php esc_html_e('Attendance mode', 'help4-builder-suite'); ?></strong><br><select name="h4_seo_performing_arts[attendance_mode]"><?php foreach (['offline','online','mixed'] as $value) : ?><option value="<?php echo esc_attr($value); ?>" <?php selected((string) $performingArts['attendance_mode'], $value); ?>><?php echo esc_html(ucfirst($value)); ?></option><?php endforeach; ?></select></label></p>
                    <?php foreach (['venue_name'=>'Venue name','street_address'=>'Street address','address_locality'=>'City / locality','address_region'=>'State / region','postal_code'=>'Postal code','address_country'=>'Country code','organizer_name'=>'Organizer name','organizer_url'=>'Organizer URL','offer_url'=>'Registration / ticket URL','offer_price'=>'Price','offer_currency'=>'Currency','course_provider_name'=>'Course provider','course_provider_url'=>'Provider URL','course_mode'=>'Course mode'] as $key => $label) : ?><p><label><strong><?php echo esc_html($label); ?></strong><br><input name="h4_seo_performing_arts[<?php echo esc_attr($key); ?>]" value="<?php echo esc_attr((string) $performingArts[$key]); ?>"></label></p><?php endforeach; ?>
                    <p style="grid-column:1/-1"><label><strong><?php esc_html_e('Performers (one factual name per line)', 'help4-builder-suite'); ?></strong><br><textarea class="widefat" name="h4_seo_performing_arts[performer_names]" rows="3"><?php echo esc_textarea(implode("\n", $performingArts['performer_names'])); ?></textarea></label></p>
                    <p style="grid-column:1/-1"><label><strong><?php esc_html_e('Course learning outcome', 'help4-builder-suite'); ?></strong><br><textarea class="widefat" name="h4_seo_performing_arts[course_outcome]" rows="3"><?php echo esc_textarea((string) $performingArts['course_outcome']); ?></textarea></label></p>
                    <p><label><strong><?php esc_html_e('Course prerequisites', 'help4-builder-suite'); ?></strong><br><textarea name="h4_seo_performing_arts[course_prerequisites]" rows="3"><?php echo esc_textarea((string) $performingArts['course_prerequisites']); ?></textarea></label></p>
                    <p><label><strong><?php esc_html_e('Instructors (one verified name per line)', 'help4-builder-suite'); ?></strong><br><textarea name="h4_seo_performing_arts[course_instructors]" rows="3"><?php echo esc_textarea(implode("\n", $performingArts['course_instructors'])); ?></textarea></label></p>
                </div>
            </section>
            <section style="border:1px solid #dcdcde;border-radius:8px;padding:1rem;background:#fff;">
                <h3 style="margin:0 0 .75rem;"><?php esc_html_e('Real Estate Listing Graph', 'help4-builder-suite'); ?></h3>
                <p class="description"><?php esc_html_e('Used when Primary Schema Type is RealEstateListing. Empty optional fields are omitted; Realty and IDX extensions can filter every graph node.', 'help4-builder-suite'); ?></p>
                <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.75rem;">
                    <p><label for="h4_seo_re_property_type"><strong><?php esc_html_e('Property type', 'help4-builder-suite'); ?></strong></label><br><select id="h4_seo_re_property_type" name="h4_seo_real_estate[property_type]">
                        <?php foreach ($this->realEstateSchema->propertyTypeLabels() as $value => $label) : ?><option value="<?php echo esc_attr($value); ?>" <?php selected((string) $realEstate['property_type'], $value); ?>><?php echo esc_html($label); ?></option><?php endforeach; ?>
                    </select></p>
                    <p><label for="h4_seo_re_listing_id"><strong><?php esc_html_e('MLS / listing ID', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_listing_id" type="text" name="h4_seo_real_estate[listing_id]" value="<?php echo esc_attr((string) $realEstate['listing_id']); ?>"></p>
                    <p><label for="h4_seo_re_listing_action"><strong><?php esc_html_e('Listing action', 'help4-builder-suite'); ?></strong></label><br><select id="h4_seo_re_listing_action" name="h4_seo_real_estate[listing_action]">
                        <?php foreach ($this->realEstateSchema->listingActionLabels() as $value => $label) : ?><option value="<?php echo esc_attr($value); ?>" <?php selected((string) $realEstate['listing_action'], $value); ?>><?php echo esc_html($label); ?></option><?php endforeach; ?>
                    </select></p>
                    <p><label for="h4_seo_re_availability"><strong><?php esc_html_e('Availability', 'help4-builder-suite'); ?></strong></label><br><select id="h4_seo_re_availability" name="h4_seo_real_estate[availability]">
                        <?php foreach ($this->realEstateSchema->availabilityLabels() as $value => $label) : ?><option value="<?php echo esc_attr($value); ?>" <?php selected((string) $realEstate['availability'], $value); ?>><?php echo esc_html($label); ?></option><?php endforeach; ?>
                    </select></p>
                    <p><label for="h4_seo_re_price"><strong><?php esc_html_e('Price', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_price" type="number" min="0" step="0.01" name="h4_seo_real_estate[price]" value="<?php echo esc_attr((string) $realEstate['price']); ?>"></p>
                    <p><label for="h4_seo_re_price_currency"><strong><?php esc_html_e('Currency', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_price_currency" type="text" maxlength="3" name="h4_seo_real_estate[price_currency]" value="<?php echo esc_attr((string) $realEstate['price_currency']); ?>" placeholder="USD"></p>
                    <p><label for="h4_seo_re_bedrooms"><strong><?php esc_html_e('Bedrooms', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_bedrooms" type="number" min="0" step="0.5" name="h4_seo_real_estate[bedrooms]" value="<?php echo esc_attr((string) $realEstate['bedrooms']); ?>"></p>
                    <p><label for="h4_seo_re_bathrooms"><strong><?php esc_html_e('Bathrooms', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_bathrooms" type="number" min="0" step="0.5" name="h4_seo_real_estate[bathrooms]" value="<?php echo esc_attr((string) $realEstate['bathrooms']); ?>"></p>
                    <p><label for="h4_seo_re_rooms"><strong><?php esc_html_e('Total rooms', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_rooms" type="number" min="0" step="1" name="h4_seo_real_estate[rooms]" value="<?php echo esc_attr((string) $realEstate['rooms']); ?>"></p>
                    <p><label for="h4_seo_re_year_built"><strong><?php esc_html_e('Year built', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_year_built" type="number" min="1000" max="<?php echo esc_attr((string) ((int) gmdate('Y') + 10)); ?>" name="h4_seo_real_estate[year_built]" value="<?php echo esc_attr((string) $realEstate['year_built']); ?>"></p>
                    <p><label for="h4_seo_re_floor_size"><strong><?php esc_html_e('Floor size', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_floor_size" type="number" min="0" step="0.01" name="h4_seo_real_estate[floor_size]" value="<?php echo esc_attr((string) $realEstate['floor_size']); ?>"> <input aria-label="<?php esc_attr_e('Floor-size unit', 'help4-builder-suite'); ?>" type="text" size="8" name="h4_seo_real_estate[floor_size_unit]" value="<?php echo esc_attr((string) $realEstate['floor_size_unit']); ?>" placeholder="sq ft"></p>
                    <p><label for="h4_seo_re_lot_size"><strong><?php esc_html_e('Lot size', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_lot_size" type="number" min="0" step="0.01" name="h4_seo_real_estate[lot_size]" value="<?php echo esc_attr((string) $realEstate['lot_size']); ?>"> <input aria-label="<?php esc_attr_e('Lot-size unit', 'help4-builder-suite'); ?>" type="text" size="8" name="h4_seo_real_estate[lot_size_unit]" value="<?php echo esc_attr((string) $realEstate['lot_size_unit']); ?>" placeholder="acre"></p>
                    <p><label for="h4_seo_re_street"><strong><?php esc_html_e('Street address', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_street" type="text" name="h4_seo_real_estate[address_street]" value="<?php echo esc_attr((string) $realEstate['address_street']); ?>"></p>
                    <p><label for="h4_seo_re_city"><strong><?php esc_html_e('City / locality', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_city" type="text" name="h4_seo_real_estate[address_locality]" value="<?php echo esc_attr((string) $realEstate['address_locality']); ?>"></p>
                    <p><label for="h4_seo_re_region"><strong><?php esc_html_e('State / region', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_region" type="text" name="h4_seo_real_estate[address_region]" value="<?php echo esc_attr((string) $realEstate['address_region']); ?>"></p>
                    <p><label for="h4_seo_re_postal"><strong><?php esc_html_e('Postal code', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_postal" type="text" name="h4_seo_real_estate[postal_code]" value="<?php echo esc_attr((string) $realEstate['postal_code']); ?>"></p>
                    <p><label for="h4_seo_re_country"><strong><?php esc_html_e('Country', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_country" type="text" name="h4_seo_real_estate[address_country]" value="<?php echo esc_attr((string) $realEstate['address_country']); ?>" placeholder="US"></p>
                    <p><label for="h4_seo_re_latitude"><strong><?php esc_html_e('Latitude', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_latitude" type="number" min="-90" max="90" step="0.0000001" name="h4_seo_real_estate[latitude]" value="<?php echo esc_attr((string) $realEstate['latitude']); ?>"></p>
                    <p><label for="h4_seo_re_longitude"><strong><?php esc_html_e('Longitude', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_longitude" type="number" min="-180" max="180" step="0.0000001" name="h4_seo_real_estate[longitude]" value="<?php echo esc_attr((string) $realEstate['longitude']); ?>"></p>
                    <p><label for="h4_seo_re_valid_through"><strong><?php esc_html_e('Offer valid through', 'help4-builder-suite'); ?></strong></label><br><input id="h4_seo_re_valid_through" type="date" name="h4_seo_real_estate[valid_through]" value="<?php echo esc_attr((string) $realEstate['valid_through']); ?>"></p>
                </div>
                <p data-h4bs-image-profile="gallery"><label for="h4_seo_re_images"><strong><?php esc_html_e('Listing image URLs', 'help4-builder-suite'); ?></strong></label><br><textarea id="h4_seo_re_images" class="widefat code" rows="5" name="h4_seo_real_estate[image_urls]" placeholder="https://example.com/mls/photo-1.jpg"><?php echo esc_textarea(implode("\n", array_map('strval', (array) $realEstate['image_urls']))); ?></textarea><span class="description"><?php esc_html_e('One HTTP or HTTPS image URL per line. The primary social image is automatically included first.', 'help4-builder-suite'); ?></span></p>
            </section>
            <section style="border:1px solid #dcdcde;border-radius:8px;padding:1rem;background:#fff;">
                <h3 style="margin:0 0 .75rem;"><?php esc_html_e('Robots Controls', 'help4-builder-suite'); ?></h3>
                <p><label><input type="checkbox" name="h4_seo_noindex" value="1" <?php checked($noindex === '1'); ?>> <?php esc_html_e('Noindex this content', 'help4-builder-suite'); ?></label></p>
                <p><label><input type="checkbox" name="h4_seo_nofollow" value="1" <?php checked($nofollow === '1'); ?>> <?php esc_html_e('Nofollow links on this content', 'help4-builder-suite'); ?></label></p>
                <p><label><input type="checkbox" name="h4_seo_noarchive" value="1" <?php checked($noarchive === '1'); ?>> <?php esc_html_e('Prevent cached archive snippets', 'help4-builder-suite'); ?></label></p>
                <p><label><input type="checkbox" name="h4_seo_nosnippet" value="1" <?php checked($nosnippet === '1'); ?>> <?php esc_html_e('Prevent text snippets', 'help4-builder-suite'); ?></label></p>
                <p>
                    <label for="h4_seo_max_image_preview"><strong><?php esc_html_e('Max Image Preview', 'help4-builder-suite'); ?></strong></label><br>
                    <select id="h4_seo_max_image_preview" name="h4_seo_max_image_preview">
                        <option value="large" <?php selected($maxImagePreview, 'large'); ?>><?php esc_html_e('Large', 'help4-builder-suite'); ?></option>
                        <option value="standard" <?php selected($maxImagePreview, 'standard'); ?>><?php esc_html_e('Standard', 'help4-builder-suite'); ?></option>
                        <option value="none" <?php selected($maxImagePreview, 'none'); ?>><?php esc_html_e('None', 'help4-builder-suite'); ?></option>
                    </select>
                </p>
            </section>
        </div>
        <?php
    }

    public function saveMetaBox(int $postId, WP_Post $post): void
    {
        if (! isset($_POST['h4bs_seo_meta_nonce'])) {
            return;
        }

        if (! wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['h4bs_seo_meta_nonce'])), 'h4bs_seo_meta')) {
            return;
        }

        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return;
        }

        if (! current_user_can('edit_post', $postId)) {
            return;
        }

        if ($post->post_type === 'revision') {
            return;
        }

        update_post_meta($postId, '_h4_seo_title', sanitize_text_field((string) wp_unslash($_POST['h4_seo_title'] ?? '')));
        update_post_meta($postId, '_h4_seo_description', sanitize_text_field((string) wp_unslash($_POST['h4_seo_description'] ?? '')));
        update_post_meta($postId, '_h4_seo_canonical', esc_url_raw((string) wp_unslash($_POST['h4_seo_canonical'] ?? '')));
        update_post_meta($postId, '_h4_seo_focus_keyword', sanitize_text_field((string) wp_unslash($_POST['h4_seo_focus_keyword'] ?? '')));
        update_post_meta(
            $postId,
            '_h4_seo_related_keywords',
            $this->sanitizeLineList(sanitize_textarea_field((string) wp_unslash($_POST['h4_seo_related_keywords'] ?? '')), false)
        );
        update_post_meta($postId, '_h4_seo_social_title', sanitize_text_field((string) wp_unslash($_POST['h4_seo_social_title'] ?? '')));
        update_post_meta($postId, '_h4_seo_social_description', sanitize_text_field((string) wp_unslash($_POST['h4_seo_social_description'] ?? '')));
        $socialImageInput = sanitize_text_field((string) wp_unslash($_POST['h4_seo_social_image_url'] ?? ''));
        $openGraphTypeInput = sanitize_text_field((string) wp_unslash($_POST['h4_seo_og_type'] ?? ''));
        update_post_meta($postId, '_h4_seo_social_image_url', $this->sanitizeHttpUrl($socialImageInput));
        update_post_meta($postId, '_h4_seo_og_type', $this->sanitizeOpenGraphType($openGraphTypeInput));
        update_post_meta($postId, '_h4_seo_social_image_alt', sanitize_text_field((string) wp_unslash($_POST['h4_seo_social_image_alt'] ?? '')));
        $twitterCreatorInput = sanitize_text_field((string) wp_unslash($_POST['h4_seo_twitter_creator'] ?? ''));
        update_post_meta($postId, '_h4_seo_twitter_creator', $this->sanitizeTwitterHandle($twitterCreatorInput));
        update_post_meta(
            $postId,
            '_h4_seo_hreflang',
            $this->discoveryStandards->sanitizeHreflangAlternates(
                sanitize_textarea_field((string) wp_unslash($_POST['h4_seo_hreflang'] ?? ''))
            )
        );
        $conversionGoal = sanitize_key((string) wp_unslash($_POST['h4_seo_conversion_goal'] ?? ''));
        if (! isset($this->conversionGoalLabels()[$conversionGoal])) {
            $conversionGoal = '';
        }
        update_post_meta($postId, '_h4_seo_conversion_goal', $conversionGoal);
        update_post_meta($postId, '_h4_seo_analytics_event', sanitize_key((string) wp_unslash($_POST['h4_seo_analytics_event'] ?? '')));
        update_post_meta($postId, '_h4_seo_primary_cta', sanitize_text_field((string) wp_unslash($_POST['h4_seo_primary_cta'] ?? '')));
        $intentProfile = sanitize_key((string) wp_unslash($_POST['h4_seo_intent_profile'] ?? ''));
        if (! isset($this->intentScorer->profileCatalog()[$intentProfile])) {
            $intentProfile = '';
        }
        update_post_meta($postId, '_h4_seo_intent_profile', $intentProfile);
        $schemaType = sanitize_key((string) wp_unslash($_POST['h4_seo_schema_type'] ?? ''));
        if (! isset($this->schemaTypeLabels()[$schemaType])) {
            $schemaType = $post->post_type === 'post' ? 'article' : 'webpage';
        }
        update_post_meta($postId, '_h4_seo_schema_type', $schemaType);
        update_post_meta($postId, '_h4_seo_commerce_product_id', absint(wp_unslash($_POST['h4_seo_commerce_product_id'] ?? 0)));
        $realEstateRaw = isset($_POST['h4_seo_real_estate']) && is_array($_POST['h4_seo_real_estate'])
            ? map_deep(wp_unslash($_POST['h4_seo_real_estate']), 'sanitize_text_field')
            : [];
        update_post_meta($postId, '_h4_seo_real_estate', $this->realEstateSchema->sanitize(is_array($realEstateRaw) ? $realEstateRaw : []));
        update_post_meta($postId, '_h4_seo_faq_items', $this->parseFaqItemsInput(sanitize_textarea_field((string) wp_unslash($_POST['h4_seo_faq_items'] ?? ''))));
        $performingArtsRaw = isset($_POST['h4_seo_performing_arts']) && is_array($_POST['h4_seo_performing_arts']) ? map_deep(wp_unslash($_POST['h4_seo_performing_arts']), 'sanitize_text_field') : [];
        update_post_meta($postId, '_h4_seo_performing_arts', $this->performingArtsSchema->sanitize(is_array($performingArtsRaw) ? $performingArtsRaw : []));
        update_post_meta($postId, '_h4_seo_og_image_id', absint(wp_unslash($_POST['h4_seo_og_image_id'] ?? 0)));
        update_post_meta($postId, '_h4_seo_noindex', ! empty($_POST['h4_seo_noindex']) ? '1' : '0');
        update_post_meta($postId, '_h4_seo_nofollow', ! empty($_POST['h4_seo_nofollow']) ? '1' : '0');
        update_post_meta($postId, '_h4_seo_noarchive', ! empty($_POST['h4_seo_noarchive']) ? '1' : '0');
        update_post_meta($postId, '_h4_seo_nosnippet', ! empty($_POST['h4_seo_nosnippet']) ? '1' : '0');
        $maxImagePreview = sanitize_key((string) wp_unslash($_POST['h4_seo_max_image_preview'] ?? 'large'));
        if (! in_array($maxImagePreview, ['large', 'standard', 'none'], true)) {
            $maxImagePreview = 'large';
        }
        update_post_meta($postId, '_h4_seo_max_image_preview', $maxImagePreview);
    }

    public function renderAuthorSeoFields(WP_User $user): void
    {
        if (! current_user_can('edit_user', $user->ID)) {
            return;
        }
        $jobTitle = (string) get_user_meta($user->ID, '_h4_seo_job_title', true);
        $credentials = (string) get_user_meta($user->ID, '_h4_seo_credentials', true);
        $expertise = get_user_meta($user->ID, '_h4_seo_expertise', true);
        $profileUrls = get_user_meta($user->ID, '_h4_seo_profile_urls', true);
        $expertise = is_array($expertise) ? $expertise : [];
        $profileUrls = is_array($profileUrls) ? $profileUrls : [];
        wp_nonce_field('h4bs_save_author_seo_' . $user->ID, 'h4bs_author_seo_nonce');
        ?>
        <h2><?php esc_html_e('Help4 Author SEO', 'help4-builder-suite'); ?></h2>
        <table class="form-table" role="presentation">
            <tr>
                <th><label for="h4bs_seo_job_title"><?php esc_html_e('Professional title', 'help4-builder-suite'); ?></label></th>
                <td><input id="h4bs_seo_job_title" class="regular-text" type="text" name="h4bs_seo_job_title" value="<?php echo esc_attr($jobTitle); ?>" placeholder="Senior WordPress Engineer"></td>
            </tr>
            <tr>
                <th><label for="h4bs_seo_credentials"><?php esc_html_e('Credentials', 'help4-builder-suite'); ?></label></th>
                <td><input id="h4bs_seo_credentials" class="regular-text" type="text" name="h4bs_seo_credentials" value="<?php echo esc_attr($credentials); ?>" placeholder="CISSP, MBA"></td>
            </tr>
            <tr>
                <th><label for="h4bs_seo_expertise"><?php esc_html_e('Areas of expertise', 'help4-builder-suite'); ?></label></th>
                <td><textarea id="h4bs_seo_expertise" class="large-text" rows="5" name="h4bs_seo_expertise" placeholder="WordPress security&#10;Technical SEO&#10;Site performance"><?php echo esc_textarea(implode("\n", array_map('strval', $expertise))); ?></textarea><p class="description"><?php esc_html_e('One real area of expertise per line. These become knowsAbout entries in author schema.', 'help4-builder-suite'); ?></p></td>
            </tr>
            <tr>
                <th><label for="h4bs_seo_profile_urls"><?php esc_html_e('Identity profile URLs', 'help4-builder-suite'); ?></label></th>
                <td><textarea id="h4bs_seo_profile_urls" class="large-text code" rows="5" name="h4bs_seo_profile_urls" placeholder="https://www.linkedin.com/in/example/&#10;https://github.com/example/"><?php echo esc_textarea(implode("\n", array_map('strval', $profileUrls))); ?></textarea><p class="description"><?php esc_html_e('One public profile URL per line. Only include profiles that identify this author.', 'help4-builder-suite'); ?></p></td>
            </tr>
        </table>
        <?php
    }

    public function saveAuthorSeoFields(int $userId): void
    {
        if (! current_user_can('edit_user', $userId) || ! isset($_POST['h4bs_author_seo_nonce'])) {
            return;
        }
        $nonce = sanitize_text_field((string) wp_unslash($_POST['h4bs_author_seo_nonce']));
        if (! wp_verify_nonce($nonce, 'h4bs_save_author_seo_' . $userId)) {
            return;
        }
        update_user_meta($userId, '_h4_seo_job_title', sanitize_text_field((string) wp_unslash($_POST['h4bs_seo_job_title'] ?? '')));
        update_user_meta($userId, '_h4_seo_credentials', sanitize_text_field((string) wp_unslash($_POST['h4bs_seo_credentials'] ?? '')));
        $expertise = $this->sanitizeLineList(sanitize_textarea_field((string) wp_unslash($_POST['h4bs_seo_expertise'] ?? '')), false);
        $profileUrls = $this->sanitizeLineList(sanitize_textarea_field((string) wp_unslash($_POST['h4bs_seo_profile_urls'] ?? '')), true);
        update_user_meta($userId, '_h4_seo_expertise', $expertise);
        update_user_meta($userId, '_h4_seo_profile_urls', $profileUrls);
    }

    public function filterDocumentTitle(string $default): string
    {
        if (is_front_page() || is_home()) {
            $frontPageTitle = $this->staticFrontPageSeoTitle();
            if ($frontPageTitle !== '') {
                return $frontPageTitle;
            }
            $settings = get_option('h4bs_settings', []);
            if (! is_array($settings)) {
                $settings = [];
            }
            $customHomeTitle = trim((string) ($settings['seo_home_title'] ?? ''));
            if ($customHomeTitle !== '') {
                return $customHomeTitle;
            }
            if (
                stripos($default, 'My blog') !== false ||
                stripos($default, 'Just another WordPress site') !== false
            ) {
                return $this->homeTitleFallback($settings);
            }
            return $default;
        }

        if (! is_singular()) {
            return $default;
        }

        $postId = get_queried_object_id();
        if (! $postId) {
            return $default;
        }

        $custom = (string) get_post_meta($postId, '_h4_seo_title', true);
        return $custom !== '' ? $custom : $default;
    }

    /**
     * @param array<string, string> $parts
     * @return array<string, string>
     */
    public function filterDocumentTitleParts(array $parts): array
    {
        if (! (is_front_page() || is_home())) {
            return $parts;
        }
        $frontPageTitle = $this->staticFrontPageSeoTitle();
        if ($frontPageTitle !== '') {
            $parts['title'] = $frontPageTitle;
            unset($parts['tagline']);
            return $parts;
        }
        $settings = Settings::all();
        $title = $parts['title'] ?? '';
        if (
            $title === '' ||
            stripos($title, 'My blog') !== false ||
            stripos($title, 'Just another WordPress site') !== false
        ) {
            $parts['title'] = $this->homeTitleFallback($settings);
            unset($parts['tagline']);
        }
        return $parts;
    }

    private function staticFrontPageSeoTitle(): string
    {
        if (! is_front_page() || get_option('show_on_front') !== 'page') {
            return '';
        }

        $postId = absint(get_queried_object_id());
        if ($postId === 0) {
            $postId = absint(get_option('page_on_front'));
        }
        if ($postId === 0) {
            return '';
        }

        return trim((string) get_post_meta($postId, '_h4_seo_title', true));
    }

    /**
     * @param array<string, mixed> $robots
     * @return array<string, mixed>
     */
    public function filterRobots(array $robots): array
    {
        if ($this->isForcedNoindexHost()) {
            $robots['noindex'] = true;
            $robots['nofollow'] = true;
            unset($robots['index'], $robots['follow']);
            return $robots;
        }

        if ((string) get_option('blog_public', '1') !== '1') {
            $robots['noindex'] = true;
            $robots['nofollow'] = true;
            unset($robots['index'], $robots['follow']);
            return $robots;
        }

        if (is_search() || is_404()) {
            $robots['noindex'] = true;
            $robots['follow'] = true;
            unset($robots['index'], $robots['nofollow']);
            return $robots;
        }

        if (! is_singular()) {
            if (! isset($robots['index'])) {
                $robots['index'] = true;
            }
            if (! isset($robots['follow'])) {
                $robots['follow'] = true;
            }
            return $robots;
        }

        $postId = get_queried_object_id();
        if (! $postId) {
            return $robots;
        }

        $noindex = (string) get_post_meta($postId, '_h4_seo_noindex', true);
        if ($noindex === '1') {
            $robots['noindex'] = true;
            $robots['nofollow'] = true;
            unset($robots['index'], $robots['follow']);
            return $robots;
        }

        $robots['index'] = true;
        if ((string) get_post_meta($postId, '_h4_seo_nofollow', true) === '1') {
            $robots['nofollow'] = true;
            unset($robots['follow']);
        } else {
            $robots['follow'] = true;
        }
        if ((string) get_post_meta($postId, '_h4_seo_noarchive', true) === '1') {
            $robots['noarchive'] = true;
        }
        if ((string) get_post_meta($postId, '_h4_seo_nosnippet', true) === '1') {
            $robots['nosnippet'] = true;
            unset($robots['max-snippet']);
        } else {
            $robots['max-snippet'] = '-1';
        }
        $maxImagePreview = sanitize_key((string) get_post_meta($postId, '_h4_seo_max_image_preview', true));
        if (! in_array($maxImagePreview, ['large', 'standard', 'none'], true)) {
            $maxImagePreview = 'large';
        }
        $robots['max-image-preview'] = $maxImagePreview;
        $robots['max-video-preview'] = '-1';
        return $robots;
    }

    public function filterRobotsTxt(string $output, bool $public): string
    {
        return $this->buildRobotsTxt($public);
    }

    public function redirectLegacySitemapUrls(): void
    {
        if (is_admin() || wp_doing_ajax()) {
            return;
        }

        $requestUri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_URI'])) : '';
        $path = (string) wp_parse_url($requestUri, PHP_URL_PATH);
        $path = trim(rawurldecode($path), '/');
        $redirects = [
            'sitemap_index.xml'    => '/wp-sitemap.xml',
            'post-sitemap.xml'     => '/wp-sitemap.xml',
            'category-sitemap.xml' => '/wp-sitemap.xml',
            'page-sitemap.xml'     => '/wp-sitemap-posts-page-1.xml',
            'help4-builder-siteshot' => '/help4-builder-website-snapshot/',
            'help4-builder-site-shot' => '/help4-builder-website-snapshot/',
        ];

        if (! isset($redirects[$path])) {
            return;
        }

        wp_safe_redirect(home_url($redirects[$path]), 301, 'Help4 Builder Suite');
        exit;
    }

    public function maybeRenderLlmsText(): void
    {
        if (is_admin() || wp_doing_ajax()) {
            return;
        }

        $requestUri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_URI'])) : '';
        $path = (string) wp_parse_url($requestUri, PHP_URL_PATH);
        $path = trim(rawurldecode($path), '/');
        $homePath = trim((string) wp_parse_url(home_url('/'), PHP_URL_PATH), '/');

        if ($homePath !== '' && str_starts_with($path, $homePath . '/')) {
            $path = substr($path, strlen($homePath) + 1);
        }

        if (! in_array(strtolower($path), ['llms.txt', 'llm.txt'], true)) {
            return;
        }

        status_header(200);
        nocache_headers();
        header('Content-Type: text/plain; charset=UTF-8');
        header('X-Content-Type-Options: nosniff');
        echo $this->buildLlmsText(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain-text response is assembled from sanitized strings.
        exit;
    }

    public function maybeRenderSeoDiscoveryFiles(): void
    {
        if (is_admin() || wp_doing_ajax()) {
            return;
        }

        $settings = Settings::all();
        $requestUri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_URI'])) : '';
        $path = trim((string) wp_parse_url($requestUri, PHP_URL_PATH), '/');
        $homePath = trim((string) wp_parse_url(home_url('/'), PHP_URL_PATH), '/');
        if ($homePath !== '' && str_starts_with($path, $homePath . '/')) {
            $path = substr($path, strlen($homePath) + 1);
        }

        $indexNowKey = $this->indexNowKey($settings);
        if (
            rest_sanitize_boolean($settings['indexnow_enabled'] ?? '0')
            && $indexNowKey !== ''
            && ! $this->isForcedNoindexHost()
            && (string) get_option('blog_public', '1') === '1'
            && hash_equals($indexNowKey . '.txt', $path)
        ) {
            status_header(200);
            nocache_headers();
            header('Content-Type: text/plain; charset=UTF-8');
            header('X-Content-Type-Options: nosniff');
            echo esc_html($indexNowKey);
            exit;
        }

        $isVideoSitemap = $path === 'h4-video-sitemap.xml' && rest_sanitize_boolean($settings['seo_video_sitemap_enabled'] ?? '0');
        $isNewsSitemap = $path === 'h4-news-sitemap.xml' && rest_sanitize_boolean($settings['seo_news_sitemap_enabled'] ?? '0');
        if (! $isVideoSitemap && ! $isNewsSitemap) {
            return;
        }
        if ($this->isForcedNoindexHost() || (string) get_option('blog_public', '1') !== '1') {
            status_header(404);
            exit;
        }

        status_header(200);
        nocache_headers();
        header('Content-Type: application/xml; charset=UTF-8');
        header('X-Robots-Tag: noindex, follow', true);
        $xml = $isVideoSitemap ? $this->buildVideoSitemap($settings) : $this->buildNewsSitemap();
        echo $xml; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- XML is assembled from escaped values.
        exit;
    }

    public function queueIndexNowTransition(string $newStatus, string $oldStatus, WP_Post $post): void
    {
        if ($newStatus !== 'publish' && $oldStatus !== 'publish') {
            return;
        }
        if (! is_post_type_viewable($post->post_type) || $post->post_type === 'attachment') {
            return;
        }
        $url = (string) get_permalink($post);
        if ($url !== '') {
            $event = $oldStatus === 'publish' && $newStatus !== 'publish' ? 'deleted' : 'updated';
            $this->queueIndexNowUrl($url, $event);
        }
    }

    public function queueIndexNowDeletion(int $postId, WP_Post $post): void
    {
        if ($post->post_status !== 'publish' || ! is_post_type_viewable($post->post_type)) {
            return;
        }
        $url = (string) get_permalink($postId);
        if ($url !== '') {
            $this->queueIndexNowUrl($url, 'deleted');
        }
    }

    /**
     * @param array<string, mixed> $product
     * @param array<string, mixed> $payload
     */
    public function queueCommerceProductChange(int $productId, array $product, array $payload): void
    {
        unset($productId, $product);
        $url = $this->commerceProductUrl($payload);
        if ($url !== '') {
            $this->queueIndexNowUrl($url, 'updated');
        }
    }

    /** @param array<string, mixed> $product */
    public function queueCommerceProductDeletion(int $productId, array $product): void
    {
        unset($productId);
        $payload = json_decode((string) ($product['payload'] ?? ''), true);
        $url = $this->commerceProductUrl(is_array($payload) ? $payload : []);
        if ($url !== '') {
            $this->queueIndexNowUrl($url, 'deleted');
        }
    }

    /** @param array<string, mixed> $payload */
    private function commerceProductUrl(array $payload): string
    {
        return esc_url_raw((string) (
            $payload['external_url']
            ?? ($payload['product_url']
            ?? ($payload['permalink'] ?? ($payload['url'] ?? '')))
        ), ['http', 'https']);
    }

    public function submitIndexNowQueue(): void
    {
        $settings = Settings::all();
        $key = $this->indexNowKey($settings);
        if (
            ! rest_sanitize_boolean($settings['indexnow_enabled'] ?? '0')
            || $key === ''
            || $this->isForcedNoindexHost()
            || (string) get_option('blog_public', '1') !== '1'
        ) {
            $this->storeIndexNowResult(0, 0, count($this->normalizedIndexNowQueue()), 'blocked', __('Submission is disabled on this site or the key is invalid.', 'help4-builder-suite'));
            return;
        }

        $queue = $this->normalizedIndexNowQueue();
        if ($queue === []) {
            return;
        }
        $host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        $now = time();
        uasort($queue, static fn (array $a, array $b): int => ((int) $a['queued_at']) <=> ((int) $b['queued_at']));
        $eligible = array_filter($queue, static fn (array $entry): bool => (int) $entry['next_attempt'] <= $now);
        $urls = array_slice(array_keys($eligible), 0, 500);
        if ($host === '' || $urls === []) {
            $this->scheduleNextIndexNowAttempt($queue);
            return;
        }

        $endpoint = esc_url_raw((string) apply_filters('h4bs_indexnow_endpoint', 'https://api.indexnow.org/indexnow'));
        $response = wp_remote_post(
            $endpoint,
            [
                'timeout' => 12,
                'redirection' => 2,
                'headers' => ['Content-Type' => 'application/json; charset=utf-8'],
                'body' => wp_json_encode([
                    'host' => $host,
                    'key' => $key,
                    'keyLocation' => home_url('/' . rawurlencode($key) . '.txt'),
                    'urlList' => $urls,
                ]),
                'user-agent' => 'Help4BuilderSuite/' . H4BS_VERSION,
            ]
        );
        $status = is_wp_error($response) ? 0 : (int) wp_remote_retrieve_response_code($response);
        $error = is_wp_error($response)
            ? $response->get_error_message()
            : trim((string) wp_remote_retrieve_response_message($response));
        if ($status >= 200 && $status < 300) {
            foreach ($urls as $url) {
                unset($queue[$url]);
            }
            update_option('h4bs_indexnow_queue', $queue, false);
            $this->storeIndexNowResult($status, count($urls), count($queue), 'accepted', $error);
            do_action('h4bs_indexnow_submitted', $urls, $status, count($queue));
            if ($queue !== []) {
                wp_schedule_single_event(time() + 10, 'h4bs_indexnow_submit');
            }
            return;
        }

        $retryable = $status === 0 || $status === 429 || $status >= 500;
        if ($retryable) {
            $nextRetry = 0;
            foreach ($urls as $url) {
                $entry = $queue[$url];
                $entry['attempts'] = min(20, (int) $entry['attempts'] + 1);
                $delay = min(6 * HOUR_IN_SECONDS, 60 * (2 ** min(8, (int) $entry['attempts'] - 1)));
                $entry['next_attempt'] = time() + $delay;
                $entry['last_status'] = $status;
                $queue[$url] = $entry;
                $nextRetry = $nextRetry === 0 ? (int) $entry['next_attempt'] : min($nextRetry, (int) $entry['next_attempt']);
            }
            update_option('h4bs_indexnow_queue', $queue, false);
            $this->storeIndexNowResult($status, count($urls), count($queue), 'retry_scheduled', $error, $nextRetry);
            $this->scheduleNextIndexNowAttempt($queue);
            return;
        }

        $failures = get_option('h4bs_indexnow_failures', []);
        $failures = is_array($failures) ? $failures : [];
        foreach ($urls as $url) {
            $failures[$url] = $queue[$url] + [
                'failed_at' => time(),
                'last_status' => $status,
                'message' => sanitize_text_field($error),
            ];
            unset($queue[$url]);
        }
        if (count($failures) > 1000) {
            $failures = array_slice($failures, -1000, null, true);
        }
        update_option('h4bs_indexnow_failures', $failures, false);
        update_option('h4bs_indexnow_queue', $queue, false);
        $this->storeIndexNowResult($status, count($urls), count($queue), 'rejected', $error);
    }

    public function handleIndexNowFullIndex(): void
    {
        if (! current_user_can('manage_options')) {
            wp_die(esc_html__('Unauthorized request.', 'help4-builder-suite'));
        }
        check_admin_referer('h4bs_indexnow_full_index', 'h4bs_indexnow_full_index_nonce');

        update_option('h4bs_indexnow_full_index', [
            'status' => 'running',
            'page' => 1,
            'queued' => 0,
            'started_at' => gmdate('c'),
            'completed_at' => '',
        ], false);
        $before = count($this->normalizedIndexNowQueue());
        $this->processIndexNowFullIndex();
        $queued = max(0, count($this->normalizedIndexNowQueue()) - $before);

        wp_safe_redirect(add_query_arg([
            'page' => 'h4bs-seo-control',
            'h4bs_full_index_started' => 1,
            'h4bs_full_index_queued' => $queued,
        ], admin_url('admin.php')));
        exit;
    }

    public function processIndexNowFullIndex(): void
    {
        $state = get_option('h4bs_indexnow_full_index', []);
        $state = is_array($state) ? $state : [];
        if (($state['status'] ?? '') !== 'running') {
            return;
        }

        $settings = Settings::all();
        if (
            ! rest_sanitize_boolean($settings['indexnow_enabled'] ?? '0')
            || $this->indexNowKey($settings) === ''
            || $this->isForcedNoindexHost()
            || (string) get_option('blog_public', '1') !== '1'
        ) {
            $state['status'] = 'blocked';
            $state['message'] = __('Enable IndexNow with a valid key on a public production site before starting a sweep.', 'help4-builder-suite');
            update_option('h4bs_indexnow_full_index', $state, false);
            return;
        }

        if (count($this->normalizedIndexNowQueue()) >= 1500) {
            wp_schedule_single_event(time() + 60, 'h4bs_indexnow_full_index_batch');
            return;
        }

        $postTypes = get_post_types(['public' => true], 'names');
        unset($postTypes['attachment']);
        $postTypes = array_values(array_filter($postTypes, 'is_post_type_viewable'));
        $page = max(1, absint($state['page'] ?? 1));
        $query = new \WP_Query([
            'post_type' => $postTypes,
            'post_status' => 'publish',
            'posts_per_page' => 500,
            'paged' => $page,
            'orderby' => 'ID',
            'order' => 'ASC',
            'fields' => 'ids',
            'no_found_rows' => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
        ]);
        $queued = 0;
        foreach ($query->posts as $postId) {
            if ((string) get_post_meta((int) $postId, '_h4_seo_noindex', true) === '1') {
                continue;
            }
            $url = (string) get_permalink((int) $postId);
            if ($url !== '') {
                $this->queueIndexNowUrl($url, 'full_index', false);
                $queued++;
            }
        }

        $state['queued'] = absint($state['queued'] ?? 0) + $queued;
        $state['page'] = $page + 1;
        $state['last_batch_at'] = gmdate('c');
        if (count($query->posts) < 500) {
            $state['status'] = 'complete';
            $state['completed_at'] = gmdate('c');
        } else {
            wp_schedule_single_event(time() + 30, 'h4bs_indexnow_full_index_batch');
        }
        update_option('h4bs_indexnow_full_index', $state, false);
        if ($queued > 0 && ! wp_next_scheduled('h4bs_indexnow_submit')) {
            wp_schedule_single_event(time() + 10, 'h4bs_indexnow_submit');
        }
    }

    public function filterFeedContent(string $content): string
    {
        if (! is_feed()) {
            return $content;
        }
        $settings = Settings::all();
        $before = $this->feedAttributionMarkup((string) ($settings['seo_rss_before_content'] ?? ''));
        $after = $this->feedAttributionMarkup((string) ($settings['seo_rss_after_content'] ?? ''));
        return $before . $content . $after;
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function indexNowKey(array $settings): string
    {
        $key = (string) preg_replace('/[^A-Za-z0-9-]/', '', (string) ($settings['indexnow_key'] ?? ''));
        return strlen($key) >= 8 && strlen($key) <= 128 ? $key : '';
    }

    private function queueIndexNowUrl(string $url, string $event = 'updated', bool $schedule = true): void
    {
        $settings = Settings::all();
        if (
            ! rest_sanitize_boolean($settings['indexnow_enabled'] ?? '0')
            || $this->indexNowKey($settings) === ''
            || $this->isForcedNoindexHost()
            || (string) get_option('blog_public', '1') !== '1'
        ) {
            return;
        }
        $url = esc_url_raw($url, ['http', 'https']);
        $homeHost = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        $urlHost = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
        if ($url === '' || $homeHost === '' || $urlHost !== $homeHost) {
            return;
        }
        $queue = $this->normalizedIndexNowQueue();
        $queue[$url] = [
            'queued_at' => time(),
            'event' => in_array($event, ['updated', 'deleted', 'full_index'], true) ? $event : 'updated',
            'attempts' => 0,
            'next_attempt' => 0,
            'last_status' => 0,
        ];
        if (count($queue) > 50000) {
            $queue = array_slice($queue, -50000, null, true);
        }
        update_option('h4bs_indexnow_queue', $queue, false);
        if ($schedule && ! wp_next_scheduled('h4bs_indexnow_submit')) {
            wp_schedule_single_event(time() + 10, 'h4bs_indexnow_submit');
        }
    }

    /**
     * @return array<string, array{queued_at: int, event: string, attempts: int, next_attempt: int, last_status: int}>
     */
    private function normalizedIndexNowQueue(): array
    {
        $stored = get_option('h4bs_indexnow_queue', []);
        if (! is_array($stored)) {
            return [];
        }
        $queue = [];
        foreach ($stored as $url => $entry) {
            $url = $this->sanitizeHttpUrl((string) $url);
            if ($url === '') {
                continue;
            }
            $entry = is_array($entry) ? $entry : ['queued_at' => absint($entry)];
            $event = sanitize_key((string) ($entry['event'] ?? 'updated'));
            $queue[$url] = [
                'queued_at' => max(1, absint($entry['queued_at'] ?? time())),
                'event' => in_array($event, ['updated', 'deleted', 'full_index'], true) ? $event : 'updated',
                'attempts' => absint($entry['attempts'] ?? 0),
                'next_attempt' => absint($entry['next_attempt'] ?? 0),
                'last_status' => absint($entry['last_status'] ?? 0),
            ];
        }
        return $queue;
    }

    /**
     * @param array<string, array{queued_at: int, event: string, attempts: int, next_attempt: int, last_status: int}> $queue
     */
    private function scheduleNextIndexNowAttempt(array $queue): void
    {
        if ($queue === [] || wp_next_scheduled('h4bs_indexnow_submit')) {
            return;
        }
        $timestamps = array_filter(array_map(static fn (array $entry): int => (int) $entry['next_attempt'], $queue));
        $timestamp = $timestamps === [] ? time() + 60 : max(time() + 10, min($timestamps));
        wp_schedule_single_event($timestamp, 'h4bs_indexnow_submit');
    }

    private function storeIndexNowResult(int $status, int $attempted, int $remaining, string $outcome, string $message = '', int $retryAt = 0): void
    {
        update_option('h4bs_indexnow_last_result', [
            'submitted_at' => gmdate('c'),
            'status' => $status,
            'url_count' => $attempted,
            'remaining' => $remaining,
            'outcome' => sanitize_key($outcome),
            'message' => sanitize_text_field($message),
            'retry_at' => $retryAt > 0 ? gmdate('c', $retryAt) : '',
        ], false);
    }

    private function feedAttributionMarkup(string $template): string
    {
        $template = trim($template);
        if ($template === '') {
            return '';
        }
        $postUrl = (string) get_permalink();
        $siteUrl = (string) home_url('/');
        $authorUrl = (string) get_author_posts_url((int) get_the_author_meta('ID'));
        $replacements = [
            '%POSTLINK%' => '<a href="' . esc_url($postUrl) . '">' . esc_html(get_the_title()) . '</a>',
            '%BLOGLINK%' => '<a href="' . esc_url($siteUrl) . '">' . esc_html(get_bloginfo('name')) . '</a>',
            '%AUTHORLINK%' => '<a href="' . esc_url($authorUrl) . '">' . esc_html(get_the_author()) . '</a>',
        ];
        $markup = str_replace(array_keys($replacements), array_values($replacements), esc_html($template));
        return '<p class="h4bs-rss-attribution">' . wp_kses_post($markup) . '</p>';
    }

    /**
     * @return array{description: string, canonical: string}
     */
    private function currentArchiveMeta(): array
    {
        $description = '';
        $canonical = '';
        if (is_category() || is_tag() || is_tax()) {
            $term = get_queried_object();
            if ($term instanceof \WP_Term) {
                $description = wp_strip_all_tags((string) term_description($term));
                $termUrl = get_term_link($term);
                if (! is_wp_error($termUrl)) {
                    $canonical = (string) $termUrl;
                }
            }
        } elseif (is_post_type_archive()) {
            $postType = get_query_var('post_type');
            $postType = is_array($postType) ? (string) reset($postType) : (string) $postType;
            $object = get_post_type_object($postType);
            $description = $object ? wp_strip_all_tags((string) $object->description) : '';
            $archiveUrl = get_post_type_archive_link($postType);
            $canonical = is_string($archiveUrl) ? $archiveUrl : '';
        } elseif (is_author()) {
            $authorId = absint(get_query_var('author'));
            $description = wp_strip_all_tags((string) get_the_author_meta('description', $authorId));
            $canonical = (string) get_author_posts_url($authorId);
        } elseif (is_search()) {
            $query = sanitize_text_field((string) get_search_query(false));
            $description = $query === ''
                ? __('Site search results.', 'help4-builder-suite')
                : sprintf(/* translators: %s: Search query. */ __('Search results for “%s”.', 'help4-builder-suite'), $query);
            $canonical = (string) get_search_link($query);
        } elseif (is_home()) {
            $postsPage = absint(get_option('page_for_posts'));
            $description = $postsPage > 0 ? wp_strip_all_tags((string) get_post_field('post_excerpt', $postsPage)) : '';
            $canonical = $postsPage > 0 ? (string) get_permalink($postsPage) : (string) home_url('/');
        } elseif (is_date()) {
            $canonical = (string) get_pagenum_link(1, false);
        }

        $paged = max(1, absint(get_query_var('paged')));
        if ($paged > 1) {
            $canonical = (string) get_pagenum_link($paged, false);
        }
        return [
            'description' => $this->normalizeMetaDescription($description),
            'canonical' => esc_url_raw($canonical, ['http', 'https']),
        ];
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function buildVideoSitemap(array $settings): string
    {
        $postTypes = get_post_types(['public' => true], 'names');
        unset($postTypes['attachment']);
        $posts = get_posts([
            'post_type' => array_values($postTypes),
            'post_status' => 'publish',
            'posts_per_page' => 1000,
            'orderby' => 'modified',
            'order' => 'DESC',
            'no_found_rows' => true,
        ]);
        $xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        $xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">' . "\n";
        foreach ($posts as $post) {
            if (! $post instanceof WP_Post || (string) get_post_meta($post->ID, '_h4_seo_noindex', true) === '1') {
                continue;
            }
            $video = $this->videoForPost($post, $settings);
            if ($video === []) {
                continue;
            }
            $xml .= "  <url>\n    <loc>" . $this->xml((string) get_permalink($post)) . "</loc>\n";
            $xml .= "    <video:video>\n";
            $xml .= '      <video:thumbnail_loc>' . $this->xml((string) $video['thumbnail']) . "</video:thumbnail_loc>\n";
            $xml .= '      <video:title>' . $this->xml((string) $video['title']) . "</video:title>\n";
            $xml .= '      <video:description>' . $this->xml((string) $video['description']) . "</video:description>\n";
            $locationTag = ! empty($video['content']) ? 'content_loc' : 'player_loc';
            $locationUrl = ! empty($video['content']) ? (string) $video['content'] : (string) $video['player'];
            $xml .= '      <video:' . $locationTag . '>' . $this->xml($locationUrl) . '</video:' . $locationTag . ">\n";
            $xml .= "    </video:video>\n  </url>\n";
        }
        return $xml . "</urlset>\n";
    }

    private function buildNewsSitemap(): string
    {
        $posts = get_posts([
            'post_type' => 'post',
            'post_status' => 'publish',
            'posts_per_page' => 1000,
            'orderby' => 'date',
            'order' => 'DESC',
            'date_query' => [['after' => '2 days ago', 'inclusive' => true]],
            'no_found_rows' => true,
        ]);
        $locale = str_replace('_', '-', (string) get_locale());
        $language = strtolower(substr($locale !== '' ? $locale : 'en', 0, 2));
        $xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        $xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">' . "\n";
        foreach ($posts as $post) {
            if (! $post instanceof WP_Post || (string) get_post_meta($post->ID, '_h4_seo_noindex', true) === '1') {
                continue;
            }
            $xml .= "  <url>\n    <loc>" . $this->xml((string) get_permalink($post)) . "</loc>\n";
            $xml .= "    <news:news>\n      <news:publication>\n";
            $xml .= '        <news:name>' . $this->xml((string) get_bloginfo('name')) . "</news:name>\n";
            $xml .= '        <news:language>' . $this->xml($language) . "</news:language>\n";
            $xml .= "      </news:publication>\n";
            $xml .= '      <news:publication_date>' . $this->xml(get_the_date(DATE_W3C, $post)) . "</news:publication_date>\n";
            $xml .= '      <news:title>' . $this->xml(get_the_title($post)) . "</news:title>\n";
            $xml .= "    </news:news>\n  </url>\n";
        }
        return $xml . "</urlset>\n";
    }

    /**
     * @param array<string, mixed> $settings
     * @return array<string, string>
     */
    private function videoForPost(WP_Post $post, array $settings): array
    {
        $content = (string) $post->post_content;
        preg_match_all('/(?:src|href)=["\']([^"\']+)["\']/i', $content, $matches);
        $urls = isset($matches[1]) && is_array($matches[1]) ? $matches[1] : [];
        $player = '';
        $direct = '';
        $youtubeId = '';
        foreach ($urls as $candidate) {
            $candidate = esc_url_raw(html_entity_decode((string) $candidate), ['http', 'https']);
            if ($candidate === '') {
                continue;
            }
            $host = strtolower((string) wp_parse_url($candidate, PHP_URL_HOST));
            $path = strtolower((string) wp_parse_url($candidate, PHP_URL_PATH));
            if (preg_match('/\.(?:mp4|m4v|webm|ogv)(?:$|\?)/i', $candidate)) {
                $direct = $candidate;
                break;
            }
            if (str_contains($host, 'youtube.com') || str_contains($host, 'youtu.be') || str_contains($host, 'vimeo.com')) {
                $player = $candidate;
                if (preg_match('#(?:youtube\.com/(?:embed/|watch\?v=)|youtu\.be/)([A-Za-z0-9_-]{6,})#', $candidate, $youtubeMatch)) {
                    $youtubeId = sanitize_key((string) $youtubeMatch[1]);
                }
                break;
            }
        }
        if ($direct === '' && $player === '') {
            return [];
        }

        $thumbnail = get_the_post_thumbnail_url($post, 'full');
        $thumbnail = is_string($thumbnail) ? $thumbnail : '';
        if ($thumbnail === '' && $youtubeId !== '') {
            $thumbnail = 'https://i.ytimg.com/vi/' . rawurlencode($youtubeId) . '/hqdefault.jpg';
        }
        if ($thumbnail === '') {
            $thumbnail = $this->settingsDefaultSocialImageUrl($settings);
        }
        if ($thumbnail === '') {
            return [];
        }
        $description = $this->normalizeMetaDescription((string) get_post_meta($post->ID, '_h4_seo_description', true));
        if ($description === '') {
            $description = $this->normalizeMetaDescription((string) get_the_excerpt($post));
        }
        if ($description === '') {
            $description = wp_trim_words(wp_strip_all_tags($content), 35);
        }
        return [
            'thumbnail' => $thumbnail,
            'title' => get_the_title($post),
            'description' => $description !== '' ? $description : get_the_title($post),
            'content' => $direct,
            'player' => $player,
        ];
    }

    private function xml(string $value): string
    {
        return htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8');
    }

    public function repairStaticRobotsTxt(): void
    {
        if (is_multisite()) {
            return;
        }
        $target = $this->buildRobotsTxt((string) get_option('blog_public') === '1');
        $path = trailingslashit(ABSPATH) . 'robots.txt';
        $current = is_readable($path) ? (string) @file_get_contents($path) : '';

        if ($current === $target) {
            return;
        }

        if ((is_file($path) && ! wp_is_writable($path)) || (! is_file($path) && ! wp_is_writable(ABSPATH))) {
            update_option('h4bs_static_robots_last_error', 'robots.txt is not writable', false);
            return;
        }

        // phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.ABSPATHDetected -- Writes the site's public robots.txt by design.
        if (@file_put_contents($path, $target, LOCK_EX) === false) {
            update_option('h4bs_static_robots_last_error', 'robots.txt write failed', false);
            return;
        }

        update_option('h4bs_static_robots_last_error', '', false);
        update_option('h4bs_static_robots_last_repair', gmdate('c'), false);
    }

    private function buildRobotsTxt(bool $public): string
    {
        if (! $public || $this->isForcedNoindexHost()) {
            return "User-agent: *\nDisallow: /\n";
        }

        $lines = "User-agent: *\nAllow: /\n";
        if ($this->isCoreSitemapEnabled()) {
            $lines .= sprintf(
                "\nSitemap: %s\n",
                esc_url_raw(home_url('/wp-sitemap.xml'))
            );
        }
        $settings = Settings::all();
        if (rest_sanitize_boolean($settings['seo_video_sitemap_enabled'] ?? '0')) {
            $lines .= 'Sitemap: ' . esc_url_raw(home_url('/h4-video-sitemap.xml')) . "\n";
        }
        if (rest_sanitize_boolean($settings['seo_news_sitemap_enabled'] ?? '0')) {
            $lines .= 'Sitemap: ' . esc_url_raw(home_url('/h4-news-sitemap.xml')) . "\n";
        }

        return (string) apply_filters('h4bs_robots_txt', $lines, $public);
    }

    private function buildLlmsText(): string
    {
        $settings = Settings::all();

        $siteName = $this->siteNameForMeta($settings);
        $tagline = $this->llmsPlainText((string) get_bloginfo('description'), 220);
        if ($tagline === '' || $this->isWeakDefaultText($tagline)) {
            $tagline = 'WordPress website resources, support paths, builder templates, SEO guidance, and service pages.';
        }

        $lines = [
            '# ' . $siteName,
            '',
            '> ' . $tagline,
            '',
            'This file helps AI assistants and search systems understand the public site. Use the canonical URLs listed here as the source of truth.',
            '',
            'Canonical site: ' . esc_url_raw(home_url('/')),
            'Preferred LLM guide: ' . esc_url_raw(home_url('/llms.txt')),
            'Alternate LLM guide: ' . esc_url_raw(home_url('/llm.txt')),
        ];

        if ($this->isCoreSitemapEnabled()) {
            $lines[] = 'Sitemap: ' . esc_url_raw(home_url('/wp-sitemap.xml'));
        }

        $supportUrl = $this->customerSafeContactUrl($settings);
        if ($supportUrl !== '') {
            $lines[] = 'Contact: ' . esc_url_raw($supportUrl);
        }

        $lines[] = '';
        $lines[] = '## Site Summary';
        $lines[] = '- ' . $tagline;
        $lines[] = '- Public pages, products, services, tutorials, and support details should be summarized from their canonical page URLs.';
        $lines[] = '- Do not invent pricing, guarantees, release dates, compatibility claims, or legal terms that are not stated on the canonical pages.';
        $lines[] = '';
        $lines[] = '## Important Pages';

        $pages = $this->llmsImportantPages();
        if ($pages === []) {
            $lines[] = '- [' . $siteName . '](' . esc_url_raw(home_url('/')) . '): ' . $tagline;
        } else {
            foreach ($pages as $page) {
                $lines[] = sprintf(
                    '- [%s](%s): %s',
                    $this->llmsPlainText((string) ($page['title'] ?? ''), 90),
                    esc_url_raw((string) ($page['url'] ?? '')),
                    $this->llmsPlainText((string) ($page['summary'] ?? ''), 220)
                );
            }
        }

        $lines[] = '';
        $lines[] = '## Help4 Builder Suite Context';
        $lines[] = '- This site is running Help4 Builder Suite, a WordPress builder and site operations plugin.';
        $lines[] = '- Core areas include visual template building, reusable header and footer templates, SEO scoring, readability checks, metadata, schema, robots and sitemap controls, forms, commerce/catalog foundations, security controls, performance tools, image SEO, and starter template libraries.';
        $lines[] = '- Help4 Builder Suite is designed to work with WordPress themes while pairing cleanly with the Help4 blank builder theme when a site owner wants a lighter starting point.';
        $lines[] = '';
        $lines[] = '## AI Assistant Instructions';
        $lines[] = '- Prefer the canonical URL and sitemap above when citing or summarizing this site.';
        $lines[] = '- Treat tutorials as instructional pages and keep product or service recommendations tied to the exact package pages.';
        $lines[] = '- For sales, support, or account questions, direct the user to the contact URL above and remind them to check email folders for replies from the site owner.';

        return implode("\n", array_map(static fn(string $line): string => rtrim($line), $lines)) . "\n";
    }

    /**
     * @return array<int, array{title: string, url: string, summary: string}>
     */
    private function llmsImportantPages(): array
    {
        $items = [];
        $seen = [];
        $append = function (WP_Post $post) use (&$items, &$seen): void {
            $postId = (int) $post->ID;
            if (
                $postId <= 0
                || isset($seen[$postId])
                || $post->post_status !== 'publish'
                || (string) get_post_meta($postId, '_h4_seo_noindex', true) === '1'
            ) {
                return;
            }

            $seen[$postId] = true;
            $items[] = $this->llmsPageItem($post);
        };

        $frontId = (int) get_option('page_on_front');
        $limit = (int) apply_filters('h4bs_seo_llms_important_pages_limit', 24, $frontId);
        $limit = max(8, min(50, $limit));
        if ($frontId > 0) {
            $front = get_post($frontId);
            if ($front instanceof WP_Post && $front->post_status === 'publish') {
                $append($front);
            }
        }

        $prioritySlugs = apply_filters('h4bs_seo_llms_priority_page_slugs', [], $frontId);
        if (is_array($prioritySlugs)) {
            foreach (array_slice($prioritySlugs, 0, 24) as $prioritySlug) {
                $prioritySlug = sanitize_title((string) $prioritySlug);
                if ($prioritySlug === '') {
                    continue;
                }

                $priorityPage = get_page_by_path($prioritySlug, OBJECT, 'page');
                if ($priorityPage instanceof WP_Post) {
                    $append($priorityPage);
                }
            }
        }

        $posts = get_posts([
            'post_type'              => 'page',
            'post_status'            => 'publish',
            'posts_per_page'         => 24,
            'orderby'                => [
                'menu_order' => 'ASC',
                'title'      => 'ASC',
            ],
            'no_found_rows'          => true,
            'ignore_sticky_posts'    => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
        ]);

        foreach ($posts as $post) {
            if (! $post instanceof WP_Post) {
                continue;
            }
            $append($post);
        }

        $items = array_slice(array_values(array_filter($items)), 0, $limit);
        $items = apply_filters('h4bs_seo_llms_important_pages', $items, $frontId);

        return is_array($items) ? array_slice(array_values($items), 0, $limit) : [];
    }

    /**
     * @return array{title: string, url: string, summary: string}
     */
    private function llmsPageItem(WP_Post $post): array
    {
        $title = get_the_title($post);
        if (! is_string($title) || trim($title) === '') {
            $title = 'Page ' . (string) $post->ID;
        }

        $summary = get_post_meta($post->ID, '_h4_seo_description', true);
        if (! is_string($summary) || trim($summary) === '') {
            $summary = has_excerpt($post) ? get_the_excerpt($post) : $post->post_content;
        }
        $summary = $this->llmsPlainText((string) $summary, 220);
        if ($summary === '') {
            $summary = 'Canonical public page on this site.';
        }

        $url = get_permalink($post);
        if (! is_string($url) || $url === '') {
            $url = home_url('/');
        }

        return [
            'title'   => $this->llmsPlainText($title, 90),
            'url'     => esc_url_raw($url),
            'summary' => $summary,
        ];
    }

    private function llmsPlainText(string $value, int $maxLength): string
    {
        $value = html_entity_decode(wp_strip_all_tags(strip_shortcodes($value)), ENT_QUOTES, 'UTF-8');
        $value = preg_replace('/\s+/', ' ', trim($value)) ?? '';
        if ($value === '') {
            return '';
        }

        if (function_exists('mb_strlen') && function_exists('mb_substr')) {
            if (mb_strlen($value) <= $maxLength) {
                return $value;
            }
            $slice = (string) mb_substr($value, 0, max(1, $maxLength - 3));
            $slice = preg_replace('/\s+\S*$/u', '', $slice) ?? $slice;
            return rtrim($slice, " \t\n\r\0\x0B.,;:") . '...';
        }

        if (strlen($value) <= $maxLength) {
            return $value;
        }
        $slice = substr($value, 0, max(1, $maxLength - 3));
        $slice = preg_replace('/\s+\S*$/', '', $slice) ?? $slice;
        return rtrim($slice, " \t\n\r\0\x0B.,;:") . '...';
    }

    /**
     * Keep noindex content out of the WordPress core XML sitemap.
     *
     * @param array<string, mixed> $args
     * @return array<string, mixed>
     */
    public function filterSitemapPostQueryArgs(array $args, string $postType): array
    {
        $existingMetaQuery = isset($args['meta_query']) && is_array($args['meta_query'])
            ? $args['meta_query']
            : [];

        // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Sitemap filtering intentionally excludes Help4 noindex content at query time.
        $args['meta_query'] = [
            'relation' => 'AND',
            $existingMetaQuery,
            [
                'relation' => 'OR',
                [
                    'key'     => '_h4_seo_noindex',
                    'compare' => 'NOT EXISTS',
                ],
                [
                    'key'     => '_h4_seo_noindex',
                    'value'   => '1',
                    'compare' => '!=',
                ],
            ],
        ];
        // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_query

        return $args;
    }

    public function renderHeadMeta(): void
    {
        $settings = Settings::all();
        foreach ($this->discoveryStandards->verificationMeta($settings) as $verificationTag) {
            printf(
                '<meta name="%s" content="%s">' . PHP_EOL,
                esc_attr($verificationTag['name']),
                esc_attr($verificationTag['content'])
            );
        }

        // Let Yoast own the core document metadata and graph when it is active.
        // Builder-specific verification and FAQ schema remain available.
        if (defined('WPSEO_VERSION')) {
            if (is_singular() && RobotsContract::isIndexable()) {
                $this->renderFaqSchema(absint(get_queried_object_id()));
            }

            return;
        }

        $metaTitle = trim((string) wp_get_document_title());
        $metaDescription = '';
        $canonical = '';
        $robots = '';
        $ogImage = '';
        $ogImageWidth = 0;
        $ogImageHeight = 0;
        $ogImageType = '';
        $socialTitle = '';
        $socialDescription = '';
        $schemaType = '';
        $openGraphOverride = '';
        $twitterCreator = '';
        $postId = 0;
        $forcedNoindex = $this->isForcedNoindexHost() || (string) get_option('blog_public', '1') !== '1';

        if (is_singular()) {
            $postId = absint(get_queried_object_id());
            if ($postId) {
                $metaDescription = trim((string) get_post_meta($postId, '_h4_seo_description', true));
                if ($metaDescription === '') {
                    $metaDescription = wp_strip_all_tags((string) get_the_excerpt($postId));
                }

                $canonical = $this->environmentCanonical(trim((string) get_post_meta($postId, '_h4_seo_canonical', true)), $postId);
                if ($canonical === '') {
                    $canonical = (string) get_permalink($postId);
                }

                $socialTitle = trim((string) get_post_meta($postId, '_h4_seo_social_title', true));
                $socialDescription = trim((string) get_post_meta($postId, '_h4_seo_social_description', true));
                $schemaType = sanitize_key((string) get_post_meta($postId, '_h4_seo_schema_type', true));
                $openGraphOverride = $this->sanitizeOpenGraphType((string) get_post_meta($postId, '_h4_seo_og_type', true));
                $twitterCreator = $this->sanitizeTwitterHandle((string) get_post_meta($postId, '_h4_seo_twitter_creator', true));

                $customSocialImage = $this->sanitizeHttpUrl((string) get_post_meta($postId, '_h4_seo_social_image_url', true));
                $customSocialImage = $this->sanitizeHttpUrl((string) apply_filters('h4bs_primary_social_image_url', $customSocialImage, $postId, 'open_graph'));
                if ($customSocialImage !== '') {
                    $ogImage = $customSocialImage;
                }

                $ogImageId = absint(get_post_meta($postId, '_h4_seo_og_image_id', true));
                if ($ogImage === '' && $ogImageId) {
                    $url = wp_get_attachment_image_url($ogImageId, 'full');
                    if (is_string($url) && $url !== '') {
                        $ogImage = $url;
                        [$ogImageWidth, $ogImageHeight] = $this->attachmentImageDimensions($ogImageId);
                        $ogImageType = $this->attachmentImageMimeType($ogImageId);
                    }
                }
                if ($ogImage === '' && has_post_thumbnail($postId)) {
                    $thumbnailId = get_post_thumbnail_id($postId);
                    $url = get_the_post_thumbnail_url($postId, 'full');
                    if (is_string($url) && $url !== '') {
                        $ogImage = $url;
                        [$ogImageWidth, $ogImageHeight] = $this->attachmentImageDimensions((int) $thumbnailId);
                        $ogImageType = $this->attachmentImageMimeType((int) $thumbnailId);
                    }
                }
            }
        } else {
            $archiveMeta = $this->currentArchiveMeta();
            $metaDescription = (string) ($archiveMeta['description'] ?? '');
            $canonical = (string) ($archiveMeta['canonical'] ?? '');
        }

        if ($metaDescription === '') {
            $metaDescription = trim((string) get_bloginfo('description'));
        }
        $metaDescription = $this->normalizeMetaDescription($metaDescription);
        $socialTitle = $socialTitle !== '' ? $socialTitle : $metaTitle;
        $socialDescription = $socialDescription !== '' ? $this->normalizeMetaDescription($socialDescription) : $metaDescription;
        if (
            $metaDescription === '' ||
            strcasecmp($metaDescription, 'Just another WordPress site') === 0
        ) {
            $metaDescription = $this->metaDescriptionFallback($settings);
        }
        if ($socialDescription === '') {
            $socialDescription = $metaDescription;
        }
        if (
            (is_front_page() || is_home()) &&
            (
                stripos($metaTitle, 'My blog') !== false ||
                stripos($metaTitle, 'Just another WordPress site') !== false
            )
        ) {
            $metaTitle = $this->homeTitleFallback($settings);
        }
        if ($socialTitle === '' || $this->isWeakDefaultText($socialTitle)) {
            $socialTitle = $metaTitle;
        }
        if ($canonical === '' && (is_front_page() || is_home())) {
            $canonical = (string) home_url('/');
        }
        if ($ogImage === '') {
            $defaultSocialImage = $this->settingsDefaultSocialImageUrl($settings);
            if ($defaultSocialImage !== '') {
                $ogImage = $defaultSocialImage;
            }
        }
        if ($ogImage === '') {
            $help4SocialCard = $this->help4SocialCardForContext($postId);
            if ($help4SocialCard !== '') {
                $ogImage = $help4SocialCard;
                $cardPath = (string) wp_parse_url($help4SocialCard, PHP_URL_PATH);
                $cardExtension = strtolower((string) pathinfo($cardPath, PATHINFO_EXTENSION));
                if (basename($cardPath) === 'help4-wordpress-hosting-social-20260716.jpg') {
                    $ogImageWidth = 1200;
                    $ogImageHeight = 630;
                    $ogImageType = 'image/jpeg';
                } elseif (in_array($cardExtension, ['jpg', 'jpeg'], true)) {
                    $ogImageWidth = 1400;
                    $ogImageHeight = 875;
                    $ogImageType = 'image/jpeg';
                } else {
                    $ogImageWidth = 1200;
                    $ogImageHeight = 630;
                    $ogImageType = 'image/png';
                }
            }
        }
        if ($ogImage === '') {
            $siteIcon = get_site_icon_url(512);
            if (is_string($siteIcon) && $siteIcon !== '') {
                $ogImage = $siteIcon;
            }
        }
        $filteredOgImage = $this->sanitizeHttpUrl((string) apply_filters('h4bs_social_image_url', $ogImage, $postId, 'open_graph'));
        if ($filteredOgImage !== $ogImage) {
            $ogImageWidth = 0;
            $ogImageHeight = 0;
            $ogImageType = '';
        }
        $ogImage = $filteredOgImage;
        $robots = $this->buildRobotsMeta($postId, $forcedNoindex);

        if ($metaDescription !== '') {
            printf('<meta name="description" content="%s">' . PHP_EOL, esc_attr($metaDescription));
        }
        printf('<meta name="robots" content="%s">' . PHP_EOL, esc_attr($robots));
        if ($canonical !== '') {
            printf('<link rel="canonical" href="%s">' . PHP_EOL, esc_url($canonical));
        }
        $languageTag = $this->discoveryStandards->currentLanguageTag();
        $locale = $this->discoveryStandards->openGraphLocale($languageTag);
        $hreflangAlternates = $this->hreflangAlternatesForContext($postId, $canonical);
        foreach ($hreflangAlternates as $alternateLanguage => $alternateUrl) {
            printf(
                '<link rel="alternate" hreflang="%s" href="%s">' . PHP_EOL,
                esc_attr($alternateLanguage),
                esc_url($alternateUrl)
            );
        }
        printf('<meta property="og:locale" content="%s">' . PHP_EOL, esc_attr($locale));
        foreach ($hreflangAlternates as $alternateLanguage => $alternateUrl) {
            if ($alternateLanguage === 'x-default' || $alternateLanguage === $languageTag) {
                continue;
            }
            printf(
                '<meta property="og:locale:alternate" content="%s">' . PHP_EOL,
                esc_attr($this->discoveryStandards->openGraphLocale($alternateLanguage))
            );
        }
        if ($socialTitle !== '') {
            printf('<meta property="og:title" content="%s">' . PHP_EOL, esc_attr($socialTitle));
            printf('<meta name="twitter:title" content="%s">' . PHP_EOL, esc_attr($socialTitle));
        }
        if ($socialDescription !== '') {
            printf('<meta property="og:description" content="%s">' . PHP_EOL, esc_attr($socialDescription));
            printf('<meta name="twitter:description" content="%s">' . PHP_EOL, esc_attr($socialDescription));
        }
        if ($canonical !== '') {
            printf('<meta property="og:url" content="%s">' . PHP_EOL, esc_url($canonical));
        }
        $automaticOpenGraphType = in_array($schemaType, ['article', 'blogposting'], true) || is_singular('post')
            ? 'article'
            : ($schemaType === 'product' ? 'product' : 'website');
        $openGraphType = $openGraphOverride !== '' ? $openGraphOverride : $automaticOpenGraphType;
        $openGraphType = $this->sanitizeOpenGraphType((string) apply_filters('h4bs_open_graph_type', $openGraphType, $postId, $schemaType));
        if ($openGraphType === '') {
            $openGraphType = $automaticOpenGraphType;
        }
        printf('<meta property="og:type" content="%s">' . PHP_EOL, esc_attr($openGraphType));
        $siteName = $this->siteNameForMeta($settings);
        printf('<meta property="og:site_name" content="%s">' . PHP_EOL, esc_attr($siteName));
        printf(
            '<meta name="twitter:card" content="%s">' . PHP_EOL,
            esc_attr($ogImage !== '' ? 'summary_large_image' : 'summary')
        );
        $twitterSite = trim((string) ($settings['twitter_site'] ?? ''));
        if ($twitterSite !== '') {
            printf('<meta name="twitter:site" content="%s">' . PHP_EOL, esc_attr($twitterSite));
        }
        $twitterCreator = $this->sanitizeTwitterHandle((string) apply_filters('h4bs_twitter_creator', $twitterCreator, $postId));
        if ($twitterCreator !== '') {
            printf('<meta name="twitter:creator" content="%s">' . PHP_EOL, esc_attr($twitterCreator));
        }
        if ($ogImage !== '') {
            $socialImageAlt = $this->socialImageAltForPost($postId, $settings, $socialTitle !== '' ? $socialTitle : $siteName);
            printf('<meta property="og:image" content="%s">' . PHP_EOL, esc_url($ogImage));
            printf('<meta property="og:image:secure_url" content="%s">' . PHP_EOL, esc_url($ogImage));
            printf('<meta name="twitter:image" content="%s">' . PHP_EOL, esc_url($ogImage));
            printf('<meta property="og:image:alt" content="%s">' . PHP_EOL, esc_attr($socialImageAlt));
            printf('<meta name="twitter:image:alt" content="%s">' . PHP_EOL, esc_attr($socialImageAlt));
            if ($ogImageWidth > 0 && $ogImageHeight > 0) {
                printf('<meta property="og:image:width" content="%d">' . PHP_EOL, absint($ogImageWidth));
                printf('<meta property="og:image:height" content="%d">' . PHP_EOL, absint($ogImageHeight));
            }
            if ($ogImageType !== '') {
                printf('<meta property="og:image:type" content="%s">' . PHP_EOL, esc_attr($ogImageType));
            }
        }
        if ($postId > 0 && $openGraphType === 'article') {
            printf('<meta property="article:published_time" content="%s">' . PHP_EOL, esc_attr(get_the_date(DATE_W3C, $postId)));
            printf('<meta property="article:modified_time" content="%s">' . PHP_EOL, esc_attr(get_the_modified_date(DATE_W3C, $postId)));
            $authorId = absint(get_post_field('post_author', $postId));
            if ($authorId > 0) {
                printf('<meta property="article:author" content="%s">' . PHP_EOL, esc_url(get_author_posts_url($authorId)));
            }
            $categories = get_the_category($postId);
            if (isset($categories[0]) && is_object($categories[0])) {
                printf('<meta property="article:section" content="%s">' . PHP_EOL, esc_attr((string) $categories[0]->name));
            }
            $tags = get_the_tags($postId);
            foreach (is_array($tags) ? array_slice($tags, 0, 20) : [] as $tag) {
                if (is_object($tag) && isset($tag->name)) {
                    printf('<meta property="article:tag" content="%s">' . PHP_EOL, esc_attr((string) $tag->name));
                }
            }
        }

        if (RobotsContract::isIndexable()) {
            $this->renderOrganizationSchema($settings);
            $this->renderWebsiteSchema($settings);
            if (is_singular()) {
                $breadcrumbId = $this->renderBreadcrumbSchema($postId, $canonical, $settings);
                $this->renderContentSchema($postId, $schemaType, $metaTitle, $metaDescription, $canonical, $ogImage, $settings, $breadcrumbId);
                $this->renderFaqSchema($postId);
                if ($openGraphType === 'article') {
                    $this->renderAuthorSchema(absint(get_post_field('post_author', $postId)), false);
                }
            } elseif (is_author()) {
                $this->renderAuthorSchema(absint(get_query_var('author')), true);
            }
        }
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function renderOrganizationSchema(array $settings): void
    {
        $brandName = $this->siteNameForMeta($settings);
        $supportUrl = $this->customerSafeContactUrl($settings);

        $schema = [
            '@context'     => 'https://schema.org',
            '@type'        => 'Organization',
            '@id'          => home_url('/') . '#organization',
            'name'         => $brandName ?: get_bloginfo('name'),
            'url'          => home_url('/'),
        ];
        $customLogoId = absint(get_theme_mod('custom_logo'));
        if ($customLogoId > 0) {
            $logoUrl = wp_get_attachment_image_url($customLogoId, 'full');
            if (is_string($logoUrl) && $logoUrl !== '') {
                $schema['logo'] = [
                    '@type' => 'ImageObject',
                    '@id' => home_url('/') . '#logo',
                    'url' => $logoUrl,
                ];
            }
        }
        $businessPhone = sanitize_text_field((string) ($settings['business_phone'] ?? ''));
        if ($supportUrl !== '' || $businessPhone !== '') {
            $contactPoint = [
                '@type' => 'ContactPoint',
                'contactType' => 'customer support',
            ];
            if ($supportUrl !== '') {
                $contactPoint['url'] = $supportUrl;
            }
            if ($businessPhone !== '') {
                $contactPoint['telephone'] = $businessPhone;
            }
            $schema['contactPoint'] = [$contactPoint];
        }
        $sameAs = $this->sameAsUrls($settings);
        if ($sameAs !== []) {
            $schema['sameAs'] = $sameAs;
        }

        $schema = apply_filters('h4bs_organization_schema', $schema, $settings);
        echo '<script type="application/ld+json">' . wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . "</script>\n";
    }

    private function renderAuthorSchema(int $authorId, bool $profilePage): void
    {
        $user = get_userdata($authorId);
        if (! $user instanceof WP_User) {
            return;
        }
        $authorUrl = (string) get_author_posts_url($authorId);
        $person = [
            '@type' => 'Person',
            '@id' => $authorUrl . '#person',
            'name' => (string) $user->display_name,
            'url' => $authorUrl,
        ];
        $description = trim(wp_strip_all_tags((string) get_the_author_meta('description', $authorId)));
        if ($description !== '') {
            $person['description'] = $description;
        }
        $jobTitle = trim((string) get_user_meta($authorId, '_h4_seo_job_title', true));
        if ($jobTitle !== '') {
            $person['jobTitle'] = $jobTitle;
        }
        $credentials = trim((string) get_user_meta($authorId, '_h4_seo_credentials', true));
        if ($credentials !== '') {
            $person['honorificSuffix'] = $credentials;
        }
        $expertise = get_user_meta($authorId, '_h4_seo_expertise', true);
        if (is_array($expertise) && $expertise !== []) {
            $person['knowsAbout'] = array_values(array_map('strval', $expertise));
        }
        $sameAs = get_user_meta($authorId, '_h4_seo_profile_urls', true);
        if (is_array($sameAs) && $sameAs !== []) {
            $person['sameAs'] = array_values(array_map('strval', $sameAs));
        }
        $avatar = get_avatar_url($authorId, ['size' => 512]);
        if (is_string($avatar) && $avatar !== '') {
            $person['image'] = $avatar;
        }
        if ($profilePage) {
            $schema = [
                '@context' => 'https://schema.org',
                '@graph' => [
                    $person,
                    [
                        '@type' => 'ProfilePage',
                        '@id' => $authorUrl . '#profile',
                        'url' => $authorUrl,
                        'name' => sprintf(/* translators: %s: Author display name. */ __('Articles by %s', 'help4-builder-suite'), (string) $user->display_name),
                        'isPartOf' => ['@id' => home_url('/') . '#website'],
                        'mainEntity' => ['@id' => $authorUrl . '#person'],
                    ],
                ],
            ];
        } else {
            $schema = ['@context' => 'https://schema.org'] + $person;
        }
        echo '<script type="application/ld+json" class="h4bs-author-schema">' . wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . "</script>\n";
    }

    private function isForcedNoindexHost(): bool
    {
        $home = (string) home_url('/');
        if ($home === '') {
            return false;
        }

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

        return $host === 'md.help4wordpress.com';
    }

    private function isHelp4OwnedHost(): bool
    {
        $host = strtolower((string) wp_parse_url((string) home_url('/'), PHP_URL_HOST));
        if ($host === '') {
            return false;
        }

        return in_array($host, [
            'help4wordpress.com',
            'www.help4wordpress.com',
            'md.help4wordpress.com',
            'plugin.help4wordpress.com',
            'plugins.help4wordpress.com',
        ], true);
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function sameAsUrls(array $settings): array
    {
        $urls = $settings['same_as_urls'] ?? [];
        if (is_string($urls)) {
            $urls = preg_split('/\r\n|\r|\n/', $urls) ?: [];
        }
        foreach (['facebook_url', 'linkedin_url', 'youtube_url'] as $legacyKey) {
            if (! empty($settings[$legacyKey])) {
                $urls[] = $settings[$legacyKey];
            }
        }
        $clean = [];
        if (is_array($urls)) {
            foreach ($urls as $url) {
                $url = $this->sanitizeHttpUrl((string) $url);
                if ($url !== '') {
                    $clean[] = $url;
                }
            }
        }
        return array_slice(array_values(array_unique($clean)), 0, 30);
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function settingsDefaultSocialImageUrl(array $settings): string
    {
        $url = trim((string) ($settings['default_social_image_url'] ?? ''));
        if ($url === '') {
            return '';
        }

        $url = esc_url_raw($url, ['http', 'https']);
        if ($url === '') {
            return '';
        }

        $scheme = strtolower((string) wp_parse_url($url, PHP_URL_SCHEME));
        return in_array($scheme, ['http', 'https'], true) ? $url : '';
    }

    private function sanitizeHttpUrl(string $url): string
    {
        $url = esc_url_raw(trim($url), ['http', 'https']);
        $scheme = strtolower((string) wp_parse_url($url, PHP_URL_SCHEME));
        return in_array($scheme, ['http', 'https'], true) ? $url : '';
    }

    private function sanitizeOpenGraphType(string $type): string
    {
        $type = strtolower(trim($type));
        return preg_match('/^[a-z][a-z0-9._:-]{0,63}$/', $type) ? $type : '';
    }

    private function sanitizeTwitterHandle(string $handle): string
    {
        $handle = trim($handle);
        if (filter_var($handle, FILTER_VALIDATE_URL)) {
            $handle = basename(trim((string) wp_parse_url($handle, PHP_URL_PATH), '/'));
        }
        $handle = ltrim($handle, '@');
        $handle = (string) preg_replace('/[^A-Za-z0-9_]/', '', $handle);
        return $handle !== '' ? '@' . substr($handle, 0, 15) : '';
    }

    /**
     * @return array<string, string>
     */
    private function environmentCanonical(string $explicit, int $postId): string
    {
        if ($explicit === '') return '';
        $explicit = $this->sanitizeHttpUrl($explicit);
        if ($explicit === '') return '';
        $currentHost = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        $explicitHost = strtolower((string) wp_parse_url($explicit, PHP_URL_HOST));
        if ($currentHost !== '' && $explicitHost !== '' && ! hash_equals($currentHost, $explicitHost)) {
            $path = (string) wp_parse_url($explicit, PHP_URL_PATH);
            if ($path === '' && $postId > 0) return (string) get_permalink($postId);
            return home_url('/' . ltrim($path, '/'));
        }
        return $explicit;
    }

    private function hreflangAlternatesForContext(int $postId, string $canonical): array
    {
        $stored = $postId > 0 ? get_post_meta($postId, '_h4_seo_hreflang', true) : [];
        $alternates = $this->discoveryStandards->sanitizeHreflangAlternates($stored);
        $filtered = apply_filters('h4bs_hreflang_alternates', $alternates, $postId, $canonical);
        $alternates = $this->discoveryStandards->sanitizeHreflangAlternates(is_array($filtered) ? $filtered : $alternates);
        $canonical = $this->sanitizeHttpUrl($canonical);
        if ($alternates === [] || $canonical === '') {
            return $alternates;
        }

        $currentLanguage = $this->discoveryStandards->currentLanguageTag();
        if (! isset($alternates[$currentLanguage])) {
            $alternates = [$currentLanguage => $canonical] + $alternates;
        }
        return array_slice($alternates, 0, 50, true);
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function socialImageAltForPost(int $postId, array $settings, string $fallback): string
    {
        $alt = $postId > 0 ? trim((string) get_post_meta($postId, '_h4_seo_social_image_alt', true)) : '';
        if ($alt === '' && $postId > 0) {
            $attachmentId = absint(get_post_meta($postId, '_h4_seo_og_image_id', true));
            if ($attachmentId === 0) {
                $attachmentId = absint(get_post_thumbnail_id($postId));
            }
            if ($attachmentId > 0) {
                $alt = trim((string) get_post_meta($attachmentId, '_wp_attachment_image_alt', true));
            }
        }
        if ($alt === '') {
            $alt = trim((string) ($settings['default_social_image_alt'] ?? ''));
        }
        if ($alt === '') {
            $alt = $fallback;
        }
        $alt = sanitize_text_field((string) apply_filters('h4bs_social_image_alt', $alt, $postId));
        return substr($alt, 0, 420);
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function primarySocialImageUrlForPost(int $postId, array $settings): string
    {
        $url = $this->sanitizeHttpUrl((string) get_post_meta($postId, '_h4_seo_social_image_url', true));
        $url = $this->sanitizeHttpUrl((string) apply_filters('h4bs_primary_social_image_url', $url, $postId, 'health'));
        if ($url !== '') {
            return $url;
        }
        $attachmentId = absint(get_post_meta($postId, '_h4_seo_og_image_id', true));
        if ($attachmentId > 0) {
            $attachmentUrl = wp_get_attachment_image_url($attachmentId, 'full');
            if (is_string($attachmentUrl) && $attachmentUrl !== '') {
                return $attachmentUrl;
            }
        }
        $featured = get_the_post_thumbnail_url($postId, 'full');
        if (is_string($featured) && $featured !== '') {
            return $featured;
        }
        return $this->settingsDefaultSocialImageUrl($settings);
    }

    /**
     * @param array<string, mixed> $result
     */
    private function indexNowResultSummary(array $result): string
    {
        if ($result === []) {
            return __('No submission has been attempted yet.', 'help4-builder-suite');
        }
        $status = absint($result['status'] ?? 0);
        $outcome = sanitize_key((string) ($result['outcome'] ?? 'unknown'));
        $count = absint($result['url_count'] ?? 0);
        $remaining = absint($result['remaining'] ?? 0);
        $when = sanitize_text_field((string) ($result['submitted_at'] ?? ''));
        return sprintf(
            /* translators: 1: Outcome. 2: HTTP status. 3: Attempted URL count. 4: Remaining URL count. 5: Timestamp. */
            __('%1$s; HTTP %2$d; %3$d attempted, %4$d remaining; %5$s', 'help4-builder-suite'),
            $outcome,
            $status,
            $count,
            $remaining,
            $when
        );
    }

    /**
     * @param array<string, mixed> $state
     */
    private function fullIndexSummary(array $state): string
    {
        if ($state === []) {
            return __('No full-site sweep has been started.', 'help4-builder-suite');
        }
        return sprintf(
            /* translators: 1: State. 2: Queued URL count. 3: Current page. */
            __('%1$s; %2$d URL(s) queued through batch %3$d.', 'help4-builder-suite'),
            sanitize_key((string) ($state['status'] ?? 'unknown')),
            absint($state['queued'] ?? 0),
            max(0, absint($state['page'] ?? 1) - 1)
        );
    }

    /**
     * @param array<string, mixed> $report
     */
    private function metadataHealthSummary(array $report): string
    {
        if ($report === []) {
            return __('Run the health check to inspect public content and sitemap pages.', 'help4-builder-suite');
        }
        $missing = is_array($report['missing'] ?? null) ? $report['missing'] : [];
        return sprintf(
            /* translators: 1: Scanned count. 2: Missing title count. 3: Missing description count. 4: Missing canonical count. 5: Missing image count. 6: Duplicate title count. 7: Duplicate description count. */
            __('%1$d scanned; missing title %2$d, description %3$d, canonical %4$d, image %5$d; duplicate titles %6$d, descriptions %7$d.', 'help4-builder-suite'),
            absint($report['scanned'] ?? 0),
            absint($missing['title'] ?? 0),
            absint($missing['description'] ?? 0),
            absint($missing['canonical'] ?? 0),
            absint($missing['social_image'] ?? 0),
            absint($report['duplicate_titles'] ?? 0),
            absint($report['duplicate_descriptions'] ?? 0)
        );
    }

    private function help4SocialCardForContext(int $postId): string
    {
        if (! $this->isHelp4OwnedHost() || ! defined('H4BS_URL')) {
            return '';
        }

        $slug = '';
        if ($postId > 0) {
            $post = get_post($postId);
            if ($post instanceof WP_Post) {
                $slug = sanitize_title((string) $post->post_name);
            }
        }

        $requestUri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_URI'])) : '';
        $path = trim((string) wp_parse_url($requestUri, PHP_URL_PATH), '/');
        $path = sanitize_title(basename($path));

        $key = $slug !== '' ? $slug : $path;
        if ($key === '' && (is_front_page() || is_home())) {
            $key = 'home';
        }

        $cards = [
            'home' => 'help4-wordpress-social.png',
            'plugin-help4-builder-suite' => 'builder-suite-social.png',
            'help4-builder-tutorials' => 'tutorials-social.png',
            'help4-builder-screenshots' => 'screenshots-social.png',
            'recent-work' => 'recent-work-social.png',
            'help4-builder-website-snapshot' => 'website-snapshot-social.png',
        ];

        $salesCards = [
            'home',
            'packages',
            'shop',
            'fully-managed-wordpress-hosting',
            'wordpress-support',
            'wordpress-hosting-faq',
            'help-4-wordpress',
            'wordpress-maintenance-care-plans',
            'emergency-wordpress-support',
            'wordpress-migration-service',
            'wordpress-speed-optimization',
            'wordpress-malware-removal',
            'design-to-wordpress',
            'white-label-wordpress-support',
        ];
        if (in_array($key, $salesCards, true)) {
            $relative = 'assets/images/social/help4-wordpress-hosting-social-20260716.jpg';
        } else {
            $filename = $cards[$key] ?? 'default-social.png';
            $relative = 'assets/images/social/' . $filename;
        }
        $pathOnDisk = defined('H4BS_PATH') ? rtrim((string) H4BS_PATH, '/\\') . DIRECTORY_SEPARATOR . $relative : '';
        if ($pathOnDisk !== '' && ! is_readable($pathOnDisk)) {
            return '';
        }

        return rtrim((string) H4BS_URL, '/') . '/' . $relative;
    }

    private function isCoreSitemapEnabled(): bool
    {
        if ($this->isForcedNoindexHost()) {
            return false;
        }

        if ((string) get_option('blog_public', '1') !== '1') {
            return false;
        }

        if (! function_exists('wp_sitemaps_get_server')) {
            return false;
        }

        /**
         * Mirrors WordPress core sitemap gate used by wp-sitemaps.php.
         *
         * @var mixed $enabled
         */
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core sitemap hook.
        $enabled = apply_filters('wp_sitemaps_enabled', true);
        return (bool) $enabled;
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function renderWebsiteSchema(array $settings): void
    {
        $name = $this->siteNameForMeta($settings);
        $home = (string) home_url('/');
        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'WebSite',
            '@id' => $home . '#website',
            'name' => $name !== '' ? $name : get_bloginfo('name'),
            'url' => $home,
            'publisher' => ['@id' => $home . '#organization'],
            'potentialAction' => [
                '@type' => 'SearchAction',
                'target' => add_query_arg('s', '{search_term_string}', $home),
                'query-input' => 'required name=search_term_string',
            ],
        ];

        echo '<script type="application/ld+json">' . wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . "</script>\n";
    }

    private function renderFaqSchema(int $postId): void
    {
        $entities = [];
        foreach ($this->faqItems($postId) as $row) {
            $entities[] = [
                '@type' => 'Question',
                'name' => $row['question'],
                'acceptedAnswer' => [
                    '@type' => 'Answer',
                    'text' => $row['answer'],
                ],
            ];
        }

        if ($entities === []) {
            return;
        }

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'FAQPage',
            '@id' => (string) get_permalink($postId) . '#faq',
            'isPartOf' => ['@id' => (string) get_permalink($postId) . '#main'],
            'mainEntity' => $entities,
        ];

        echo '<script type="application/ld+json">' . wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . "</script>\n";
    }

    private function schemaAvailability(string $availability): string
    {
        return match (sanitize_key($availability)) {
            'in_stock' => 'https://schema.org/InStock',
            'preorder' => 'https://schema.org/PreOrder',
            'backorder' => 'https://schema.org/BackOrder',
            default => 'https://schema.org/OutOfStock',
        };
    }

    private function schemaItemCondition(string $condition): string
    {
        return match (sanitize_key($condition)) {
            'used' => 'https://schema.org/UsedCondition',
            'refurbished' => 'https://schema.org/RefurbishedCondition',
            default => 'https://schema.org/NewCondition',
        };
    }

    /**
     * @param mixed $atts
     */
    public function renderBreadcrumbsShortcode($atts): string
    {
        if (! is_singular()) {
            return '';
        }

        $items = $this->breadcrumbItems(absint(get_queried_object_id()), Settings::all());
        if (count($items) < 2) {
            return '';
        }

        ob_start();
        ?>
        <nav class="h4-seo-breadcrumbs" aria-label="<?php esc_attr_e('Breadcrumb', 'help4-builder-suite'); ?>" style="font-size:.92rem;margin:0 0 1rem;">
            <ol style="display:flex;flex-wrap:wrap;gap:.35rem;align-items:center;list-style:none;margin:0;padding:0;">
                <?php foreach ($items as $index => $item) : ?>
                    <li style="display:flex;gap:.35rem;align-items:center;">
                        <?php if ($index > 0) : ?><span aria-hidden="true">/</span><?php endif; ?>
                        <?php if ($index < count($items) - 1 && (string) ($item['url'] ?? '') !== '') : ?>
                            <a href="<?php echo esc_url((string) $item['url']); ?>"><?php echo esc_html((string) ($item['name'] ?? '')); ?></a>
                        <?php else : ?>
                            <span aria-current="page"><?php echo esc_html((string) ($item['name'] ?? '')); ?></span>
                        <?php endif; ?>
                    </li>
                <?php endforeach; ?>
            </ol>
        </nav>
        <?php
        return (string) ob_get_clean();
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function renderBreadcrumbSchema(int $postId, string $canonical, array $settings): string
    {
        $items = $this->breadcrumbItems($postId, $settings);
        if (count($items) < 2) {
            return '';
        }

        $schemaItems = [];
        foreach ($items as $index => $item) {
            $schemaItems[] = [
                '@type' => 'ListItem',
                'position' => $index + 1,
                'name' => $item['name'],
                'item' => $item['url'],
            ];
        }

        $schemaBase = $this->normalizeBreadcrumbUrl($canonical !== '' ? $canonical : (string) get_permalink($postId));
        if ($schemaBase === '') {
            return '';
        }
        $breadcrumbId = $schemaBase . '#breadcrumb';
        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'BreadcrumbList',
            '@id' => $breadcrumbId,
            'itemListElement' => $schemaItems,
        ];

        echo '<script type="application/ld+json">' . wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . "</script>\n";

        return $breadcrumbId;
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function renderContentSchema(int $postId, string $schemaTypeKey, string $title, string $description, string $canonical, string $image, array $settings, string $breadcrumbId = ''): void
    {
        if ($postId <= 0) {
            return;
        }

        $schemaType = $this->schemaTypeName($schemaTypeKey);
        if ($schemaType === 'FAQPage') {
            return;
        }

        if ($schemaTypeKey === 'courselist') {
            $records = apply_filters('h4bs_visible_course_records', [], $postId);
            $list = $this->performingArtsSchema->courseItemList(is_array($records) ? $records : [], $canonical !== '' ? $canonical : (string) get_permalink($postId));
            $list = is_array($list) ? $this->finalizeContentSchema($list, $postId, $settings, '') : [];
            if ($list !== []) echo '<script type="application/ld+json" class="h4bs-course-list-schema">' . wp_json_encode($list, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . "</script>\n";
            return;
        }

        $charset = (string) get_bloginfo('charset');
        $charset = $charset !== '' ? $charset : 'UTF-8';
        $title = sanitize_text_field(html_entity_decode(wp_strip_all_tags($title), ENT_QUOTES | ENT_HTML5, $charset));
        $description = sanitize_text_field(html_entity_decode(wp_strip_all_tags($description), ENT_QUOTES | ENT_HTML5, $charset));

        if ($schemaType === 'RealEstateListing') {
            $realEstate = $this->realEstateSchema->forPost($postId);
            $shouldRender = (bool) apply_filters('h4bs_should_render_real_estate_schema', true, $postId, $realEstate);
            if (! $shouldRender) {
                return;
            }
            $graph = $this->realEstateSchema->buildGraph($postId, $realEstate, $title, $description, $canonical, $image, $settings);
            $graph = is_array($graph) ? $this->finalizeContentSchema($graph, $postId, $settings, $breadcrumbId) : [];
            if ($graph !== []) {
                echo '<script type="application/ld+json" class="h4bs-real-estate-schema">' . wp_json_encode($graph, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . "</script>\n";
            }
            return;
        }

        if (in_array($schemaType, ['Event', 'Course'], true) && ! $this->performingArtsSchema->eligible($schemaType, $this->performingArtsSchema->forPost($postId))) {
            $schemaType = 'WebPage';
        }

        $permalink = $canonical !== '' ? $canonical : (string) get_permalink($postId);
        $title = $title !== '' ? $title : get_the_title($postId);
        $title = sanitize_text_field(html_entity_decode(wp_strip_all_tags($title), ENT_QUOTES | ENT_HTML5, $charset));
        $schema = [
            '@context' => 'https://schema.org',
            '@type' => $schemaType,
            '@id' => $permalink . '#main',
            'url' => $permalink,
            'name' => $title,
            'description' => $description,
            'inLanguage' => str_replace('_', '-', (string) get_locale()),
            'isPartOf' => [
                '@type' => 'WebSite',
                '@id' => home_url('/') . '#website',
                'url' => home_url('/'),
                'name' => $this->siteNameForMeta($settings),
            ],
            'datePublished' => get_the_date(DATE_W3C, $postId),
            'dateModified' => get_the_modified_date(DATE_W3C, $postId),
        ];
        if ($breadcrumbId !== '') {
            $schema['breadcrumb'] = ['@id' => $breadcrumbId];
        }

        if ($image !== '') {
            $schema['image'] = $image;
            $schema['thumbnailUrl'] = $image;
        }

        if (in_array($schemaType, ['Article', 'BlogPosting'], true)) {
            $authorId = (int) get_post_field('post_author', $postId);
            $schema['headline'] = $title;
            $schema['author'] = $authorId > 0
                ? ['@type' => 'Person', '@id' => get_author_posts_url($authorId) . '#person', 'name' => get_the_author_meta('display_name', $authorId)]
                : ['@type' => 'Organization', '@id' => home_url('/') . '#organization', 'name' => get_bloginfo('name')];
            $schema['publisher'] = ['@id' => home_url('/') . '#organization'];
        }

        if ($schemaType === 'Service') {
            $schema['provider'] = ['@id' => home_url('/') . '#organization'];
            $schema['serviceType'] = $title;
        }

        if ($schemaType === 'SoftwareApplication') {
            $schema['applicationCategory'] = 'DeveloperApplication';
            $schema['applicationSubCategory'] = 'WebsiteBuilder';
            $schema['operatingSystem'] = 'WordPress';
            $schema['softwareVersion'] = defined('H4BS_VERSION') ? (string) H4BS_VERSION : '';
            $schema['downloadUrl'] = home_url('/help4-builder-suite/latest.zip');
            $schema['publisher'] = ['@id' => home_url('/') . '#organization'];
            $schema['offers'] = [
                '@type' => 'Offer',
                'price' => '0.00',
                'priceCurrency' => 'USD',
                'url' => $permalink,
            ];
            $filteredSoftwareSchema = apply_filters('h4bs_seo_software_application_schema', $schema, $postId, $settings);
            if (! is_array($filteredSoftwareSchema)) {
                return;
            }
            $schema = $filteredSoftwareSchema;
        }

        if ($schemaType === 'Product') {
            $commerceProductId = absint(get_post_meta($postId, '_h4_seo_commerce_product_id', true));
            $commerceProduct = $this->commerceDiscovery->productById($commerceProductId, $settings);
            if (is_array($commerceProduct)) {
                $schema['name'] = (string) ($commerceProduct['name'] ?? $schema['name']);
                $schema['description'] = (string) ($commerceProduct['description'] ?? $schema['description']);
                $schema['sku'] = (string) ($commerceProduct['sku'] ?? '');
                $schema['brand'] = [
                    '@type' => 'Brand',
                    'name' => (string) ($commerceProduct['brand'] ?? $this->siteNameForMeta($settings)),
                ];
                if ((string) ($commerceProduct['image'] ?? '') !== '') {
                    $schema['image'] = (string) $commerceProduct['image'];
                    $schema['thumbnailUrl'] = (string) $commerceProduct['image'];
                }
                $gtin = (string) ($commerceProduct['gtin'] ?? '');
                if (in_array(strlen($gtin), [8, 12, 13, 14], true)) {
                    $schema['gtin' . strlen($gtin)] = $gtin;
                }
                if ((string) ($commerceProduct['mpn'] ?? '') !== '') {
                    $schema['mpn'] = (string) $commerceProduct['mpn'];
                }
                if ((string) ($commerceProduct['product_category'] ?? '') !== '') {
                    $schema['category'] = (string) $commerceProduct['product_category'];
                }
                $offerUrl = (string) ($commerceProduct['landing_url'] ?? '');
                $schema['offers'] = [
                    '@type' => 'Offer',
                    'url' => $offerUrl !== '' ? $offerUrl : $permalink,
                    'price' => number_format((float) ($commerceProduct['price'] ?? 0), 2, '.', ''),
                    'priceCurrency' => (string) ($commerceProduct['currency'] ?? ''),
                    'availability' => $this->schemaAvailability((string) ($commerceProduct['availability'] ?? '')),
                    'itemCondition' => $this->schemaItemCondition((string) ($commerceProduct['condition'] ?? 'new')),
                    'seller' => ['@id' => home_url('/') . '#organization'],
                ];
            } else {
                $schema['brand'] = [
                    '@type' => 'Brand',
                    'name' => $this->siteNameForMeta($settings),
                ];
            }
            $filteredProductSchema = apply_filters('h4bs_seo_product_schema', $schema, $postId, $commerceProduct, $settings);
            if (! is_array($filteredProductSchema)) {
                return;
            }
            $schema = $filteredProductSchema;
        }

        if ($schemaType === 'LocalBusiness') {
            $schema['name'] = $this->siteNameForMeta($settings);
            $schema['telephone'] = (string) ($settings['business_phone'] ?? '');
            $schema['sameAs'] = $this->sameAsUrls($settings);
        }

        if (in_array($schemaType, ['Event', 'Course'], true)) {
            $schema = $this->performingArtsSchema->extend($schema, $schemaType, $this->performingArtsSchema->forPost($postId));
        }

        $schema = $this->finalizeContentSchema($schema, $postId, $settings, $breadcrumbId);
        if ($schema === []) {
            return;
        }
        echo '<script type="application/ld+json">' . wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . "</script>\n";
    }

    /**
     * Apply the public content-schema extension point, then enforce the final
     * graph and breadcrumb-reference contract before JSON-LD is emitted.
     *
     * @param array<string, mixed> $schema
     * @param array<string, mixed> $settings
     * @return array<string, mixed>
     */
    private function finalizeContentSchema(array $schema, int $postId, array $settings, string $breadcrumbId): array
    {
        $filtered = apply_filters('h4bs_content_schema', $schema, $postId, $settings);
        if (! is_array($filtered) || $filtered === []) {
            return [];
        }
        $schema = $filtered;

        if (isset($schema['@graph'])) {
            if (! is_array($schema['@graph'])) {
                return [];
            }
            $nodes = [];
            $seen = [];
            $singletons = [];
            foreach ($schema['@graph'] as $node) {
                if (! is_array($node) || ! $this->hasSchemaType($node)) {
                    continue;
                }
                $nodeId = isset($node['@id']) && is_string($node['@id']) ? trim($node['@id']) : '';
                $nodeType = $this->firstSchemaType($node);
                $identity = $nodeId !== ''
                    ? 'id:' . $nodeId
                    : 'node:' . hash('sha256', (string) wp_json_encode($node));
                if (isset($seen[$identity])) {
                    continue;
                }
                if (in_array($nodeType, ['BreadcrumbList', 'Organization'], true)) {
                    if (isset($singletons[$nodeType])) {
                        continue;
                    }
                    $singletons[$nodeType] = true;
                }
                $seen[$identity] = true;
                $nodes[] = $this->normalizeContentBreadcrumbReference($node, $breadcrumbId);
            }
            if ($nodes === []) {
                return [];
            }
            $schema['@graph'] = $nodes;
        } elseif (! $this->hasSchemaType($schema)) {
            return [];
        }

        return $this->normalizeContentBreadcrumbReference($schema, $breadcrumbId);
    }

    /**
     * @param array<string, mixed> $schema
     * @return array<string, mixed>
     */
    private function normalizeContentBreadcrumbReference(array $schema, string $breadcrumbId): array
    {
        if (! array_key_exists('breadcrumb', $schema)) {
            return $schema;
        }
        if ($breadcrumbId === '') {
            unset($schema['breadcrumb']);
            return $schema;
        }
        $schema['breadcrumb'] = ['@id' => $breadcrumbId];
        return $schema;
    }

    /**
     * @param array<string, mixed> $schema
     */
    private function hasSchemaType(array $schema): bool
    {
        $type = $schema['@type'] ?? null;
        if (is_string($type)) {
            return trim($type) !== '';
        }
        if (! is_array($type)) {
            return false;
        }
        foreach ($type as $candidate) {
            if (is_string($candidate) && trim($candidate) !== '') {
                return true;
            }
        }
        return false;
    }

    /**
     * @param array<string, mixed> $schema
     */
    private function firstSchemaType(array $schema): string
    {
        $type = $schema['@type'] ?? '';
        if (is_string($type)) {
            return trim($type);
        }
        if (is_array($type)) {
            foreach ($type as $candidate) {
                if (is_string($candidate) && trim($candidate) !== '') {
                    return trim($candidate);
                }
            }
        }
        return '';
    }

    /**
     * @return array<int, array{name: string, url: string}>
     */
    private function breadcrumbItems(int $postId, array $settings = []): array
    {
        if ($postId <= 0) {
            return [];
        }

        $settingsJson = wp_json_encode($settings, JSON_UNESCAPED_SLASHES);
        $cacheKey = $postId . ':' . (is_front_page() ? 'front' : 'content') . ':' . hash('sha256', is_string($settingsJson) ? $settingsJson : '');
        if (array_key_exists($cacheKey, $this->breadcrumbItemsCache)) {
            return $this->breadcrumbItemsCache[$cacheKey];
        }

        $items = [
            [
                'name' => __('Home', 'help4-builder-suite'),
                'url' => home_url('/'),
            ],
        ];
        $postType = get_post_type($postId);
        if ($postType === 'post') {
            $categories = get_the_category($postId);
            if (is_array($categories) && ! empty($categories)) {
                $category = $categories[0];
                $ancestors = array_reverse(get_ancestors((int) $category->term_id, 'category'));
                foreach ($ancestors as $ancestorId) {
                    $term = get_term((int) $ancestorId, 'category');
                    if ($term && ! is_wp_error($term)) {
                        $termLink = get_term_link($term);
                        if (is_wp_error($termLink) || ! is_string($termLink)) {
                            continue;
                        }
                        $items[] = [
                            'name' => (string) $term->name,
                            'url' => $termLink,
                        ];
                    }
                }
                $categoryLink = get_category_link((int) $category->term_id);
                if (is_string($categoryLink) && $categoryLink !== '') {
                    $items[] = [
                        'name' => (string) $category->name,
                        'url' => $categoryLink,
                    ];
                }
            }
        } elseif ($postType === 'page') {
            $ancestors = array_reverse(get_post_ancestors($postId));
            foreach ($ancestors as $ancestorId) {
                $items[] = [
                    'name' => get_the_title((int) $ancestorId),
                    'url' => (string) get_permalink((int) $ancestorId),
                ];
            }
        } elseif (is_string($postType) && $postType !== '') {
            $object = get_post_type_object($postType);
            if ($object && ! empty($object->has_archive)) {
                $archive = get_post_type_archive_link($postType);
                if (is_string($archive) && $archive !== '') {
                    $items[] = [
                        'name' => (string) ($object->labels->name ?? $postType),
                        'url' => $archive,
                    ];
                }
            }
        }

        $items[] = [
            'name' => get_the_title($postId),
            'url' => (string) get_permalink($postId),
        ];

        /**
         * Filters the complete ordered breadcrumb candidates used by both the
         * visible shortcode and BreadcrumbList JSON-LD. Each final item must
         * contain a plain-text `name` and an absolute HTTP(S) `url`.
         *
         * @param array<int, array{name: string, url: string}> $items
         * @param int                                           $postId
         * @param array<string, mixed>                          $settings
         */
        $filtered = apply_filters('h4bs_breadcrumb_items', $items, $postId, $settings);
        $items = $this->normalizeBreadcrumbItems($filtered);

        // The current front-page contract is intentionally breadcrumb-free.
        if (is_front_page()) {
            $items = [];
        }

        $this->breadcrumbItemsCache[$cacheKey] = $items;
        return $items;
    }

    /**
     * @param mixed $items
     * @return array<int, array{name: string, url: string}>
     */
    private function normalizeBreadcrumbItems($items): array
    {
        if (! is_array($items)) {
            return [];
        }
        $normalized = [];
        $seenUrls = [];
        foreach ($items as $item) {
            if (! is_array($item)) {
                continue;
            }
            $name = sanitize_text_field(wp_strip_all_tags((string) ($item['name'] ?? '')));
            $url = $this->normalizeBreadcrumbUrl((string) ($item['url'] ?? ''));
            if ($name === '' || $url === '' || isset($seenUrls[$url])) {
                continue;
            }
            $seenUrls[$url] = true;
            $normalized[] = [
                'name' => $name,
                'url' => $url,
            ];
        }
        return $normalized;
    }

    private function normalizeBreadcrumbUrl(string $url): string
    {
        $url = trim(html_entity_decode($url, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
        if ($url === '') {
            return '';
        }
        if (preg_match('#^[a-z][a-z0-9+.-]*:#i', $url) && ! preg_match('#^https?://#i', $url)) {
            return '';
        }
        $home = (string) home_url('/');
        $homeParts = wp_parse_url($home);
        if (! is_array($homeParts)) {
            return '';
        }
        if (str_starts_with($url, '//')) {
            $homeScheme = strtolower((string) ($homeParts['scheme'] ?? 'https'));
            $url = $homeScheme . ':' . $url;
        } elseif (! preg_match('#^https?://#i', $url)) {
            $homeBase = untrailingslashit($home);
            if (str_starts_with($url, '?')) {
                $url = $homeBase . '/' . $url;
            } else {
                $url = $homeBase . '/' . ltrim($url, '/');
            }
        }

        $parts = wp_parse_url($url);
        if (! is_array($parts)) {
            return '';
        }
        $scheme = strtolower((string) ($parts['scheme'] ?? ''));
        $host = strtolower((string) ($parts['host'] ?? ''));
        if (! in_array($scheme, ['http', 'https'], true) || $host === '' || isset($parts['user']) || isset($parts['pass'])) {
            return '';
        }
        if (str_contains($host, ':') && filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            $host = '[' . $host . ']';
        }
        $port = isset($parts['port']) ? absint($parts['port']) : 0;
        $portPart = $port > 0 && ! (($scheme === 'http' && $port === 80) || ($scheme === 'https' && $port === 443))
            ? ':' . $port
            : '';
        $path = '/' . ltrim((string) ($parts['path'] ?? '/'), '/');
        $path = (string) preg_replace('#/{2,}#', '/', $path);
        $segments = [];
        foreach (explode('/', $path) as $segment) {
            if ($segment === '' || $segment === '.') {
                continue;
            }
            if ($segment === '..') {
                array_pop($segments);
                continue;
            }
            $segments[] = $segment;
        }
        $path = '/' . implode('/', $segments);
        if ($path === '/') {
            $path = '/';
        } elseif (preg_match('/\.[a-z0-9]{1,8}$/i', basename($path))) {
            $path = untrailingslashit($path);
        } else {
            $path = user_trailingslashit($path, 'breadcrumb');
        }
        $query = isset($parts['query']) && (string) $parts['query'] !== '' ? '?' . (string) $parts['query'] : '';
        return esc_url_raw($scheme . '://' . $host . $portPart . $path . $query, ['http', 'https']);
    }

    private function buildRobotsMeta(int $postId, bool $forcedNoindex): string
    {
        if ($forcedNoindex || ($postId > 0 && (string) get_post_meta($postId, '_h4_seo_noindex', true) === '1')) {
            $directives = ['noindex' => true, 'nofollow' => true];
        } elseif (is_search() || is_404()) {
            $directives = ['noindex' => true, 'follow' => true, 'max-snippet' => '-1', 'max-image-preview' => 'large', 'max-video-preview' => '-1'];
        } else {
            $directives = ['index' => true];
            $directives[$postId > 0 && (string) get_post_meta($postId, '_h4_seo_nofollow', true) === '1' ? 'nofollow' : 'follow'] = true;
            if ($postId > 0 && (string) get_post_meta($postId, '_h4_seo_noarchive', true) === '1') {
                $directives['noarchive'] = true;
            }
            if ($postId > 0 && (string) get_post_meta($postId, '_h4_seo_nosnippet', true) === '1') {
                $directives['nosnippet'] = true;
            } else {
                $directives['max-snippet'] = '-1';
            }
            $maxImagePreview = $postId > 0 ? sanitize_key((string) get_post_meta($postId, '_h4_seo_max_image_preview', true)) : 'large';
            if (! in_array($maxImagePreview, ['large', 'standard', 'none'], true)) {
                $maxImagePreview = 'large';
            }
            $directives['max-image-preview'] = $maxImagePreview;
            $directives['max-video-preview'] = '-1';
        }

        /** @var array<string, bool|string> $directives */
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Extensions must participate in WordPress core's single robots contract.
        $directives = apply_filters('wp_robots', $directives);
        if (! is_array($directives)) {
            $directives = ['noindex' => true, 'nofollow' => true];
        }
        if (! empty($directives['noindex'])) unset($directives['index']);
        if (! empty($directives['nofollow'])) unset($directives['follow']);

        $parts = [];
        foreach ($directives as $name => $value) {
            $name = sanitize_key((string) $name);
            if ($name === '' || $value === false || $value === '') continue;
            $parts[] = $value === true ? $name : $name . ':' . sanitize_text_field((string) $value);
        }

        return implode(',', array_values(array_unique($parts)));
    }

    /**
     * @return array<string, string>
     */
    private function schemaTypeLabels(): array
    {
        return [
            'webpage' => __('WebPage', 'help4-builder-suite'),
            'article' => __('Article', 'help4-builder-suite'),
            'blogposting' => __('BlogPosting', 'help4-builder-suite'),
            'service' => __('Service', 'help4-builder-suite'),
            'product' => __('Product', 'help4-builder-suite'),
            'software_application' => __('SoftwareApplication', 'help4-builder-suite'),
            'localbusiness' => __('LocalBusiness', 'help4-builder-suite'),
            'realestatelisting' => __('RealEstateListing', 'help4-builder-suite'),
            'faqpage' => __('FAQPage', 'help4-builder-suite'),
            'event' => __('Event', 'help4-builder-suite'),
            'course' => __('Course / Class', 'help4-builder-suite'),
            'courselist' => __('Course List (3+ factual courses)', 'help4-builder-suite'),
        ];
    }

    private function schemaTypeName(string $schemaTypeKey): string
    {
        $map = [
            'webpage' => 'WebPage',
            'article' => 'Article',
            'blogposting' => 'BlogPosting',
            'service' => 'Service',
            'product' => 'Product',
            'software_application' => 'SoftwareApplication',
            'localbusiness' => 'LocalBusiness',
            'realestatelisting' => 'RealEstateListing',
            'faqpage' => 'FAQPage',
            'event' => 'Event',
            'course' => 'Course',
            'courselist' => 'ItemList',
        ];

        $key = sanitize_key($schemaTypeKey);
        return $map[$key] ?? 'WebPage';
    }

    private function parseFaqItemsInput(string $raw): string
    {
        $raw = trim($raw);
        if ($raw === '') {
            return '';
        }

        $rows = [];
        $decoded = json_decode($raw, true);
        if (is_array($decoded)) {
            foreach ($decoded as $row) {
                if (! is_array($row)) {
                    continue;
                }
                $question = sanitize_text_field((string) ($row['question'] ?? ''));
                $answer = sanitize_textarea_field((string) ($row['answer'] ?? ''));
                if ($question !== '' && $answer !== '') {
                    $rows[] = ['question' => $question, 'answer' => $answer];
                }
            }
        } else {
            foreach (preg_split('/\r\n|\r|\n/', $raw) ?: [] as $line) {
                $line = trim((string) $line);
                if ($line === '') {
                    continue;
                }
                $parts = preg_split('/\s*\|\s*|\s*=>\s*|\t+/', $line, 2);
                if (! is_array($parts) || count($parts) < 2) {
                    continue;
                }
                $question = sanitize_text_field((string) ($parts[0] ?? ''));
                $answer = sanitize_textarea_field((string) ($parts[1] ?? ''));
                if ($question !== '' && $answer !== '') {
                    $rows[] = ['question' => $question, 'answer' => $answer];
                }
            }
        }

        if ($rows === []) {
            return '';
        }

        return (string) wp_json_encode($rows, JSON_UNESCAPED_SLASHES);
    }

    private function formatFaqItemsForTextarea(string $raw): string
    {
        $decoded = json_decode($raw, true);
        if (! is_array($decoded)) {
            return $raw;
        }

        $lines = [];
        foreach ($decoded as $row) {
            if (! is_array($row)) {
                continue;
            }
            $question = sanitize_text_field((string) ($row['question'] ?? ''));
            $answer = sanitize_textarea_field((string) ($row['answer'] ?? ''));
            if ($question !== '' && $answer !== '') {
                $lines[] = $question . ' | ' . $answer;
            }
        }

        return implode("\n", $lines);
    }

    /**
     * @return array<int, array{question: string, answer: string}>
     */
    private function faqItems(int $postId): array
    {
        if ($postId <= 0) {
            return [];
        }

        $raw = (string) get_post_meta($postId, '_h4_seo_faq_items', true);
        if ($raw === '') {
            return [];
        }

        $decoded = json_decode($raw, true);
        if (! is_array($decoded)) {
            return [];
        }

        $items = [];
        foreach ($decoded as $row) {
            if (! is_array($row)) {
                continue;
            }
            $question = sanitize_text_field((string) ($row['question'] ?? ''));
            $answer = sanitize_textarea_field((string) ($row['answer'] ?? ''));
            if ($question !== '' && $answer !== '') {
                $items[] = ['question' => $question, 'answer' => $answer];
            }
        }

        return $items;
    }

    /**
     * @return array{0: int, 1: int}
     */
    private function attachmentImageDimensions(int $attachmentId): array
    {
        if ($attachmentId <= 0) {
            return [0, 0];
        }

        $metadata = wp_get_attachment_metadata($attachmentId);
        if (! is_array($metadata)) {
            return [0, 0];
        }

        return [
            (int) ($metadata['width'] ?? 0),
            (int) ($metadata['height'] ?? 0),
        ];
    }

    private function attachmentImageMimeType(int $attachmentId): string
    {
        if ($attachmentId <= 0) {
            return '';
        }

        $mime = get_post_mime_type($attachmentId);
        if (! is_string($mime) || $mime === '') {
            return '';
        }

        return str_starts_with($mime, 'image/') ? $mime : '';
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function imageSeoAuditRows(int $limit): array
    {
        $attachments = get_posts([
            'post_type' => 'attachment',
            'post_status' => 'inherit',
            'post_mime_type' => 'image',
            'posts_per_page' => max(1, min(200, $limit)),
            'orderby' => 'date',
            'order' => 'DESC',
        ]);

        $rows = [];
        foreach ($attachments as $attachment) {
            if (! $attachment instanceof WP_Post) {
                continue;
            }
            $metadata = wp_get_attachment_metadata($attachment->ID);
            $width = is_array($metadata) ? (int) ($metadata['width'] ?? 0) : 0;
            $height = is_array($metadata) ? (int) ($metadata['height'] ?? 0) : 0;
            $file = is_array($metadata) ? (string) ($metadata['file'] ?? '') : '';
            $parentTitle = '';
            if ((int) $attachment->post_parent > 0) {
                $parentTitle = get_the_title((int) $attachment->post_parent);
            }
            $thumb = wp_get_attachment_image($attachment->ID, [80, 80], true, [
                'style' => 'width:80px;height:80px;object-fit:cover;border-radius:6px;background:#f6f7f7;',
            ]);

            $rows[] = [
                'id' => $attachment->ID,
                'title' => get_the_title($attachment),
                'alt' => trim((string) get_post_meta($attachment->ID, '_wp_attachment_image_alt', true)),
                'parent' => $parentTitle !== '' ? $parentTitle : __('Unattached', 'help4-builder-suite'),
                'size' => $width > 0 && $height > 0 ? $width . ' x ' . $height : __('Unknown', 'help4-builder-suite'),
                'file' => $file,
                'thumb' => is_string($thumb) ? $thumb : '',
            ];
        }

        usort(
            $rows,
            static function (array $a, array $b): int {
                $aMissing = empty($a['alt']) ? 0 : 1;
                $bMissing = empty($b['alt']) ? 0 : 1;
                return $aMissing <=> $bMissing;
            }
        );

        return $rows;
    }

    private function generateAttachmentAltText(WP_Post $attachment): string
    {
        $candidates = [];
        $title = trim(wp_strip_all_tags(get_the_title($attachment)));
        if ($title !== '' && ! preg_match('/^\d+$/', $title)) {
            $candidates[] = $title;
        }

        $file = (string) get_attached_file($attachment->ID);
        if ($file !== '') {
            $basename = pathinfo($file, PATHINFO_FILENAME);
            $basename = trim((string) preg_replace('/[-_]+/', ' ', $basename));
            $basename = trim((string) preg_replace('/\s+/', ' ', $basename));
            if ($basename !== '' && ! preg_match('/^\d+$/', $basename)) {
                $candidates[] = ucwords($basename);
            }
        }

        if ((int) $attachment->post_parent > 0) {
            $parentTitle = trim(wp_strip_all_tags(get_the_title((int) $attachment->post_parent)));
            if ($parentTitle !== '') {
                $candidates[] = $parentTitle;
            }
        }

        $alt = '';
        foreach ($candidates as $candidate) {
            $candidate = sanitize_text_field((string) $candidate);
            if ($candidate === '' || strcasecmp($candidate, 'image') === 0) {
                continue;
            }
            $alt = $candidate;
            break;
        }

        if ($alt === '') {
            return '';
        }

        if ($this->textLength($alt) > 125) {
            $alt = function_exists('mb_substr') ? (string) mb_substr($alt, 0, 122) : substr($alt, 0, 122);
            $alt = rtrim($alt, " \t\n\r\0\x0B.,;:");
        }

        return $alt;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function seoAuditRows(int $limit): array
    {
        $postTypes = get_post_types(['public' => true], 'names');
        unset($postTypes['attachment']);
        $posts = get_posts([
            'post_type' => array_values($postTypes),
            'post_status' => 'publish',
            'posts_per_page' => max(1, min(300, $limit)),
            'orderby' => 'modified',
            'order' => 'DESC',
        ]);

        $rows = [];
        foreach ($posts as $post) {
            if (! $post instanceof WP_Post) {
                continue;
            }
            $analysis = $this->analyzePost($post);
            $scores = is_array($analysis['scores'] ?? null) ? $analysis['scores'] : [];
            $metrics = is_array($analysis['metrics'] ?? null) ? $analysis['metrics'] : [];
            $schemaKey = sanitize_key((string) get_post_meta($post->ID, '_h4_seo_schema_type', true));
            $conversionGoal = sanitize_key((string) get_post_meta($post->ID, '_h4_seo_conversion_goal', true));
            $postTypeObject = get_post_type_object($post->post_type);
            $rows[] = [
                'id' => $post->ID,
                'title' => get_the_title($post),
                'type' => $postTypeObject ? (string) $postTypeObject->labels->singular_name : $post->post_type,
                'score' => $analysis['score'],
                'label' => $analysis['label'],
                'warnings' => $analysis['warnings'],
                'readability_score' => (int) ($scores['readability'] ?? 0),
                'readability_label' => $this->scoreLabel((int) ($scores['readability'] ?? 0)),
                'readability_summary' => sprintf(
                    /* translators: 1: Reading grade. 2: Word count. */
                    __('Grade %1$s, %2$d words', 'help4-builder-suite'),
                    (string) ($metrics['reading_grade'] ?? '0'),
                    (int) ($metrics['word_count'] ?? 0)
                ),
                'analytics_score' => (int) ($scores['analytics'] ?? 0),
                'adsense_score' => (int) ($scores['adsense'] ?? 0),
                'intent_score' => (int) ($scores['intent'] ?? 0),
                'intent_profile' => sanitize_key((string) ($analysis['intent_profile'] ?? 'general')),
                'intent_profile_label' => (string) ($analysis['intent_profile_label'] ?? __('General website', 'help4-builder-suite')),
                'conversion_goal' => $this->conversionGoalName($conversionGoal),
                'keyword' => (string) get_post_meta($post->ID, '_h4_seo_focus_keyword', true),
                'schema' => $this->schemaTypeName($schemaKey !== '' ? $schemaKey : ($post->post_type === 'post' ? 'article' : 'webpage')),
                'robots' => $this->buildRobotsMeta($post->ID, false),
                'modified' => get_the_modified_date('Y-m-d H:i', $post),
            ];
        }

        return $rows;
    }

    /**
     * @return array<int, array{title: string, url: string, reason: string, score: int}>
     */
    private function internalLinkSuggestions(WP_Post $source): array
    {
        $sourceText = implode(' ', [
            get_the_title($source),
            (string) get_post_meta($source->ID, '_h4_seo_focus_keyword', true),
            wp_strip_all_tags((string) $source->post_excerpt),
            wp_strip_all_tags((string) $source->post_content),
        ]);
        $sourceTokens = $this->linkSuggestionTokens($sourceText);
        if ($sourceTokens === []) {
            return [];
        }
        $postTypes = get_post_types(['public' => true], 'names');
        unset($postTypes['attachment']);
        $candidates = get_posts([
            'post_type' => array_values($postTypes),
            'post_status' => 'publish',
            'posts_per_page' => 160,
            'orderby' => 'modified',
            'order' => 'DESC',
            'no_found_rows' => true,
        ]);
        $suggestions = [];
        foreach ($candidates as $candidate) {
            if (! $candidate instanceof WP_Post || $candidate->ID === $source->ID || (string) get_post_meta($candidate->ID, '_h4_seo_noindex', true) === '1') {
                continue;
            }
            $url = (string) get_permalink($candidate);
            if ($url === '' || str_contains((string) $source->post_content, $url)) {
                continue;
            }
            $candidateTokens = $this->linkSuggestionTokens(implode(' ', [
                get_the_title($candidate),
                (string) get_post_meta($candidate->ID, '_h4_seo_focus_keyword', true),
                wp_strip_all_tags((string) $candidate->post_excerpt),
            ]));
            $shared = array_intersect_key($sourceTokens, $candidateTokens);
            $score = count($shared);
            if ($score < 2) {
                continue;
            }
            $suggestions[] = [
                'title' => get_the_title($candidate),
                'url' => $url,
                'reason' => sprintf(
                    /* translators: %d: Number of shared meaningful topic terms. */
                    _n('%d shared topic term', '%d shared topic terms', $score, 'help4-builder-suite'),
                    $score
                ),
                'score' => $score,
            ];
        }
        usort($suggestions, static fn (array $a, array $b): int => $b['score'] <=> $a['score']);
        return array_slice($suggestions, 0, 6);
    }

    /**
     * @return array{summary: array<string, int>, items: array<int, array<string, mixed>>, priority_items: array<int, array<string, mixed>>}
     */
    private function internalLinkGraph(int $limit): array
    {
        $postTypes = get_post_types(['public' => true], 'names');
        unset($postTypes['attachment']);
        $posts = get_posts([
            'post_type' => array_values($postTypes),
            'post_status' => 'publish',
            'posts_per_page' => max(1, min(500, $limit)),
            'orderby' => 'modified',
            'order' => 'DESC',
            'no_found_rows' => true,
        ]);

        $homeHost = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        $homeHost = preg_replace('/^www\./', '', $homeHost) ?: $homeHost;
        $frontPageId = absint(get_option('page_on_front'));
        $nodes = [];
        $pathToPost = [];

        foreach ($posts as $post) {
            if (! $post instanceof WP_Post || (string) get_post_meta($post->ID, '_h4_seo_noindex', true) === '1') {
                continue;
            }

            $url = (string) get_permalink($post);
            $path = $this->normalizeInternalLinkPath($url, $homeHost);
            if ($url === '' || $path === '') {
                continue;
            }

            $postTypeObject = get_post_type_object($post->post_type);
            $nodes[$post->ID] = [
                'id' => $post->ID,
                'title' => get_the_title($post),
                'type' => $postTypeObject ? (string) $postTypeObject->labels->singular_name : $post->post_type,
                'url' => $url,
                'edit_url' => (string) (get_edit_post_link($post->ID, 'raw') ?: ''),
                'inbound' => 0,
                'outbound' => 0,
                'outbound_ids' => [],
                'status' => 'strong',
                'status_label' => __('Connected', 'help4-builder-suite'),
            ];
            $pathToPost[$path] = $post->ID;
        }

        foreach ($posts as $post) {
            if (! $post instanceof WP_Post || ! isset($nodes[$post->ID])) {
                continue;
            }

            $content = implode("\n", [
                (string) $post->post_content,
                (string) $post->post_excerpt,
            ]);
            $content = (string) apply_filters('h4bs_seo_internal_link_content', $content, $post);
            $urls = array_merge(
                $this->extractEditorialLinkUrls($content),
                $this->builderLayoutLinkUrls($post->ID)
            );
            $urls = apply_filters('h4bs_seo_internal_link_urls', $urls, $post);
            $urls = is_array($urls) ? $urls : [];

            $outboundIds = [];
            foreach ($urls as $candidateUrl) {
                if (! is_scalar($candidateUrl)) {
                    continue;
                }
                $path = $this->normalizeInternalLinkPath((string) $candidateUrl, $homeHost);
                $targetId = $path !== '' ? absint($pathToPost[$path] ?? 0) : 0;
                if ($targetId <= 0 || $targetId === $post->ID || ! isset($nodes[$targetId])) {
                    continue;
                }
                $outboundIds[$targetId] = true;
            }

            $nodes[$post->ID]['outbound_ids'] = array_map('intval', array_keys($outboundIds));
            $nodes[$post->ID]['outbound'] = count($outboundIds);
            foreach (array_keys($outboundIds) as $targetId) {
                $nodes[(int) $targetId]['inbound']++;
            }
        }

        $summary = [
            'scanned' => count($nodes),
            'connected' => 0,
            'orphan_candidates' => 0,
            'weak_candidates' => 0,
        ];
        foreach ($nodes as $postId => &$node) {
            $isOrphanCandidate = (int) $postId !== $frontPageId && (int) $node['inbound'] === 0;
            $isWeakCandidate = ! $isOrphanCandidate && ((int) $node['inbound'] < 2 || (int) $node['outbound'] < 2);
            if ($isOrphanCandidate) {
                $node['status'] = 'orphan_candidate';
                $node['status_label'] = __('Content-orphan candidate', 'help4-builder-suite');
                $summary['orphan_candidates']++;
            } elseif ($isWeakCandidate) {
                $node['status'] = 'weak_candidate';
                $node['status_label'] = __('Weak contextual coverage', 'help4-builder-suite');
                $summary['weak_candidates']++;
            } else {
                $summary['connected']++;
            }
        }
        unset($node);

        $items = array_values($nodes);
        usort($items, static function (array $a, array $b): int {
            $statusOrder = ['orphan_candidate' => 0, 'weak_candidate' => 1, 'strong' => 2];
            $statusComparison = ($statusOrder[(string) ($a['status'] ?? 'strong')] ?? 2) <=> ($statusOrder[(string) ($b['status'] ?? 'strong')] ?? 2);
            if ($statusComparison !== 0) {
                return $statusComparison;
            }
            $inboundComparison = (int) ($a['inbound'] ?? 0) <=> (int) ($b['inbound'] ?? 0);
            if ($inboundComparison !== 0) {
                return $inboundComparison;
            }
            return strcasecmp((string) ($a['title'] ?? ''), (string) ($b['title'] ?? ''));
        });
        $priorityItems = array_values(array_filter(
            $items,
            static fn (array $item): bool => (string) ($item['status'] ?? 'strong') !== 'strong'
        ));

        $graph = [
            'summary' => $summary,
            'items' => $items,
            'priority_items' => array_slice($priorityItems, 0, 50),
        ];
        $filtered = apply_filters('h4bs_seo_internal_link_graph', $graph, $limit);
        return is_array($filtered) ? $filtered : $graph;
    }

    /**
     * @return array<int, string>
     */
    private function extractEditorialLinkUrls(string $content): array
    {
        if ($content === '') {
            return [];
        }

        $urls = [];
        if (class_exists('WP_HTML_Tag_Processor')) {
            $processor = new \WP_HTML_Tag_Processor($content);
            while ($processor->next_tag('a')) {
                $href = $processor->get_attribute('href');
                if (is_string($href) && trim($href) !== '') {
                    $urls[] = html_entity_decode(trim($href), ENT_QUOTES | ENT_HTML5, 'UTF-8');
                }
            }
        } elseif (preg_match_all('/<a\b[^>]*\bhref\s*=\s*(["\'])(.*?)\1/is', $content, $matches)) {
            foreach ((array) ($matches[2] ?? []) as $href) {
                $urls[] = html_entity_decode(trim((string) $href), ENT_QUOTES | ENT_HTML5, 'UTF-8');
            }
        }

        return array_values(array_unique(array_filter($urls)));
    }

    /**
     * @return array<int, string>
     */
    private function builderLayoutLinkUrls(int $postId): array
    {
        $raw = get_post_meta($postId, '_h4_builder_layout', true);
        if (is_string($raw)) {
            $decoded = json_decode($raw, true);
            if (! is_array($decoded)) {
                $decoded = json_decode(wp_unslash($raw), true);
            }
            $layout = is_array($decoded) ? $decoded : [];
        } else {
            $layout = is_array($raw) ? $raw : [];
        }
        if ($layout === []) {
            return [];
        }

        $urls = [];
        $walk = function (mixed $value, string $key = '') use (&$walk, &$urls): void {
            if (is_array($value)) {
                foreach ($value as $childKey => $childValue) {
                    $walk($childValue, is_string($childKey) ? $childKey : $key);
                }
                return;
            }
            if (! is_string($value) || $value === '') {
                return;
            }
            if (preg_match('/(?:html|content|text|description)/i', $key)) {
                $urls = array_merge($urls, $this->extractEditorialLinkUrls($value));
            }
            if (preg_match('/(?:url|href|link)/i', $key) && preg_match('#^(?:https?:)?//|^/#i', trim($value))) {
                $urls[] = trim($value);
            }
        };
        $walk($layout);

        return array_values(array_unique(array_filter($urls)));
    }

    private function normalizeInternalLinkPath(string $url, string $homeHost): string
    {
        $url = html_entity_decode(trim($url), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        if ($url === '' || str_starts_with($url, '#') || preg_match('#^(?:mailto|tel|sms|javascript|data):#i', $url)) {
            return '';
        }
        if (str_starts_with($url, '//')) {
            $url = (is_ssl() ? 'https:' : 'http:') . $url;
        }

        $parts = wp_parse_url($url);
        if (! is_array($parts)) {
            return '';
        }
        $host = strtolower((string) ($parts['host'] ?? ''));
        $host = preg_replace('/^www\./', '', $host) ?: $host;
        if ($host !== '' && $homeHost !== '' && $host !== $homeHost) {
            return '';
        }

        $path = rawurldecode((string) ($parts['path'] ?? ''));
        if ($path === '') {
            $path = '/';
        }
        $path = '/' . ltrim($path, '/');
        $path = preg_replace('#/+#', '/', $path) ?: $path;
        $path = untrailingslashit($path);
        return $path === '' ? '/' : $path;
    }

    /**
     * @return array<string, true>
     */
    private function linkSuggestionTokens(string $text): array
    {
        $text = strtolower(wp_strip_all_tags($text));
        $words = preg_split('/[^\p{L}\p{N}]+/u', $text) ?: [];
        $stop = array_fill_keys([
            'about', 'after', 'again', 'also', 'been', 'before', 'being', 'between', 'could', 'does', 'each',
            'from', 'have', 'help', 'into', 'more', 'most', 'other', 'over', 'page', 'site', 'some', 'than',
            'that', 'their', 'them', 'then', 'there', 'these', 'they', 'this', 'through', 'under', 'using',
            'very', 'want', 'were', 'what', 'when', 'where', 'which', 'while', 'with', 'wordpress', 'would', 'your',
        ], true);
        $tokens = [];
        foreach ($words as $word) {
            $word = sanitize_key((string) $word);
            if (strlen($word) < 4 || isset($stop[$word])) {
                continue;
            }
            $tokens[$word] = true;
        }
        return $tokens;
    }

    /**
     * @return array{score: int, label: string, warnings: array<int, string>, scores: array<string, int>, metrics: array<string, mixed>}
     */
    private function analyzePost(WP_Post $post): array
    {
        $warnings = [];
        $searchScore = 0;
        $contentHtml = (string) $post->post_content;
        $plainText = $this->plainTextForAnalysis($contentHtml);
        $metrics = $this->readabilityMetrics($post, $plainText, $contentHtml);

        $title = trim((string) get_post_meta($post->ID, '_h4_seo_title', true));
        if ($title === '') {
            $title = get_the_title($post);
        }
        $titleLength = $this->textLength($title);
        if ($titleLength >= 35 && $titleLength <= 65) {
            $searchScore += 18;
        } elseif ($titleLength >= 20 && $titleLength <= 75) {
            $searchScore += 10;
            $warnings[] = __('Search title is usable, but tighter titles usually make better Google result labels.', 'help4-builder-suite');
        } else {
            $warnings[] = __('SEO title needs a clearer search-result length.', 'help4-builder-suite');
        }

        $description = trim((string) get_post_meta($post->ID, '_h4_seo_description', true));
        if ($description === '') {
            $description = wp_strip_all_tags((string) get_the_excerpt($post));
        }
        $description = $this->normalizeMetaDescription($description);
        $descriptionLength = $this->textLength($description);
        if ($descriptionLength >= 120 && $descriptionLength <= 165) {
            $searchScore += 18;
        } elseif ($descriptionLength >= 80 && $descriptionLength <= 175) {
            $searchScore += 10;
            $warnings[] = __('Description is close, but tighten it into a clear one- or two-sentence snippet.', 'help4-builder-suite');
        } else {
            $warnings[] = __('Meta description is missing, thin, or too long.', 'help4-builder-suite');
        }

        $keyword = trim((string) get_post_meta($post->ID, '_h4_seo_focus_keyword', true));
        if ($keyword !== '') {
            $searchScore += 10;
            if (stripos($title, $keyword) !== false) {
                $searchScore += 5;
            } else {
                $warnings[] = __('Primary keyword/intent is not reflected in the SEO title.', 'help4-builder-suite');
            }
            if ($description !== '' && stripos($description, $keyword) !== false) {
                $searchScore += 5;
            } else {
                $warnings[] = __('Primary keyword/intent is not reflected in the description.', 'help4-builder-suite');
            }
            $plainLower = strtolower($plainText);
            $keywordLower = strtolower($keyword);
            $keywordOccurrences = substr_count($plainLower, $keywordLower);
            $keywordDensity = (int) ($metrics['word_count'] ?? 0) > 0
                ? round(($keywordOccurrences / (int) $metrics['word_count']) * 100, 2)
                : 0.0;
            $metrics['keyphrase_occurrences'] = $keywordOccurrences;
            $metrics['keyphrase_density'] = $keywordDensity;
            if ($keywordOccurrences === 0) {
                $warnings[] = __('Use the primary topic naturally in the body copy at least once.', 'help4-builder-suite');
            } elseif ($keywordDensity > 3.0) {
                $warnings[] = __('The primary phrase appears unusually often. Rewrite repeated uses naturally instead of stuffing the phrase.', 'help4-builder-suite');
            }
            $introduction = strtolower(wp_trim_words($plainText, 120, ''));
            if ($introduction !== '' && ! str_contains($introduction, $keywordLower)) {
                $warnings[] = __('Clarify the primary topic near the beginning so readers know the page matches their intent.', 'help4-builder-suite');
            }
            $keywordSlug = sanitize_title($keyword);
            if ($keywordSlug !== '' && ! str_contains((string) $post->post_name, $keywordSlug)) {
                $warnings[] = __('Consider a short URL slug that reflects the primary topic.', 'help4-builder-suite');
            }
            preg_match_all('/<h[2-6]\b[^>]*>(.*?)<\/h[2-6]>/is', $contentHtml, $headingMatches);
            $headingText = strtolower(wp_strip_all_tags(implode(' ', is_array($headingMatches[1] ?? null) ? $headingMatches[1] : [])));
            if ($headingText !== '' && ! str_contains($headingText, $keywordLower)) {
                $warnings[] = __('Use the main topic or a natural variation in a descriptive subheading where it helps readers.', 'help4-builder-suite');
            }
            $relatedKeywords = get_post_meta($post->ID, '_h4_seo_related_keywords', true);
            $relatedKeywords = is_array($relatedKeywords) ? $relatedKeywords : [];
            $relatedFound = 0;
            foreach ($relatedKeywords as $relatedKeyword) {
                $relatedKeyword = strtolower(trim((string) $relatedKeyword));
                if ($relatedKeyword !== '' && str_contains($plainLower, $relatedKeyword)) {
                    $relatedFound++;
                }
            }
            $metrics['related_keyphrases_found'] = $relatedFound;
            $metrics['related_keyphrases_total'] = count($relatedKeywords);
            if ($relatedKeywords !== [] && $relatedFound === 0) {
                $warnings[] = __('Related keyphrases are configured but none appear naturally in the page body.', 'help4-builder-suite');
            }
        } else {
            $warnings[] = __('Add a primary keyword or search intent.', 'help4-builder-suite');
        }

        $schemaKey = sanitize_key((string) get_post_meta($post->ID, '_h4_seo_schema_type', true));
        if ($schemaKey !== '' && isset($this->schemaTypeLabels()[$schemaKey])) {
            $searchScore += 10;
        } else {
            $searchScore += 6;
            $warnings[] = __('Choose the most specific schema type for this page.', 'help4-builder-suite');
        }

        if ($this->primarySocialImageUrlForPost($post->ID, Settings::all()) !== '') {
            $searchScore += 8;
        } else {
            $warnings[] = __('Add a featured or social preview image.', 'help4-builder-suite');
        }

        $canonical = trim((string) get_post_meta($post->ID, '_h4_seo_canonical', true));
        if ($canonical === '' || filter_var($canonical, FILTER_VALIDATE_URL)) {
            $searchScore += 6;
        } else {
            $warnings[] = __('Canonical URL is not valid.', 'help4-builder-suite');
        }

        $wordCount = (int) ($metrics['word_count'] ?? 0);
        if ($wordCount >= 300) {
            $searchScore += 10;
        } elseif ($wordCount >= 120) {
            $searchScore += 6;
            $warnings[] = __('Content body is a little thin for competitive SEO.', 'help4-builder-suite');
        } else {
            $warnings[] = __('Add more useful body copy, proof, FAQs, or tutorial detail.', 'help4-builder-suite');
        }

        if ((int) ($metrics['heading_count'] ?? 0) > 0) {
            $searchScore += 5;
        } else {
            $warnings[] = __('Add headings so readers and search engines can understand the page structure.', 'help4-builder-suite');
        }

        if ((int) ($metrics['image_count'] ?? 0) === 0 || (int) ($metrics['missing_image_alt'] ?? 0) === 0) {
            $searchScore += 5;
        } else {
            $warnings[] = __('Some images need alt text for accessibility and image search context.', 'help4-builder-suite');
        }

        if ($wordCount >= 150 && (int) ($metrics['internal_link_count'] ?? 0) === 0) {
            $warnings[] = __('Add at least one useful internal link so visitors and crawlers can continue through the site.', 'help4-builder-suite');
        }
        if ((int) ($metrics['invalid_link_count'] ?? 0) > 0) {
            $warnings[] = __('Replace empty or invalid links before publishing.', 'help4-builder-suite');
        }

        [$readabilityScore, $readabilityWarnings] = $this->scoreReadability($metrics);
        [$analyticsScore, $analyticsWarnings] = $this->scoreAnalyticsReadiness($post, $title, $description, $contentHtml);
        [$adsenseScore, $adsenseWarnings] = $this->scoreAdsenseQuality($post, $metrics, $readabilityScore);

        $searchScore = max(0, min(100, $searchScore));
        $score = (int) round(($searchScore * 0.4) + ($readabilityScore * 0.25) + ($analyticsScore * 0.2) + ($adsenseScore * 0.15));
        $score = max(0, min(100, $score));
        $warnings = array_values(array_unique(array_merge($warnings, $readabilityWarnings, $analyticsWarnings, $adsenseWarnings)));

        $analysis = [
            'score' => $score,
            'label' => $this->scoreLabel($score),
            'warnings' => $warnings,
            'scores' => [
                'search' => $searchScore,
                'readability' => $readabilityScore,
                'analytics' => $analyticsScore,
                'adsense' => $adsenseScore,
            ],
            'metrics' => $metrics,
        ];

        return $this->intentScorer->enrich($post, $analysis, Settings::all());
    }

    /**
     * @return array<int, array{key: string, title: string, meaning: string, fix: string}>
     */
    private function scorePlaybookCards(): array
    {
        return [
            [
                'key' => 'search',
                'title' => __('Search score', 'help4-builder-suite'),
                'meaning' => __('Checks whether Google-style result basics are usable: title, description, intent, schema, canonical, headings, and image context.', 'help4-builder-suite'),
                'fix' => __('Start with a clear title, helpful description, matching intent keyword, specific schema type, and at least one useful heading.', 'help4-builder-suite'),
            ],
            [
                'key' => 'readability',
                'title' => __('Readability score', 'help4-builder-suite'),
                'meaning' => __('Checks whether a normal visitor can scan and understand the copy without fighting long sentences or dense paragraphs.', 'help4-builder-suite'),
                'fix' => __('Split long sentences, shorten paragraphs, add headings, and explain the page in plain language before adding polish.', 'help4-builder-suite'),
            ],
            [
                'key' => 'analytics',
                'title' => __('Analytics-ready score', 'help4-builder-suite'),
                'meaning' => __('Checks whether the page has a clear goal, CTA, and measurable next step that can map to Analytics reports or key events.', 'help4-builder-suite'),
                'fix' => __('Choose a conversion goal, name the suggested event, and add a visible action such as a form, quote button, checkout link, or download.', 'help4-builder-suite'),
            ],
            [
                'key' => 'adsense',
                'title' => __('AdSense quality score', 'help4-builder-suite'),
                'meaning' => __('Checks for useful depth, readable organization, image context, low link clutter, and page quality signals that help ad-supported content.', 'help4-builder-suite'),
                'fix' => __('Add original substance, examples, screenshots, FAQs, or steps, then make sure images have alt text and the page is not just links.', 'help4-builder-suite'),
            ],
            [
                'key' => 'intent',
                'title' => __('Business-intent score', 'help4-builder-suite'),
                'meaning' => __('Uses a separate local, ecommerce, publisher, international, or general checklist so the page is measured against the job it actually performs.', 'help4-builder-suite'),
                'fix' => __('Open the intent-specific checklist on the page and complete the lowest-scoring factual signals first.', 'help4-builder-suite'),
            ],
        ];
    }

    /**
     * @param array<string, mixed> $scores
     * @return array<int, array{title: string, fix: string}>
     */
    private function scoreActionPlan(array $scores): array
    {
        $cards = [];
        foreach ($this->scorePlaybookCards() as $card) {
            $key = (string) ($card['key'] ?? '');
            $cards[] = [
                'title' => (string) ($card['title'] ?? ''),
                'fix' => (string) ($card['fix'] ?? ''),
                'score' => (int) ($scores[$key] ?? 0),
            ];
        }

        usort(
            $cards,
            static function (array $a, array $b): int {
                return ((int) ($a['score'] ?? 0)) <=> ((int) ($b['score'] ?? 0));
            }
        );

        $actions = [];
        foreach ($cards as $card) {
            if ((int) ($card['score'] ?? 0) >= 80) {
                continue;
            }
            $actions[] = [
                'title' => (string) ($card['title'] ?? ''),
                'fix' => (string) ($card['fix'] ?? ''),
            ];
            if (count($actions) >= 2) {
                break;
            }
        }

        return $actions;
    }

    private function scoreLabel(int $score): string
    {
        if ($score >= 90) {
            return __('Ready', 'help4-builder-suite');
        }
        if ($score >= 80) {
            return __('Strong', 'help4-builder-suite');
        }
        if ($score >= 65) {
            return __('Needs polish', 'help4-builder-suite');
        }

        return __('Needs work', 'help4-builder-suite');
    }

    /**
     * @return array<string, string>
     */
    private function conversionGoalLabels(): array
    {
        return [
            '' => __('Choose a goal', 'help4-builder-suite'),
            'lead' => __('Lead or contact request', 'help4-builder-suite'),
            'sale' => __('Sale or checkout action', 'help4-builder-suite'),
            'signup' => __('Signup or subscription', 'help4-builder-suite'),
            'download' => __('Download or install', 'help4-builder-suite'),
            'reader' => __('Reader engagement', 'help4-builder-suite'),
        ];
    }

    private function conversionGoalName(string $goal): string
    {
        $labels = $this->conversionGoalLabels();
        return (string) ($labels[$goal] ?? $labels['']);
    }

    private function suggestAnalyticsEventName(WP_Post $post, string $goal): string
    {
        $defaults = [
            'lead' => 'generate_lead',
            'sale' => 'begin_checkout',
            'signup' => 'sign_up',
            'download' => 'file_download',
            'reader' => 'article_read',
        ];
        if (isset($defaults[$goal])) {
            return $defaults[$goal];
        }

        return $post->post_type === 'post' ? 'article_read' : 'page_engagement';
    }

    private function plainTextForAnalysis(string $html): string
    {
        $html = strip_shortcodes($html);
        $html = preg_replace('/<(br|\/p|\/h[1-6]|\/li|\/div|\/section|\/article)\b[^>]*>/i', '. ', $html) ?? $html;
        $text = html_entity_decode(wp_strip_all_tags($html), ENT_QUOTES, 'UTF-8');
        return preg_replace('/\s+/u', ' ', trim($text)) ?? trim($text);
    }

    /**
     * @return array<string, mixed>
     */
    private function readabilityMetrics(WP_Post $post, string $text, string $html): array
    {
        $wordCount = $this->wordCountForText($text);
        $sentenceWordCounts = $this->sentenceWordCounts($text);
        $sentenceCount = max(1, count($sentenceWordCounts));
        $avgSentenceWords = $wordCount > 0 ? round($wordCount / $sentenceCount, 1) : 0.0;
        $longSentenceCount = 0;
        foreach ($sentenceWordCounts as $sentenceWords) {
            if ($sentenceWords > 24) {
                $longSentenceCount++;
            }
        }

        $syllables = $this->syllableCountForText($text);
        $readingEase = 0.0;
        $readingGrade = 0.0;
        if ($wordCount > 0) {
            $wordsPerSentence = $wordCount / $sentenceCount;
            $syllablesPerWord = max(1, $syllables) / $wordCount;
            $readingEase = round(max(0, min(100, 206.835 - (1.015 * $wordsPerSentence) - (84.6 * $syllablesPerWord))), 1);
            $readingGrade = round(max(0, (0.39 * $wordsPerSentence) + (11.8 * $syllablesPerWord) - 15.59), 1);
        }

        $headingCount = preg_match_all('/<h[1-6]\b/i', $html);
        $listCount = preg_match_all('/<(ul|ol)\b/i', $html);
        $linkStats = $this->linkStatsForContent($html);
        $imageStats = $this->imageStatsForContent($post, $html);

        return [
            'word_count' => $wordCount,
            'sentence_count' => $sentenceCount,
            'avg_sentence_words' => $avgSentenceWords,
            'long_sentence_count' => $longSentenceCount,
            'paragraph_count' => $this->paragraphCountForContent($html, $wordCount),
            'reading_ease' => $readingEase,
            'reading_grade' => $readingGrade,
            'heading_count' => is_int($headingCount) ? $headingCount : 0,
            'list_count' => is_int($listCount) ? $listCount : 0,
            'link_count' => (int) $linkStats['total'],
            'internal_link_count' => (int) $linkStats['internal'],
            'external_link_count' => (int) $linkStats['external'],
            'invalid_link_count' => (int) $linkStats['invalid'],
            'image_count' => (int) ($imageStats['count'] ?? 0),
            'missing_image_alt' => (int) ($imageStats['missing_alt'] ?? 0),
        ];
    }

    /**
     * @param array<string, mixed> $metrics
     * @return array{0: int, 1: array<int, string>}
     */
    private function scoreReadability(array $metrics): array
    {
        $warnings = [];
        $score = 0;
        $wordCount = (int) ($metrics['word_count'] ?? 0);
        $sentenceCount = (int) ($metrics['sentence_count'] ?? 0);
        $avgSentenceWords = (float) ($metrics['avg_sentence_words'] ?? 0);
        $longSentenceCount = (int) ($metrics['long_sentence_count'] ?? 0);
        $paragraphCount = (int) ($metrics['paragraph_count'] ?? 0);
        $readingEase = (float) ($metrics['reading_ease'] ?? 0);
        $longSentenceRatio = $sentenceCount > 0 ? $longSentenceCount / $sentenceCount : 1;

        if ($wordCount >= 150) {
            $score += 15;
        } elseif ($wordCount >= 80) {
            $score += 8;
            $warnings[] = __('Readable, but the page may need more context for visitors.', 'help4-builder-suite');
        } else {
            $warnings[] = __('Add enough useful copy that a first-time visitor understands the page.', 'help4-builder-suite');
        }

        if ($sentenceCount >= 2) {
            $score += 10;
        } else {
            $warnings[] = __('Add more than one sentence so the content has a real explanation.', 'help4-builder-suite');
        }

        if ($avgSentenceWords >= 8 && $avgSentenceWords <= 20) {
            $score += 25;
        } elseif ($avgSentenceWords > 0 && $avgSentenceWords <= 24) {
            $score += 16;
            $warnings[] = __('A few sentences are running long; trim or split them for easier reading.', 'help4-builder-suite');
        } else {
            $score += 8;
            $warnings[] = __('Break long sentences into simpler plain-English steps.', 'help4-builder-suite');
        }

        if ($longSentenceRatio <= 0.2) {
            $score += 20;
        } elseif ($longSentenceRatio <= 0.35) {
            $score += 10;
            $warnings[] = __('Some long sentences may make the page harder to scan.', 'help4-builder-suite');
        } else {
            $warnings[] = __('Too many sentences are over 24 words; split them before publishing.', 'help4-builder-suite');
        }

        if ($paragraphCount >= 3) {
            $score += 15;
        } elseif ($paragraphCount >= 1) {
            $score += 8;
            $warnings[] = __('Use shorter paragraphs or sections so visitors can skim the page.', 'help4-builder-suite');
        } else {
            $warnings[] = __('Add paragraph structure to make the page readable.', 'help4-builder-suite');
        }

        if ($readingEase >= 50) {
            $score += 15;
        } elseif ($readingEase >= 35) {
            $score += 10;
            $warnings[] = __('Reading ease is acceptable, but simpler phrasing would help non-technical visitors.', 'help4-builder-suite');
        } else {
            $score += 5;
            $warnings[] = __('The copy reads too dense; simplify jargon and shorten clauses.', 'help4-builder-suite');
        }

        return [max(0, min(100, $score)), $warnings];
    }

    /**
     * @return array{0: int, 1: array<int, string>}
     */
    private function scoreAnalyticsReadiness(WP_Post $post, string $title, string $description, string $html): array
    {
        $warnings = [];
        $score = 0;
        $goal = sanitize_key((string) get_post_meta($post->ID, '_h4_seo_conversion_goal', true));
        $eventName = sanitize_key((string) get_post_meta($post->ID, '_h4_seo_analytics_event', true));
        $primaryCta = trim((string) get_post_meta($post->ID, '_h4_seo_primary_cta', true));

        if ($goal !== '' && isset($this->conversionGoalLabels()[$goal])) {
            $score += 25;
        } else {
            $warnings[] = __('Choose the main conversion goal so this page can map cleanly to Analytics reports.', 'help4-builder-suite');
        }

        if ($eventName !== '') {
            $score += 15;
        } elseif ($goal !== '') {
            $score += 10;
        } else {
            $warnings[] = __('Add or accept a GA4-friendly event name for the page action.', 'help4-builder-suite');
        }

        if ($this->contentHasCta($html, $primaryCta)) {
            $score += 25;
        } else {
            $warnings[] = __('Add a clear call to action so Analytics can measure a meaningful next step.', 'help4-builder-suite');
        }

        if ($this->contentHasMeasurableAction($html)) {
            $score += 15;
        } else {
            $warnings[] = __('Add a form, link, button, checkout path, phone link, or download action visitors can take.', 'help4-builder-suite');
        }

        if ((string) get_post_meta($post->ID, '_h4_seo_noindex', true) !== '1') {
            $score += 10;
        } else {
            $warnings[] = __('This page is noindexed, so it should not be treated as a search acquisition page.', 'help4-builder-suite');
        }

        if ($title !== '' && $description !== '') {
            $score += 10;
        }

        return [max(0, min(100, $score)), $warnings];
    }

    /**
     * @param array<string, mixed> $metrics
     * @return array{0: int, 1: array<int, string>}
     */
    private function scoreAdsenseQuality(WP_Post $post, array $metrics, int $readabilityScore): array
    {
        $warnings = [];
        $score = 0;
        $wordCount = (int) ($metrics['word_count'] ?? 0);
        $headingCount = (int) ($metrics['heading_count'] ?? 0);
        $linkCount = (int) ($metrics['link_count'] ?? 0);
        $imageCount = (int) ($metrics['image_count'] ?? 0);
        $missingImageAlt = (int) ($metrics['missing_image_alt'] ?? 0);
        $listCount = (int) ($metrics['list_count'] ?? 0);

        if ($wordCount >= 600) {
            $score += 25;
        } elseif ($wordCount >= 300) {
            $score += 18;
        } elseif ($wordCount >= 150) {
            $score += 10;
            $warnings[] = __('AdSense-style quality is stronger when the page gives more complete, original value.', 'help4-builder-suite');
        } else {
            $warnings[] = __('The page is too thin for ad-supported or highly competitive content.', 'help4-builder-suite');
        }

        if ($readabilityScore >= 70) {
            $score += 20;
        } elseif ($readabilityScore >= 55) {
            $score += 12;
            $warnings[] = __('Improve readability before treating this as an ad-supported page.', 'help4-builder-suite');
        } else {
            $warnings[] = __('Readability is weak for AdSense-style visitor quality.', 'help4-builder-suite');
        }

        if ($headingCount >= 2) {
            $score += 15;
        } elseif ($headingCount === 1) {
            $score += 8;
            $warnings[] = __('Add more section headings so the page feels organized and complete.', 'help4-builder-suite');
        } else {
            $warnings[] = __('Add headings before treating this page as a quality content surface.', 'help4-builder-suite');
        }

        if ($imageCount > 0 && $missingImageAlt === 0) {
            $score += 15;
        } elseif ($imageCount > 0) {
            $score += 8;
            $warnings[] = __('Add descriptive alt text to images before pushing this page harder.', 'help4-builder-suite');
        } else {
            $score += 5;
            $warnings[] = __('Add useful images or screenshots near the related copy where they help the reader.', 'help4-builder-suite');
        }

        if ($linkCount === 0 || $wordCount >= ($linkCount * 35)) {
            $score += 10;
        } else {
            $warnings[] = __('The page has a lot of links for its amount of copy; add substance or reduce clutter.', 'help4-builder-suite');
        }

        $faqItems = trim((string) get_post_meta($post->ID, '_h4_seo_faq_items', true));
        if ($listCount > 0 || $faqItems !== '') {
            $score += 10;
        } else {
            $warnings[] = __('Add FAQs, steps, examples, or lists to make the page more useful.', 'help4-builder-suite');
        }

        if ((string) get_post_meta($post->ID, '_h4_seo_nosnippet', true) !== '1') {
            $score += 5;
        }

        return [max(0, min(100, $score)), $warnings];
    }

    private function wordCountForText(string $text): int
    {
        if ($text === '') {
            return 0;
        }
        $matched = preg_match_all('/[\p{L}\p{N}][\p{L}\p{N}\'-]*/u', $text);
        return is_int($matched) ? $matched : 0;
    }

    /**
     * @return array<int, int>
     */
    private function sentenceWordCounts(string $text): array
    {
        $text = trim($text);
        if ($text === '') {
            return [];
        }

        $sentences = preg_split('/(?<=[.!?])\s+/u', $text);
        if (! is_array($sentences) || $sentences === []) {
            return [$this->wordCountForText($text)];
        }

        $counts = [];
        foreach ($sentences as $sentence) {
            $count = $this->wordCountForText((string) $sentence);
            if ($count > 0) {
                $counts[] = $count;
            }
        }

        return $counts !== [] ? $counts : [$this->wordCountForText($text)];
    }

    private function syllableCountForText(string $text): int
    {
        $words = [];
        preg_match_all('/[a-zA-Z]+/', $text, $matches);
        if (isset($matches[0]) && is_array($matches[0])) {
            $words = $matches[0];
        }

        $count = 0;
        foreach ($words as $word) {
            $count += $this->syllableCountForWord((string) $word);
        }

        return $count;
    }

    private function syllableCountForWord(string $word): int
    {
        $word = strtolower(preg_replace('/[^a-z]/i', '', $word) ?? '');
        if ($word === '') {
            return 1;
        }
        $word = preg_replace('/e$/', '', $word) ?? $word;
        $matched = preg_match_all('/[aeiouy]+/', $word);
        return max(1, is_int($matched) ? $matched : 1);
    }

    private function paragraphCountForContent(string $html, int $wordCount): int
    {
        $matched = preg_match_all('/<(p|li|h[1-6])\b/i', $html);
        if (is_int($matched) && $matched > 0) {
            return $matched;
        }

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

        return max(1, (int) ceil($wordCount / 120));
    }

    /**
     * @return array{total: int, internal: int, external: int, invalid: int}
     */
    private function linkStatsForContent(string $html): array
    {
        $stats = ['total' => 0, 'internal' => 0, 'external' => 0, 'invalid' => 0];
        $urls = [];
        if (class_exists('WP_HTML_Tag_Processor')) {
            $processor = new \WP_HTML_Tag_Processor($html);
            while ($processor->next_tag('a')) {
                $urls[] = (string) $processor->get_attribute('href');
            }
        } else {
            preg_match_all('/<a\b[^>]*href=["\']([^"\']*)["\']/i', $html, $matches);
            $urls = isset($matches[1]) && is_array($matches[1]) ? $matches[1] : [];
        }
        $homeHost = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        foreach ($urls as $url) {
            $url = trim(html_entity_decode((string) $url));
            if ($url === '' || preg_match('#^(?:javascript|data):#i', $url)) {
                $stats['invalid']++;
                continue;
            }
            if (str_starts_with($url, '#') || preg_match('#^(?:mailto|tel):#i', $url)) {
                continue;
            }
            $stats['total']++;
            $host = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
            if ($host === '' || $host === $homeHost) {
                $stats['internal']++;
            } else {
                $stats['external']++;
            }
        }
        return $stats;
    }

    /**
     * @return array{count: int, missing_alt: int}
     */
    private function imageStatsForContent(WP_Post $post, string $html): array
    {
        $count = 0;
        $missingAlt = 0;
        $matched = preg_match_all('/<img\b[^>]*>/i', $html, $matches);
        if (is_int($matched) && isset($matches[0]) && is_array($matches[0])) {
            foreach ($matches[0] as $tag) {
                $count++;
                if (! preg_match('/\salt\s*=\s*([\'"])(.*?)\1/i', (string) $tag, $altMatch)) {
                    $missingAlt++;
                    continue;
                }
                $alt = html_entity_decode(wp_strip_all_tags((string) ($altMatch[2] ?? '')), ENT_QUOTES, 'UTF-8');
                if (trim($alt) === '') {
                    $missingAlt++;
                }
            }
        }

        $thumbnailId = get_post_thumbnail_id($post);
        if ($thumbnailId) {
            $count++;
            $alt = trim((string) get_post_meta((int) $thumbnailId, '_wp_attachment_image_alt', true));
            if ($alt === '') {
                $missingAlt++;
            }
        }

        return [
            'count' => $count,
            'missing_alt' => $missingAlt,
        ];
    }

    private function contentHasCta(string $html, string $primaryCta): bool
    {
        if (trim($primaryCta) !== '') {
            return true;
        }

        $text = wp_strip_all_tags($html);
        return preg_match('/\b(contact|call|quote|book|buy|checkout|cart|subscribe|signup|sign up|download|start|request|schedule|join|get started|learn more)\b/i', $text) === 1
            || preg_match('/<(a|button|input)\b[^>]*(contact|quote|checkout|download|subscribe|submit|tel:|mailto:|book|schedule|get-started)/i', $html) === 1
            || preg_match('/\[(h4_contact_form|h4_form|h4_newsletter_form|contact-form-7|gravityform)\b/i', $html) === 1;
    }

    private function contentHasMeasurableAction(string $html): bool
    {
        return preg_match('/<a\b[^>]+href=/i', $html) === 1
            || preg_match('/<(button|form|input)\b/i', $html) === 1
            || preg_match('/\[(h4_contact_form|h4_form|h4_newsletter_form|h4_pricing_table|h4_comparison_table|contact-form-7|gravityform)\b/i', $html) === 1;
    }

    private function textLength(string $value): int
    {
        $value = html_entity_decode(wp_strip_all_tags($value), ENT_QUOTES, 'UTF-8');
        if (function_exists('mb_strlen')) {
            return (int) mb_strlen($value);
        }

        return strlen($value);
    }

    /**
     * @return array<int, string>
     */
    private function sanitizeLineList(string $raw, bool $urls): array
    {
        $clean = [];
        foreach (preg_split('/[\r\n]+/', $raw) ?: [] as $line) {
            $line = trim($line);
            if ($line === '') {
                continue;
            }
            $value = $urls
                ? esc_url_raw($line, ['http', 'https'])
                : sanitize_text_field($line);
            if ($value !== '') {
                $clean[] = $value;
            }
        }
        return array_slice(array_values(array_unique($clean)), 0, 100);
    }

    private function normalizeMetaDescription(string $value): string
    {
        $value = html_entity_decode(wp_strip_all_tags($value), ENT_QUOTES, 'UTF-8');
        $value = preg_replace('/\s+/', ' ', trim($value)) ?? '';
        if ($value === '') {
            return '';
        }
        if (function_exists('mb_strlen') && function_exists('mb_substr')) {
            if (mb_strlen($value) > 165) {
                $value = $this->truncateMetaDescription($value);
            }
            return $value;
        }
        if (strlen($value) > 165) {
            $value = $this->truncateMetaDescription($value);
        }
        return $value;
    }

    private function truncateMetaDescription(string $value): string
    {
        $slice = function_exists('mb_substr')
            ? (string) mb_substr($value, 0, 162)
            : substr($value, 0, 162);
        $wordSafe = preg_replace('/\s+\S*$/u', '', $slice);
        if (is_string($wordSafe) && $this->textLength($wordSafe) >= 80) {
            $slice = $wordSafe;
        }

        return rtrim($slice, " \t\n\r\0\x0B.,;:") . '...';
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function siteNameForMeta(array $settings): string
    {
        $candidates = [
            get_bloginfo('name'),
            $this->customerSafeBrandName($settings),
            $this->isHelp4OwnedHost() ? 'Help4 WordPress' : '',
            $this->hostNameForMeta(),
        ];

        foreach ($candidates as $candidate) {
            $candidate = trim(wp_strip_all_tags((string) $candidate));
            if ($candidate === '' || $this->isWeakDefaultText($candidate)) {
                continue;
            }
            return $candidate;
        }

        return $this->isHelp4OwnedHost() ? 'Help4 WordPress' : __('Website', 'help4-builder-suite');
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function customerSafeBrandName(array $settings): string
    {
        $brand = trim(wp_strip_all_tags((string) ($settings['brand_name'] ?? '')));
        if ($brand === '') {
            return '';
        }

        if (! $this->isHelp4OwnedHost() && strcasecmp($brand, 'Help4 WordPress') === 0) {
            return '';
        }

        return $brand;
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function customerSafeContactUrl(array $settings): string
    {
        $configured = esc_url_raw((string) ($settings['support_url'] ?? ''));
        if ($configured !== '') {
            return $configured;
        }

        return $this->isHelp4OwnedHost() ? ContactRoutes::supportTicketUrl($settings) : '';
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function homeTitleFallback(array $settings): string
    {
        if ($this->isHelp4OwnedHost()) {
            return 'Help4 WordPress | Websites done differently';
        }

        $siteName = $this->siteNameForMeta($settings);
        return $siteName !== '' ? $siteName . ' | Website' : __('Website', 'help4-builder-suite');
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function metaDescriptionFallback(array $settings): string
    {
        if ($this->isHelp4OwnedHost()) {
            return 'Help4 WordPress delivers builder templates, SEO tools, security controls, performance tuning, and support workflows for WordPress websites.';
        }

        $siteName = $this->siteNameForMeta($settings);
        return sprintf(
            /* translators: %s: Site name. */
            __('%s website information, services, resources, and contact details.', 'help4-builder-suite'),
            $siteName !== '' ? $siteName : __('This website', 'help4-builder-suite')
        );
    }

    private function hostNameForMeta(): string
    {
        $host = strtolower((string) wp_parse_url((string) home_url('/'), PHP_URL_HOST));
        $host = (string) preg_replace('/^www\./', '', $host);
        if ($host === '') {
            return '';
        }

        $label = str_replace(['-', '.'], ' ', $host);
        $label = trim((string) preg_replace('/\s+/', ' ', $label));
        return $label !== '' ? sanitize_text_field(ucwords($label)) : '';
    }

    private function isWeakDefaultText(string $value): bool
    {
        $value = trim($value);
        return $value === ''
            || strcasecmp($value, 'My blog') === 0
            || strcasecmp($value, 'Just another WordPress site') === 0;
    }
}
