<?php

/**
 * Head Output Service
 *
 * Handles outputting SEO tags in the <head> section of frontend pages.
 *
 * @package ProRank\SEO\Frontend
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Frontend;

use ProRank\SEO\Plugin;
use ProRank\SEO\Core\Compatibility\CompetitorDetector;
use ProRank\SEO\Core\SettingsManager;
use ProRank\SEO\Modules\OnPageSEO\TitlesMetaModule;
use ProRank\SEO\Modules\BaseModule;

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


/**
 * HeadOutput class
 */


class HeadOutput {

    /**
     * Plugin instance
     *
     * @var Plugin
     */
    private Plugin $plugin;

    /**
     * Settings Manager
     *
     * @var SettingsManager|null
     */
    private ?SettingsManager $settings = null;

    /**
     * Memoized data for current request
     *
     * @var array
     */
    private array $cache = [];

    /**
     * Titles Meta Module instance (may be TitlesMetaModule or MetaModule)
     *
     * @var BaseModule|null
     */
    private ?BaseModule $titles_meta = null;

    /**
     * SEO conflict safe mode state.
     *
     * @var bool
     */
    private bool $seo_conflict_safe_mode = false;

    /**
     * Active SEO competitors for this request.
     *
     * @var array<int,string>
     */
    private array $seo_competitors = [];
    
    /**
     * Constructor
     *
     * @param Plugin $plugin Plugin instance.
     */
    public function __construct(Plugin $plugin) {
        $this->plugin = $plugin;
        
        if ($plugin->is_initialized()) {
            $this->settings = $plugin->settings();
            
            // Get TitlesMetaModule instance if available
            $module_manager = $plugin->modules();
            if ($module_manager && $module_manager->is_active('titles-meta')) {
                // Try to get the module directly
                $active_modules = $module_manager->get_active_modules();
                if (isset($active_modules['titles-meta'])) {
                    $this->titles_meta = $active_modules['titles-meta'];
                }
            }
        }

        $detector = new CompetitorDetector();
        $this->seo_conflict_safe_mode = $detector->should_enable_seo_safe_mode();
        $this->seo_competitors = $detector->get_active_seo_competitors();
    }
    
    /**
     * Initialize head output
     *
     * @return void
     */
    public function init(): void {
        if ($this->seo_conflict_safe_mode) {
            add_action('wp_head', [$this, 'output_safe_mode_comment'], 0);
            return;
        }

        // Output meta tags
        add_action('wp_head', [$this, 'output_meta_tags'], 1);

        // Remove WordPress core canonical to prevent duplicates
        remove_action('wp_head', 'rel_canonical');

        // Filter document title
        add_filter('pre_get_document_title', [$this, 'filter_document_title'], 10);
        add_filter('document_title_parts', [$this, 'filter_document_title_parts'], 10);
    }

    /**
     * Output a debug comment when SEO conflict safe mode is active.
     *
     * @return void
     */
    public function output_safe_mode_comment(): void {
        static $printed = false;
        if ($printed) {
            return;
        }

        $printed = true;
        $names = array_values(array_filter(array_map('sanitize_text_field', $this->seo_competitors)));

        if (empty($names)) {
            echo "\n<!-- ProRank SEO safe mode active: frontend SEO tags disabled due to active SEO plugin conflict. -->\n";
            return;
        }

        echo "\n<!-- ProRank SEO safe mode active: frontend SEO tags disabled while these SEO plugins are active: " . esc_html(implode(', ', $names)) . " -->\n";
    }
    
    /**
     * Output meta tags in head
     *
     * @return void
     */
    public function output_meta_tags(): void {
        static $output_done = false;
        if ($output_done) return;
        $output_done = true;
        // Get current context
        $context = $this->get_current_context();
        $basic_schema = $this->get_basic_schema_settings();
        $schema_module_active = $this->is_schema_module_active();
        
        // Check if white label is active
        $show_branding = true;
        if ($this->settings) {
            $white_label_settings = $this->settings->get('white_label', []);
            if (!empty($white_label_settings['hide_branding'])) {
                $show_branding = false;
            }
        }
        
        // Output ProRank SEO branding comment (if not white labeled)
        if ($show_branding) {
            echo "\n<!-- =========================================================== -->\n";
            echo "<!-- ProRank SEO - The Complete WordPress Optimization Plugin - https://prorank.io -->\n";
            echo "<!-- =========================================================== -->\n";
        }
        
        // Output meta description
        $description = $this->get_meta_description($context);
        if (!empty($description)) {
            printf(
                '<meta name="description" content="%s" />' . "\n",
                esc_attr($this->normalize_meta_text($description))
            );
        }
        
        // Output canonical URL
        $canonical = $this->get_canonical_url($context);
        if (!empty($canonical)) {
            printf(
                '<link rel="canonical" href="%s" />' . "\n",
                esc_url($canonical)
            );
        }

        // Output prev/next pagination links
        $this->output_pagination_links($context);

        // Output meta robots
        $robots = $this->get_meta_robots($context);
        if (!empty($robots)) {
            printf(
                '<meta name="robots" content="%s" />' . "\n",
                esc_attr(implode(', ', $robots))
            );
        }
        
        // Output Open Graph tags
        $this->output_open_graph_tags($context);
        
        // Output Twitter Card tags
        $this->output_twitter_card_tags($context);

        // Output Article Schema JSON-LD (for posts) when enabled
        if (!empty($basic_schema['enable_article_schema'])) {
            // Avoid duplicate Article schema when SchemaMarkupModule handles it.
            if (!$schema_module_active) {
                $post = $context['type'] === 'singular' && $context['post_id'] ? get_post($context['post_id']) : null;
                $schema_type = $post instanceof \WP_Post ? $this->get_article_schema_type($post) : 'Article';
                if ($schema_type === 'Article' || $schema_type === 'BlogPosting' || $schema_type === 'NewsArticle') {
                    $this->output_article_schema($context);
                }
            }
        }

        // Output contextual schema (WebPage, Product, PodcastEpisode, LocalBusiness)
        $this->output_contextual_schema($context);

        // Output PodcastSeries schema for podcast archives
        $this->output_podcast_series_schema($context);

        // Output Video/Audio OG tags for media attachments
        $this->output_media_og_tags($context);

        // Output closing ProRank SEO branding comment (if not white labeled)
        if ($show_branding) {
            echo "<!-- /ProRank -->\n\n";
        }
    }

    /**
     * Determine if the basic schema module is active.
     *
     * @return bool
     */
    private function is_schema_module_active(): bool {
        $module_manager = $this->plugin->modules();
        if (!$module_manager || !method_exists($module_manager, 'is_active')) {
            return false;
        }

        return $module_manager->is_active('schema-markup');
    }

    /**
     * Get basic schema settings.
     *
     * @return array
     */
    private function get_basic_schema_settings(): array {
        $defaults = [
            'enable_website_schema' => true,
            'enable_organization_schema' => true,
            'enable_article_schema' => true,
            'enable_breadcrumb_schema' => true,
        ];

        $settings = get_option('prorank_basic_schema_settings', []);
        if (!is_array($settings)) {
            $settings = [];
        }

        return array_merge($defaults, $settings);
    }
    
    /**
     * Filter document title
     *
     * @param string $title Current title.
     * @return string Modified title.
     */
    public function filter_document_title(string $title): string {
        $context = $this->get_current_context();

        // Handle static front page or posts page title
        if (in_array($context['type'], ['front_page', 'home'], true) && $context['post_id']) {
            $seo_title = get_post_meta($context['post_id'], '_prorank_seo_title', true);
            if (!empty($seo_title)) {
                return $this->process_dynamic_variables($seo_title, $context);
            }
            $page_title = get_the_title($context['post_id']);
            if (!empty($page_title)) {
                $sep = $this->titles_meta ? $this->get_titles_meta_setting('title_separator', '-') : '-';
                return $page_title . ' ' . $sep . ' ' . get_bloginfo('name');
            }
            return get_bloginfo('name');
        }

        // Handle posts index as homepage (is_home + is_front_page).
        if ($context['type'] === 'home') {
            $seo_title = $this->get_seo_title($context);
            if (!empty($seo_title)) {
                return $seo_title;
            }

            // Recover from malformed titles like "- Site Name".
            if (!empty($title) && preg_match('/^[\s\-\|\:\x{2013}\x{2014}\x{2022}]+/u', $title) !== 1) {
                return $title;
            }

            return 'Home - ' . get_bloginfo('name');
        }

        // If already filtered, return as is
        if (!empty($title)) {
            return $title;
        }

        $seo_title = $this->get_seo_title($context);

        return !empty($seo_title) ? $seo_title : $title;
    }
    
    /**
     * Filter document title parts
     *
     * @param array $title_parts Title parts array.
     * @return array Modified title parts.
     */
    public function filter_document_title_parts(array $title_parts): array {
        $context = $this->get_current_context();

        if (in_array($context['type'], ['front_page', 'home'], true) && $context['post_id']) {
            $seo_title = get_post_meta($context['post_id'], '_prorank_seo_title', true);
            if (!empty($seo_title)) {
                $title_parts['title'] = $this->process_dynamic_variables($seo_title, $context);
            } elseif (empty($title_parts['title'])) {
                $title_parts['title'] = get_the_title($context['post_id']);
            }
        } elseif ($context['type'] === 'home' && empty(trim((string) ($title_parts['title'] ?? '')))) {
            $home_title = $this->get_seo_title($context);
            $title_parts['title'] = !empty($home_title) ? $home_title : 'Home';
        } elseif ($context['type'] === 'singular' && $context['post_id']) {
            $seo_title = get_post_meta($context['post_id'], '_prorank_seo_title', true);
            if (!empty($seo_title)) {
                $title_parts['title'] = $seo_title;
            }
        }

        return $title_parts;
    }
    
    /**
     * Get current context
     *
     * @return array Context data.
     */
    private function get_current_context(): array {
        // Memoize context
        if (isset($this->cache['context'])) {
            return $this->cache['context'];
        }
        
        $context = [
            'type' => '',
            'post_id' => 0,
            'term_id' => 0,
            'author_id' => 0,
        ];
        
        if (is_front_page() && !is_home()) {
            $context['type'] = 'front_page';
            $context['post_id'] = get_option('page_on_front');
        } elseif (is_home() && !is_front_page()) {
            $context['type'] = 'home';
            $context['post_id'] = get_option('page_for_posts');
        } elseif (is_home() && is_front_page()) {
            $context['type'] = 'home';
        } elseif (is_singular()) {
            $context['type'] = 'singular';
            $context['post_id'] = get_the_ID();
        } elseif (is_category() || is_tag() || is_tax()) {
            $context['type'] = 'taxonomy';
            $term = get_queried_object();
            if ($term instanceof \WP_Term) {
                $context['term_id'] = $term->term_id;
            }
        } elseif (is_author()) {
            $context['type'] = 'author';
            $context['author_id'] = get_queried_object_id();
        } elseif (is_search()) {
            $context['type'] = 'search';
        } elseif (is_404()) {
            $context['type'] = '404';
        } elseif (is_archive()) {
            $context['type'] = 'archive';
        }
        
        $this->cache['context'] = $context;
        
        return $context;
    }
    
    /**
     * Get SEO title for current context
     *
     * @param array $context Current context.
     * @return string SEO title.
     */
    private function get_seo_title(array $context): string {
        // For singular posts/pages
        if ($context['type'] === 'singular' && $context['post_id']) {
            $seo_title = get_post_meta($context['post_id'], '_prorank_seo_title', true);
            if (!empty($seo_title)) {
                return $this->maybe_capitalize_title($this->process_dynamic_variables($seo_title, $context));
            }
        }

        if (in_array($context['type'], ['home', 'front_page'], true) && $context['post_id']) {
            $seo_title = get_post_meta($context['post_id'], '_prorank_seo_title', true);
            if (!empty($seo_title)) {
                return $this->maybe_capitalize_title($this->process_dynamic_variables($seo_title, $context));
            }
        }

        // Homepage (blog or front page)
        if (in_array($context['type'], ['home', 'front_page'], true)) {
            $home_title = (string) $this->get_titles_meta_setting('homepage_title', '');

            // Backward compatibility for the modern homepage settings model.
            if ($home_title === '' && $context['type'] === 'front_page') {
                $static_title_type = (string) $this->get_titles_meta_setting('static_homepage_title_type', 'auto');
                if ($static_title_type === 'custom') {
                    $home_title = (string) $this->get_titles_meta_setting('static_homepage_custom_title', '');
                }
            }

            if ($home_title === '' && $context['type'] === 'home') {
                $home_title_type = (string) $this->get_titles_meta_setting('homepage_title_type', 'auto');
                if ($home_title_type === 'custom') {
                    $home_title = (string) $this->get_titles_meta_setting('homepage_custom_title', '');
                } else {
                    $format = (string) $this->get_titles_meta_setting('homepage_title_format', 'sitename_tagline');
                    switch ($format) {
                        case 'sitename':
                            $home_title = '%sitename%';
                            break;
                        case 'tagline_sitename':
                            $home_title = '%tagline% %sep% %sitename%';
                            break;
                        case 'tagline':
                            $home_title = '%tagline%';
                            break;
                        case 'sitename_tagline':
                        default:
                            $home_title = '%sitename% %sep% %tagline%';
                            break;
                    }
                }
            }

            if (!empty($home_title)) {
                $resolved_home_title = $this->maybe_capitalize_title($this->process_titles_template($home_title, $context['type']));

                // Ignore malformed stored templates that only render separator/site fragments.
                if (preg_match('/^[\s\-\|\:\x{2013}\x{2014}\x{2022}]+/u', $resolved_home_title) === 1) {
                    return $context['type'] === 'home' ? 'Home' : get_bloginfo('name');
                }

                return $resolved_home_title;
            }

            if ($context['post_id']) {
                $page_title = get_the_title((int) $context['post_id']);
                if (!empty($page_title)) {
                    $sep = $this->titles_meta ? $this->get_titles_meta_setting('title_separator', '-') : '-';
                    return $this->maybe_capitalize_title($page_title . ' ' . $sep . ' ' . get_bloginfo('name'));
                }
            }
        }

        $woocommerce_title = $this->get_woocommerce_archive_title();
        if ($woocommerce_title !== '') {
            return $this->maybe_capitalize_title($woocommerce_title);
        }

        // Author archives
        if ($context['type'] === 'author') {
            $author_title = $this->get_titles_meta_setting('author_archive_title');
            if (!empty($author_title)) {
                return $this->maybe_capitalize_title($this->process_titles_template($author_title, 'author'));
            }
        }

        // Search results
        if ($context['type'] === 'search') {
            $search_title = $this->get_titles_meta_setting('search_title');
            if (!empty($search_title)) {
                return $this->maybe_capitalize_title($this->process_titles_template($search_title, 'search'));
            }
        }

        // 404 page
        if ($context['type'] === '404') {
            $error_title = $this->get_titles_meta_setting('error_404_title');
            if (!empty($error_title)) {
                return $this->maybe_capitalize_title($this->process_titles_template($error_title, '404'));
            }
        }

        // Date archives
        if (is_date()) {
            $date_title = $this->get_titles_meta_setting('date_archive_title');
            if (!empty($date_title)) {
                return $this->maybe_capitalize_title($this->process_titles_template($date_title, 'archive'));
            }
        }

        // Taxonomy archives
        if ($context['type'] === 'taxonomy') {
            $term = get_queried_object();
            if ($term instanceof \WP_Term) {
                $taxonomy_settings = $this->get_titles_meta_taxonomy_settings($term->taxonomy);
                if (!empty($taxonomy_settings['archive_title'])) {
                    return $this->maybe_capitalize_title($this->process_titles_template($taxonomy_settings['archive_title'], 'archive'));
                }
            }
        }

        // Post type archives
        if (is_post_type_archive()) {
            $post_type = get_query_var('post_type');
            if ($post_type) {
                $post_type_settings = $this->get_titles_meta_post_type_settings((string) $post_type);
                if (!empty($post_type_settings['archive_title'])) {
                    return $this->maybe_capitalize_title($this->process_titles_template($post_type_settings['archive_title'], 'archive'));
                }
            }
        }

        // Post type singles
        if ($context['type'] === 'singular' && $context['post_id']) {
            $post = get_post($context['post_id']);
            if ($post) {
                $post_type_settings = $this->get_titles_meta_post_type_settings($post->post_type);
                if (!empty($post_type_settings['single_title'])) {
                    return $this->maybe_capitalize_title($this->process_titles_template($post_type_settings['single_title'], 'post'));
                }
            }
        }
        
        // Check global default templates
        if ($this->titles_meta) {
            // Get template based on page type
            $template = '';
            if (is_singular('post')) {
                $template = $this->get_titles_meta_setting('post_title_template');
            } elseif (is_page()) {
                $template = $this->get_titles_meta_setting('page_title_template');
            } elseif (is_archive()) {
                $template = $this->get_titles_meta_setting('archive_title_template');
            } elseif (is_author()) {
                $template = $this->get_titles_meta_setting('author_title_template');
            } elseif (is_search()) {
                $template = $this->get_titles_meta_setting('search_title_template');
            } elseif (is_404()) {
                $template = $this->get_titles_meta_setting('404_title_template');
            }
            
            if ($template) {
                // Map 'singular' context to 'post' or 'page' for process_template()
                $template_context = $context['type'];
                if ($template_context === 'singular' && $context['post_id']) {
                    $post_obj = get_post($context['post_id']);
                    $template_context = ($post_obj && $post_obj->post_type === 'page') ? 'page' : 'post';
                }
                $resolved_template_title = $this->maybe_capitalize_title($this->process_titles_template($template, $template_context));

                if (in_array($context['type'], ['home', 'front_page'], true) && preg_match('/^[\s\-\|\:\x{2013}\x{2014}\x{2022}]+/u', ltrim($resolved_template_title)) === 1) {
                    if ($context['type'] === 'front_page' && !empty($context['post_id'])) {
                        $front_page_title = get_the_title((int) $context['post_id']);
                        if (!empty($front_page_title)) {
                            return $front_page_title;
                        }
                    }

                    return 'Home';
                }

                return $resolved_template_title;
            }
        }
        
        // Fallback for singular posts/pages: construct title directly from context
        if ($context['type'] === 'singular' && $context['post_id']) {
            $post = get_post($context['post_id']);
            if ($post && !empty($post->post_title)) {
                $sep = $this->titles_meta ? $this->get_titles_meta_setting('title_separator', '-') : '-';
                return $this->maybe_capitalize_title($post->post_title . ' ' . $sep . ' ' . get_bloginfo('name'));
            }
        }

        // For now, return empty to use WordPress default
        return '';
    }
    
    /**
     * Get meta description for current context
     *
     * @param array $context Current context.
     * @return string Meta description.
     */
    private function get_meta_description(array $context): string {
        // For singular posts/pages
        if ($context['type'] === 'singular' && $context['post_id']) {
            $description = get_post_meta($context['post_id'], '_prorank_seo_description', true);
            if (!empty($description)) {
                return $this->process_dynamic_variables($description, $context);
            }

            // Post type description template
            $post = get_post($context['post_id']);
            if ($post) {
                $post_type_settings = $this->get_titles_meta_post_type_settings($post->post_type);
                if (!empty($post_type_settings['single_description'])) {
                    return $this->process_titles_template($post_type_settings['single_description'], 'post');
                }
            }

            // Fallback to global default template
            if ($this->titles_meta) {
                $template = $this->get_titles_meta_setting('post_meta_template');
                if ($template) {
                    return $this->process_titles_template($template, 'post');
                }
            }
        }

        // Homepage description
        if (in_array($context['type'], ['home', 'front_page'], true)) {
            // Check post-level meta first for static front page or posts page
            if ($context['post_id']) {
                $description = get_post_meta($context['post_id'], '_prorank_seo_description', true);
                if (!empty($description)) {
                    return $this->process_dynamic_variables($description, $context);
                }
            }

            $home_description = (string) $this->get_titles_meta_setting('homepage_description', '');

            // Backward compatibility for the modern homepage settings model.
            if ($home_description === '' && $context['type'] === 'front_page') {
                $static_meta_type = (string) $this->get_titles_meta_setting('static_homepage_meta_type', 'auto');
                if ($static_meta_type === 'custom') {
                    $home_description = (string) $this->get_titles_meta_setting('static_homepage_custom_meta', '');
                }
            }

            if ($home_description === '' && $context['type'] === 'home') {
                $home_meta_type = (string) $this->get_titles_meta_setting('homepage_meta_type', 'auto');
                if ($home_meta_type === 'custom') {
                    $home_description = (string) $this->get_titles_meta_setting('homepage_custom_meta', '');
                } else {
                    $source = (string) $this->get_titles_meta_setting('homepage_auto_meta_source', 'tagline');
                    if ($source === 'tagline') {
                        $home_description = '%tagline%';
                    }
                }
            }

            if (!empty($home_description)) {
                return $this->process_titles_template($home_description, $context['type']);
            }

            // Fallback: auto-generate from static front page content
            if ($context['type'] === 'front_page' && $context['post_id']) {
                $post = get_post($context['post_id']);
                if ($post) {
                    if (!empty($post->post_excerpt)) {
                        return wp_strip_all_tags($post->post_excerpt);
                    }
                    $content = wp_strip_all_tags($post->post_content);
                    $content = preg_replace('/\s+/', ' ', trim($content));
                    if (!empty($content)) {
                        return wp_trim_words($content, 30, '...');
                    }
                }
            }
        }

        $woocommerce_description = $this->get_woocommerce_archive_description();
        if ($woocommerce_description !== '') {
            return $woocommerce_description;
        }

        // Author archives
        if ($context['type'] === 'author') {
            $author_description = $this->get_titles_meta_setting('author_archive_description');
            if (!empty($author_description)) {
                return $this->process_titles_template($author_description, 'author');
            }
        }

        // Taxonomy archives
        if ($context['type'] === 'taxonomy') {
            $term = get_queried_object();
            if ($term instanceof \WP_Term) {
                $custom_description = get_term_meta($term->term_id, '_prorank_seo_description', true);
                if (!empty($custom_description)) {
                    return wp_strip_all_tags($custom_description);
                }

                $taxonomy_settings = $this->get_titles_meta_taxonomy_settings($term->taxonomy);
                if (!empty($taxonomy_settings['archive_description'])) {
                    return $this->process_titles_template($taxonomy_settings['archive_description'], 'archive');
                }
                // Fallback to the term's own description
                $term_description = term_description($term->term_id, $term->taxonomy);
                if (!empty($term_description)) {
                    return wp_strip_all_tags($term_description);
                }
            }
        }

        // Post type archives
        if (is_post_type_archive()) {
            $post_type = get_query_var('post_type');
            if ($post_type) {
                $post_type_settings = $this->get_titles_meta_post_type_settings((string) $post_type);
                if (!empty($post_type_settings['archive_description'])) {
                    return $this->process_titles_template($post_type_settings['archive_description'], 'archive');
                }
            }
        }

        // Handle other contexts with global defaults
        if ($this->titles_meta) {
            $template = '';
            if (is_page()) {
                $template = $this->get_titles_meta_setting('page_meta_template');
            } elseif (is_archive()) {
                $template = $this->get_titles_meta_setting('archive_meta_template');
            } elseif (is_author()) {
                $template = $this->get_titles_meta_setting('author_meta_template');
                if (empty($template)) {
                    $template = $this->get_titles_meta_setting('author_description_template');
                }
            } elseif (is_search()) {
                $template = $this->get_titles_meta_setting('search_meta_template');
            } elseif (is_404()) {
                $template = $this->get_titles_meta_setting('404_meta_template');
                if (empty($template)) {
                    $template = $this->get_titles_meta_setting('404_description_template');
                }
            }
            
            if ($template) {
                return $this->process_titles_template($template, $context['type']);
            }
        }
        
        return '';
    }

    /**
     * Resolve WooCommerce shop/product archive titles through the shared head output.
     *
     * @return string
     */
    private function get_woocommerce_archive_title(): string {
        if (!class_exists('\WooCommerce')) {
            return '';
        }

        if (function_exists('is_shop') && is_shop()) {
            $shop_page_id = function_exists('wc_get_page_id') ? (int) wc_get_page_id('shop') : 0;
            $title = $shop_page_id > 0 ? (string) get_post_meta($shop_page_id, '_prorank_seo_title', true) : '';

            if ($title === '' && $shop_page_id > 0) {
                $title = (string) get_the_title($shop_page_id);
            }

            if ($title === '') {
                $title = (string) get_option('woocommerce_shop_page_title', __('Products', 'prorank-seo'));
            }

            $paged = (int) get_query_var('paged', 0);
            if ($title !== '' && $paged > 1) {
                $title .= sprintf(
                    /* translators: %d: current archive page number */
                    __(' - Page %d', 'prorank-seo'),
                    $paged
                );
            }

            return $title;
        }

        if (function_exists('is_product_taxonomy') && is_product_taxonomy()) {
            $term = get_queried_object();
            if (!$term instanceof \WP_Term) {
                return '';
            }

            $title = (string) get_term_meta($term->term_id, '_prorank_seo_title', true);
            if ($title === '') {
                $title = (string) $term->name;
            }

            $paged = (int) get_query_var('paged', 0);
            if ($title !== '' && $paged > 1) {
                $title .= sprintf(
                    /* translators: %d: current archive page number */
                    __(' - Page %d', 'prorank-seo'),
                    $paged
                );
            }

            return $title;
        }

        return '';
    }

    /**
     * Resolve WooCommerce shop/product archive descriptions through the shared head output.
     *
     * @return string
     */
    private function get_woocommerce_archive_description(): string {
        if (!class_exists('\WooCommerce')) {
            return '';
        }

        if (function_exists('is_shop') && is_shop()) {
            $shop_page_id = function_exists('wc_get_page_id') ? (int) wc_get_page_id('shop') : 0;
            $description = $shop_page_id > 0 ? (string) get_post_meta($shop_page_id, '_prorank_seo_description', true) : '';

            if ($description === '' && $shop_page_id > 0) {
                $description = (string) get_the_excerpt($shop_page_id);
            }

            if ($description === '') {
                $description = sprintf(
                    /* translators: %1$s: shop label, %2$s: site name */
                    __('Shop for %1$s at %2$s. Browse our collection of quality products with competitive prices and fast shipping.', 'prorank-seo'),
                    get_option('woocommerce_shop_page_title', __('Products', 'prorank-seo')),
                    get_bloginfo('name')
                );
            }

            return wp_strip_all_tags($description);
        }

        if (function_exists('is_product_category') && is_product_category()) {
            $term = get_queried_object();
            if (!$term instanceof \WP_Term) {
                return '';
            }

            $description = (string) get_term_meta($term->term_id, '_prorank_seo_description', true);

            if ($description === '') {
                $description = wp_strip_all_tags((string) term_description($term->term_id, $term->taxonomy));
            }

            if ($description === '') {
                $description = sprintf(
                    /* translators: %1$s: category name, %2$s: site name, %3$s: lowercase category name */
                    __('Browse %1$s products at %2$s. Find the best deals on quality %3$s items.', 'prorank-seo'),
                    $term->name,
                    get_bloginfo('name'),
                    strtolower($term->name)
                );
            }

            return wp_strip_all_tags($description);
        }

        if (function_exists('is_product_tag') && is_product_tag()) {
            $term = get_queried_object();
            if (!$term instanceof \WP_Term) {
                return '';
            }

            return sprintf(
                /* translators: %1$s: tag name, %2$s: tag name, %3$s: site name */
                __('Products tagged with %1$s. Discover items related to %2$s at %3$s.', 'prorank-seo'),
                $term->name,
                $term->name,
                get_bloginfo('name')
            );
        }

        return '';
    }

    /**
     * Get canonical URL for current context
     *
     * Handles pagination properly - canonical points to page 1 unless settings say otherwise.
     *
     * @param array $context Current context.
     * @return string Canonical URL.
     */
    private function get_canonical_url(array $context): string {
        // For singular posts/pages
        if ($context['type'] === 'singular' && $context['post_id']) {
            // Check for custom canonical
            $canonical = get_post_meta($context['post_id'], '_prorank_seo_canonical_url', true);
            if (!empty($canonical)) {
                return $canonical;
            }

            // Get base permalink
            $canonical = get_permalink($context['post_id']);

            // Handle paginated singular content (<!--nextpage-->)
            $page = get_query_var('page', 0);
            if ($page > 1) {
                // Check if we should include page in canonical
                $include_pagination = $this->titles_meta
                    ? $this->get_titles_meta_setting('canonical_include_pagination', false)
                    : false;

                if ($include_pagination) {
                    // Add page number to canonical
                    global $wp_rewrite;
                    if ($wp_rewrite->using_permalinks()) {
                        $canonical = trailingslashit($canonical) . $page . '/';
                    } else {
                        $canonical = add_query_arg('page', $page, $canonical);
                    }
                }
                // Otherwise canonical stays as page 1 (default behavior)
            }

            return $canonical;
        }

        // Front page
        if ($context['type'] === 'front_page') {
            return home_url('/');
        }

        // Home (blog page)
        if ($context['type'] === 'home') {
            $blog_page = get_option('page_for_posts');
            if ($blog_page) {
                return get_permalink($blog_page);
            }
            return home_url('/');
        }

        // Taxonomy archives
        if ($context['type'] === 'taxonomy' && $context['term_id']) {
            $term = get_term($context['term_id']);
            if ($term && !is_wp_error($term)) {
                $custom_canonical = get_term_meta($term->term_id, '_prorank_seo_canonical', true);
                if (!empty($custom_canonical)) {
                    return esc_url_raw($custom_canonical);
                }

                $canonical = get_term_link($term);

                // Handle pagination for archives
                $paged = get_query_var('paged', 0);
                if ($paged > 1) {
                    $include_pagination = $this->titles_meta
                        ? $this->get_titles_meta_setting('canonical_include_pagination', false)
                        : false;

                    if ($include_pagination) {
                        global $wp_rewrite;
                        if ($wp_rewrite->using_permalinks()) {
                            $canonical = trailingslashit($canonical) . 'page/' . $paged . '/';
                        } else {
                            $canonical = add_query_arg('paged', $paged, $canonical);
                        }
                    }
                }

                return $canonical;
            }
        }

        // Author archives
        if ($context['type'] === 'author' && $context['author_id']) {
            $canonical = get_author_posts_url($context['author_id']);

            // Handle pagination
            $paged = get_query_var('paged', 0);
            if ($paged > 1) {
                $include_pagination = $this->titles_meta
                    ? $this->get_titles_meta_setting('canonical_include_pagination', false)
                    : false;

                if ($include_pagination) {
                    global $wp_rewrite;
                    if ($wp_rewrite->using_permalinks()) {
                        $canonical = trailingslashit($canonical) . 'page/' . $paged . '/';
                    } else {
                        $canonical = add_query_arg('paged', $paged, $canonical);
                    }
                }
            }

            return $canonical;
        }

        // Search pages - typically no canonical, return empty
        if ($context['type'] === 'search') {
            return '';
        }

        // 404 pages - no canonical
        if ($context['type'] === '404') {
            return '';
        }

        // Date archives and other archive types
        if ($context['type'] === 'archive') {
            // Get the current archive URL
            if (is_date()) {
                if (is_day()) {
                    return get_day_link(get_query_var('year'), get_query_var('monthnum'), get_query_var('day'));
                } elseif (is_month()) {
                    return get_month_link(get_query_var('year'), get_query_var('monthnum'));
                } elseif (is_year()) {
                    return get_year_link(get_query_var('year'));
                }
            }

            // Post type archives
            if (is_post_type_archive()) {
                return get_post_type_archive_link(get_query_var('post_type'));
            }
        }

        // Fallback - use current URL without query params
        global $wp;
        $canonical = home_url($wp->request);

        // Ensure trailing slash consistency
        if (substr($canonical, -1) !== '/' && !preg_match('/\.[a-z0-9]+$/i', $canonical)) {
            $canonical = trailingslashit($canonical);
        }

        return $canonical;
    }

    /**
     * Output prev/next pagination links for paginated content
     *
     * @param array $context Current context.
     * @return void
     */
    private function output_pagination_links(array $context): void {
        // Handle paginated singular content (<!--nextpage-->)
        if ($context['type'] === 'singular' && $context['post_id']) {
            global $page, $numpages;

            if ($numpages > 1) {
                $permalink = get_permalink($context['post_id']);

                // Previous page
                if ($page > 1) {
                    $prev_page = $page - 1;
                    if ($prev_page === 1) {
                        $prev_url = $permalink;
                    } else {
                        global $wp_rewrite;
                        if ($wp_rewrite->using_permalinks()) {
                            $prev_url = trailingslashit($permalink) . $prev_page . '/';
                        } else {
                            $prev_url = add_query_arg('page', $prev_page, $permalink);
                        }
                    }
                    printf('<link rel="prev" href="%s" />' . "\n", esc_url($prev_url));
                }

                // Next page
                if ($page < $numpages) {
                    $next_page = $page + 1;
                    global $wp_rewrite;
                    if ($wp_rewrite->using_permalinks()) {
                        $next_url = trailingslashit($permalink) . $next_page . '/';
                    } else {
                        $next_url = add_query_arg('page', $next_page, $permalink);
                    }
                    printf('<link rel="next" href="%s" />' . "\n", esc_url($next_url));
                }
            }

            return;
        }

        // Handle paginated archives
        $paged = get_query_var('paged', 1);
        if ($paged < 1) {
            $paged = 1;
        }

        global $wp_query;
        $max_pages = $wp_query->max_num_pages;

        if ($max_pages <= 1) {
            return;
        }

        // Get base URL for current archive
        $base_url = '';

        if ($context['type'] === 'taxonomy' && $context['term_id']) {
            $term = get_term($context['term_id']);
            if ($term && !is_wp_error($term)) {
                $base_url = get_term_link($term);
            }
        } elseif ($context['type'] === 'author' && $context['author_id']) {
            $base_url = get_author_posts_url($context['author_id']);
        } elseif ($context['type'] === 'home') {
            $base_url = get_option('page_for_posts')
                ? get_permalink(get_option('page_for_posts'))
                : home_url('/');
        }

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

        global $wp_rewrite;

        // Previous page
        if ($paged > 1) {
            $prev_page = $paged - 1;
            if ($prev_page === 1) {
                $prev_url = $base_url;
            } else {
                if ($wp_rewrite->using_permalinks()) {
                    $prev_url = trailingslashit($base_url) . 'page/' . $prev_page . '/';
                } else {
                    $prev_url = add_query_arg('paged', $prev_page, $base_url);
                }
            }
            printf('<link rel="prev" href="%s" />' . "\n", esc_url($prev_url));
        }

        // Next page
        if ($paged < $max_pages) {
            $next_page = $paged + 1;
            if ($wp_rewrite->using_permalinks()) {
                $next_url = trailingslashit($base_url) . 'page/' . $next_page . '/';
            } else {
                $next_url = add_query_arg('paged', $next_page, $base_url);
            }
            printf('<link rel="next" href="%s" />' . "\n", esc_url($next_url));
        }
    }
    
    /**
     * Get meta robots directives for current context
     *
     * Handles: singular, archives, search, password-protected, paginated content
     *
     * @param array $context Current context.
     * @return array Meta robots directives.
     */
    private function get_meta_robots(array $context): array {
        $robots = [];

        // For singular posts/pages
        if ($context['type'] === 'singular' && $context['post_id']) {
            $post = get_post($context['post_id']);

            // Check for post-level robots meta
            $post_robots = get_post_meta($context['post_id'], '_prorank_meta_robots', true);
            if (is_array($post_robots) && !empty($post_robots)) {
                $robots = $post_robots;
            }

            // Post type robots overrides
            if (empty($robots) && $post) {
                $post_type_settings = $this->get_titles_meta_post_type_settings($post->post_type);
                if (!empty($post_type_settings) && ($post_type_settings['robots'] ?? 'default') === 'custom') {
                    $robots = $this->build_custom_robots($post_type_settings);
                }
            }

            // PASSWORD-PROTECTED posts - always noindex
            if ($post && post_password_required($post)) {
                $noindex_password = $this->titles_meta
                    ? $this->get_titles_meta_setting('noindex_password_protected', true)
                    : true;

                if ($noindex_password && !in_array('noindex', $robots, true)) {
                    $robots[] = 'noindex';
                }
            }

            // PAGINATED singular content (<!--nextpage-->)
            $page = get_query_var('page', 0);
            if ($page > 1) {
                $noindex_paginated_singles = $this->titles_meta
                    ? $this->get_titles_meta_setting('noindex_paginated_singles', false)
                    : false;

                if ($noindex_paginated_singles && !in_array('noindex', $robots, true)) {
                    $robots[] = 'noindex';
                }
            }

            // ATTACHMENTS - check if should be noindexed
            if ($post && $post->post_type === 'attachment') {
                $noindex_attachments = $this->titles_meta
                    ? $this->get_titles_meta_setting('noindex_attachments', true)
                    : true;

                if ($noindex_attachments && !in_array('noindex', $robots, true)) {
                    $robots[] = 'noindex';
                }
            }

            // Apply global defaults if no post-level robots set
            if ($this->titles_meta && empty($robots)) {
                $defaults = $this->get_titles_meta_robots_defaults();

                if (!$defaults['index']) {
                    $robots[] = 'noindex';
                }
                if (!$defaults['follow']) {
                    $robots[] = 'nofollow';
                }
                if (!$defaults['archive']) {
                    $robots[] = 'noarchive';
                }
                if (!$defaults['imageindex']) {
                    $robots[] = 'noimageindex';
                }
                if (!$defaults['snippet']) {
                    $robots[] = 'nosnippet';
                }

                // Advanced robots meta directives
                if (isset($defaults['max-snippet']) && $defaults['max-snippet'] !== -1) {
                    $robots[] = 'max-snippet:' . $defaults['max-snippet'];
                }
                if (isset($defaults['max-video-preview']) && $defaults['max-video-preview'] !== -1) {
                    $robots[] = 'max-video-preview:' . $defaults['max-video-preview'];
                }
                if (isset($defaults['max-image-preview']) && $defaults['max-image-preview'] && $defaults['max-image-preview'] !== 'large') {
                    $robots[] = 'max-image-preview:' . $defaults['max-image-preview'];
                }
            }
        }

        // Homepage robots override
        if (empty($robots) && in_array($context['type'], ['home', 'front_page'], true)) {
            $home_robots = $this->get_titles_meta_setting('homepage_robots', '');
            if (!empty($home_robots)) {
                $robots = $this->parse_robots_string($home_robots);
            }
        }

        // Author robots override
        if (empty($robots) && $context['type'] === 'author') {
            $mode = $this->get_titles_meta_setting('author_robots_meta', 'default');
            if ($mode === 'custom') {
                $robots = $this->build_custom_robots([
                    'noindex' => !($this->get_titles_meta_setting('author_robots_index', true)),
                    'nofollow' => !($this->get_titles_meta_setting('author_robots_follow', true)),
                    'noarchive' => !($this->get_titles_meta_setting('author_robots_archive', true)),
                    'noimageindex' => !($this->get_titles_meta_setting('author_robots_imageindex', true)),
                    'nosnippet' => !($this->get_titles_meta_setting('author_robots_snippet', true)),
                ]);

                $author_max_snippet = (int) $this->get_titles_meta_setting('author_max_snippet', -1);
                $author_max_video = (int) $this->get_titles_meta_setting('author_max_video_preview', -1);
                $author_max_image = $this->get_titles_meta_setting('author_max_image_preview', 'large');

                if ($author_max_snippet !== -1) {
                    $robots[] = 'max-snippet:' . $author_max_snippet;
                }
                if ($author_max_video !== -1) {
                    $robots[] = 'max-video-preview:' . $author_max_video;
                }
                if ($author_max_image && $author_max_image !== 'large') {
                    $robots[] = 'max-image-preview:' . $author_max_image;
                }
            }
        }

        // Taxonomy robots override
        if ($context['type'] === 'taxonomy') {
            $term = get_queried_object();
            if ($term instanceof \WP_Term) {
                $term_noindex = get_term_meta($term->term_id, '_prorank_seo_noindex', true);
                $term_nofollow = get_term_meta($term->term_id, '_prorank_seo_nofollow', true);

                if ($term_noindex && !in_array('noindex', $robots, true)) {
                    $robots[] = 'noindex';
                }
                if ($term_nofollow && !in_array('nofollow', $robots, true)) {
                    $robots[] = 'nofollow';
                }

                $taxonomy_settings = $this->get_titles_meta_taxonomy_settings($term->taxonomy);
                if (!empty($taxonomy_settings) && ($taxonomy_settings['robots'] ?? 'default') === 'custom') {
                    $robots = array_merge($robots, $this->build_custom_robots($taxonomy_settings));
                }
            }
        }

        // Post type archive robots override
        if (empty($robots) && is_post_type_archive()) {
            $post_type = get_query_var('post_type');
            if ($post_type) {
                $post_type_settings = $this->get_titles_meta_post_type_settings((string) $post_type);
                if (!empty($post_type_settings) && ($post_type_settings['robots'] ?? 'default') === 'custom') {
                    $robots = $this->build_custom_robots($post_type_settings);
                }
            }
        }

        // Handle archive noindex settings
        if (is_archive() && $this->titles_meta) {
            global $wp_query;

            // Empty archives
            if ($wp_query->found_posts === 0 && $this->get_titles_meta_setting('noindex_empty_archives')) {
                if (!in_array('noindex', $robots, true)) {
                    $robots[] = 'noindex';
                }
            }

            // Paginated archive pages
            if (is_paged() && $this->get_titles_meta_setting('noindex_paginated_pages')) {
                if (!in_array('noindex', $robots, true)) {
                    $robots[] = 'noindex';
                }
            }

            // Subpages (global pagination)
            if (is_paged() && $this->get_titles_meta_setting('noindex_subpages', false)) {
                if (!in_array('noindex', $robots, true)) {
                    $robots[] = 'noindex';
                }
            }

            // Date archives
            if (is_date() && $this->get_titles_meta_setting('noindex_date_archives', false)) {
                if (!in_array('noindex', $robots, true)) {
                    $robots[] = 'noindex';
                }
            }

            // Author archives
            if (is_author() && $this->get_titles_meta_setting('noindex_author_archives', false)) {
                if (!in_array('noindex', $robots, true)) {
                    $robots[] = 'noindex';
                }
            }

            // Format archives (aside, gallery, link, image, quote, status, video, audio, chat)
            if (is_tax('post_format') && $this->get_titles_meta_setting('noindex_format_archives', true)) {
                if (!in_array('noindex', $robots, true)) {
                    $robots[] = 'noindex';
                }
            }
        }

        // Search results - noindex by default
        if (is_search()) {
            $noindex_search = $this->titles_meta
                ? $this->get_titles_meta_setting('noindex_search_pages', $this->get_titles_meta_setting('noindex_search', true))
                : true;

            if ($noindex_search && !in_array('noindex', $robots, true)) {
                $robots[] = 'noindex';
            }
        }

        // Global paginated subpages (e.g. /page/2/)
        if (is_paged() && $this->get_titles_meta_setting('noindex_subpages', false)) {
            if (!in_array('noindex', $robots, true)) {
                $robots[] = 'noindex';
            }
        }

        // 404 pages - always noindex
        if ($context['type'] === '404') {
            if (!in_array('noindex', $robots, true)) {
                $robots[] = 'noindex';
            }
        }

        // Remove duplicates and ensure proper index/noindex handling
        $robots = array_unique($robots);

        // If we have both 'index' and 'noindex', remove 'index' (noindex takes precedence)
        if (in_array('noindex', $robots, true) && in_array('index', $robots, true)) {
            $robots = array_diff($robots, ['index']);
        }

        return array_values($robots);
    }

    /**
     * Get a setting from the titles/meta module if available.
     *
     * @param string $key Setting key.
     * @param mixed  $default Default value.
     * @return mixed
     */
    private function get_titles_meta_setting(string $key, $default = null) {
        if ($this->titles_meta) {
            try {
                if (is_callable([$this->titles_meta, 'get_setting'])) {
                    return $this->titles_meta->get_setting($key, $default);
                }
            } catch (\Throwable $e) {
                // Fall back to defaults when module exposes non-public methods.
            }
        }

        return $default;
    }

    /**
     * Process a template via titles/meta module when available.
     *
     * @param string $template Template string.
     * @param string $context Context identifier.
     * @return string
     */
    private function process_titles_template(string $template, string $context = ''): string {
        if ($this->titles_meta && is_callable([$this->titles_meta, 'process_template'])) {
            try {
                return $this->titles_meta->process_template($template, $context);
            } catch (\Throwable $e) {
                return $template;
            }
        }

        return $template;
    }

    /**
     * Get robots defaults safely from Titles/Meta module.
     *
     * @return array<string,mixed>
     */
    private function get_titles_meta_robots_defaults(): array {
        $defaults = [
            'index' => true,
            'follow' => true,
            'archive' => true,
            'imageindex' => true,
            'snippet' => true,
            'max-snippet' => -1,
            'max-video-preview' => -1,
            'max-image-preview' => 'large',
        ];

        if (!$this->titles_meta || !is_callable([$this->titles_meta, 'get_robots_defaults'])) {
            return $defaults;
        }

        try {
            $module_defaults = $this->titles_meta->get_robots_defaults();
            if (is_array($module_defaults)) {
                return array_merge($defaults, $module_defaults);
            }
        } catch (\Throwable $e) {
            return $defaults;
        }

        return $defaults;
    }

    /**
     * Get post type titles/meta settings array.
     *
     * @param string $post_type Post type slug.
     * @return array
     */
    private function get_titles_meta_post_type_settings(string $post_type): array {
        $settings = $this->get_titles_meta_setting('post_type_' . $post_type, []);
        return is_array($settings) ? $settings : [];
    }

    /**
     * Get taxonomy titles/meta settings array.
     *
     * @param string $taxonomy Taxonomy slug.
     * @return array
     */
    private function get_titles_meta_taxonomy_settings(string $taxonomy): array {
        $settings = $this->get_titles_meta_setting('taxonomy_' . $taxonomy, []);
        return is_array($settings) ? $settings : [];
    }

    /**
     * Build robots directives array from custom settings.
     *
     * @param array $settings Settings array.
     * @return array
     */
    private function build_custom_robots(array $settings): array {
        $robots = [];
        if (!empty($settings['noindex'])) {
            $robots[] = 'noindex';
        }
        if (!empty($settings['nofollow'])) {
            $robots[] = 'nofollow';
        }
        if (!empty($settings['noarchive'])) {
            $robots[] = 'noarchive';
        }
        if (!empty($settings['noimageindex'])) {
            $robots[] = 'noimageindex';
        }
        if (!empty($settings['nosnippet'])) {
            $robots[] = 'nosnippet';
        }

        return $robots;
    }

    /**
     * Parse a robots string into directives.
     *
     * @param string $robots Robots string (e.g. "index,follow").
     * @return array
     */
    private function parse_robots_string(string $robots): array {
        $parts = array_map('trim', explode(',', strtolower($robots)));
        $parts = array_filter($parts, static function ($value) {
            return $value !== '';
        });
        return array_values($parts);
    }

    /**
     * Capitalize title when enabled in settings.
     *
     * @param string $title Title string.
     * @return string
     */
    private function maybe_capitalize_title(string $title): string {
        if ($title === '') {
            return $title;
        }

        $capitalize = (bool) $this->get_titles_meta_setting('capitalize_titles', false);
        if (!$capitalize) {
            return $title;
        }

        if (function_exists('mb_convert_case')) {
            return mb_convert_case($title, MB_CASE_TITLE, 'UTF-8');
        }

        return ucwords(strtolower($title));
    }

    /**
     * Resolve article schema type for posts based on settings.
     *
     * @param \WP_Post $post Post object.
     * @return string
     */
    private function get_article_schema_type(\WP_Post $post): string {
        $schema_type = get_post_meta($post->ID, '_prorank_schema_type', true);

        $post_type_settings = $this->get_titles_meta_post_type_settings($post->post_type);
        if (!empty($post_type_settings['schema_type'])) {
            $schema_type = $post_type_settings['schema_type'];
            if ($schema_type === 'Article' && !empty($post_type_settings['article_type'])) {
                $schema_type = $post_type_settings['article_type'];
            }
        }

        $allowed = ['Article', 'BlogPosting', 'NewsArticle'];
        if (!in_array($schema_type, $allowed, true)) {
            $schema_type = 'Article';
        }

        return $schema_type;
    }
    
    /**
     * Process dynamic variables in text
     *
     * @param string $text Text with variables.
     * @param array  $context Current context.
     * @return string Processed text.
     */
    private function process_dynamic_variables(string $text, array $context): string {
        if ($text === '') {
            return $text;
        }

        try {
            if (!isset($this->cache['dynamic_variables_parser'])) {
                $this->cache['dynamic_variables_parser'] = new \ProRank\SEO\Core\DynamicVariables();
            }

            /** @var \ProRank\SEO\Core\DynamicVariables $parser */
            $parser = $this->cache['dynamic_variables_parser'];
        } catch (\Throwable $e) {
            return $text;
        }

        $context_object = null;
        $context_type = '';

        if (($context['type'] ?? '') === 'singular' && !empty($context['post_id'])) {
            $post = get_post((int) $context['post_id']);
            if ($post instanceof \WP_Post) {
                $context_object = $post;
                $context_type = $post->post_type ?: 'post';
            }
        } elseif (($context['type'] ?? '') === 'taxonomy' && !empty($context['term_id'])) {
            $term = get_term((int) $context['term_id']);
            if ($term instanceof \WP_Term) {
                $context_object = $term;
                $context_type = 'term';
            }
        } elseif (($context['type'] ?? '') === 'archive') {
            $context_type = 'archive';
        }

        try {
            return $parser->parse($text, $context_object, $context_type);
        } catch (\Throwable $e) {
            return $text;
        }
    }

    /**
     * Normalize text before meta tag output to avoid escaped-entity artifacts.
     *
     * @param string $value Meta text.
     * @return string
     */
    private function normalize_meta_text(string $value): string {
        if ($value === '') {
            return '';
        }

        // Stored values can contain slashes and encoded entities (e.g. \&quot;).
        $normalized = stripslashes($value);
        $normalized = preg_replace('/\\\\+(?=&(?:#\d+|#x[0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]+);)/', '', $normalized);
        $normalized = str_replace(['\\"', "\\'"], ['"', "'"], $normalized);
        $normalized = html_entity_decode($normalized, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $normalized = wp_strip_all_tags($normalized);
        $normalized = preg_replace('/\s+/', ' ', trim($normalized));

        return is_string($normalized) ? $normalized : '';
    }

    /**
     * Output Open Graph tags
     *
     * @param array $context Current context.
     * @return void
     */
    private function output_open_graph_tags(array $context): void {
        // Basic Open Graph tags
        echo '<meta property="og:locale" content="' . esc_attr(get_locale()) . '" />' . "\n";
        echo '<meta property="og:type" content="' . esc_attr($this->get_og_type($context)) . '" />' . "\n";
        echo '<meta property="og:site_name" content="' . esc_attr(get_bloginfo('name')) . '" />' . "\n";
        
        // Title
        $og_title = $this->get_og_title($context);
        if (!empty($og_title)) {
            echo '<meta property="og:title" content="' . esc_attr($og_title) . '" />' . "\n";
        }
        
        // Description
        $og_description = $this->get_og_description($context);
        if (!empty($og_description)) {
            echo '<meta property="og:description" content="' . esc_attr($this->normalize_meta_text($og_description)) . '" />' . "\n";
        }
        
        // URL
        $og_url = $this->get_og_url($context);
        if (!empty($og_url)) {
            echo '<meta property="og:url" content="' . esc_url($og_url) . '" />' . "\n";
        }
        
        // Image
        $og_image = $this->get_og_image($context);
        if (!empty($og_image)) {
            echo '<meta property="og:image" content="' . esc_url($og_image['url']) . '" />' . "\n";
            if (!empty($og_image['width'])) {
                echo '<meta property="og:image:width" content="' . esc_attr($og_image['width']) . '" />' . "\n";
            }
            if (!empty($og_image['height'])) {
                echo '<meta property="og:image:height" content="' . esc_attr($og_image['height']) . '" />' . "\n";
            }
            if (!empty($og_image['type'])) {
                echo '<meta property="og:image:type" content="' . esc_attr($og_image['type']) . '" />' . "\n";
            }
        }
        
        // Article specific tags
        if ($context['type'] === 'singular' && $context['post_id']) {
            $post = get_post($context['post_id']);
            if ($post && in_array($post->post_type, ['post'], true)) {
                echo '<meta property="article:published_time" content="' . esc_attr(get_the_date('c', $post)) . '" />' . "\n";
                echo '<meta property="article:modified_time" content="' . esc_attr(get_the_modified_date('c', $post)) . '" />' . "\n";
                
                // Author
                $author_id = $post->post_author;
                if ($author_id) {
                    $author_url = get_author_posts_url($author_id);
                    echo '<meta property="article:author" content="' . esc_url($author_url) . '" />' . "\n";
                }
            }
        }
    }
    
    /**
     * Output Twitter Card tags
     *
     * @param array $context Current context.
     * @return void
     */
    private function output_twitter_card_tags(array $context): void {
        // Card type
        $card_type = $this->get_twitter_card_type($context);
        echo '<meta name="twitter:card" content="' . esc_attr($card_type) . '" />' . "\n";
        
        // Title
        $twitter_title = $this->get_twitter_title($context);
        if (!empty($twitter_title)) {
            echo '<meta name="twitter:title" content="' . esc_attr($twitter_title) . '" />' . "\n";
        }
        
        // Description
        $twitter_description = $this->get_twitter_description($context);
        if (!empty($twitter_description)) {
            echo '<meta name="twitter:description" content="' . esc_attr($this->normalize_meta_text($twitter_description)) . '" />' . "\n";
        }
        
        // Image
        $twitter_image = $this->get_twitter_image($context);
        if (!empty($twitter_image)) {
            echo '<meta name="twitter:image" content="' . esc_url($twitter_image) . '" />' . "\n";
        }
        
        // Site username (if configured in settings)
        if ($this->titles_meta) {
            $twitter_username = $this->get_titles_meta_setting('twitter_username');
            if (!empty($twitter_username)) {
                // Ensure it starts with @
                if (strpos($twitter_username, '@') !== 0) {
                    $twitter_username = '@' . $twitter_username;
                }
                echo '<meta name="twitter:site" content="' . esc_attr($twitter_username) . '" />' . "\n";
            }
        }
    }
    
    /**
     * Get Open Graph type
     *
     * @param array $context Current context.
     * @return string OG type.
     */
    private function get_og_type(array $context): string {
        if ($context['type'] === 'singular' && $context['post_id']) {
            $post = get_post($context['post_id']);
            if ($post) {
                if ($post->post_type === 'post') {
                    return 'article';
                } elseif ($post->post_type === 'page') {
                    return 'website';
                } elseif ($post->post_type === 'product') {
                    return 'product';
                }
            }
        }
        
        return 'website';
    }
    
    /**
     * Get Open Graph title
     *
     * @param array $context Current context.
     * @return string OG title.
     */
    private function get_og_title(array $context): string {
        if ($context['post_id']) {
            $og_title = get_post_meta($context['post_id'], '_prorank_og_title', true);
            if (empty($og_title)) {
                $og_title = get_post_meta($context['post_id'], '_prorank_seo_og_title', true);
            }
            if (!empty($og_title)) {
                return $this->process_dynamic_variables($og_title, $context);
            }
            
            // Fallback to SEO title
            $seo_title = get_post_meta($context['post_id'], '_prorank_seo_title', true);
            if (!empty($seo_title)) {
                return $this->process_dynamic_variables($seo_title, $context);
            }
            
            // Fallback to post title
            $post = get_post($context['post_id']);
            if ($post) {
                return $post->post_title;
            }
        }
        
        // Handle other contexts
        if ($this->titles_meta) {
            $seo_title = $this->get_seo_title($context);
            if (!empty($seo_title)) {
                $seo_title = trim(html_entity_decode(stripslashes($seo_title), ENT_QUOTES | ENT_HTML5, 'UTF-8'));

                // Protect social title on homepage from malformed leading-separator templates.
                if (in_array(($context['type'] ?? ''), ['home', 'front_page'], true) && preg_match('/^[\-\|\:\x{2013}\x{2014}\x{2022}]/u', ltrim($seo_title)) === 1) {
                    return 'Home - ' . get_bloginfo('name');
                }

                if (in_array(($context['type'] ?? ''), ['home', 'front_page'], true) && $seo_title === 'Home') {
                    return 'Home - ' . get_bloginfo('name');
                }

                return $seo_title;
            }
        }

        if (in_array(($context['type'] ?? ''), ['home', 'front_page'], true)) {
            return 'Home - ' . get_bloginfo('name');
        }
        
        return get_bloginfo('name');
    }
    
    /**
     * Get Open Graph description
     *
     * @param array $context Current context.
     * @return string OG description.
     */
    private function get_og_description(array $context): string {
        if ($context['post_id']) {
            $og_description = get_post_meta($context['post_id'], '_prorank_og_description', true);
            if (!empty($og_description)) {
                return $this->process_dynamic_variables($og_description, $context);
            }
            
            // Fallback to meta description
            $meta_description = get_post_meta($context['post_id'], '_prorank_seo_description', true);
            if (!empty($meta_description)) {
                return $this->process_dynamic_variables($meta_description, $context);
            }
            
            // Fallback to excerpt
            $post = get_post($context['post_id']);
            if ($post && !empty($post->post_excerpt)) {
                return wp_strip_all_tags($post->post_excerpt);
            }
        }
        
        // Handle other contexts
        $meta_description = $this->get_meta_description($context);
        if (!empty($meta_description)) {
            return $this->maybe_enhance_author_share_description($meta_description, $context);
        }
        
        return $this->maybe_enhance_author_share_description(get_bloginfo('description'), $context);
    }

    /**
     * Enrich author archive share text when enhanced sharing is enabled.
     */
    private function maybe_enhance_author_share_description(string $description, array $context): string {
        if (($context['type'] ?? '') !== 'author' || !$this->titles_meta) {
            return $description;
        }

        if (!$this->get_titles_meta_setting('author_slack_enhanced', false)) {
            return $description;
        }

        $author = get_queried_object();
        if (!$author instanceof \WP_User) {
            return $description;
        }

        $post_count = (int) count_user_posts($author->ID, 'post', true);
        $suffix = $post_count > 0
            ? sprintf(
                _n('%1$s has published %2$d post.', '%1$s has published %2$d posts.', $post_count, 'prorank-seo'),
                $author->display_name,
                $post_count
            )
            : sprintf(__('Author archive for %s.', 'prorank-seo'), $author->display_name);

        if ($description === '') {
            return $suffix;
        }

        if (strpos($description, $suffix) !== false) {
            return $description;
        }

        return trim($description . ' ' . $suffix);
    }
    
    /**
     * Get Open Graph URL
     *
     * @param array $context Current context.
     * @return string OG URL.
     */
    private function get_og_url(array $context): string {
        // Use canonical URL if available
        return $this->get_canonical_url($context);
    }
    
    /**
     * Get Open Graph image
     *
     * Fallback chain (2025 best practice):
     * 1. Custom OG Image (post meta)
     * 2. Featured Image
     * 3. Default OG Image (from Titles & Meta settings)
     * 4. Site Logo (from Site Basics settings)
     * 5. Organization Logo (from Site Basics settings)
     *
     * @param array $context Current context.
     * @return array Image data with url, width, height, type.
     */
    private function get_og_image(array $context): array {
        $image_data = [];

        // Step 1 & 2: Check for custom OG image or featured image (any context with a post_id)
        if ($context['post_id']) {
            // Check for custom OG image by attachment ID
            $og_image_id = get_post_meta($context['post_id'], '_prorank_og_image_id', true);

            if ($og_image_id) {
                $image_data = $this->get_image_data_from_attachment($og_image_id);
            }

            // Check for custom OG image by URL (used by importers and direct meta)
            if (empty($image_data)) {
                $og_image_url = get_post_meta($context['post_id'], '_prorank_seo_og_image', true);
                if (!empty($og_image_url)) {
                    $image_data = ['url' => $og_image_url];
                }
            }

            // Fallback to featured image
            if (empty($image_data)) {
                $thumb_id = get_post_thumbnail_id($context['post_id']);
                if ($thumb_id) {
                    $image_data = $this->get_image_data_from_attachment($thumb_id);
                }
            }

        }

        // Step 3: Default OG image from Titles & Meta settings
        if (empty($image_data) && $this->titles_meta) {
            $default_image_id = $this->get_titles_meta_setting('og_default_image');
            if ($default_image_id) {
                $image_data = $this->get_image_data_from_attachment($default_image_id);
            } elseif ($default_image_url = $this->get_titles_meta_setting('og_default_image_url')) {
                // Use direct URL if provided
                $image_data['url'] = $default_image_url;
            }
        }

        // Step 4 & 5: Fallback to Site Basics (default site image, then logos)
        if (empty($image_data)) {
            $site_basics = get_option('prorank_seo_site_basics', []);

            // Try default site image first
            if (!empty($site_basics['site_image'])) {
                $image_data['url'] = $site_basics['site_image'];

                if (is_numeric($site_basics['site_image'])) {
                    $image_data = $this->get_image_data_from_attachment((int) $site_basics['site_image']);
                } elseif (!empty($site_basics['site_image_id'])) {
                    $image_data = $this->get_image_data_from_attachment((int) $site_basics['site_image_id']);
                }
            } elseif (!empty($site_basics['site_image_id'])) {
                $image_data = $this->get_image_data_from_attachment((int) $site_basics['site_image_id']);
            }

            // Try site logo next
            if (empty($image_data) && !empty($site_basics['site_logo'])) {
                $image_data['url'] = $site_basics['site_logo'];

                // Try to get dimensions if it's an attachment ID
                if (is_numeric($site_basics['site_logo'])) {
                    $image_data = $this->get_image_data_from_attachment((int) $site_basics['site_logo']);
                } elseif (!empty($site_basics['site_logo_id'])) {
                    $image_data = $this->get_image_data_from_attachment((int) $site_basics['site_logo_id']);
                }
            } elseif (empty($image_data) && !empty($site_basics['site_logo_id'])) {
                $image_data = $this->get_image_data_from_attachment((int) $site_basics['site_logo_id']);
            }
            // Then try organization logo
            elseif (empty($image_data) && !empty($site_basics['org_logo'])) {
                $image_data['url'] = $site_basics['org_logo'];

                // Try to get dimensions if it's an attachment ID
                if (is_numeric($site_basics['org_logo'])) {
                    $image_data = $this->get_image_data_from_attachment((int) $site_basics['org_logo']);
                } elseif (!empty($site_basics['org_logo_id'])) {
                    $image_data = $this->get_image_data_from_attachment((int) $site_basics['org_logo_id']);
                }
            } elseif (empty($image_data) && !empty($site_basics['org_logo_id'])) {
                $image_data = $this->get_image_data_from_attachment((int) $site_basics['org_logo_id']);
            }
        }

        if (empty($image_data)) {
            $custom_logo_id = (int) get_theme_mod('custom_logo');
            if ($custom_logo_id > 0) {
                $image_data = $this->get_image_data_from_attachment($custom_logo_id);
            }
        }

        if (empty($image_data)) {
            $site_icon_id = (int) get_option('site_icon');
            if ($site_icon_id > 0) {
                $image_data = $this->get_image_data_from_attachment($site_icon_id);
            }
        }

        return $image_data;
    }

    /**
     * Get image data from attachment ID.
     *
     * Saved SEO settings can store attachment IDs as numeric strings, so this
     * helper normalizes them before calling WordPress media APIs.
     *
     * @param mixed $attachment_id Attachment ID or numeric string ID.
     * @return array Image data with url, width, height, type.
     */
    private function get_image_data_from_attachment($attachment_id): array {
        $image_data = [];

        if (is_string($attachment_id)) {
            $attachment_id = trim($attachment_id);
        }

        if (!is_numeric($attachment_id)) {
            return $image_data;
        }

        $attachment_id = (int) $attachment_id;
        if ($attachment_id <= 0) {
            return $image_data;
        }

        $image_src = wp_get_attachment_image_src($attachment_id, 'large');
        if ($image_src) {
            $image_data['url'] = $image_src[0];
            $image_data['width'] = $image_src[1];
            $image_data['height'] = $image_src[2];

            // Get mime type
            $mime_type = get_post_mime_type($attachment_id);
            if ($mime_type) {
                $image_data['type'] = $mime_type;
            }
        }

        return $image_data;
    }
    
    /**
     * Get Twitter card type
     *
     * @param array $context Current context.
     * @return string Card type.
     */
    private function get_twitter_card_type(array $context): string {
        if ($context['type'] === 'singular' && $context['post_id']) {
            $card_type = get_post_meta($context['post_id'], '_prorank_twitter_card_type', true);
            if (!empty($card_type)) {
                return $card_type;
            }
        }
        
        // Default from settings or summary
        if ($this->titles_meta) {
            $default_type = $this->get_titles_meta_setting('twitter_card_type');
            if (!empty($default_type)) {
                return $default_type;
            }
        }
        
        return 'summary';
    }
    
    /**
     * Get Twitter title
     *
     * @param array $context Current context.
     * @return string Twitter title.
     */
    private function get_twitter_title(array $context): string {
        if ($context['type'] === 'singular' && $context['post_id']) {
            $twitter_title = get_post_meta($context['post_id'], '_prorank_twitter_title', true);
            if (!empty($twitter_title)) {
                return $this->process_dynamic_variables($twitter_title, $context);
            }
        }
        
        // Fallback to OG title
        return $this->get_og_title($context);
    }
    
    /**
     * Get Twitter description
     *
     * @param array $context Current context.
     * @return string Twitter description.
     */
    private function get_twitter_description(array $context): string {
        if ($context['type'] === 'singular' && $context['post_id']) {
            $twitter_description = get_post_meta($context['post_id'], '_prorank_twitter_description', true);
            if (!empty($twitter_description)) {
                return $this->process_dynamic_variables($twitter_description, $context);
            }
        }
        
        // Fallback to OG description
        return $this->get_og_description($context);
    }
    
    /**
     * Get Twitter image
     *
     * @param array $context Current context.
     * @return string Twitter image URL.
     */
    private function get_twitter_image(array $context): string {
        if ($context['type'] === 'singular' && $context['post_id']) {
            // Check for custom Twitter image
            $twitter_image_id = get_post_meta($context['post_id'], '_prorank_twitter_image_id', true);
            
            if ($twitter_image_id) {
                $image_url = wp_get_attachment_url($twitter_image_id);
                if ($image_url) {
                    return $image_url;
                }
            }
        }
        
        // Fallback to OG image
        $og_image = $this->get_og_image($context);
        return !empty($og_image['url']) ? $og_image['url'] : '';
    }

    /**
     * Output Article Schema JSON-LD
     *
     * Outputs complete Article schema with headline, image, wordCount, articleSection
     * for better rich snippets and AI Overview citations (2025 standards)
     *
     * @param array $context Current context.
     * @return void
     */
    private function output_article_schema(array $context): void {
        // Only output for singular posts
        if ($context['type'] !== 'singular' || !$context['post_id']) {
            return;
        }

        $post = get_post($context['post_id']);
        if (!$post || $post->post_type !== 'post') {
            return;
        }

        $schema_type = $this->get_article_schema_type($post);

        // Build Article schema
        $schema = [
            '@context' => 'https://schema.org',
            '@type' => $schema_type,
        ];

        // Headline (required) - use SEO title or post title
        $seo_title = get_post_meta($context['post_id'], '_prorank_seo_title', true);
        $schema['headline'] = !empty($seo_title)
            ? $this->process_dynamic_variables($seo_title, $context)
            : $post->post_title;

        // Name (same as headline for consistency)
        $schema['name'] = $schema['headline'];

        // Description
        $description = get_post_meta($context['post_id'], '_prorank_seo_description', true);
        if (empty($description) && !empty($post->post_excerpt)) {
            $description = wp_strip_all_tags($post->post_excerpt);
        }
        if (!empty($description)) {
            $schema['description'] = $description;
        }

        // Image (featured image or OG image)
        $og_image = $this->get_og_image($context);
        if (!empty($og_image['url'])) {
            $schema['image'] = [
                '@type' => 'ImageObject',
                'url' => $og_image['url'],
            ];
            if (!empty($og_image['width'])) {
                $schema['image']['width'] = $og_image['width'];
            }
            if (!empty($og_image['height'])) {
                $schema['image']['height'] = $og_image['height'];
            }
        }

        // Word count (2025 standard for content depth signals)
        $content = wp_strip_all_tags($post->post_content);
        $word_count = str_word_count($content);
        if ($word_count > 0) {
            $schema['wordCount'] = $word_count;
        }

        // Article section (primary category)
        $categories = get_the_category($context['post_id']);
        if (!empty($categories)) {
            // Get primary category (first one, or Yoast primary if available)
            $primary_category = $categories[0];

            // Check for custom primary category setting
            $primary_cat_id = get_post_meta($context['post_id'], '_prorank_primary_category', true);
            if ($primary_cat_id) {
                foreach ($categories as $cat) {
                    if ($cat->term_id == $primary_cat_id) {
                        $primary_category = $cat;
                        break;
                    }
                }
            }

            $schema['articleSection'] = $primary_category->name;
        }

        // Date published and modified
        $schema['datePublished'] = get_the_date('c', $post);
        $schema['dateModified'] = get_the_modified_date('c', $post);

        // Main entity of page
        $schema['mainEntityOfPage'] = [
            '@type' => 'WebPage',
            '@id' => get_permalink($context['post_id']),
        ];

        // URL
        $schema['url'] = get_permalink($context['post_id']);

        // Author with E-E-A-T properties (2025 standard)
        $author_id = $post->post_author;
        if ($author_id) {
            $author = get_userdata($author_id);
            if ($author) {
                $author_name = $this->resolve_schema_author_name((int) $author_id, $author);
                $author_schema = [
                    '@type' => 'Person',
                    'name' => $author_name,
                    'url' => get_author_posts_url($author_id),
                ];

                // E-E-A-T fields from user meta
                $job_title = get_user_meta($author_id, 'prorank_job_title', true);
                if (!empty($job_title)) {
                    $author_schema['jobTitle'] = $job_title;
                }

                $works_for = get_user_meta($author_id, 'prorank_works_for', true);
                if (!empty($works_for)) {
                    $author_schema['worksFor'] = [
                        '@type' => 'Organization',
                        'name' => $works_for,
                    ];
                }

                $knows_about = get_user_meta($author_id, 'prorank_knows_about', true);
                if (!empty($knows_about)) {
                    // Can be comma-separated list
                    $topics = array_map('trim', explode(',', $knows_about));
                    $author_schema['knowsAbout'] = count($topics) === 1 ? $topics[0] : $topics;
                }

                $alumni_of = get_user_meta($author_id, 'prorank_alumni_of', true);
                if (!empty($alumni_of)) {
                    $author_schema['alumniOf'] = [
                        '@type' => 'EducationalOrganization',
                        'name' => $alumni_of,
                    ];
                }

                // Author description/bio
                $author_bio = get_the_author_meta('description', $author_id);
                if (!empty($author_bio)) {
                    $author_schema['description'] = wp_strip_all_tags($author_bio);
                }

                // Author image
                $avatar_url = get_avatar_url($author_id, ['size' => 150]);
                if ($avatar_url) {
                    $author_schema['image'] = $avatar_url;
                }

                // Author social profiles for sameAs
                $same_as = [];
                $social_fields = [
                    'twitter' => 'https://twitter.com/',
                    'facebook' => '',
                    'linkedin' => '',
                    'instagram' => 'https://instagram.com/',
                ];

                foreach ($social_fields as $field => $prefix) {
                    $value = get_user_meta($author_id, $field, true);
                    if (!empty($value)) {
                        if (!empty($prefix) && strpos($value, 'http') !== 0) {
                            $value = $prefix . ltrim($value, '@');
                        }
                        $same_as[] = $value;
                    }
                }

                if (!empty($same_as)) {
                    $author_schema['sameAs'] = $same_as;
                }

                $schema['author'] = $author_schema;
            }
        }

        // Publisher (organization from site basics)
        $site_basics = get_option('prorank_seo_site_basics', []);
        $publisher_name = !empty($site_basics['org_name'])
            ? $site_basics['org_name']
            : get_bloginfo('name');

        $publisher = [
            '@type' => 'Organization',
            'name' => $publisher_name,
        ];

        // Publisher logo
        $logo_url = !empty($site_basics['org_logo'])
            ? $site_basics['org_logo']
            : (!empty($site_basics['site_logo']) ? $site_basics['site_logo'] : '');

        if (!empty($logo_url)) {
            $publisher['logo'] = [
                '@type' => 'ImageObject',
                'url' => $logo_url,
            ];
        }

        $schema['publisher'] = $publisher;

        // isAccessibleForFree (typically true for blog posts)
        $schema['isAccessibleForFree'] = true;

        // Output JSON-LD
        echo '<script type="application/ld+json">' . "\n";
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-LD script body.
        echo wp_json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        echo "\n" . '</script>' . "\n";
    }

    /**
     * Resolve a non-empty author name for schema output.
     *
     * @param int             $author_id Author ID.
     * @param \WP_User|object $author    Author object.
     * @return string
     */
    private function resolve_schema_author_name(int $author_id, $author): string {
        $candidates = [
            isset($author->display_name) ? (string) $author->display_name : '',
            isset($author->nickname) ? (string) $author->nickname : '',
            isset($author->user_nicename) ? (string) $author->user_nicename : '',
            isset($author->user_login) ? (string) $author->user_login : '',
        ];

        $first_name = (string) get_user_meta($author_id, 'first_name', true);
        $last_name = (string) get_user_meta($author_id, 'last_name', true);
        $full_name = trim($first_name . ' ' . $last_name);
        if ($full_name !== '') {
            $candidates[] = $full_name;
        }

        foreach ($candidates as $candidate) {
            $clean = trim(wp_strip_all_tags((string) $candidate));
            if ($clean !== '') {
                return $clean;
            }
        }

        $site_name = trim(wp_strip_all_tags((string) get_bloginfo('name')));
        return $site_name !== '' ? $site_name : 'Editorial Team';
    }

    /**
     * Output Video/Audio Open Graph tags for media attachments
     *
     * Adds og:video and og:audio tags for multimedia content (2025 standard)
     *
     * @param array $context Current context.
     * @return void
     */
    private function output_media_og_tags(array $context): void {
        // Only for singular attachment pages
        if ($context['type'] !== 'singular' || !$context['post_id']) {
            return;
        }

        $post = get_post($context['post_id']);
        if (!$post || $post->post_type !== 'attachment') {
            return;
        }

        $mime_type = get_post_mime_type($context['post_id']);
        if (!$mime_type) {
            return;
        }

        $attachment_url = wp_get_attachment_url($context['post_id']);
        if (!$attachment_url) {
            return;
        }

        // Check if video
        if (strpos($mime_type, 'video/') === 0) {
            echo '<meta property="og:video" content="' . esc_url($attachment_url) . '" />' . "\n";
            echo '<meta property="og:video:type" content="' . esc_attr($mime_type) . '" />' . "\n";

            // Try to get video dimensions
            $metadata = wp_get_attachment_metadata($context['post_id']);
            if (!empty($metadata['width'])) {
                echo '<meta property="og:video:width" content="' . esc_attr($metadata['width']) . '" />' . "\n";
            }
            if (!empty($metadata['height'])) {
                echo '<meta property="og:video:height" content="' . esc_attr($metadata['height']) . '" />' . "\n";
            }
            if (!empty($metadata['length'])) {
                // Duration in seconds - convert to ISO 8601 duration
                $duration = 'PT' . $metadata['length'] . 'S';
                echo '<meta property="video:duration" content="' . esc_attr($duration) . '" />' . "\n";
            }

            // Change og:type to video
            // Note: This is tricky since og:type is already output. For proper implementation,
            // we'd need to intercept earlier. For now, add video-specific properties.
        }
        // Check if audio
        elseif (strpos($mime_type, 'audio/') === 0) {
            echo '<meta property="og:audio" content="' . esc_url($attachment_url) . '" />' . "\n";
            echo '<meta property="og:audio:type" content="' . esc_attr($mime_type) . '" />' . "\n";

            // Audio duration if available
            $metadata = wp_get_attachment_metadata($context['post_id']);
            if (!empty($metadata['length'])) {
                $duration = 'PT' . $metadata['length'] . 'S';
                echo '<meta property="audio:duration" content="' . esc_attr($duration) . '" />' . "\n";
            }
        }
    }

    /**
     * Output WebPage schema for pages
     *
     * @param array $context Current context.
     * @return void
     */
    private function output_webpage_schema(array $context): void {
        if ($context['type'] !== 'singular' || !$context['post_id']) {
            return;
        }

        $post = get_post($context['post_id']);
        if (!$post || $post->post_type !== 'page') {
            return;
        }

        // Skip if Article schema would be more appropriate (blog page)
        if (get_option('page_for_posts') == $context['post_id']) {
            return;
        }

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'WebPage',
            'name' => $post->post_title,
            'url' => get_permalink($context['post_id']),
        ];

        // Description
        $description = get_post_meta($context['post_id'], '_prorank_seo_description', true);
        if (empty($description) && !empty($post->post_excerpt)) {
            $description = wp_strip_all_tags($post->post_excerpt);
        }
        if (!empty($description)) {
            $schema['description'] = $description;
        }

        // Date modified
        $schema['dateModified'] = get_the_modified_date('c', $post);

        // Image
        $og_image = $this->get_og_image($context);
        if (!empty($og_image['url'])) {
            $schema['image'] = $og_image['url'];
        }

        // Breadcrumb (if we have it)
        $schema['isPartOf'] = [
            '@type' => 'WebSite',
            'name' => get_bloginfo('name'),
            'url' => home_url('/'),
        ];

        echo '<script type="application/ld+json">' . "\n";
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-LD script body.
        echo wp_json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        echo "\n" . '</script>' . "\n";
    }

    /**
     * Output Product schema for WooCommerce products
     *
     * @param array $context Current context.
     * @return void
     */
    private function output_product_schema(array $context): void {
        if ($context['type'] !== 'singular' || !$context['post_id']) {
            return;
        }

        // Check if WooCommerce is active
        if (!function_exists('wc_get_product')) {
            return;
        }

        $post = get_post($context['post_id']);
        if (!$post || $post->post_type !== 'product') {
            return;
        }

        $product = wc_get_product($context['post_id']);
        if (!$product) {
            return;
        }

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'Product',
            'name' => $product->get_name(),
            'url' => get_permalink($context['post_id']),
        ];

        // Description
        $description = $product->get_short_description();
        if (empty($description)) {
            $description = wp_trim_words(wp_strip_all_tags($product->get_description()), 30);
        }
        if (!empty($description)) {
            $schema['description'] = $description;
        }

        // SKU
        if ($product->get_sku()) {
            $schema['sku'] = $product->get_sku();
        }

        // Image
        $image_id = $product->get_image_id();
        if ($image_id) {
            $image_url = wp_get_attachment_url($image_id);
            if ($image_url) {
                $schema['image'] = $image_url;
            }
        }

        // Brand (from custom taxonomy or attribute)
        $brand = $product->get_attribute('brand');
        if (!empty($brand)) {
            $schema['brand'] = [
                '@type' => 'Brand',
                'name' => $brand,
            ];
        }

        // Offers
        $offer = [
            '@type' => 'Offer',
            'url' => get_permalink($context['post_id']),
            'priceCurrency' => get_woocommerce_currency(),
            'price' => $product->get_price(),
            'availability' => $product->is_in_stock()
                ? 'https://schema.org/InStock'
                : 'https://schema.org/OutOfStock',
        ];

        // Valid until (optional - for sale prices)
        if ($product->is_on_sale() && $product->get_date_on_sale_to()) {
            $offer['priceValidUntil'] = $product->get_date_on_sale_to()->date('Y-m-d');
        }

        $schema['offers'] = $offer;

        // Reviews/Ratings
        if ($product->get_review_count() > 0) {
            $schema['aggregateRating'] = [
                '@type' => 'AggregateRating',
                'ratingValue' => $product->get_average_rating(),
                'reviewCount' => $product->get_review_count(),
            ];
        }

        echo '<script type="application/ld+json">' . "\n";
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-LD script body.
        echo wp_json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        echo "\n" . '</script>' . "\n";
    }

    /**
     * Output PodcastEpisode schema
     *
     * @param array $context Current context.
     * @return void
     */
    private function output_podcast_schema(array $context): void {
        if ($context['type'] !== 'singular' || !$context['post_id']) {
            return;
        }

        $post = get_post($context['post_id']);
        if (!$post) {
            return;
        }

        // Check if this is a podcast episode post type
        $podcast_post_types = apply_filters('prorank_seo_podcast_post_types', ['podcast', 'episode', 'podcast_episode', 'prorank_podcast']);
        if (!in_array($post->post_type, $podcast_post_types, true)) {
            return;
        }

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'PodcastEpisode',
            'name' => $post->post_title,
            'url' => get_permalink($context['post_id']),
            'datePublished' => get_the_date('c', $post),
        ];

        // Description
        $description = get_post_meta($context['post_id'], '_prorank_seo_description', true);
        if (empty($description) && !empty($post->post_excerpt)) {
            $description = wp_strip_all_tags($post->post_excerpt);
        }
        if (!empty($description)) {
            $schema['description'] = $description;
        }

        // Image
        $og_image = $this->get_og_image($context);
        if (!empty($og_image['url'])) {
            $schema['image'] = $og_image['url'];
        }

        // Episode number (from post meta with _prorank_podcast_ prefix)
        $episode_number = get_post_meta($context['post_id'], '_prorank_podcast_episode_number', true);
        if ($episode_number) {
            $schema['episodeNumber'] = (int) $episode_number;
        }

        // Season number
        $season_number = get_post_meta($context['post_id'], '_prorank_podcast_season_number', true);
        if ($season_number) {
            $schema['partOfSeason'] = [
                '@type' => 'PodcastSeason',
                'seasonNumber' => (int) $season_number,
            ];
        }

        // Duration (from post meta, expects HH:MM:SS format)
        $duration = get_post_meta($context['post_id'], '_prorank_podcast_duration', true);
        if ($duration) {
            // Convert HH:MM:SS to ISO 8601 duration
            if (preg_match('/^(\d+):(\d+):(\d+)$/', $duration, $matches)) {
                $hours = (int) $matches[1];
                $minutes = (int) $matches[2];
                $seconds = (int) $matches[3];
                $duration = 'PT';
                if ($hours > 0) $duration .= $hours . 'H';
                if ($minutes > 0) $duration .= $minutes . 'M';
                if ($seconds > 0) $duration .= $seconds . 'S';
            } elseif (is_numeric($duration)) {
                $duration = 'PT' . $duration . 'S';
            }
            $schema['timeRequired'] = $duration;
        }

        // Audio file
        $audio_url = get_post_meta($context['post_id'], '_prorank_podcast_audio_url', true);
        if (!empty($audio_url)) {
            $schema['associatedMedia'] = [
                '@type' => 'MediaObject',
                'contentUrl' => $audio_url,
                'encodingFormat' => 'audio/mpeg',
            ];
        }

        // Video file (Podcast 2.0)
        $video_url = get_post_meta($context['post_id'], '_prorank_podcast_video_url', true);
        if (!empty($video_url)) {
            if (!isset($schema['associatedMedia'])) {
                $schema['associatedMedia'] = [];
            }
            $schema['video'] = [
                '@type' => 'VideoObject',
                'contentUrl' => $video_url,
            ];
        }

        // Transcript URL for accessibility
        $transcript_url = get_post_meta($context['post_id'], '_prorank_podcast_transcript_url', true);
        if (!empty($transcript_url)) {
            $schema['transcript'] = $transcript_url;
        }

        // Part of (Podcast Series) - get from unified option
        $podcast_settings = get_option('prorank_seo_podcast', []);
        $podcast_name = $podcast_settings['podcast_title'] ?? get_bloginfo('name') . ' Podcast';
        $podcast_author = $podcast_settings['podcast_author'] ?? get_bloginfo('name');
        $podcast_image = $podcast_settings['podcast_image'] ?? '';

        $series_schema = [
            '@type' => 'PodcastSeries',
            'name' => $podcast_name,
            'url' => get_feed_link('podcast'),
        ];

        // Add podcast image
        if (!empty($podcast_image)) {
            $series_schema['image'] = $podcast_image;
        }

        // Add author/creator to series
        if (!empty($podcast_author)) {
            $series_schema['author'] = [
                '@type' => 'Person',
                'name' => $podcast_author,
            ];
        }

        $schema['partOfSeries'] = $series_schema;

        // Add episode creator (author of the post)
        $author_id = $post->post_author;
        if ($author_id) {
            $author_name = get_the_author_meta('display_name', $author_id);
            if ($author_name) {
                $schema['creator'] = [
                    '@type' => 'Person',
                    'name' => $author_name,
                ];
            }
        }

        // Add actors/hosts from episode meta (podcast:person support)
        $episode_hosts = get_post_meta($context['post_id'], '_prorank_podcast_hosts', true);
        if (!empty($episode_hosts) && is_array($episode_hosts)) {
            $actors = [];
            foreach ($episode_hosts as $host) {
                if (!empty($host['name'])) {
                    $actor = [
                        '@type' => 'Person',
                        'name' => $host['name'],
                    ];
                    if (!empty($host['url'])) {
                        $actor['url'] = $host['url'];
                    }
                    if (!empty($host['img'])) {
                        $actor['image'] = $host['img'];
                    }
                    $actors[] = $actor;
                }
            }
            if (!empty($actors)) {
                $schema['actor'] = $actors;
            }
        }

        // Add guests if present
        $episode_guests = get_post_meta($context['post_id'], '_prorank_podcast_guests', true);
        if (!empty($episode_guests) && is_array($episode_guests)) {
            $contributors = [];
            foreach ($episode_guests as $guest) {
                if (!empty($guest['name'])) {
                    $contributor = [
                        '@type' => 'Person',
                        'name' => $guest['name'],
                    ];
                    if (!empty($guest['url'])) {
                        $contributor['url'] = $guest['url'];
                    }
                    if (!empty($guest['img'])) {
                        $contributor['image'] = $guest['img'];
                    }
                    $contributors[] = $contributor;
                }
            }
            if (!empty($contributors)) {
                $schema['contributor'] = $contributors;
            }
        }

        echo '<script type="application/ld+json">' . "\n";
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-LD script body.
        echo wp_json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        echo "\n" . '</script>' . "\n";
    }

    /**
     * Output PodcastSeries schema for podcast archive/feed pages
     *
     * @param array $context Current context.
     * @return void
     */
    private function output_podcast_series_schema(array $context): void {
        // Only output on podcast archive or specific podcast series pages
        if ($context['type'] !== 'archive') {
            return;
        }

        // Check if this is a podcast archive
        $queried_object = get_queried_object();
        if (!$queried_object) {
            return;
        }

        // Check if viewing prorank_podcast archive or podcast_series taxonomy
        $is_podcast_archive = is_post_type_archive('prorank_podcast');
        $is_series_archive = is_tax('podcast_series');

        if (!$is_podcast_archive && !$is_series_archive) {
            return;
        }

        $podcast_settings = get_option('prorank_seo_podcast', []);

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'PodcastSeries',
            'name' => $podcast_settings['podcast_title'] ?? get_bloginfo('name') . ' Podcast',
            'description' => $podcast_settings['podcast_description'] ?? $podcast_settings['podcast_subtitle'] ?? get_bloginfo('description'),
            'url' => get_feed_link('podcast'),
            'webFeed' => get_feed_link('podcast'),
        ];

        // Add podcast image
        if (!empty($podcast_settings['podcast_image'])) {
            $schema['image'] = $podcast_settings['podcast_image'];
        }

        // Add author
        if (!empty($podcast_settings['podcast_author'])) {
            $schema['author'] = [
                '@type' => 'Person',
                'name' => $podcast_settings['podcast_author'],
            ];
        }

        // Add publisher (organization)
        $schema['publisher'] = [
            '@type' => 'Organization',
            'name' => get_bloginfo('name'),
            'url' => home_url('/'),
        ];

        // Add category/genre
        if (!empty($podcast_settings['podcast_category'])) {
            $schema['genre'] = $podcast_settings['podcast_category'];
        }

        // Add language
        $language = $podcast_settings['podcast_language'] ?? str_replace('_', '-', get_locale());
        $schema['inLanguage'] = $language;

        // Get episode count
        $episode_count = wp_count_posts('prorank_podcast');
        if ($episode_count && isset($episode_count->publish)) {
            $schema['numberOfEpisodes'] = (int) $episode_count->publish;
        }

        // Add episodes as contained items
        $recent_episodes = get_posts([
            'post_type' => 'prorank_podcast',
            'posts_per_page' => 10,
            'post_status' => 'publish',
            'orderby' => 'date',
            'order' => 'DESC',
        ]);

        if (!empty($recent_episodes)) {
            $episodes = [];
            foreach ($recent_episodes as $episode) {
                $episode_data = [
                    '@type' => 'PodcastEpisode',
                    'name' => $episode->post_title,
                    'url' => get_permalink($episode->ID),
                    'datePublished' => get_the_date('c', $episode),
                ];

                $episode_number = get_post_meta($episode->ID, '_prorank_podcast_episode_number', true);
                if ($episode_number) {
                    $episode_data['episodeNumber'] = (int) $episode_number;
                }

                $episodes[] = $episode_data;
            }
            $schema['episode'] = $episodes;
        }

        echo '<script type="application/ld+json">' . "\n";
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-LD script body.
        echo wp_json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        echo "\n" . '</script>' . "\n";
    }

    /**
     * Output LocalBusiness schema for location posts
     *
     * @param array $context Current context.
     * @return void
     */
    private function output_local_business_schema(array $context): void {
        if ($context['type'] !== 'singular' || !$context['post_id']) {
            return;
        }

        $post = get_post($context['post_id']);
        if (!$post) {
            return;
        }

        // Check if this is a location post type
        $location_post_types = apply_filters('prorank_seo_location_post_types', ['prorank_location', 'location', 'locations', 'store', 'branch']);
        if (!in_array($post->post_type, $location_post_types, true)) {
            return;
        }

        // Get business type (default to LocalBusiness)
        $business_type = get_post_meta($context['post_id'], '_prorank_business_type', true);
        if (empty($business_type)) {
            $business_type = 'LocalBusiness';
        }

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => $business_type,
            'name' => $post->post_title,
            'url' => get_permalink($context['post_id']),
        ];

        // Description
        $description = get_post_meta($context['post_id'], '_prorank_seo_description', true);
        if (empty($description) && !empty($post->post_excerpt)) {
            $description = wp_strip_all_tags($post->post_excerpt);
        }
        if (!empty($description)) {
            $schema['description'] = $description;
        }

        // Image
        $og_image = $this->get_og_image($context);
        if (!empty($og_image['url'])) {
            $schema['image'] = $og_image['url'];
        }

        // Address
        $address = [];
        $street = get_post_meta($context['post_id'], '_prorank_street_address', true);
        $city = get_post_meta($context['post_id'], '_prorank_city', true);
        $state = get_post_meta($context['post_id'], '_prorank_state', true);
        $postal = get_post_meta($context['post_id'], '_prorank_postal_code', true);
        $country = get_post_meta($context['post_id'], '_prorank_country', true);

        if ($street || $city || $state || $postal || $country) {
            $address['@type'] = 'PostalAddress';
            if ($street) $address['streetAddress'] = $street;
            if ($city) $address['addressLocality'] = $city;
            if ($state) $address['addressRegion'] = $state;
            if ($postal) $address['postalCode'] = $postal;
            if ($country) $address['addressCountry'] = $country;
            $schema['address'] = $address;
        }

        // Phone
        $phone = get_post_meta($context['post_id'], '_prorank_phone', true);
        if (!empty($phone)) {
            $schema['telephone'] = $phone;
        }

        // Email
        $email = get_post_meta($context['post_id'], '_prorank_email', true);
        if (!empty($email)) {
            $schema['email'] = $email;
        }

        // Geo coordinates
        $lat = get_post_meta($context['post_id'], '_prorank_latitude', true);
        $lng = get_post_meta($context['post_id'], '_prorank_longitude', true);
        if ($lat && $lng) {
            $schema['geo'] = [
                '@type' => 'GeoCoordinates',
                'latitude' => $lat,
                'longitude' => $lng,
            ];
        }

        // Opening hours
        $opening_hours = get_post_meta($context['post_id'], '_prorank_opening_hours', true);
        if (!empty($opening_hours) && is_array($opening_hours)) {
            $schema['openingHoursSpecification'] = $opening_hours;
        }

        // Price range
        $price_range = get_post_meta($context['post_id'], '_prorank_price_range', true);
        if (!empty($price_range)) {
            $schema['priceRange'] = $price_range;
        }

        echo '<script type="application/ld+json">' . "\n";
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-LD script body.
        echo wp_json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        echo "\n" . '</script>' . "\n";
    }

    /**
     * Output all contextual schemas
     *
     * This method determines which schema to output based on the current context.
     * Follows 2025 best practices for schema type selection.
     *
     * @param array $context Current context.
     * @return void
     */
    private function output_contextual_schema(array $context): void {
        // Skip if not singular
        if ($context['type'] !== 'singular' || !$context['post_id']) {
            return;
        }

        $post = get_post($context['post_id']);
        if (!$post) {
            return;
        }

        // Check post type and output appropriate schema
        switch ($post->post_type) {
            case 'post':
                // Article schema already handled in output_article_schema()
                break;

            case 'page':
                $this->output_webpage_schema($context);
                break;

            case 'product':
                // Product schema is handled by ProductSchemaModule when active
                if (!class_exists('\ProRank\SEO\Modules\eCommerce\ProductSchemaModule')) {
                    $this->output_product_schema($context);
                }
                break;

            case 'podcast':
            case 'episode':
            case 'podcast_episode':
            case 'prorank_podcast':
                $this->output_podcast_schema($context);
                break;

            case 'prorank_location':
            case 'location':
            case 'locations':
            case 'store':
            case 'branch':
                $this->output_local_business_schema($context);
                break;

            default:
                // Check for custom schema type from post meta
                $custom_schema_type = get_post_meta($context['post_id'], '_prorank_schema_type', true);
                if ($custom_schema_type) {
                    switch ($custom_schema_type) {
                        case 'Article':
                        case 'BlogPosting':
                        case 'NewsArticle':
                            // Use article schema
                            break;
                        case 'WebPage':
                            $this->output_webpage_schema($context);
                            break;
                        case 'Product':
                            $this->output_product_schema($context);
                            break;
                        case 'PodcastEpisode':
                            $this->output_podcast_schema($context);
                            break;
                        case 'LocalBusiness':
                            $this->output_local_business_schema($context);
                            break;
                    }
                }
                break;
        }
    }
}
