<?php

namespace WPSN\Modules\MetaTags;

use WPSN\Core\AbstractModule;
use WPSN\Core\Branding;
use WPSN\SEO\MetaKeys;
use WPSN\SEO\SeoMetaRepository;
use WPSN\SEO\DocumentResolver;
use WPSN\Domain\SeoMeta;
use WPSN\Modules\Revisions\RevisionsInstaller;
use WPSN\Modules\Revisions\RevisionsRepository;

defined('ABSPATH') || exit;

final class MetaTagsModule extends AbstractModule
{
    public function id(): string
    {
        return 'meta_tags';
    }

    public function name(): string
    {
        return __('Meta Tags', 'wp-seo-next');
    }

    public function description(): string
    {
        return __('Title, description, canonical, robots meta + per-post overrides.', 'wp-seo-next');
    }

    public function defaultEnabled(): bool
    {
        return true;
    }

    public function register(): void
    {
        // Storage / UI
        add_action('init', [$this, 'registerMetaKeys']);
        add_action('init', [$this, 'registerTermUiHooks'], 20);
        add_action('add_meta_boxes', [$this, 'addMetaBox']);
        add_action('save_post', [$this, 'saveMetaBox'], 10, 2);

        // Quick actions.
        add_action('admin_post_wpsn_seo_undo_last', [$this, 'handleUndoLastSeoChange']);
        add_action('admin_notices', [$this, 'maybeUndoNotice']);

        // Front-end output
        add_filter('pre_get_document_title', [$this, 'filterDocumentTitle'], 20);
        add_action('wp_head', [$this, 'renderVerificationMeta'], 2);
        add_action('wp_head', [$this, 'renderMetaDescription'], 5);

        // Canonical + hreflang alternates
        remove_action('wp_head', 'rel_canonical');
        add_action('wp_head', [$this, 'renderCanonical'], 1);
        add_action('wp_head', [$this, 'renderHreflangAlternates'], 2);

        // Robots meta (via WP Robots API).
        add_filter('wp_robots', [$this, 'filterWpRobots'], 20);
    }

    /**
     * Render optional site verification meta tags.
     *
     * These are used by Google Search Console / Bing / Yandex / Baidu / Ahrefs.
     *
     * Stored values are plain tokens (not full meta tags).
     */
    public function renderVerificationMeta(): void
    {
        // Frontend only (wp_head).
        if (is_admin()) {
            return;
        }

        $settings = $this->container->get('settings');
        $ver = $settings->get('verification', []);
        if (!is_array($ver)) {
            return;
        }

        $map = [
            'google' => 'google-site-verification',
            'bing'   => 'msvalidate.01',
            'yandex' => 'yandex-verification',
            'baidu'  => 'baidu-site-verification',
            'ahrefs' => 'ahrefs-site-verification',
        ];

        $out = [];
        foreach ($map as $key => $metaName) {
            $val = isset($ver[$key]) ? trim((string) $ver[$key]) : '';
            if ($val === '') {
                continue;
            }
            $out[] = '<meta name="' . esc_attr($metaName) . '" content="' . esc_attr($val) . '" />';
        }

        if (empty($out)) {
            return;
        }

        echo "\n" . '<!-- WP SEO Next: site verification -->' . "\n";
        echo implode("\n", $out) . "\n";
    }

    public function maybeUndoNotice(): void
    {
        if (!is_admin()) {
            return;
        }

        $code = isset($_GET['wpsn_seo_notice']) ? sanitize_key((string) wp_unslash($_GET['wpsn_seo_notice'])) : '';
        if ($code === '') {
            return;
        }

        $type = 'success';
        $msg = '';

        switch ($code) {
            case 'undo_done':
                $type = 'success';
                $msg = __('SEO change rolled back successfully.', 'wp-seo-next');
                break;
            case 'undo_none':
                $type = 'info';
                $msg = __('Nothing to undo yet.', 'wp-seo-next');
                break;
            case 'undo_denied':
                $type = 'error';
                $msg = __('Permission denied.', 'wp-seo-next');
                break;
            case 'undo_invalid':
                $type = 'error';
                $msg = __('Invalid undo request.', 'wp-seo-next');
                break;
            case 'undo_failed':
            default:
                $type = 'error';
                $msg = __('Undo failed. Please check Revisions for details.', 'wp-seo-next');
                break;
        }

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

        if (!in_array($type, ['success', 'error', 'warning', 'info'], true)) {
            $type = 'success';
        }

        echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p>' . esc_html($msg) . '</p></div>';
    }

    public function handleUndoLastSeoChange(): void
    {
        $objectType = isset($_GET['object_type']) ? sanitize_key((string) wp_unslash($_GET['object_type'])) : '';
        $objectId = isset($_GET['object_id']) ? (int) $_GET['object_id'] : 0;

        $nonceAction = 'wpsn_seo_undo_last_' . $objectType . '_' . $objectId;
        check_admin_referer($nonceAction, 'wpsn_nonce');

        $return = isset($_GET['return']) ? (string) wp_unslash($_GET['return']) : '';
        $return = is_string($return) ? $return : '';

        $fallback = admin_url();
        if ($objectType === 'post' && $objectId > 0) {
            $fallback = admin_url('post.php?post=' . $objectId . '&action=edit');
        } elseif ($objectType === 'term' && $objectId > 0) {
            $term = get_term($objectId);
            if ($term && !is_wp_error($term) && isset($term->taxonomy)) {
                $link = get_edit_term_link($objectId, (string) $term->taxonomy);
                if (is_string($link) && $link !== '') {
                    $fallback = $link;
                }
            }
        }

        $return = $return !== '' ? wp_validate_redirect($return, $fallback) : $fallback;

        if ($objectType === '' || $objectId <= 0) {
            $this->redirectUndoNotice($return, 'undo_invalid');
        }

        // Capability checks.
        if ($objectType === 'post') {
            if (!current_user_can('edit_post', $objectId)) {
                $this->redirectUndoNotice($return, 'undo_denied');
            }
        } elseif ($objectType === 'term') {
            $term = get_term($objectId);
            if (!$term || is_wp_error($term) || !isset($term->taxonomy)) {
                $this->redirectUndoNotice($return, 'undo_invalid');
            }
            $taxonomy = (string) $term->taxonomy;
            $taxObj = function_exists('get_taxonomy') ? get_taxonomy($taxonomy) : null;
            $capEdit = ($taxObj && isset($taxObj->cap) && isset($taxObj->cap->edit_terms)) ? (string) $taxObj->cap->edit_terms : '';
            if ($capEdit !== '') {
                if (!current_user_can($capEdit)) {
                    $this->redirectUndoNotice($return, 'undo_denied');
                }
            } else {
                if (!current_user_can('manage_categories')) {
                    $this->redirectUndoNotice($return, 'undo_denied');
                }
            }
        } else {
            $this->redirectUndoNotice($return, 'undo_invalid');
        }

        RevisionsInstaller::maybeInstall();
        $repo = new RevisionsRepository($this->container->get('logger'));

        // Only undo manual SEO override changes (meta_tags + admin_lists) that were not rolled back yet.
        $row = $repo->findLatest([
            'object_type' => $objectType,
            'object_id' => $objectId,
            'sources' => ['meta_tags', 'admin_lists', 'frontend_inspector'],
            'action' => 'update',
            'only_unrolled' => true,
        ]);

        if (!is_array($row)) {
            $this->redirectUndoNotice($return, 'undo_none');
        }

        $revisionId = (int) ($row['id'] ?? 0);
        $objectSubtype = isset($row['object_subtype']) ? sanitize_key((string) $row['object_subtype']) : '';

        $before = $row['before_decoded'] ?? null;
        if (!is_array($before) || !isset($before['meta']) || !is_array($before['meta'])) {
            $this->redirectUndoNotice($return, 'undo_failed');
        }

        // Snapshot current state in the same shape.
        $current = ['meta' => []];
        foreach ($before['meta'] as $k => $v) {
            $key = is_string($k) ? (string) $k : '';
            if ($key === '') {
                continue;
            }

            if ($objectType === 'post') {
                $exists = function_exists('metadata_exists') ? metadata_exists('post', $objectId, $key) : true;
                $current['meta'][$key] = $exists ? (string) get_post_meta($objectId, $key, true) : null;
            } else {
                $exists = function_exists('metadata_exists') ? metadata_exists('term', $objectId, $key) : true;
                $current['meta'][$key] = $exists ? (string) get_term_meta($objectId, $key, true) : null;
            }
        }

        // Apply rollback.
        foreach ($before['meta'] as $k => $v) {
            $key = is_string($k) ? (string) $k : '';
            if ($key === '') {
                continue;
            }

            if ($v === null) {
                if ($objectType === 'post') {
                    delete_post_meta($objectId, $key);
                } else {
                    delete_term_meta($objectId, $key);
                }
                continue;
            }

            $val = is_scalar($v) ? (string) $v : '';
            if ($val === '') {
                if ($objectType === 'post') {
                    delete_post_meta($objectId, $key);
                } else {
                    delete_term_meta($objectId, $key);
                }
            } else {
                if ($objectType === 'post') {
                    update_post_meta($objectId, $key, $val);
                } else {
                    update_term_meta($objectId, $key, $val);
                }
            }
        }

        // Notify dependent modules (sitemaps, analysis, etc.).
        do_action('seo/meta/updated', $objectId, $objectType);

        // Record rollback as a new revision.
        $rollbackId = $repo->insert([
            'object_type' => $objectType,
            'object_id' => $objectId,
            'object_subtype' => $objectSubtype,
            'action' => 'rollback',
            'source' => 'revisions',
            'actor_user_id' => (int) get_current_user_id(),
            'before' => $current,
            'after' => $before,
            'meta' => [
                'rollback_of' => $revisionId,
                'context' => 'undo_button',
            ],
        ]);

        if ($rollbackId > 0 && $revisionId > 0) {
            $repo->markRolledBack($revisionId, $rollbackId);
        }

        $this->redirectUndoNotice($return, 'undo_done');
    }

    private function redirectUndoNotice(string $return, string $notice): void
    {
        $url = (string) remove_query_arg(['wpsn_seo_notice'], $return);
        $url = (string) add_query_arg(['wpsn_seo_notice' => sanitize_key($notice)], $url);
        wp_safe_redirect($url);
        exit;
    }

    public function registerMetaKeys(): void
    {
        // Register meta for all public post types (except attachments).
        $postTypes = get_post_types(['public' => true], 'names');
        foreach ($postTypes as $pt) {
            if ($pt === 'attachment') {
                continue;
            }

            register_post_meta($pt, MetaKeys::TITLE, [
                'type'              => 'string',
                'single'            => true,
                'show_in_rest'      => true,
                'sanitize_callback' => 'sanitize_text_field',
                'auth_callback'     => static function (bool $allowed, string $metaKey, int $postId, int $userId, string $cap, array $caps): bool {
                    return current_user_can('edit_post', $postId);
                },
            ]);

            register_post_meta($pt, MetaKeys::DESCRIPTION, [
                'type'              => 'string',
                'single'            => true,
                'show_in_rest'      => true,
                'sanitize_callback' => 'sanitize_textarea_field',
                'auth_callback'     => static function (bool $allowed, string $metaKey, int $postId, int $userId, string $cap, array $caps): bool {
                    return current_user_can('edit_post', $postId);
                },
            ]);

            register_post_meta($pt, MetaKeys::CANONICAL, [
                'type'              => 'string',
                'single'            => true,
                'show_in_rest'      => true,
                'sanitize_callback' => static function ($value): string {
                    return is_string($value) ? esc_url_raw($value) : '';
                },
                'auth_callback'     => static function (bool $allowed, string $metaKey, int $postId): bool {
                    return current_user_can('edit_post', $postId);
                },
            ]);

            register_post_meta($pt, MetaKeys::ROBOTS_INDEX, [
                'type'              => 'string',
                'single'            => true,
                'show_in_rest'      => true,
                'sanitize_callback' => [self::class, 'sanitizeRobotsTriStateIndex'],
                'auth_callback'     => static function (bool $allowed, string $metaKey, int $postId): bool {
                    return current_user_can('edit_post', $postId);
                },
            ]);

            register_post_meta($pt, MetaKeys::ROBOTS_FOLLOW, [
                'type'              => 'string',
                'single'            => true,
                'show_in_rest'      => true,
                'sanitize_callback' => [self::class, 'sanitizeRobotsTriStateFollow'],
                'auth_callback'     => static function (bool $allowed, string $metaKey, int $postId): bool {
                    return current_user_can('edit_post', $postId);
                },
            ]);
        }

        // Register term meta.
        $taxonomies = get_taxonomies(['public' => true], 'names');
        foreach ($taxonomies as $taxonomy) {
            $taxonomy = sanitize_key((string) $taxonomy);
            if ($taxonomy === '') {
                continue;
            }

            $authCb = static function (bool $allowed, string $metaKey, int $termId, ...$rest): bool {
                $term = get_term($termId);
                if (!$term || is_wp_error($term)) {
                    return current_user_can('manage_options');
                }
                $tax = get_taxonomy($term->taxonomy);
                if (!$tax) {
                    return current_user_can('manage_options');
                }
                return current_user_can($tax->cap->edit_terms);
            };

            register_term_meta($taxonomy, MetaKeys::TITLE, [
                'type'              => 'string',
                'single'            => true,
                'show_in_rest'      => true,
                'sanitize_callback' => 'sanitize_text_field',
                'auth_callback'     => $authCb,
            ]);

            register_term_meta($taxonomy, MetaKeys::DESCRIPTION, [
                'type'              => 'string',
                'single'            => true,
                'show_in_rest'      => true,
                'sanitize_callback' => 'sanitize_textarea_field',
                'auth_callback'     => $authCb,
            ]);

            register_term_meta($taxonomy, MetaKeys::CANONICAL, [
                'type'              => 'string',
                'single'            => true,
                'show_in_rest'      => true,
                'sanitize_callback' => static function ($value): string {
                    return is_string($value) ? esc_url_raw($value) : '';
                },
                'auth_callback'     => $authCb,
            ]);

            register_term_meta($taxonomy, MetaKeys::ROBOTS_INDEX, [
                'type'              => 'string',
                'single'            => true,
                'show_in_rest'      => true,
                'sanitize_callback' => [self::class, 'sanitizeRobotsTriStateIndex'],
                'auth_callback'     => $authCb,
            ]);

            register_term_meta($taxonomy, MetaKeys::ROBOTS_FOLLOW, [
                'type'              => 'string',
                'single'            => true,
                'show_in_rest'      => true,
                'sanitize_callback' => [self::class, 'sanitizeRobotsTriStateFollow'],
                'auth_callback'     => $authCb,
            ]);
        }

    }

    public static function sanitizeRobotsTriStateIndex($value): string
    {
        $value = is_string($value) ? trim($value) : '';
        $allowed = ['default', 'index', 'noindex'];
        return in_array($value, $allowed, true) ? $value : '';
    }

    public static function sanitizeRobotsTriStateFollow($value): string
    {
        $value = is_string($value) ? trim($value) : '';
        $allowed = ['default', 'follow', 'nofollow'];
        return in_array($value, $allowed, true) ? $value : '';
    }

    
    public function registerTermUiHooks(): void
    {
        if (!is_admin()) {
            return;
        }

        $taxonomies = get_taxonomies(['public' => true], 'names');
        foreach ($taxonomies as $taxonomy) {
            $taxonomy = sanitize_key((string) $taxonomy);
            if ($taxonomy === '') {
                continue;
            }

            add_action($taxonomy . '_add_form_fields', [$this, 'renderTermFieldsAdd']);
            add_action($taxonomy . '_edit_form_fields', [$this, 'renderTermFieldsEdit'], 10, 2);
            add_action('created_' . $taxonomy, [$this, 'saveTermFields'], 10, 2);
            add_action('edited_' . $taxonomy, [$this, 'saveTermFields'], 10, 2);
        }
    }

    public function renderTermFieldsAdd(string $taxonomy): void
    {
        $tax = get_taxonomy($taxonomy);
        if (!$tax || empty($tax->public)) {
            return;
        }
        if (!current_user_can($tax->cap->edit_terms)) {
            return;
        }

        wp_nonce_field('wpsn_save_term_meta', 'wpsn_term_meta_nonce');

        ?>
        <div class="form-field">
            <label><strong><?php echo esc_html(Branding::labelWithBrand($this->settings, __('SEO Title', 'wp-seo-next'))); ?></strong></label>
            <input type="text" name="wpsn_term_meta[seo_title]" value="" />
            <p class="description"><?php echo esc_html__('Leave empty to use template.', 'wp-seo-next'); ?></p>
        </div>

        <div class="form-field">
            <label><strong><?php echo esc_html(Branding::labelWithBrand($this->settings, __('Meta Description', 'wp-seo-next'))); ?></strong></label>
            <textarea name="wpsn_term_meta[meta_description]" rows="3" cols="40"></textarea>
            <p class="description"><?php echo esc_html__('Leave empty to use template.', 'wp-seo-next'); ?></p>
        </div>

        <div class="form-field">
            <label><strong><?php echo esc_html(Branding::labelWithBrand($this->settings, __('Preview', 'wp-seo-next'))); ?></strong></label>
            <p class="description"><?php echo esc_html__('Preview is available after saving the term.', 'wp-seo-next'); ?></p>
        </div>

        <hr style="margin:16px 0;" />

        <div class="form-field">
            <label><strong><?php echo esc_html(Branding::labelWithBrand($this->settings, __('Canonical URL', 'wp-seo-next'))); ?></strong></label>
            <input type="url" name="wpsn_term_meta[canonical]" value="" placeholder="https://..." />
            <p class="description"><?php echo esc_html__('Leave empty for automatic canonical.', 'wp-seo-next'); ?></p>
        </div>

        <div class="form-field">
            <label><strong><?php echo esc_html(Branding::labelWithBrand($this->settings, __('Robots', 'wp-seo-next'))); ?></strong></label>

            <p style="margin:6px 0 0;">
                <span style="display:inline-block; min-width: 140px;"><?php echo esc_html__('Indexing', 'wp-seo-next'); ?></span>
                <select name="wpsn_term_meta[robots_index]">
                    <option value="default"><?php echo esc_html__('Inherit defaults', 'wp-seo-next'); ?></option>
                    <option value="index"><?php echo esc_html__('Index', 'wp-seo-next'); ?></option>
                    <option value="noindex"><?php echo esc_html__('Noindex', 'wp-seo-next'); ?></option>
                </select>
            </p>

            <p style="margin:6px 0 0;">
                <span style="display:inline-block; min-width: 140px;"><?php echo esc_html__('Following', 'wp-seo-next'); ?></span>
                <select name="wpsn_term_meta[robots_follow]">
                    <option value="default"><?php echo esc_html__('Inherit defaults', 'wp-seo-next'); ?></option>
                    <option value="follow"><?php echo esc_html__('Follow', 'wp-seo-next'); ?></option>
                    <option value="nofollow"><?php echo esc_html__('Nofollow', 'wp-seo-next'); ?></option>
                </select>
            </p>
        </div>
        <?php
    }

    public function renderTermFieldsEdit(\WP_Term $term, string $taxonomy): void
    {
        $tax = get_taxonomy($taxonomy);
        if (!$tax || empty($tax->public)) {
            return;
        }
        if (!current_user_can($tax->cap->edit_terms)) {
            return;
        }

        wp_nonce_field('wpsn_save_term_meta', 'wpsn_term_meta_nonce');

        $title = (string) get_term_meta($term->term_id, MetaKeys::TITLE, true);
        $desc  = (string) get_term_meta($term->term_id, MetaKeys::DESCRIPTION, true);
        $canon = (string) get_term_meta($term->term_id, MetaKeys::CANONICAL, true);

        $rIndex  = (string) get_term_meta($term->term_id, MetaKeys::ROBOTS_INDEX, true);
        $rFollow = (string) get_term_meta($term->term_id, MetaKeys::ROBOTS_FOLLOW, true);

        $rIndex  = $rIndex !== '' ? $rIndex : 'default';
        $rFollow = $rFollow !== '' ? $rFollow : 'default';

        $historyUrl = admin_url('tools.php?page=wpsn-revisions&object_type=term&object_id=' . (int) $term->term_id);
        $returnUrl = get_edit_term_link((int) $term->term_id, (string) $taxonomy);
        if (!is_string($returnUrl) || $returnUrl === '') {
            $returnUrl = admin_url('term.php?taxonomy=' . rawurlencode((string) $taxonomy) . '&tag_ID=' . (int) $term->term_id);
        }
        $undoUrl = wp_nonce_url(
            admin_url('admin-post.php?action=wpsn_seo_undo_last&object_type=term&object_id=' . (int) $term->term_id . '&return=' . rawurlencode($returnUrl)),
            'wpsn_seo_undo_last_term_' . (int) $term->term_id,
            'wpsn_nonce'
        );

        ?>
        <tr class="form-field">
            <th scope="row"><label><?php echo esc_html(Branding::labelWithBrand($this->settings, __('History', 'wp-seo-next'))); ?></label></th>
            <td>
                <a class="button button-secondary" href="<?php echo esc_url($historyUrl); ?>"><?php echo esc_html__('Open Revisions', 'wp-seo-next'); ?></a>
                <a class="button button-secondary" href="<?php echo esc_url($undoUrl); ?>" onclick="return confirm('<?php echo esc_js(__('Undo last SEO change for this term?', 'wp-seo-next')); ?>');"><?php echo esc_html__('Undo last SEO change', 'wp-seo-next'); ?></a>
                <p class="description" style="margin-top:8px;">
                    <?php echo esc_html__('Undo will roll back the latest not-yet-rolled-back SEO override change made in WP SEO Next.', 'wp-seo-next'); ?>
                </p>
            </td>
        </tr>
        <tr class="form-field">
            <th scope="row"><label for="wpsn_term_seo_title"><?php echo esc_html(Branding::labelWithBrand($this->settings, __('SEO Title', 'wp-seo-next'))); ?></label></th>
            <td>
                <input id="wpsn_term_seo_title" type="text" class="regular-text" name="wpsn_term_meta[seo_title]" value="<?php echo esc_attr($title); ?>" />
                <p class="description"><?php echo esc_html__('Leave empty to use template.', 'wp-seo-next'); ?></p>
            </td>
        </tr>

        <tr class="form-field">
            <th scope="row"><label for="wpsn_term_meta_description"><?php echo esc_html(Branding::labelWithBrand($this->settings, __('Meta Description', 'wp-seo-next'))); ?></label></th>
            <td>
                <textarea id="wpsn_term_meta_description" name="wpsn_term_meta[meta_description]" rows="4" cols="40" class="large-text"><?php echo esc_textarea($desc); ?></textarea>
                <p class="description"><?php echo esc_html__('Leave empty to use template.', 'wp-seo-next'); ?></p>
            </td>
        </tr>

        <?php
        // Snippet preview (computed output).
        $previewTitle = '';
        $previewDesc = '';
        $previewCanon = '';
        try {
            /** @var \WPSN\SEO\SeoMetaRepository $repo */
            $repo = $this->container->get('meta_repo');
            $link = get_term_link($term->term_id, $taxonomy);
            $url = is_string($link) ? $link : (string) home_url('/');
            $ctx = new \WPSN\Domain\SeoDocumentContext(\WPSN\Domain\SeoDocumentContext::TYPE_TERM, (int) $term->term_id, (string) $taxonomy, $url, 1);
            $meta = $repo->forContext($ctx);
            if ($meta instanceof \WPSN\Domain\SeoMeta) {
                $previewTitle = (string) $meta->title;
                $previewDesc = (string) $meta->description;
                $previewCanon = (string) $meta->canonical;
            }
        } catch (\Throwable $e) {
            $previewTitle = '';
            $previewDesc = '';
            $previewCanon = '';
        }

        $previewTitle = trim($previewTitle);
        $previewDesc = trim($previewDesc);
        $previewCanon = trim($previewCanon);
        ?>

        <tr class="form-field">
            <th scope="row"><label><?php echo esc_html(Branding::labelWithBrand($this->settings, __('Preview', 'wp-seo-next'))); ?></label></th>
            <td>
                <div style="max-width: 680px; border: 1px solid #dcdcde; background: #fff; padding: 12px 14px; border-radius: 6px;">
                    <div style="font-size: 18px; line-height: 1.3; margin: 0 0 4px; color: #1a0dab;">
                        <?php echo esc_html($previewTitle !== '' ? $previewTitle : __('(empty title)', 'wp-seo-next')); ?>
                    </div>
                    <div style="font-size: 13px; line-height: 1.3; margin: 0 0 6px; color: #006621;">
                        <?php echo esc_html($previewCanon !== '' ? $previewCanon : (string) get_term_link($term)); ?>
                    </div>
                    <div style="font-size: 13px; line-height: 1.45; margin: 0; color: #4d5156;">
                        <?php echo esc_html($previewDesc !== '' ? $previewDesc : __('(empty description)', 'wp-seo-next')); ?>
                    </div>
                </div>
                <p class="description" style="margin-top:8px;">
                    <?php echo esc_html__('This preview shows the computed output (templates + overrides). Search engines may display it differently.', 'wp-seo-next'); ?>
                </p>
            </td>
        </tr>

        <tr class="form-field">
            <th scope="row"><label for="wpsn_term_canonical"><?php echo esc_html(Branding::labelWithBrand($this->settings, __('Canonical URL', 'wp-seo-next'))); ?></label></th>
            <td>
                <input id="wpsn_term_canonical" type="url" class="regular-text" name="wpsn_term_meta[canonical]" value="<?php echo esc_attr($canon); ?>" placeholder="https://..." />
                <p class="description"><?php echo esc_html__('Leave empty for automatic canonical.', 'wp-seo-next'); ?></p>
            </td>
        </tr>

        <tr class="form-field">
            <th scope="row"><label><?php echo esc_html(Branding::labelWithBrand($this->settings, __('Robots', 'wp-seo-next'))); ?></label></th>
            <td>
                <p style="margin:0;">
                    <span style="display:inline-block; min-width: 140px;"><?php echo esc_html__('Indexing', 'wp-seo-next'); ?></span>
                    <select name="wpsn_term_meta[robots_index]">
                        <option value="default" <?php selected($rIndex, 'default'); ?>><?php echo esc_html__('Inherit defaults', 'wp-seo-next'); ?></option>
                        <option value="index" <?php selected($rIndex, 'index'); ?>><?php echo esc_html__('Index', 'wp-seo-next'); ?></option>
                        <option value="noindex" <?php selected($rIndex, 'noindex'); ?>><?php echo esc_html__('Noindex', 'wp-seo-next'); ?></option>
                    </select>
                </p>

                <p style="margin:8px 0 0;">
                    <span style="display:inline-block; min-width: 140px;"><?php echo esc_html__('Following', 'wp-seo-next'); ?></span>
                    <select name="wpsn_term_meta[robots_follow]">
                        <option value="default" <?php selected($rFollow, 'default'); ?>><?php echo esc_html__('Inherit defaults', 'wp-seo-next'); ?></option>
                        <option value="follow" <?php selected($rFollow, 'follow'); ?>><?php echo esc_html__('Follow', 'wp-seo-next'); ?></option>
                        <option value="nofollow" <?php selected($rFollow, 'nofollow'); ?>><?php echo esc_html__('Nofollow', 'wp-seo-next'); ?></option>
                    </select>
                </p>
            </td>
        </tr>
        <?php
    }

    public function saveTermFields(int $termId, int $ttId = 0): void
    {
        $taxonomy = isset($_POST['taxonomy']) ? sanitize_key((string) wp_unslash($_POST['taxonomy'])) : '';
        if ($taxonomy === '') {
            $term = get_term($termId);
            $taxonomy = ($term && !is_wp_error($term)) ? (string) $term->taxonomy : '';
        }

        $tax = $taxonomy !== '' ? get_taxonomy($taxonomy) : null;
        if (!$tax || empty($tax->public)) {
            return;
        }
        if (!current_user_can($tax->cap->edit_terms)) {
            return;
        }

        if (!isset($_POST['wpsn_term_meta_nonce']) || !wp_verify_nonce((string) wp_unslash($_POST['wpsn_term_meta_nonce']), 'wpsn_save_term_meta')) {
            return;
        }

        $incoming = $_POST['wpsn_term_meta'] ?? [];
        $incoming = is_array($incoming) ? wp_unslash($incoming) : [];

        $title = isset($incoming['seo_title']) ? sanitize_text_field((string) $incoming['seo_title']) : '';
        $desc  = isset($incoming['meta_description']) ? sanitize_textarea_field((string) $incoming['meta_description']) : '';

        $canon = isset($incoming['canonical']) ? (string) $incoming['canonical'] : '';
        $canon = self::sanitizeCanonicalInput($canon);

        $rIndex  = isset($incoming['robots_index']) ? self::sanitizeRobotsTriStateIndex((string) $incoming['robots_index']) : '';
        $rFollow = isset($incoming['robots_follow']) ? self::sanitizeRobotsTriStateFollow((string) $incoming['robots_follow']) : '';

        // Unified revision snapshot (manual edits via term editor).
        $trackedKeys = [
            MetaKeys::TITLE,
            MetaKeys::DESCRIPTION,
            MetaKeys::CANONICAL,
            MetaKeys::ROBOTS_INDEX,
            MetaKeys::ROBOTS_FOLLOW,
        ];

        $beforeMeta = [];
        foreach ($trackedKeys as $k) {
            $exists = function_exists('metadata_exists') ? metadata_exists('term', $termId, $k) : true;
            $beforeMeta[$k] = $exists ? (string) get_term_meta($termId, $k, true) : null;
        }

        $afterMeta = [
            MetaKeys::TITLE => trim($title) !== '' ? $title : null,
            MetaKeys::DESCRIPTION => trim($desc) !== '' ? $desc : null,
            MetaKeys::CANONICAL => trim($canon) !== '' ? $canon : null,
            MetaKeys::ROBOTS_INDEX => ($rIndex !== '' && $rIndex !== 'default') ? $rIndex : null,
            MetaKeys::ROBOTS_FOLLOW => ($rFollow !== '' && $rFollow !== 'default') ? $rFollow : null,
        ];

        $this->updateOrDeleteTermMeta($termId, MetaKeys::TITLE, $title);
        $this->updateOrDeleteTermMeta($termId, MetaKeys::DESCRIPTION, $desc);
        $this->updateOrDeleteTermMeta($termId, MetaKeys::CANONICAL, $canon);

        if ($rIndex !== '' && $rIndex !== 'default') {
            update_term_meta($termId, MetaKeys::ROBOTS_INDEX, $rIndex);
        } else {
            delete_term_meta($termId, MetaKeys::ROBOTS_INDEX);
        }

        if ($rFollow !== '' && $rFollow !== 'default') {
            update_term_meta($termId, MetaKeys::ROBOTS_FOLLOW, $rFollow);
        } else {
            delete_term_meta($termId, MetaKeys::ROBOTS_FOLLOW);
        }

        // Record revision only if something actually changed.
        if ($beforeMeta !== $afterMeta) {
            RevisionsInstaller::maybeInstall();
            $revRepo = new RevisionsRepository($this->container->get('logger'));
            $revRepo->insert([
                'object_type' => 'term',
                'object_id' => $termId,
                'object_subtype' => $taxonomy,
                'action' => 'update',
                'source' => 'meta_tags',
                'actor_user_id' => (int) get_current_user_id(),
                'before' => ['meta' => $beforeMeta],
                'after' => ['meta' => $afterMeta],
                'meta' => [
                    'context' => 'term_fields',
                    'taxonomy' => $taxonomy,
                ],
            ]);
        }

        // Internal event (refresh sitemaps/IndexNow, etc.).
        $this->container->get('events')->dispatch('seo/meta/updated', [$termId, 'term']);
    }

    private function updateOrDeleteTermMeta(int $termId, string $key, string $value): void
    {
        $value = trim($value);
        if ($value === '') {
            delete_term_meta($termId, $key);
            return;
        }
        update_term_meta($termId, $key, $value);
    }

    private static function sanitizeCanonicalInput(string $url): string
    {
        $url = trim($url);
        if ($url === '') {
            return '';
        }

        // Convert scheme-relative and site-relative URLs.
        if (strpos($url, '//') === 0) {
            $url = set_url_scheme($url);
        } elseif (strpos($url, '/') === 0 && strpos($url, '//') !== 0) {
            $url = (string) home_url($url);
        }

        $url = esc_url_raw($url);
        if ($url === '') {
            return '';
        }

        if (function_exists('wp_http_validate_url')) {
            $validated = wp_http_validate_url($url);
            if (!$validated) {
                return '';
            }
        }

        return $url;
    }

public function addMetaBox(): void
    {
        $boxTitle = Branding::labelWithBrand($this->settings, __('SEO', 'wp-seo-next'));

        $postTypes = get_post_types(['public' => true], 'names');
        foreach ($postTypes as $pt) {
            if ($pt === 'attachment') {
                continue;
            }
            add_meta_box(
                'wpsn_seo_meta',
                $boxTitle,
                [$this, 'renderMetaBox'],
                $pt,
                'normal',
                'default'
            );
        }
    }

    public function renderMetaBox(\WP_Post $post): void
    {
        wp_nonce_field('wpsn_save_meta', 'wpsn_nonce');

        $title = (string) get_post_meta($post->ID, MetaKeys::TITLE, true);
        $desc  = (string) get_post_meta($post->ID, MetaKeys::DESCRIPTION, true);
        $focus = (string) get_post_meta($post->ID, MetaKeys::FOCUS_KEYWORD, true);
        $canon = (string) get_post_meta($post->ID, MetaKeys::CANONICAL, true);

        $rIndex  = (string) get_post_meta($post->ID, MetaKeys::ROBOTS_INDEX, true);
        $rFollow = (string) get_post_meta($post->ID, MetaKeys::ROBOTS_FOLLOW, true);

        $rIndex  = $rIndex !== '' ? $rIndex : 'default';
        $rFollow = $rFollow !== '' ? $rFollow : 'default';

        $historyUrl = admin_url('tools.php?page=wpsn-revisions&object_type=post&object_id=' . (int) $post->ID);
        $returnUrl = admin_url('post.php?post=' . (int) $post->ID . '&action=edit');
        $undoUrl = wp_nonce_url(
            admin_url('admin-post.php?action=wpsn_seo_undo_last&object_type=post&object_id=' . (int) $post->ID . '&return=' . rawurlencode($returnUrl)),
            'wpsn_seo_undo_last_post_' . (int) $post->ID,
            'wpsn_nonce'
        );

        ?>
        <div style="display:flex; gap:8px; align-items:center; margin: 0 0 10px;">
            <a href="<?php echo esc_url($historyUrl); ?>" class="button button-small button-secondary"><?php echo esc_html__('History / Revisions', 'wp-seo-next'); ?></a>
            <a href="<?php echo esc_url($undoUrl); ?>" class="button button-small button-secondary" onclick="return confirm('<?php echo esc_js(__('Undo last SEO change for this post?', 'wp-seo-next')); ?>');"><?php echo esc_html__('Undo last SEO change', 'wp-seo-next'); ?></a>
        </div>
        <p>
            <label for="wpsn_seo_title"><strong><?php echo esc_html__('SEO Title', 'wp-seo-next'); ?></strong></label><br/>
            <input type="text"
                   id="wpsn_seo_title"
                   name="wpsn_meta[seo_title]"
                   value="<?php echo esc_attr($title); ?>"
                   class="widefat"
                   placeholder="<?php echo esc_attr__('Leave empty to use template.', 'wp-seo-next'); ?>"
            />
        </p>

        <p>
            <label for="wpsn_meta_description"><strong><?php echo esc_html__('Meta Description', 'wp-seo-next'); ?></strong></label><br/>
            <textarea id="wpsn_meta_description"
                      name="wpsn_meta[meta_description]"
                      class="widefat"
                      rows="3"
                      placeholder="<?php echo esc_attr__('Leave empty to use template.', 'wp-seo-next'); ?>"
            ><?php echo esc_textarea($desc); ?></textarea>
        </p>

        <p>
            <label for="wpsn_focus_keyword"><strong><?php echo esc_html__('Focus keyword', 'wp-seo-next'); ?></strong></label><br/>
            <input type="text"
                   id="wpsn_focus_keyword"
                   name="wpsn_meta[focus_keyword]"
                   value="<?php echo esc_attr($focus); ?>"
                   class="widefat"
                   placeholder="<?php echo esc_attr__('Optional: used for on-page analysis checks.', 'wp-seo-next'); ?>"
            />
        </p>

        <hr/>

        <p>
            <label for="wpsn_canonical"><strong><?php echo esc_html__('Canonical URL', 'wp-seo-next'); ?></strong></label><br/>
            <input type="url"
                   id="wpsn_canonical"
                   name="wpsn_meta[canonical]"
                   value="<?php echo esc_attr($canon); ?>"
                   class="widefat"
                   placeholder="<?php echo esc_attr__('Leave empty for automatic canonical.', 'wp-seo-next'); ?>"
            />
        </p>

        <p>
            <label><strong><?php echo esc_html__('Robots', 'wp-seo-next'); ?></strong></label><br/>
            <span style="display:inline-block; min-width: 140px;"><?php echo esc_html__('Indexing', 'wp-seo-next'); ?></span>
            <select name="wpsn_meta[robots_index]">
                <option value="default" <?php selected($rIndex, 'default'); ?>><?php echo esc_html__('Inherit defaults', 'wp-seo-next'); ?></option>
                <option value="index" <?php selected($rIndex, 'index'); ?>><?php echo esc_html__('Index', 'wp-seo-next'); ?></option>
                <option value="noindex" <?php selected($rIndex, 'noindex'); ?>><?php echo esc_html__('Noindex', 'wp-seo-next'); ?></option>
            </select>
        </p>

        <p>
            <span style="display:inline-block; min-width: 140px;"><?php echo esc_html__('Following', 'wp-seo-next'); ?></span>
            <select name="wpsn_meta[robots_follow]">
                <option value="default" <?php selected($rFollow, 'default'); ?>><?php echo esc_html__('Inherit defaults', 'wp-seo-next'); ?></option>
                <option value="follow" <?php selected($rFollow, 'follow'); ?>><?php echo esc_html__('Follow', 'wp-seo-next'); ?></option>
                <option value="nofollow" <?php selected($rFollow, 'nofollow'); ?>><?php echo esc_html__('Nofollow', 'wp-seo-next'); ?></option>
            </select>
        </p>
        <?php
    }

    public function saveMetaBox(int $postId, \WP_Post $post): void
    {
        // Basic guards.
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return;
        }
        if (wp_is_post_revision($postId) || wp_is_post_autosave($postId)) {
            return;
        }
        if (!isset($_POST['wpsn_nonce']) || !wp_verify_nonce((string) wp_unslash($_POST['wpsn_nonce']), 'wpsn_save_meta')) {
            return;
        }
        if (!current_user_can('edit_post', $postId)) {
            return;
        }

        $incoming = $_POST['wpsn_meta'] ?? [];
        $incoming = is_array($incoming) ? wp_unslash($incoming) : [];

        $title = isset($incoming['seo_title']) ? sanitize_text_field((string) $incoming['seo_title']) : '';
        $desc  = isset($incoming['meta_description']) ? sanitize_textarea_field((string) $incoming['meta_description']) : '';
        $focus = isset($incoming['focus_keyword']) ? sanitize_text_field((string) $incoming['focus_keyword']) : '';

        $canon = isset($incoming['canonical']) ? esc_url_raw((string) $incoming['canonical']) : '';

        $rIndex  = isset($incoming['robots_index']) ? self::sanitizeRobotsTriStateIndex((string) $incoming['robots_index']) : '';
        $rFollow = isset($incoming['robots_follow']) ? self::sanitizeRobotsTriStateFollow((string) $incoming['robots_follow']) : '';

        // Unified revision snapshot (manual edits via post editor).
        $trackedKeys = [
            MetaKeys::TITLE,
            MetaKeys::DESCRIPTION,
            MetaKeys::FOCUS_KEYWORD,
            MetaKeys::CANONICAL,
            MetaKeys::ROBOTS_INDEX,
            MetaKeys::ROBOTS_FOLLOW,
        ];

        $beforeMeta = [];
        foreach ($trackedKeys as $k) {
            $exists = function_exists('metadata_exists') ? metadata_exists('post', $postId, $k) : true;
            $beforeMeta[$k] = $exists ? (string) get_post_meta($postId, $k, true) : null;
        }

        $afterMeta = [
            MetaKeys::TITLE => trim($title) !== '' ? $title : null,
            MetaKeys::DESCRIPTION => trim($desc) !== '' ? $desc : null,
            MetaKeys::FOCUS_KEYWORD => trim($focus) !== '' ? $focus : null,
            MetaKeys::CANONICAL => trim($canon) !== '' ? $canon : null,
            MetaKeys::ROBOTS_INDEX => ($rIndex !== '' && $rIndex !== 'default') ? $rIndex : null,
            MetaKeys::ROBOTS_FOLLOW => ($rFollow !== '' && $rFollow !== 'default') ? $rFollow : null,
        ];

        if ($title !== '') {
            update_post_meta($postId, MetaKeys::TITLE, $title);
        } else {
            delete_post_meta($postId, MetaKeys::TITLE);
        }

        if ($desc !== '') {
            update_post_meta($postId, MetaKeys::DESCRIPTION, $desc);
        } else {
            delete_post_meta($postId, MetaKeys::DESCRIPTION);
        }

        if ($focus !== '') {
            update_post_meta($postId, MetaKeys::FOCUS_KEYWORD, $focus);
        } else {
            delete_post_meta($postId, MetaKeys::FOCUS_KEYWORD);
        }

        if ($canon !== '') {
            update_post_meta($postId, MetaKeys::CANONICAL, $canon);
        } else {
            delete_post_meta($postId, MetaKeys::CANONICAL);
        }

        if ($rIndex !== '' && $rIndex !== 'default') {
            update_post_meta($postId, MetaKeys::ROBOTS_INDEX, $rIndex);
        } else {
            delete_post_meta($postId, MetaKeys::ROBOTS_INDEX);
        }

        if ($rFollow !== '' && $rFollow !== 'default') {
            update_post_meta($postId, MetaKeys::ROBOTS_FOLLOW, $rFollow);
        } else {
            delete_post_meta($postId, MetaKeys::ROBOTS_FOLLOW);
        }

        // Record revision only if something actually changed.
        if ($beforeMeta !== $afterMeta) {
            RevisionsInstaller::maybeInstall();
            $revRepo = new RevisionsRepository($this->container->get('logger'));
            $revRepo->insert([
                'object_type' => 'post',
                'object_id' => $postId,
                'object_subtype' => (string) ($post->post_type ?? ''),
                'action' => 'update',
                'source' => 'meta_tags',
                'actor_user_id' => (int) get_current_user_id(),
                'before' => ['meta' => $beforeMeta],
                'after' => ['meta' => $afterMeta],
                'meta' => [
                    'context' => 'meta_box',
                ],
            ]);
        }

        // Internal event.
        $this->container->get('events')->dispatch('seo/meta/updated', [$postId, 'post']);
    }

    public function filterDocumentTitle(string $title): string
    {
        static $in = false;
        if ($in) {
            return $title;
        }

        if (is_admin() || is_feed()) {
            return $title;
        }

        $in = true;

        try {
            /** @var DocumentResolver $resolver */
            $resolver = $this->container->get('document_resolver');
            $ctx = $resolver->resolve();

            /** @var SeoMetaRepository $repo */
            $repo = $this->container->get('meta_repo');

            $meta = $repo->forContext($ctx);

            if ($meta instanceof SeoMeta && trim($meta->title) !== '') {
                return $meta->title;
            }

            return $title;
        } finally {
            $in = false;
        }
    }

    public function renderMetaDescription(): void
    {
        if (is_admin() || is_feed()) {
            return;
        }

        /** @var DocumentResolver $resolver */
        $resolver = $this->container->get('document_resolver');
        $ctx = $resolver->resolve();

        /** @var SeoMetaRepository $repo */
        $repo = $this->container->get('meta_repo');

        $meta = $repo->forContext($ctx);

        $desc = is_string($meta->description) ? trim($meta->description) : '';
        if ($desc === '') {
            return;
        }

        echo "\n" . '<meta name="description" content="' . esc_attr($desc) . '" />' . "\n";
    }

    public function renderCanonical(): void
    {
        if (is_admin() || is_feed()) {
            return;
        }

        /** @var DocumentResolver $resolver */
        $resolver = $this->container->get('document_resolver');
        $ctx = $resolver->resolve();

        /** @var SeoMetaRepository $repo */
        $repo = $this->container->get('meta_repo');
        $meta = $repo->forContext($ctx);

        $url = is_string($meta->canonical) ? trim($meta->canonical) : '';
        if ($url === '') {
            return;
        }

        echo "\n" . '<link rel="canonical" href="' . esc_url($url) . '" />' . "\n";
    }

    public function renderHreflangAlternates(): void
    {
        if (is_admin() || is_feed()) {
            return;
        }

        /** @var DocumentResolver $resolver */
        $resolver = $this->container->get('document_resolver');
        $ctx = $resolver->resolve();

        /** @var SeoMetaRepository $repo */
        $repo = $this->container->get('meta_repo');
        $meta = $repo->forContext($ctx);

        /** @var \WPSN\SEO\HreflangResolver $hreflang */
        $hreflang = $this->container->get('hreflang_resolver');
        $alternates = $hreflang->resolve($ctx, $meta);
        if (empty($alternates)) {
            return;
        }

        echo "\n";
        foreach ($alternates as $alternate) {
            $lang = isset($alternate['hreflang']) ? trim((string) $alternate['hreflang']) : '';
            $href = isset($alternate['href']) ? trim((string) $alternate['href']) : '';
            if ($lang === '' || $href === '') {
                continue;
            }
            echo '<link rel="alternate" hreflang="' . esc_attr($lang) . '" href="' . esc_url($href) . '" />' . "\n";
        }
    }

    /**
     * Integrate with WP Robots API.
     *
     * @param array $robots
     * @return array
     */
    public function filterWpRobots(array $robots): array
    {
        if (is_admin() || is_feed()) {
            return $robots;
        }

        /** @var DocumentResolver $resolver */
        $resolver = $this->container->get('document_resolver');
        $ctx = $resolver->resolve();

        /** @var SeoMetaRepository $repo */
        $repo = $this->container->get('meta_repo');
        $meta = $repo->forContext($ctx);

        // Respect core/other plugins that force noindex/nofollow (e.g. "Discourage search engines").
        $forcedNoindex  = !empty($robots['noindex']);
        $forcedNofollow = !empty($robots['nofollow']);

        // Avoid duplicates.
        unset($robots['index'], $robots['follow']);

        $noindex  = !empty($meta->robots['noindex']);
        $nofollow = !empty($meta->robots['nofollow']);

        if (!$forcedNoindex) {
            if ($noindex) {
                $robots['noindex'] = true;
                unset($robots['index']);
            } else {
                $robots['index'] = true;
                unset($robots['noindex']);
            }
        }

        if (!$forcedNofollow) {
            if ($nofollow) {
                $robots['nofollow'] = true;
                unset($robots['follow']);
            } else {
                $robots['follow'] = true;
                unset($robots['nofollow']);
            }
        }

        // Advanced directives.
        foreach (['max-image-preview', 'max-snippet', 'max-video-preview'] as $key) {
            if (isset($meta->robots[$key]) && is_string($meta->robots[$key]) && trim($meta->robots[$key]) !== '') {
                $robots[$key] = trim($meta->robots[$key]);
            }
        }

        foreach (['noarchive', 'nosnippet'] as $key) {
            if (!empty($meta->robots[$key])) {
                $robots[$key] = true;
            } else {
                unset($robots[$key]);
            }
        }

        $robots = $this->applyUrlNoindexRules($robots);

        return $robots;
    }

    /**
     * Apply stored URL-pattern rules (from Crawl Budget URL items) via wp_robots.
     */
    private function applyUrlNoindexRules(array $robots): array
    {
        $rules = $this->settings->get('robots.url_noindex_rules', []);
        if (!is_array($rules) || empty($rules)) {
            return $robots;
        }

        $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
        $path = (string) wp_parse_url($uri, PHP_URL_PATH);
        $query = (string) wp_parse_url($uri, PHP_URL_QUERY);

        if ($path === '') {
            return $robots;
        }
        if ($path[0] !== '/') {
            $path = '/' . ltrim($path, '/');
        }

        $q = [];
        if ($query !== '') {
            parse_str($query, $q);
            if (!is_array($q)) {
                $q = [];
            }
        }

        $matched = false;

        foreach ($rules as $r) {
            if (!is_array($r)) {
                continue;
            }

            if (empty($r['enabled'])) {
                continue;
            }

            $match = isset($r['match']) ? (string) $r['match'] : '';
            $pattern = isset($r['pattern']) ? (string) $r['pattern'] : '';
            $scope = isset($r['path_prefix']) ? (string) $r['path_prefix'] : '';

            $pattern = trim($pattern);
            $scope = trim($scope);

            if ($match === '' || $pattern === '') {
                continue;
            }

            if ($scope !== '') {
                if ($scope[0] !== '/') {
                    $scope = '/' . ltrim($scope, '/');
                }
                if (strpos($path, $scope) !== 0) {
                    continue;
                }
            }

            if ($match === 'path_prefix') {
                if ($pattern[0] !== '/') {
                    $pattern = '/' . ltrim($pattern, '/');
                }
                if (strpos($path, $pattern) === 0) {
                    $matched = true;
                }
            } elseif ($match === 'path_regex') {
                $re = '#' . $pattern . '#';
                if (@preg_match($re, $path) === 1) {
                    $matched = true;
                }
            } elseif ($match === 'query_param') {
                $key = sanitize_key($pattern);
                if ($key !== '' && array_key_exists($key, $q)) {
                    $matched = true;
                }
            } elseif ($match === 'query_param_int_gt1') {
                // Safer matcher for pagination-like params (e.g. ?paged=2).
                // Matches only when the param is present AND its numeric value is > 1.
                $key = sanitize_key($pattern);
                if ($key !== '' && array_key_exists($key, $q)) {
                    $val = $q[$key];
                    if (is_array($val)) {
                        $val = reset($val);
                    }
                    $val = is_scalar($val) ? (string) $val : '';
                    $val = trim((string) $val);
                    if ($val !== '' && is_numeric($val) && (int) $val > 1) {
                        $matched = true;
                    }
                }
            } elseif ($match === 'query_param_prefix') {
                $prefix = sanitize_key($pattern);
                if ($prefix !== '') {
                    foreach (array_keys($q) as $k) {
                        $k = sanitize_key((string) $k);
                        if ($k !== '' && strpos($k, $prefix) === 0) {
                            $matched = true;
                            break;
                        }
                    }
                }
            }

            if ($matched) {
                break;
            }
        }

        if ($matched) {
            $robots['noindex'] = true;
            unset($robots['index']);
        }

        return $robots;
    }
}

