<?php

/**
 * Bulk Edit Integration for Post List Tables
 *
 * @package ProRank\SEO\Admin
 */

declare(strict_types=1);

namespace ProRank\SEO\Admin;

use ProRank\SEO\Core\FocusKeywords;

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


/**
 * BulkEditIntegration class
 */


class BulkEditIntegration {

    /**
     * Valid SEO filter values
     */
    private const VALID_SEO_FILTERS = ['good', 'ok', 'poor', 'none'];

    /**
     * Valid keyword filter values
     */
    private const VALID_KW_FILTERS = ['set', 'none'];

    /**
     * Supported post types for filtering
     */
    private const SUPPORTED_POST_TYPES = ['post', 'page'];

    /**
     * Supported post-level robots directives for quick/bulk edit.
     */
    private const META_ROBOTS_DIRECTIVES = ['noindex', 'nofollow', 'noarchive', 'nosnippet', 'noimageindex'];

    /**
     * Constructor
     */
    public function __construct() {
        $this->init_hooks();
    }

    /**
     * Initialize hooks
     */
    private function init_hooks(): void {
        // Add bulk actions to post list
        add_filter('bulk_actions-edit-post', [$this, 'add_bulk_actions']);
        add_filter('bulk_actions-edit-page', [$this, 'add_bulk_actions']);

        // Handle bulk actions
        add_filter('handle_bulk_actions-edit-post', [$this, 'handle_bulk_action'], 10, 3);
        add_filter('handle_bulk_actions-edit-page', [$this, 'handle_bulk_action'], 10, 3);

        // Add columns to post list
        add_filter('manage_posts_columns', [$this, 'add_seo_columns']);
        add_filter('manage_pages_columns', [$this, 'add_seo_columns']);

        // Display column content
        add_action('manage_posts_custom_column', [$this, 'display_seo_column'], 10, 2);
        add_action('manage_pages_custom_column', [$this, 'display_seo_column'], 10, 2);

        // Make columns sortable
        add_filter('manage_edit-post_sortable_columns', [$this, 'make_columns_sortable']);
        add_filter('manage_edit-page_sortable_columns', [$this, 'make_columns_sortable']);

        // Add quick edit fields
        add_action('quick_edit_custom_box', [$this, 'add_quick_edit_fields'], 10, 2);
        add_action('bulk_edit_custom_box', [$this, 'add_bulk_edit_fields'], 10, 2);

        // Save quick edit data
        add_action('save_post', [$this, 'save_quick_edit_data']);

        // Add admin scripts
        add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']);

        // Add admin notices
        add_action('admin_notices', [$this, 'display_bulk_action_notices']);

        // Add filter dropdowns
        add_action('restrict_manage_posts', [$this, 'add_filter_dropdowns']);

        // Filter posts by SEO status
        add_filter('pre_get_posts', [$this, 'filter_posts_by_seo']);

        // Ensure proper sorting for SEO score column
        add_filter('posts_clauses', [$this, 'orderby_seo_score'], 10, 2);
    }

    /**
     * Keep ProRank columns readable without forcing core columns into vertical text.
     */
    public function render_list_table_column_styles(): void {
        $screen = function_exists('get_current_screen') ? get_current_screen() : null;
        $post_type = $screen && isset($screen->post_type) ? (string) $screen->post_type : '';
        if (!in_array($post_type, self::SUPPORTED_POST_TYPES, true)) {
            return;
        }
        ?>
        <style>
            .wp-list-table.posts {
                table-layout: fixed;
                width: 100%;
            }
            .wp-list-table.posts th,
            .wp-list-table.posts td {
                word-break: normal;
                overflow-wrap: normal;
            }
            .wp-list-table.posts th {
                white-space: nowrap;
            }
            .wp-list-table.posts .column-cb {
                width: 36px;
            }
            .wp-list-table.posts .column-title {
                width: 190px;
            }
            .wp-list-table.posts .column-prorank_seo_score {
                width: 78px;
                text-align: center;
            }
            .wp-list-table.posts .column-prorank_views,
            .wp-list-table.posts .column-prorank_trend {
                width: 68px;
                text-align: center;
            }
            .wp-list-table.posts .column-prorank_focus_keyword {
                width: 120px;
            }
            .wp-list-table.posts .column-prorank_seo_title {
                width: 230px;
            }
            .wp-list-table.posts .column-prorank_seo_desc {
                width: 260px;
            }
            .wp-list-table.posts .column-author {
                width: 90px;
            }
            .wp-list-table.posts .column-categories {
                width: 96px;
            }
            .wp-list-table.posts .column-tags {
                width: 64px;
            }
            .wp-list-table.posts .column-comments {
                width: 44px;
                text-align: center;
            }
            .wp-list-table.posts .column-date {
                width: 108px;
            }
            .wp-list-table.posts .column-author,
            .wp-list-table.posts .column-categories,
            .wp-list-table.posts .column-tags,
            .wp-list-table.posts .column-date {
                overflow: hidden;
                text-overflow: ellipsis;
            }
            .wp-list-table.posts .prorank-meta-column {
                max-width: 100%;
            }
            .wp-list-table.posts .prorank-meta-column .meta-text {
                display: block;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
        </style>
        <?php
    }
    
    /**
     * Add bulk actions to dropdown
     *
     * @param array $bulk_actions Existing bulk actions.
     * @return array
     */
    public function add_bulk_actions(array $bulk_actions): array {
        // Check if bulk editing is enabled for this post type
        $screen = get_current_screen();
        $post_type = $screen->post_type ?? 'post';
        
        $settings = get_option('prorank_seo_titles_meta', []);
        if (!is_array($settings) || empty($settings)) {
            $settings = get_option('prorank_titles_meta_settings', []);
        }
        $post_type_settings = $settings["post_type_{$post_type}"] ?? [];
        
        // Always enable for posts and pages, check settings for others
        if ($post_type === 'post' || $post_type === 'page' || !empty($post_type_settings['bulk_editing'])) {
            $bulk_actions['prorank_bulk_edit_seo'] = __('Edit SEO Settings', 'prorank-seo');
            
            // Add AI optimization if user has Pro license
            if ($this->has_ai_access()) {
                $bulk_actions['prorank_ai_optimize_seo'] = __('AI Optimize SEO', 'prorank-seo');
            }
        }
        
        return $bulk_actions;
    }
    
    /**
     * Handle bulk action
     *
     * @param string $redirect_to Redirect URL.
     * @param string $doaction Action being performed.
     * @param array  $post_ids Post IDs.
     * @return string
     */
    public function handle_bulk_action(string $redirect_to, string $doaction, array $post_ids): string {
        if ($doaction === 'prorank_bulk_edit_seo') {
            // Use the dedicated hidden bulk-edit page so WordPress can resolve the
            // callback reliably even when the bulk editor is opened directly.
            $redirect_to = add_query_arg([
                'page' => 'prorank-bulk-edit',
                'post_ids' => implode(',', $post_ids),
                'post_type' => get_post_type($post_ids[0]),
            ], admin_url('admin.php'));
            
        } elseif ($doaction === 'prorank_ai_optimize_seo') {
            $redirect_to = add_query_arg([
                'prorank_ai_error' => 'unavailable',
                'prorank_ai_total' => count($post_ids),
            ], $redirect_to);
        }
        
        return $redirect_to;
    }
    
    /**
     * Add SEO columns to post list
     *
     * @param array $columns Existing columns.
     * @return array
     */
    public function add_seo_columns(array $columns): array {
        $new_columns = [];
        $seo_columns_added = false;

        foreach ($columns as $key => $label) {
            // Always preserve the column
            $new_columns[$key] = $label;

            // Add SEO columns after title
            if ($key === 'title' && !$seo_columns_added) {
                $new_columns['prorank_seo_score'] = '<span class="prorank-seo-column-header" title="' . esc_attr__('SEO Score', 'prorank-seo') . '">SEO</span>';
                $seo_columns_added = true;
            }
        }

        // Fallback: if title column wasn't found, add Title and SEO columns after cb
        if (!$seo_columns_added) {
            $final_columns = [];
            $title_in_original = array_key_exists('title', $new_columns);
            $title_label = $columns['title'] ?? __('Title', 'prorank-seo');
            foreach ($new_columns as $key => $label) {
                $final_columns[$key] = $label;
                if ($key === 'cb' && !$title_in_original) {
                    $final_columns['title'] = $title_label;
                    $final_columns['prorank_seo_score'] = '<span class="prorank-seo-column-header" title="' . esc_attr__('SEO Score', 'prorank-seo') . '">SEO</span>';
                }
            }
            return $final_columns;
        }

        return $new_columns;
    }
    
    /**
     * Display SEO column content
     *
     * @param string $column Column name.
     * @param int    $post_id Post ID.
     */
    public function display_seo_column(string $column, int $post_id): void {
        switch ($column) {
            case 'prorank_seo_score':
                $score = get_post_meta($post_id, '_prorank_seo_score', true);
                $this->display_seo_score_circle($score, $post_id);
                break;

            case 'prorank_focus_keyword':
                $keyword = FocusKeywords::get_primary($post_id);
                $this->display_focus_keyword($keyword);
                break;

            case 'prorank_seo_title':
                $title = get_post_meta($post_id, '_prorank_seo_title', true);
                $this->display_meta_with_progress($title, 60, 'title');
                $this->render_inline_seo_state($post_id);
                break;

            case 'prorank_seo_desc':
                $desc = get_post_meta($post_id, '_prorank_seo_description', true);
                $this->display_meta_with_progress($desc, 160, 'description');
                break;
        }
    }

    /**
     * Display SEO status as a compact badge while keeping numeric score for tooltips and sorting.
     *
     * @param mixed $score SEO score.
     * @param int   $post_id Post ID.
     */
    private function display_seo_score_circle($score, int $post_id): void {
        if ($score === '' || $score === null) {
            $score = $this->refresh_derived_seo_score($post_id);

            if ($score === null) {
                echo '<span class="prorank-status-badge empty" aria-label="' . esc_attr__('SEO status: Not analyzed', 'prorank-seo') . '" title="' . esc_attr__('SEO status: Not analyzed', 'prorank-seo') . '">' . esc_html__('No data', 'prorank-seo') . '</span>';
                return;
            }
        }

        $missing_fields = $this->get_missing_required_seo_fields($post_id);
        if (!empty($missing_fields)) {
            $score = intval($score);
            $missing_label = implode(', ', $missing_fields);
            $aria_label = sprintf(
                /* translators: 1: score number, 2: comma-separated missing SEO fields */
                __('SEO Status: %1$d/100. Missing %2$s.', 'prorank-seo'),
                $score,
                $missing_label
            );

            echo '<span class="prorank-status-badge needs-work"';
            echo ' data-score="' . esc_attr($score) . '"';
            echo ' data-post-id="' . esc_attr($post_id) . '"';
            echo ' aria-label="' . esc_attr($aria_label) . '"';
            echo ' title="' . esc_attr($aria_label) . '">';
            echo esc_html((string) $score);
            echo '</span>';
            return;
        }

        $score = intval($score);
        $class = $score >= 80 ? 'strong' : ($score >= 60 ? 'solid' : 'needs-work');
        $label = $score >= 80 ? __('Strong', 'prorank-seo') : ($score >= 60 ? __('Solid', 'prorank-seo') : __('Low', 'prorank-seo'));

        // Accessibility: meaningful aria-label
        $aria_label = sprintf(
            /* translators: 1: status label, 2: score number */
            __('SEO Status: %1$s (%2$d/100)', 'prorank-seo'),
            $label,
            $score
        );

        echo '<span class="prorank-status-badge ' . esc_attr($class) . '"';
        echo ' data-score="' . esc_attr($score) . '"';
        echo ' data-post-id="' . esc_attr($post_id) . '"';
        echo ' aria-label="' . esc_attr($aria_label) . '"';
/* translators: 1: status label, 2: score number */
        echo ' title="' . esc_attr(sprintf(__('SEO Status: %1$s (%2$d/100)', 'prorank-seo'), $label, $score)) . '">';
        echo esc_html((string) $score);
        echo '</span>';
    }

    /**
     * Required list-table fields that must be present before a row can be marked healthy.
     *
     * The stored score can be imported or stale. The list-table status must not say
     * "Good" while the visible SEO title/description columns are empty.
     *
     * @return array<int,string>
     */
    private function get_missing_required_seo_fields(int $post_id): array {
        $missing = [];

        if (trim((string) get_post_meta($post_id, '_prorank_seo_title', true)) === '') {
            $missing[] = __('SEO title', 'prorank-seo');
        }

        if (trim((string) get_post_meta($post_id, '_prorank_seo_description', true)) === '') {
            $missing[] = __('meta description', 'prorank-seo');
        }

        return $missing;
    }

    /**
     * Derive the saved-post content score when no stored score exists.
     */
    private function refresh_derived_seo_score(int $post_id): ?int {
        $score = $this->derive_seo_score_from_metadata($post_id);

        if ($score === null) {
            delete_post_meta($post_id, '_prorank_seo_score');
            return null;
        }

        update_post_meta($post_id, '_prorank_seo_score', $score);
        return $score;
    }

    /**
     * Calculate the saved-post content score using the same major inputs as the editor panel.
     */
    private function derive_seo_score_from_metadata(int $post_id): ?int {
        $post = get_post($post_id);
        if (!$post instanceof \WP_Post) {
            return null;
        }

        $content = (string) $post->post_content;
        $plain_content = trim($this->normalize_score_text($content));
        $seo_title = trim((string) get_post_meta($post_id, '_prorank_seo_title', true));
        $description = trim((string) get_post_meta($post_id, '_prorank_seo_description', true));

        if ($plain_content === '' && $seo_title === '' && $description === '') {
            return null;
        }

        $score = 0;
        $weight = 0.0;
        $settings = $this->get_content_analysis_settings();
        $word_count = str_word_count($plain_content);

        if (class_exists('\ProRank\SEO\Modules\Content\Readability')) {
            $readability = \ProRank\SEO\Modules\Content\Readability::calculate_flesch_kincaid($plain_content);
            if ($readability !== null) {
                $readability_normalized = min(100, max(0, (((float) $readability) / $settings['target_readability_score']) * 100));
                $score += $readability_normalized * 0.3;
                $weight += 0.3;
            }
        }

        $score += $this->calculate_structure_score_for_list($content) * 0.15;
        $weight += 0.15;

        $score += $this->calculate_keyword_score_for_list($post_id, [
            'title' => (string) $post->post_title,
            'seo_title' => $seo_title,
            'description' => $description,
            'url' => (string) get_permalink($post_id),
            'content' => $content,
        ]) * 0.35;
        $weight += 0.35;

        $min_content_length = $settings['min_content_length'];
        $content_length_score = $word_count >= max($min_content_length, 1000)
            ? 100
            : ($word_count >= max($min_content_length, 600)
                ? 75
                : ($word_count >= $min_content_length ? 50 : 0));
        $score += $content_length_score * 0.2;
        $weight += 0.2;

        return $weight > 0 ? max(1, min(100, (int) round($score / $weight))) : null;
    }

    private function get_content_analysis_settings(): array {
        $stored = get_option('prorank_seo_content_analysis', []);
        $stored = is_array($stored) ? $stored : [];

        return [
            'min_content_length' => max(100, (int) ($stored['min_content_length'] ?? 300)),
            'target_readability_score' => max(30, min(90, (int) ($stored['target_readability_score'] ?? 60))),
        ];
    }

    private function calculate_structure_score_for_list(string $content): int {
        $score = 100;
        $dom = new \DOMDocument();
        @$dom->loadHTML('<?xml encoding="utf-8" ?>' . $content);

        $headings = [];
        for ($i = 1; $i <= 6; $i++) {
            foreach ($dom->getElementsByTagName('h' . $i) as $heading) {
                $headings[] = $i;
            }
        }

        if (empty($headings)) {
            $score -= 20;
        } else {
            $prev_level = 0;
            foreach ($headings as $level) {
                if ($level > $prev_level + 1 && $prev_level > 0) {
                    $score -= 10;
                    break;
                }
                $prev_level = $level;
            }
        }

        $long_paragraphs = 0;
        foreach ($dom->getElementsByTagName('p') as $paragraph) {
            if (str_word_count((string) $paragraph->textContent) > 150) {
                $long_paragraphs++;
            }
        }
        if ($long_paragraphs > 0) {
            $score -= min(15, $long_paragraphs * 5);
        }

        $has_lists = $dom->getElementsByTagName('ul')->length + $dom->getElementsByTagName('ol')->length > 0;
        if (!$has_lists && $this->safe_strlen($content) > 1000) {
            $score -= 10;
        }

        $images_without_alt = 0;
        foreach ($dom->getElementsByTagName('img') as $image) {
            if (trim((string) $image->getAttribute('alt')) === '') {
                $images_without_alt++;
            }
        }
        if ($images_without_alt > 0) {
            $score -= min(20, $images_without_alt * 5);
        }

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

    private function calculate_keyword_score_for_list(int $post_id, array $context): int {
        $keywords = FocusKeywords::get($post_id);
        if (empty($keywords)) {
            return 0;
        }

        $scores = [];
        $primary_index = 0;
        foreach ($keywords as $index => $keyword_row) {
            if (!empty($keyword_row['primary'])) {
                $primary_index = $index;
            }
            $scores[$index] = $this->calculate_single_keyword_score((string) ($keyword_row['keyword'] ?? ''), $context);
        }

        $primary_score = $scores[$primary_index] ?? 0;
        $secondary_scores = [];
        foreach ($scores as $index => $keyword_score) {
            if ($index !== $primary_index) {
                $secondary_scores[] = $keyword_score;
            }
        }

        if (empty($secondary_scores)) {
            return $primary_score;
        }

        return (int) round($primary_score * 0.7 + (array_sum($secondary_scores) / count($secondary_scores)) * 0.3);
    }

    private function calculate_single_keyword_score(string $keyword, array $context): int {
        $normalized_keyword = $this->normalize_keyword_text($keyword);
        if ($normalized_keyword === '') {
            return 0;
        }

        $keyword_terms = array_values(array_filter(explode(' ', $normalized_keyword)));
        $body_text = $this->normalize_score_text((string) $context['content']);
        $total_words = count(array_values(array_filter(explode(' ', $body_text))));
        $occurrence_count = $this->count_keyword_occurrences($body_text, $normalized_keyword);
        $density = $total_words > 0 && !empty($keyword_terms)
            ? (($occurrence_count * count($keyword_terms)) / $total_words) * 100
            : 0;

        $effective_title = (string) ($context['seo_title'] ?: $context['title']);
        $checks = [
            'title' => [
                'weight' => 20,
                'applicable' => $effective_title !== '',
                'passed' => $this->contains_keyword($effective_title, $normalized_keyword, $keyword_terms),
            ],
            'description' => [
                'weight' => 15,
                'applicable' => (string) $context['description'] !== '',
                'passed' => $this->contains_keyword((string) $context['description'], $normalized_keyword, $keyword_terms),
            ],
            'url' => [
                'weight' => 15,
                'applicable' => (string) $context['url'] !== '',
                'passed' => $this->contains_keyword(str_replace(['-', '_', '/', '=', '?', '&'], ' ', (string) $context['url']), $normalized_keyword, $keyword_terms),
            ],
            'first_paragraph' => [
                'weight' => 15,
                'applicable' => $this->extract_first_paragraph_for_score((string) $context['content']) !== '',
                'passed' => $this->contains_keyword($this->extract_first_paragraph_for_score((string) $context['content']), $normalized_keyword, $keyword_terms),
            ],
            'headings' => [
                'weight' => 15,
                'applicable' => !empty($this->extract_heading_texts_for_score((string) $context['content'])),
                'passed' => $this->any_contains_keyword($this->extract_heading_texts_for_score((string) $context['content']), $normalized_keyword, $keyword_terms),
            ],
            'density' => [
                'weight' => 15,
                'applicable' => $total_words > 0,
                'passed' => $occurrence_count > 0 && $density >= 0.5 && $density <= 2.5,
            ],
            'image_alt' => [
                'weight' => 5,
                'applicable' => !empty($this->extract_image_alt_texts_for_score((string) $context['content'])),
                'passed' => $this->any_contains_keyword($this->extract_image_alt_texts_for_score((string) $context['content']), $normalized_keyword, $keyword_terms),
            ],
        ];

        $possible = 0;
        $earned = 0;
        foreach ($checks as $check) {
            if ($check['applicable']) {
                $possible += $check['weight'];
                if ($check['passed']) {
                    $earned += $check['weight'];
                }
            }
        }

        return $possible > 0 ? (int) round(($earned / $possible) * 100) : 0;
    }

    private function normalize_score_text(string $value): string {
        $value = strip_shortcodes($value);
        $value = preg_replace('/<script[\s\S]*?<\/script>/i', ' ', $value) ?: $value;
        $value = preg_replace('/<style[\s\S]*?<\/style>/i', ' ', $value) ?: $value;
        $value = wp_strip_all_tags($value);
        $value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $value = strtolower($value);
        $value = preg_replace('/[^a-z0-9]+/', ' ', $value) ?: '';
        return trim(preg_replace('/\s+/', ' ', $value) ?: '');
    }

    private function normalize_keyword_text(string $value): string {
        return $this->normalize_score_text($value);
    }

    private function contains_keyword(string $value, string $normalized_keyword, array $keyword_terms): bool {
        $normalized_value = $this->normalize_score_text($value);
        if ($normalized_value === '' || $normalized_keyword === '') {
            return false;
        }
        if (strpos($normalized_value, $normalized_keyword) !== false) {
            return true;
        }
        foreach ($keyword_terms as $term) {
            if (strpos($normalized_value, $term) === false) {
                return false;
            }
        }
        return !empty($keyword_terms);
    }

    private function any_contains_keyword(array $values, string $normalized_keyword, array $keyword_terms): bool {
        foreach ($values as $value) {
            if ($this->contains_keyword((string) $value, $normalized_keyword, $keyword_terms)) {
                return true;
            }
        }
        return false;
    }

    private function count_keyword_occurrences(string $normalized_content, string $normalized_keyword): int {
        if ($normalized_content === '' || $normalized_keyword === '') {
            return 0;
        }
        preg_match_all('/\b' . preg_quote($normalized_keyword, '/') . '\b/', $normalized_content, $matches);
        return count($matches[0] ?? []);
    }

    private function extract_first_paragraph_for_score(string $content): string {
        if (preg_match('/<p\b[^>]*>([\s\S]*?)<\/p>/i', $content, $match)) {
            return (string) ($match[1] ?? '');
        }
        $plain = wp_strip_all_tags(strip_shortcodes($content));
        $parts = preg_split('/\n\s*\n/', $plain) ?: [];
        return (string) ($parts[0] ?? substr($plain, 0, 320));
    }

    private function extract_heading_texts_for_score(string $content): array {
        preg_match_all('/<h[1-6]\b[^>]*>([\s\S]*?)<\/h[1-6]>/i', $content, $matches);
        return array_values(array_filter(array_map('wp_strip_all_tags', $matches[1] ?? [])));
    }

    private function extract_image_alt_texts_for_score(string $content): array {
        preg_match_all('/<img\b[^>]*\balt=(["\'])(.*?)\1/i', $content, $matches);
        return array_map('html_entity_decode', $matches[2] ?? []);
    }

    /**
     * Display focus keyword badge
     *
     * @param string $keyword Focus keyword.
     */
    private function display_focus_keyword(string $keyword): void {
        if ($keyword) {
            echo '<span class="prorank-keyword-badge set" title="' . esc_attr($keyword) . '">';
            $keyword_length = $this->safe_strlen($keyword);
            $keyword_display = $keyword_length > 15
                ? $this->safe_substr($keyword, 0, 15) . '...'
                : $keyword;
            echo esc_html($keyword_display);
            echo '</span>';
        } else {
            echo '<span class="prorank-keyword-badge empty" aria-label="' . esc_attr__('No focus keyword set', 'prorank-seo') . '">—</span>';
        }
    }

    /**
     * Display meta field with progress bar
     *
     * @param string $value     The meta value.
     * @param int    $max_length Maximum recommended length.
     * @param string $type      Type of meta (title or description).
     */
    private function display_meta_with_progress(string $value, int $max_length, string $type): void {
        if (!$value) {
            echo '<div class="prorank-meta-column empty">';
/* translators: %1$: value */
            echo '<span class="meta-text empty" aria-label="' . esc_attr(sprintf(__('No %1$s set', 'prorank-seo'), $type)) . '">—</span>';
            echo '</div>';
            return;
        }

        // Strip HTML and count multibyte characters correctly
        $clean_value = wp_strip_all_tags($value);
        $length = $this->safe_strlen($clean_value);
        $percent = min(100, ($length / $max_length) * 100);

        // Use SEO-focused length ranges instead of a generic percentage threshold.
        // Title: good 30-60, warning 20-29, over otherwise.
        // Description: good 120-160, warning 90-119 or 161-180, over otherwise.
        if ($type === 'title') {
            if ($length >= 30 && $length <= 60) {
                $class = 'good';
            } elseif ($length >= 20 && $length < 30) {
                $class = 'warning';
            } else {
                $class = 'over';
            }
        } elseif ($type === 'description') {
            if ($length >= 120 && $length <= 160) {
                $class = 'good';
            } elseif (($length >= 90 && $length < 120) || ($length > 160 && $length <= 180)) {
                $class = 'warning';
            } else {
                $class = 'over';
            }
        } else {
            $class = $length > $max_length ? 'over' : ($percent > 80 ? 'warning' : 'good');
        }

        // Accessibility: aria-label for screen readers
        $type_label = $type === 'title' ? __('SEO Title', 'prorank-seo') : __('Meta Description', 'prorank-seo');
        $aria_label = sprintf(
            /* translators: 1: type (SEO Title/Meta Description), 2: current length, 3: max length */
            __('%1$s: %2$d of %3$d characters', 'prorank-seo'),
            $type_label,
            $length,
            $max_length
        );

        echo '<div class="prorank-meta-column" aria-label="' . esc_attr($aria_label) . '">';

        // Store full value in data attribute for quick edit prefill
        echo '<span class="meta-text" data-full-value="' . esc_attr($clean_value) . '">';
        echo esc_html($this->safe_substr($clean_value, 0, 40));
        if ($length > 40) {
            echo '...';
        }
        echo '</span>';

        // Progress bar
        echo '<div class="prorank-progress-bar ' . esc_attr($class) . '" role="progressbar" aria-valuenow="' . esc_attr($length) . '" aria-valuemin="0" aria-valuemax="' . esc_attr($max_length) . '">';
        echo '<div class="progress-fill" style="width: ' . esc_attr($percent) . '%"></div>';
        echo '</div>';

        // Character count
        echo '<span class="char-count">' . esc_html($length) . '/' . esc_html($max_length) . '</span>';

        echo '</div>';
    }
    
    /**
     * Make columns sortable
     *
     * @param array $columns Columns.
     * @return array
     */
    public function make_columns_sortable(array $columns): array {
        $columns['prorank_seo_score'] = 'prorank_seo_score';
        return $columns;
    }
    
    /**
     * Add quick edit fields
     *
     * @param string $column_name Column name.
     * @param string $post_type Post type.
     */
    public function add_quick_edit_fields(string $column_name, string $post_type): void {
        if ($column_name !== 'prorank_seo_score') {
            return;
        }
        ?>
        <fieldset class="inline-edit-col-right prorank-quick-edit-fields">
            <div class="inline-edit-col">
                <h4><?php esc_html_e('ProRank SEO', 'prorank-seo'); ?></h4>
                <?php wp_nonce_field('prorank_quick_edit', 'prorank_quick_edit_nonce'); ?>
                <label>
                    <span class="title"><?php esc_html_e('Focus Keyword', 'prorank-seo'); ?></span>
                    <span class="input-text-wrap">
                        <input type="text" name="prorank_focus_keyword" class="pkeyword" value="" />
                    </span>
                </label>
                <label>
                    <span class="title"><?php esc_html_e('SEO Title', 'prorank-seo'); ?></span>
                    <span class="input-text-wrap">
                        <input type="text" name="prorank_seo_title" class="ptitle" value="" maxlength="60" />
                        <span class="prorank-char-counter" data-max="60"></span>
                    </span>
                </label>
                <label>
                    <span class="title"><?php esc_html_e('Meta Description', 'prorank-seo'); ?></span>
                    <span class="input-text-wrap">
                        <textarea name="prorank_seo_description" class="pdesc" rows="2" maxlength="200"></textarea>
                        <span class="prorank-char-counter" data-max="160"></span>
                    </span>
                </label>
                <label>
                    <span class="title"><?php esc_html_e('Canonical URL', 'prorank-seo'); ?></span>
                    <span class="input-text-wrap">
                        <input type="url" name="prorank_seo_canonical_url" class="pcanonical" value="" placeholder="<?php esc_attr_e('Default permalink', 'prorank-seo'); ?>" />
                    </span>
                </label>
                <div class="prorank-quick-edit-robots">
                    <span class="title"><?php esc_html_e('Robots', 'prorank-seo'); ?></span>
                    <span class="input-text-wrap prorank-robot-options">
                        <?php foreach ($this->get_meta_robots_labels() as $directive => $label) : ?>
                            <label class="prorank-robot-option">
                                <input type="checkbox" name="prorank_meta_robots[]" value="<?php echo esc_attr($directive); ?>" />
                                <span><?php echo esc_html($label); ?></span>
                            </label>
                        <?php endforeach; ?>
                    </span>
                </div>
            </div>
        </fieldset>
        <?php
    }
    
    /**
     * Add bulk edit fields
     *
     * @param string $column_name Column name.
     * @param string $post_type Post type.
     */
    public function add_bulk_edit_fields(string $column_name, string $post_type): void {
        if ($column_name !== 'prorank_seo_score') {
            return;
        }
        ?>
        <fieldset class="inline-edit-col-right prorank-bulk-edit-fields">
            <div class="inline-edit-col">
                <h4><?php esc_html_e('ProRank SEO Settings', 'prorank-seo'); ?></h4>
                <?php wp_nonce_field('prorank_bulk_edit', 'prorank_bulk_edit_nonce'); ?>
                <p class="description">
                    <?php esc_html_e('Leave fields empty to keep existing values', 'prorank-seo'); ?>
                </p>
                <label>
                    <span class="title"><?php esc_html_e('SEO Title', 'prorank-seo'); ?></span>
                    <span class="input-text-wrap">
                        <input type="text" name="prorank_bulk_seo_title" value="" placeholder="<?php esc_attr_e('No change', 'prorank-seo'); ?>" />
                    </span>
                </label>
                <label>
                    <span class="title"><?php esc_html_e('Meta Description', 'prorank-seo'); ?></span>
                    <span class="input-text-wrap">
                        <textarea name="prorank_bulk_seo_description" rows="2" placeholder="<?php esc_attr_e('No change', 'prorank-seo'); ?>"></textarea>
                    </span>
                </label>
                <label>
                    <span class="title"><?php esc_html_e('Canonical URL', 'prorank-seo'); ?></span>
                    <span class="input-text-wrap">
                        <input type="url" name="prorank_bulk_seo_canonical_url" value="" placeholder="<?php esc_attr_e('No change', 'prorank-seo'); ?>" />
                    </span>
                </label>
                <label class="prorank-bulk-checkbox">
                    <input type="checkbox" name="prorank_bulk_clear_canonical_url" value="1" />
                    <span class="checkbox-title"><?php esc_html_e('Clear existing canonical URL', 'prorank-seo'); ?></span>
                </label>
                <div class="prorank-bulk-robots">
                    <span class="title"><?php esc_html_e('Robots', 'prorank-seo'); ?></span>
                    <span class="input-text-wrap prorank-robot-select-grid">
                        <?php foreach ($this->get_meta_robots_labels() as $directive => $label) : ?>
                            <label class="prorank-bulk-robot-field">
                                <span class="prorank-bulk-robot-label"><?php echo esc_html($label); ?></span>
                                <select name="<?php echo esc_attr('prorank_bulk_' . $directive); ?>">
                                    <option value=""><?php esc_html_e('No change', 'prorank-seo'); ?></option>
                                    <option value="enable"><?php esc_html_e('Enable', 'prorank-seo'); ?></option>
                                    <option value="disable"><?php esc_html_e('Disable', 'prorank-seo'); ?></option>
                                </select>
                            </label>
                        <?php endforeach; ?>
                    </span>
                </div>
                <?php if ($this->has_ai_access()): ?>
                <label>
                    <input type="checkbox" name="prorank_ai_optimize" value="1" />
                    <span class="checkbox-title"><?php esc_html_e('Use AI to optimize titles and descriptions', 'prorank-seo'); ?></span>
                </label>
                <?php endif; ?>
            </div>
        </fieldset>
        <?php
    }
    
    /**
     * Save quick edit data
     *
     * @param int $post_id Post ID.
     */
    public function save_quick_edit_data(int $post_id): void {
        if ( \ProRank\SEO\Plugin::is_elementor_builder_save_request() ) {
            return;
        }

        // Prevent autosave
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return;
        }

        // Check capability
        if (!current_user_can('edit_post', $post_id)) {
            return;
        }

        // Check for bulk edit
        if (prorank_get_request_var('bulk_edit') !== '') {
            // Verify bulk edit nonce
            $bulk_edit_nonce = (string) prorank_get_input_value($_REQUEST, 'prorank_bulk_edit_nonce', '');
            if ($bulk_edit_nonce === '' || !wp_verify_nonce($bulk_edit_nonce, 'prorank_bulk_edit')) {
                return;
            }

            $bulk_seo_title = prorank_get_request_var('prorank_bulk_seo_title');
            if ($bulk_seo_title !== '') {
                update_post_meta($post_id, '_prorank_seo_title', $bulk_seo_title);
            }
            $bulk_seo_description = (string) prorank_get_input_value($_REQUEST, 'prorank_bulk_seo_description', '');
            if ($bulk_seo_description !== '') {
                update_post_meta($post_id, '_prorank_seo_description', sanitize_textarea_field($bulk_seo_description));
            }

            $bulk_canonical_url = '';
            if (isset($_REQUEST['prorank_bulk_seo_canonical_url'])) {
                $bulk_canonical_url = esc_url_raw(trim(wp_unslash((string) $_REQUEST['prorank_bulk_seo_canonical_url'])));
            }
            if ($bulk_canonical_url !== '') {
                update_post_meta($post_id, '_prorank_seo_canonical_url', $bulk_canonical_url);
            } elseif (!empty($_REQUEST['prorank_bulk_clear_canonical_url'])) {
                delete_post_meta($post_id, '_prorank_seo_canonical_url');
            }

            $current_meta_robots = $this->normalize_meta_robots(get_post_meta($post_id, '_prorank_meta_robots', true));
            $updated_meta_robots = $this->apply_bulk_meta_robots($current_meta_robots, $_REQUEST);
            if ($updated_meta_robots !== $current_meta_robots) {
                update_post_meta($post_id, '_prorank_meta_robots', $updated_meta_robots);
            }

            // Handle AI optimization
            if (prorank_get_request_var('prorank_ai_optimize') !== '') {
                // AI metadata generation is premium-only. The free plugin must
                // never enqueue the stale background optimizer hook.
            }
            $this->refresh_derived_seo_score($post_id);
            return;
        }

        // Verify quick edit nonce
        $quick_edit_nonce = (string) prorank_get_input_value($_REQUEST, 'prorank_quick_edit_nonce', '');
        if ($quick_edit_nonce === '' || !wp_verify_nonce($quick_edit_nonce, 'prorank_quick_edit')) {
            return;
        }

        // Save Focus Keyword (preserve existing secondary keywords)
        if (prorank_has_input($_REQUEST, 'prorank_focus_keyword')) {
            $kw_value = prorank_get_request_var('prorank_focus_keyword');
            if ($kw_value !== '') {
                $existing  = FocusKeywords::get($post_id);
                $secondary = array_filter($existing, function ($kw) { return empty($kw['primary']); });
                $input     = array_merge([['keyword' => $kw_value, 'primary' => true]], array_values($secondary));
            } else {
                $input = [];
            }
            FocusKeywords::save($post_id, $input);
        }

        // Save SEO Title
        if (prorank_has_input($_REQUEST, 'prorank_seo_title')) {
            update_post_meta($post_id, '_prorank_seo_title', prorank_get_request_var('prorank_seo_title'));
        }

        // Save Meta Description
        if (prorank_has_input($_REQUEST, 'prorank_seo_description')) {
            update_post_meta(
                $post_id,
                '_prorank_seo_description',
                sanitize_textarea_field((string) prorank_get_input_value($_REQUEST, 'prorank_seo_description', ''))
            );
        }

        if (isset($_REQUEST['prorank_seo_canonical_url'])) {
            $canonical_url = esc_url_raw(trim(wp_unslash((string) $_REQUEST['prorank_seo_canonical_url'])));
            if ($canonical_url === '') {
                delete_post_meta($post_id, '_prorank_seo_canonical_url');
            } else {
                update_post_meta($post_id, '_prorank_seo_canonical_url', $canonical_url);
            }
        }

        $meta_robots = $this->normalize_meta_robots($_REQUEST['prorank_meta_robots'] ?? []);
        update_post_meta($post_id, '_prorank_meta_robots', $meta_robots);

        $this->refresh_derived_seo_score($post_id);
    }
    
    /**
     * Enqueue admin scripts
     *
     * @param string $hook Hook suffix.
     */
    public function enqueue_scripts(string $hook): void {
        if ($hook !== 'edit.php') {
            return;
        }

        $css_path = PRORANK_PLUGIN_DIR . 'assets/css/admin/bulk-edit.css';
        $js_path = PRORANK_PLUGIN_DIR . 'assets/js/admin/bulk-edit.js';
        $css_version = file_exists($css_path) ? filemtime($css_path) : PRORANK_VERSION;
        $js_version = file_exists($js_path) ? filemtime($js_path) : PRORANK_VERSION;
        
        wp_enqueue_style(
            'prorank-bulk-edit',
            PRORANK_PLUGIN_URL . 'assets/css/admin/bulk-edit.css',
            [],
            $css_version
        );
        
        wp_enqueue_script(
            'prorank-bulk-edit',
            PRORANK_PLUGIN_URL . 'assets/js/admin/bulk-edit.js',
            ['jquery'],
            $js_version,
            true
        );
        
        wp_localize_script('prorank-bulk-edit', 'prorankBulkEdit', [
            'nonce' => wp_create_nonce('prorank_bulk_edit'),
            'ajaxurl' => admin_url('admin-ajax.php'),
            'strings' => [
                'optimizing' => __('Optimizing with AI...', 'prorank-seo'),
                'error' => __('An error occurred', 'prorank-seo'),
            ],
        ]);
    }
    
    /**
     * Display bulk action notices
     */
    public function display_bulk_action_notices(): void {
        $ai_error = prorank_get_request_var('prorank_ai_error');
        if ($ai_error === 'unavailable') {
            echo '<div class="notice notice-error is-dismissible"><p>';
            echo esc_html__('AI Optimize SEO is available in ProRank Premium with a configured AI provider key.', 'prorank-seo');
            echo '</p></div>';
            return;
        }

        if (prorank_get_request_var('prorank_ai_optimized') !== '') {
            $optimized = intval(prorank_get_request_var('prorank_ai_optimized'));
            $total = intval(prorank_get_request_var('prorank_ai_total'));

            printf(
                '<div class="notice notice-success is-dismissible"><p>%s</p></div>',
                sprintf(
                    /* translators: 1: optimized posts, 2: total posts */
                    esc_html__( 'AI SEO optimization saved: %1$s of %2$s items.', 'prorank-seo' ),
                    esc_html( number_format_i18n( (int) $optimized ) ),
                    esc_html( number_format_i18n( (int) $total ) )
                )
            );
        }
    }

    /**
     * Add filter dropdowns to posts list
     */
    public function add_filter_dropdowns(): void {
        global $typenow;

        // Only show for supported post types
        if (!in_array($typenow, self::SUPPORTED_POST_TYPES, true)) {
            return;
        }

        // SEO Score Filter - sanitize to known enum values
        $seo_filter_value = prorank_get_query_var('prorank_seo_filter');
        $seo_selected = in_array($seo_filter_value, self::VALID_SEO_FILTERS, true)
            ? $seo_filter_value : '';

        echo '<select name="prorank_seo_filter" class="prorank-seo-filter">';
        echo '<option value="">' . esc_html__('All SEO Scores', 'prorank-seo') . '</option>';
        echo '<option value="good"' . selected($seo_selected, 'good', false) . '>' . esc_html__('Good (80+)', 'prorank-seo') . '</option>';
        echo '<option value="ok"' . selected($seo_selected, 'ok', false) . '>' . esc_html__('OK (60-79)', 'prorank-seo') . '</option>';
        echo '<option value="poor"' . selected($seo_selected, 'poor', false) . '>' . esc_html__('Poor (<60)', 'prorank-seo') . '</option>';
        echo '<option value="none"' . selected($seo_selected, 'none', false) . '>' . esc_html__('Not Analyzed', 'prorank-seo') . '</option>';
        echo '</select>';

        // Focus Keyword Filter - sanitize to known enum values
        $kw_filter_value = prorank_get_query_var('prorank_keyword_filter');
        $kw_selected = in_array($kw_filter_value, self::VALID_KW_FILTERS, true)
            ? $kw_filter_value : '';

        echo '<select name="prorank_keyword_filter" class="prorank-keyword-filter">';
        echo '<option value="">' . esc_html__('All Keywords', 'prorank-seo') . '</option>';
        echo '<option value="set"' . selected($kw_selected, 'set', false) . '>' . esc_html__('Keyword Set', 'prorank-seo') . '</option>';
        echo '<option value="none"' . selected($kw_selected, 'none', false) . '>' . esc_html__('No Keyword', 'prorank-seo') . '</option>';
        echo '</select>';
    }

    /**
     * Filter posts by SEO status
     *
     * @param \WP_Query $query The query object.
     */
    public function filter_posts_by_seo($query): void {
        if (!is_admin() || !$query->is_main_query()) {
            return;
        }

        // Only filter supported post types
        $post_type = $query->get('post_type');
        if (!in_array($post_type, self::SUPPORTED_POST_TYPES, true)) {
            return;
        }

        // Sanitize filter values to known enums
        $seo_filter_value = prorank_get_query_var('prorank_seo_filter');
        $kw_filter_value = prorank_get_query_var('prorank_keyword_filter');
        $seo_filter = in_array($seo_filter_value, self::VALID_SEO_FILTERS, true)
            ? $seo_filter_value : '';
        $kw_filter = in_array($kw_filter_value, self::VALID_KW_FILTERS, true)
            ? $kw_filter_value : '';

        // Early return if no filters selected (performance)
        if (empty($seo_filter) && empty($kw_filter)) {
            return;
        }

        $meta_query = [];
        $seo_meta_query = null;
        $kw_meta_query = null;

        // SEO Status filters
        if ($seo_filter === 'good') {
            $seo_meta_query = [
                'relation' => 'AND',
                [
                    'key'     => '_prorank_seo_score',
                    'value'   => 80,
                    'compare' => '>=',
                    'type'    => 'NUMERIC',
                ],
                $this->non_empty_meta_clause('_prorank_seo_title'),
                $this->non_empty_meta_clause('_prorank_seo_description'),
            ];
        } elseif ($seo_filter === 'ok') {
            $seo_meta_query = [
                'relation' => 'AND',
                [
                    'key'     => '_prorank_seo_score',
                    'value'   => [60, 79],
                    'compare' => 'BETWEEN',
                    'type'    => 'NUMERIC',
                ],
                $this->non_empty_meta_clause('_prorank_seo_title'),
                $this->non_empty_meta_clause('_prorank_seo_description'),
            ];
        } elseif ($seo_filter === 'poor') {
            $seo_meta_query = [
                'relation' => 'OR',
                [
                    'key'     => '_prorank_seo_score',
                    'value'   => 60,
                    'compare' => '<',
                    'type'    => 'NUMERIC',
                ],
                $this->missing_meta_clause('_prorank_seo_title'),
                $this->missing_meta_clause('_prorank_seo_description'),
            ];
        } elseif ($seo_filter === 'none') {
            $seo_meta_query = [
                'key'     => '_prorank_seo_score',
                'compare' => 'NOT EXISTS',
            ];
        }

        // Keyword filter - EXISTS includes empty values, so check for non-empty
        if ($kw_filter === 'set') {
            $kw_meta_query = [
                'key'     => '_prorank_seo_focus_keyword',
                'value'   => '',
                'compare' => '!=',
            ];
        } elseif ($kw_filter === 'none') {
            $kw_meta_query = [
                'relation' => 'OR',
                [
                    'key'     => '_prorank_seo_focus_keyword',
                    'compare' => 'NOT EXISTS',
                ],
                [
                    'key'     => '_prorank_seo_focus_keyword',
                    'value'   => '',
                    'compare' => '=',
                ],
            ];
        }

        if ($seo_meta_query !== null) {
            $meta_query[] = $seo_meta_query;
        }

        if ($kw_meta_query !== null) {
            $meta_query[] = $kw_meta_query;
        }

        if (!empty($meta_query)) {
            $meta_query['relation'] = 'AND';
            $query->set('meta_query', $meta_query);
        }
    }

    /**
     * Meta query clause for a required non-empty SEO field.
     *
     * @return array<string,string>
     */
    private function non_empty_meta_clause(string $meta_key): array {
        return [
            'key'     => $meta_key,
            'value'   => '',
            'compare' => '!=',
        ];
    }

    /**
     * Meta query clause for a missing or empty SEO field.
     *
     * @return array<string,mixed>
     */
    private function missing_meta_clause(string $meta_key): array {
        return [
            'relation' => 'OR',
            [
                'key'     => $meta_key,
                'compare' => 'NOT EXISTS',
            ],
            [
                'key'     => $meta_key,
                'value'   => '',
                'compare' => '=',
            ],
        ];
    }

    /**
     * Handle SEO score sorting without dropping posts missing scores.
     *
     * @param array     $clauses Query clauses.
     * @param \WP_Query $query   Query object.
     * @return array
     */
    public function orderby_seo_score(array $clauses, \WP_Query $query): array {
        if (!is_admin() || !$query->is_main_query()) {
            return $clauses;
        }

        $post_type = $query->get('post_type');
        if (!in_array($post_type, self::SUPPORTED_POST_TYPES, true)) {
            return $clauses;
        }

        if ($query->get('orderby') !== 'prorank_seo_score') {
            return $clauses;
        }

        global $wpdb;

        if (strpos($clauses['join'], 'prorank_seo_score_meta') === false) {
            $clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS prorank_seo_score_meta"
                . " ON ({$wpdb->posts}.ID = prorank_seo_score_meta.post_id"
                . " AND prorank_seo_score_meta.meta_key = '_prorank_seo_score')";
        }

        $order = strtoupper((string) $query->get('order')) === 'ASC' ? 'ASC' : 'DESC';
        $clauses['orderby'] = " (prorank_seo_score_meta.meta_value IS NULL) ASC,"
            . " CAST(prorank_seo_score_meta.meta_value AS UNSIGNED) {$order} ";

        return $clauses;
    }

    /**
     * Safe multibyte string length with fallback.
     *
     * @param string $value String to measure.
     * @return int
     */
    private function safe_strlen(string $value): int {
        if (function_exists('mb_strlen')) {
            return mb_strlen($value, 'UTF-8');
        }

        return strlen($value);
    }

    /**
     * Safe multibyte substring with fallback.
     *
     * @param string $value  String to slice.
     * @param int    $start  Start offset.
     * @param int    $length Length.
     * @return string
     */
    private function safe_substr(string $value, int $start, int $length): string {
        if (function_exists('mb_substr')) {
            return mb_substr($value, $start, $length, 'UTF-8');
        }

        return substr($value, $start, $length);
    }

    /**
     * Render hidden row data used to prefill quick edit.
     */
    private function render_inline_seo_state(int $post_id): void {
        $canonical_url = (string) get_post_meta($post_id, '_prorank_seo_canonical_url', true);
        $meta_robots = $this->normalize_meta_robots(get_post_meta($post_id, '_prorank_meta_robots', true));

        echo '<span class="prorank-inline-seo-state" hidden';
        echo ' data-canonical-url="' . esc_attr($canonical_url) . '"';
        echo ' data-meta-robots="' . esc_attr(implode(',', $meta_robots)) . '"';
        echo '></span>';
    }

    /**
     * Labels for post-level robots directives.
     *
     * @return array<string,string>
     */
    private function get_meta_robots_labels(): array {
        return [
            'noindex' => __('No Index', 'prorank-seo'),
            'nofollow' => __('No Follow', 'prorank-seo'),
            'noarchive' => __('No Archive', 'prorank-seo'),
            'nosnippet' => __('No Snippet', 'prorank-seo'),
            'noimageindex' => __('No Image Index', 'prorank-seo'),
        ];
    }

    /**
     * Normalize robots directives to the allowed set.
     *
     * @param mixed $meta_robots Robots input.
     * @return array<int,string>
     */
    private function normalize_meta_robots($meta_robots): array {
        if (!is_array($meta_robots)) {
            $meta_robots = empty($meta_robots) ? [] : (array) $meta_robots;
        }

        $normalized = array_map(
            'sanitize_key',
            array_filter(array_map('strval', $meta_robots))
        );

        return array_values(array_intersect(self::META_ROBOTS_DIRECTIVES, array_unique($normalized)));
    }

    /**
     * Apply bulk robots actions to the current directive list.
     *
     * @param array<int,string> $current Current directives.
     * @param array<mixed>      $request Request payload.
     * @return array<int,string>
     */
    private function apply_bulk_meta_robots(array $current, array $request): array {
        foreach (self::META_ROBOTS_DIRECTIVES as $directive) {
            $action = sanitize_key((string) ($request['prorank_bulk_' . $directive] ?? ''));
            if ($action === 'enable' && !in_array($directive, $current, true)) {
                $current[] = $directive;
            } elseif ($action === 'disable') {
                $current = array_values(array_diff($current, [$directive]));
            }
        }

        return array_values(array_intersect(self::META_ROBOTS_DIRECTIVES, $current));
    }

    /**
     * Check if user has AI access
     *
     * @return bool
     */
    private function has_ai_access(): bool {
        return false;
    }
    
}
