<?php
/**
 * Rank Forge — Robots Meta (Noindex/Nofollow)
 *
 * Per-post, per-taxonomy, per-archive search visibility control.
 * Outputs <meta name="robots"> in wp_head.
 *
 * FREE feature — available on all plans.
 *
 * @package RankForge
 * @since 1.2.0
 */

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

class RANKFORGE_Robots_Meta {
    private static $instance = null;
    public static function instance() { if (!self::$instance) self::$instance = new self(); return self::$instance; }

    private function __construct() {
        // (re-fix): integrate with WordPress core wp_robots filter instead of
        // emitting a separate <meta name="robots"> tag. Outputting our own tag in
        // wp_head produced TWO robots tags on the page (core's + ours), and the
        // tester only saw the core one on indexable pages. With the filter WP
        // merges our directives into the single tag it already prints.
        add_filter('wp_robots', [$this, 'filter_wp_robots']);
        // NB: term archive og:/description tags are emitted by
        // RANKFORGE_Meta_Generator::output_meta_tags now. We only keep the
        // <title> override here because Meta_Generator's filter_document_title
        // only runs on singular views.
        add_filter('pre_get_document_title', [$this, 'filter_term_document_title'], 20);

        // Taxonomy term edit screens
        $taxonomies = get_taxonomies(['public' => true], 'names');
        foreach ($taxonomies as $tax) {
            add_action("{$tax}_edit_form_fields", [$this, 'render_term_fields'], 10, 2);
            add_action("edited_{$tax}", [$this, 'save_term_fields'], 10, 2);
        }
    }

    /**
 * (re-fix): mutate core's wp_robots array so the single <meta
 * name="robots"> tag WordPress already emits reflects our settings.
 * Result on an indexable page: content="index, follow, max-image-preview:large";
 * on a hidden page: content="noindex, nofollow".
 */
    public function filter_wp_robots($robots) {
        if (!is_array($robots)) $robots = [];

        $site_discouraged = !get_option('blog_public');
        $noindex = !empty($robots['noindex']) || !empty($robots['none']) || $site_discouraged;
        $nofollow = !empty($robots['nofollow']) || !empty($robots['none']) || $site_discouraged;

        // opt-in rule — paginated archive/category/search pages get
        // `noindex, follow` (crawlers still traverse the links to content,
        // but duplicate paginated result pages stay out of the index).
        // Excludes page 1 (the canonical list page) so it keeps full SEO.
        if (get_option('rankforge_noindex_paginated', false) && is_paged()) {
            $noindex = true;
            // Keep follow — we want crawlers to reach articles/products.
        }

        if (is_singular()) {
            $post_id   = get_the_ID();
            $pt_robots = get_option('rankforge_posttype_robots', []);
            $post_type = get_post_type($post_id);
            if (!empty($pt_robots[$post_type]['noindex']))  $noindex  = true;
            if (!empty($pt_robots[$post_type]['nofollow'])) $nofollow = true;
            // hiding THIS post/page/CPT from search ("Show in search
            // results" off → _rankforge_noindex) should also stop crawlers
            // following its links (noindex → nofollow) so we don't waste crawl
            // budget. Scoped to the explicit per-post toggle: the global
            // post-type / paginated / taxonomy rules keep their own
            // noindex,follow behavior, and the separate nofollow checkbox still
            // independently controls nofollow on INDEXED posts.
            if (get_post_meta($post_id, '_rankforge_noindex', true))  { $noindex = true; $nofollow = true; }
            if (get_post_meta($post_id, '_rankforge_nofollow', true)) $nofollow = true;
        } elseif (is_category() || is_tag() || is_tax()) {
            $term_id = get_queried_object_id();
            if (get_term_meta($term_id, '_rankforge_noindex', true))  $noindex  = true;
            if (get_term_meta($term_id, '_rankforge_nofollow', true)) $nofollow = true;
        } elseif (is_post_type_archive()) {
            $pt = get_queried_object()->name ?? '';
            $archive_robots = get_option('rankforge_archive_robots', []);
            if (!empty($archive_robots[$pt]['noindex']))  $noindex  = true;
            if (!empty($archive_robots[$pt]['nofollow'])) $nofollow = true;
        }

        // (15.04 QA): Rebuild the array in a fixed order so the final
        // <meta> tag matches industry convention: index/noindex → follow/nofollow
        // → max-image-preview. PHP associative arrays preserve insertion
        // order, and core's wp_robots_to_string serializes in that order.
        $max_img = $robots['max-image-preview'] ?? 'large';
        $out = [];
        $out[$noindex ? 'noindex' : 'index'] = true;
        $out[$nofollow ? 'nofollow' : 'follow'] = true;
        if (!$noindex) {
            $out['max-image-preview'] = $max_img;
        }

        return $out;
    }

    /**
 * Render a full Rank Forge section on taxonomy term edit screens —
 * title/description overrides plus noindex/nofollow — so editors can
 * control category SEO the same way they do for posts.
 */
    public function render_term_fields($term, $taxonomy) {
        $noindex     = get_term_meta($term->term_id, '_rankforge_noindex', true);
        $nofollow    = get_term_meta($term->term_id, '_rankforge_nofollow', true);
        $meta_title  = get_term_meta($term->term_id, '_rankforge_meta_title', true);
        $meta_desc   = get_term_meta($term->term_id, '_rankforge_meta_description', true);
        ?>
        <tr class="form-field">
            <th colspan="2" style="padding:0;">
                <div style="margin:24px 0 4px;padding:12px 16px;background:linear-gradient(135deg,#FF6B35,#E05520);color:#fff;border-radius:8px 8px 0 0;font-weight:600;font-size:14px;letter-spacing:0.3px;display:flex;align-items:center;gap:8px;">
                    <span class="dashicons dashicons-chart-line" style="font-size:18px;width:18px;height:18px;"></span>
                    <?php esc_html_e('Rank Forge', 'rankforge'); ?>
                </div>
            </th>
        </tr>
        <tr class="form-field">
            <th scope="row"><label for="rankforge_meta_title"><?php esc_html_e('Meta Title', 'rankforge'); ?></label></th>
            <td>
                <?php // no maxlength — advise (recommended length in description below), never block input. ?>
                <input type="text" id="rankforge_meta_title" name="rankforge_meta_title" value="<?php echo esc_attr($meta_title); ?>" style="width:100%;max-width:520px;">
                <p class="description"><?php esc_html_e('Overrides the default archive title. Leave empty to use the term name + site template.', 'rankforge'); ?></p>
            </td>
        </tr>
        <tr class="form-field">
            <th scope="row"><label for="rankforge_meta_description"><?php esc_html_e('Meta Description', 'rankforge'); ?></label></th>
            <td>
                <textarea id="rankforge_meta_description" name="rankforge_meta_description" rows="3" style="width:100%;max-width:520px;"><?php echo esc_textarea($meta_desc); ?></textarea>
                <p class="description"><?php esc_html_e('Shown under the title in search results. ~155 characters recommended.', 'rankforge'); ?></p>
            </td>
        </tr>
        <tr class="form-field">
            <th scope="row"><label><?php esc_html_e('Search Visibility', 'rankforge'); ?></label></th>
            <td>
                <label style="display:block;margin-bottom:6px;">
                    <input type="checkbox" name="rankforge_noindex" value="1" <?php checked($noindex); ?>>
                    <?php esc_html_e('Hide from search results (noindex)', 'rankforge'); ?>
                </label>
                <label style="display:block;">
                    <input type="checkbox" name="rankforge_nofollow" value="1" <?php checked($nofollow); ?>>
                    <?php esc_html_e('Do not follow links (nofollow)', 'rankforge'); ?>
                </label>
                <p class="description"><?php esc_html_e('Unchecked = visible in search engines.', 'rankforge'); ?></p>
            </td>
        </tr>
        <?php
    }

    /**
 * Save term meta on taxonomy edit.
 */
    public function save_term_fields($term_id) {
        if (!current_user_can('manage_categories')) return;
        // Hooked to edited_{$tax}: WordPress core verifies the term-edit screen
        // nonce in wp-admin/edit-tags.php before this fires, and the fields are
        // rendered into that core form. Capability checked above.
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified by core term-edit screen (edit-tags.php) before edited_{$tax}; capability checked above.
        update_term_meta($term_id, '_rankforge_noindex',  !empty($_POST['rankforge_noindex'])  ? '1' : '');
        update_term_meta($term_id, '_rankforge_nofollow', !empty($_POST['rankforge_nofollow']) ? '1' : '');

        if (isset($_POST['rankforge_meta_title'])) {
            update_term_meta($term_id, '_rankforge_meta_title', sanitize_text_field(wp_unslash($_POST['rankforge_meta_title'])));
        }
        if (isset($_POST['rankforge_meta_description'])) {
            update_term_meta($term_id, '_rankforge_meta_description', sanitize_textarea_field(wp_unslash($_POST['rankforge_meta_description'])));
        }
        // phpcs:enable WordPress.Security.NonceVerification.Missing
    }

    /**
 * Replace <title> on a term archive with the configured override.
 */
    public function filter_term_document_title($title) {
        if (is_admin() || is_feed()) return $title;
        if (!(is_category() || is_tag() || is_tax())) return $title;
        $term_id = get_queried_object_id();
        if (!$term_id) return $title;
        $override = get_term_meta($term_id, '_rankforge_meta_title', true);
        return !empty($override) ? $override : $title;
    }

    /**
 * Emit <meta name="description"> and og:* for term archives when an
 * override is set on the term.
 */
    public function output_term_meta_tags() {
        if (!(is_category() || is_tag() || is_tax())) return;
        $term = get_queried_object();
        if (!$term || empty($term->term_id)) return;

        $desc = get_term_meta($term->term_id, '_rankforge_meta_description', true);
        if (empty($desc) && !empty($term->description)) {
            $desc = wp_strip_all_tags($term->description);
        }
        if (empty($desc)) return;

        $title = get_term_meta($term->term_id, '_rankforge_meta_title', true) ?: $term->name;
        $url   = get_term_link($term);

        echo '<meta name="description" content="' . esc_attr($desc) . '">' . "\n";
        echo '<meta property="og:type" content="website">' . "\n";
        echo '<meta property="og:title" content="' . esc_attr($title) . '">' . "\n";
        echo '<meta property="og:description" content="' . esc_attr($desc) . '">' . "\n";
        if (!is_wp_error($url)) {
            echo '<meta property="og:url" content="' . esc_url($url) . '">' . "\n";
        }
    }

    /**
 * Public API: check if a post is noindex.
 * Used by sitemap to exclude posts.
 */
    public static function is_noindex($post_id) {
        if (get_post_meta($post_id, '_rankforge_noindex', true)) return true;
        // Check post-type-level setting
        $pt_robots = get_option('rankforge_posttype_robots', []);
        $post_type = get_post_type($post_id);
        return !empty($pt_robots[$post_type]['noindex']);
    }
}
