<?php
/**
 * Rank Booster Preview & Push Live
 *
 * Handles:
 *  - the_content filter to show draft content via nonced URL
 *  - Preview URL generation
 *  - Push live: save revision, apply draft, optionally update dates
 */

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

class Rank_Booster_Preview {

    const META_KEY               = '_rank_booster_draft_content';
    const META_TITLE_KEY         = '_rank_booster_draft_title';
    const META_META_KEY          = '_rank_booster_draft_meta';
    const META_TITLE_TAG_KEY     = '_rank_booster_draft_title_tag';
    const META_ELEMENTOR_PREVIEW = '_rank_booster_preview_elementor_data';
    const META_BREAKDANCE_PREVIEW = '_rank_booster_preview_breakdance_data';
    const META_CUSTOMFIELDS_PREVIEW = '_rank_booster_preview_customfields';

    public static function init() {
        // Only register frontend preview filters when the preview param is present.
        // This avoids adding 7+ filter callbacks on every normal page load.
        if ( ! isset( $_GET['rank_booster_preview'] ) ) {
            return;
        }

        // Priority 7: before do_blocks (9) and wpautop (10) so draft content gets full processing
        add_filter( 'the_content', array( __CLASS__, 'filter_content' ), 7 );
        add_filter( 'the_title', array( __CLASS__, 'filter_title' ), 10, 2 );
        add_filter( 'document_title_parts', array( __CLASS__, 'filter_document_title' ) );

        // SEO title tag — override Yoast / RankMath / default
        add_filter( 'wpseo_title', array( __CLASS__, 'filter_seo_title' ) );
        add_filter( 'rank_math/frontend/title', array( __CLASS__, 'filter_seo_title' ) );

        // Meta description — override Yoast / RankMath / inject via wp_head
        add_filter( 'wpseo_metadesc', array( __CLASS__, 'filter_seo_metadesc' ) );
        add_filter( 'rank_math/frontend/description', array( __CLASS__, 'filter_seo_metadesc' ) );
        add_action( 'wp_head', array( __CLASS__, 'maybe_output_meta_description' ), 1 );

        // Elementor: swap in preview data at the rendering stage.
        // Uses Elementor's own builder_content_data filter (more reliable than get_post_metadata).
        add_filter( 'elementor/frontend/builder_content_data', array( __CLASS__, 'filter_elementor_frontend_data' ), 10, 2 );

        // Breakdance: swap in the precomputed preview tree via the meta read.
        add_filter( 'get_post_metadata', array( __CLASS__, 'filter_breakdance_preview_data' ), 10, 4 );

        // Custom fields / ACF: swap in the rewritten field values via meta read.
        add_filter( 'get_post_metadata', array( __CLASS__, 'filter_customfields_preview' ), 10, 4 );
    }

    /**
     * During a valid preview, return the rewritten draft value for any custom
     * field that was optimized, so the theme/ACF renders the AI version.
     *
     * @param mixed  $value
     * @param int    $object_id
     * @param string $meta_key
     * @param bool   $single
     * @return mixed
     */
    /**
     * Resolve where appended content (FAQ vs summary) should go, from the job's
     * selected checks. Language-agnostic — does not rely on heading wording.
     *   add_faq      → 'append'  (bottom)
     *   quick_summary → 'prepend' (top)
     *   both / neither → 'auto'   (let the provider's content heuristic decide)
     *
     * @param object $job
     * @return string
     */
    public static function faq_summary_position( $job ) {
        $selected = ! empty( $job->selected_checks ) ? json_decode( $job->selected_checks, true ) : array();
        if ( ! is_array( $selected ) ) {
            $selected = array();
        }
        $has_faq     = in_array( 'add_faq', $selected, true );
        $has_summary = in_array( 'quick_summary', $selected, true );
        if ( $has_faq && ! $has_summary ) {
            return 'append';
        }
        if ( $has_summary && ! $has_faq ) {
            return 'prepend';
        }
        return 'auto';
    }

    public static function filter_customfields_preview( $value, $object_id, $meta_key, $single ) {
        if ( $meta_key === '' || $meta_key[0] === '_' ) {
            return $value; // skip internal/reference keys fast
        }
        $nonce = isset( $_GET['rank_booster_preview'] ) ? sanitize_text_field( wp_unslash( $_GET['rank_booster_preview'] ) ) : '';
        if ( ! $nonce || ! wp_verify_nonce( $nonce, 'rank_booster_preview_' . $object_id ) ) {
            return $value;
        }

        static $maps = array();
        if ( ! array_key_exists( $object_id, $maps ) ) {
            $raw = get_post_meta( $object_id, self::META_CUSTOMFIELDS_PREVIEW, true );
            $maps[ $object_id ] = is_array( $raw ) ? $raw : ( is_string( $raw ) ? json_decode( $raw, true ) : array() );
            if ( ! is_array( $maps[ $object_id ] ) ) {
                $maps[ $object_id ] = array();
            }
        }

        $map = $maps[ $object_id ];
        if ( ! isset( $map[ $meta_key ] ) ) {
            return $value;
        }
        return $single ? array( $map[ $meta_key ] ) : array( $map[ $meta_key ] );
    }

    /**
     * Return the precomputed Breakdance preview tree for `_breakdance_data`
     * during a valid preview request. One value per read; non-destructive.
     *
     * @param mixed  $value
     * @param int    $object_id
     * @param string $meta_key
     * @param bool   $single
     * @return mixed
     */
    public static function filter_breakdance_preview_data( $value, $object_id, $meta_key, $single ) {
        if ( $meta_key !== '_breakdance_data' ) {
            return $value;
        }
        $nonce = isset( $_GET['rank_booster_preview'] ) ? sanitize_text_field( wp_unslash( $_GET['rank_booster_preview'] ) ) : '';
        if ( ! $nonce || ! wp_verify_nonce( $nonce, 'rank_booster_preview_' . $object_id ) ) {
            return $value;
        }

        static $reading = false;
        if ( $reading ) {
            return $value;
        }
        $reading = true;
        $preview = get_post_meta( $object_id, self::META_BREAKDANCE_PREVIEW, true );
        $reading = false;

        if ( empty( $preview ) ) {
            return $value;
        }
        return $single ? array( $preview ) : array( $preview );
    }

    /* ═══════════════════════════════════════════════
       Content filter — swap in draft when previewing
       ═══════════════════════════════════════════════ */

    public static function filter_content( $content ) {
        if ( ! isset( $_GET['rank_booster_preview'] ) ) {
            return $content;
        }

        $nonce   = sanitize_text_field( wp_unslash( $_GET['rank_booster_preview'] ) );
        $post_id = get_the_ID();

        if ( ! $post_id || ! wp_verify_nonce( $nonce, 'rank_booster_preview_' . $post_id ) ) {
            return $content;
        }

        $draft = get_post_meta( $post_id, self::META_KEY, true );
        if ( $draft ) {
            return $draft;
        }

        return $content;
    }

    public static function filter_title( $title, $post_id = 0 ) {
        if ( ! isset( $_GET['rank_booster_preview'] ) ) {
            return $title;
        }

        $nonce = sanitize_text_field( wp_unslash( $_GET['rank_booster_preview'] ) );
        if ( ! $post_id ) {
            $post_id = get_the_ID();
        }

        if ( ! $post_id || ! wp_verify_nonce( $nonce, 'rank_booster_preview_' . $post_id ) ) {
            return $title;
        }

        $draft_title = get_post_meta( $post_id, self::META_TITLE_KEY, true );
        return $draft_title ? $draft_title : $title;
    }

    public static function filter_document_title( $parts ) {
        if ( ! isset( $_GET['rank_booster_preview'] ) || ! is_singular() ) {
            return $parts;
        }

        $nonce   = sanitize_text_field( wp_unslash( $_GET['rank_booster_preview'] ) );
        $post_id = get_queried_object_id();

        if ( ! $post_id || ! wp_verify_nonce( $nonce, 'rank_booster_preview_' . $post_id ) ) {
            return $parts;
        }

        // Prefer SEO title tag over H1 for document title
        $title_tag = get_post_meta( $post_id, self::META_TITLE_TAG_KEY, true );
        if ( $title_tag ) {
            $parts['title'] = $title_tag;
        } else {
            $draft_title = get_post_meta( $post_id, self::META_TITLE_KEY, true );
            if ( $draft_title ) {
                $parts['title'] = $draft_title;
            }
        }

        return $parts;
    }

    /**
     * Override Yoast / RankMath SEO title during preview.
     */
    public static function filter_seo_title( $title ) {
        if ( ! isset( $_GET['rank_booster_preview'] ) || ! is_singular() ) {
            return $title;
        }

        $nonce   = sanitize_text_field( wp_unslash( $_GET['rank_booster_preview'] ) );
        $post_id = get_queried_object_id();

        if ( ! $post_id || ! wp_verify_nonce( $nonce, 'rank_booster_preview_' . $post_id ) ) {
            return $title;
        }

        $title_tag = get_post_meta( $post_id, self::META_TITLE_TAG_KEY, true );
        return $title_tag ? $title_tag : $title;
    }

    /**
     * Override Yoast / RankMath meta description during preview.
     */
    public static function filter_seo_metadesc( $desc ) {
        if ( ! isset( $_GET['rank_booster_preview'] ) || ! is_singular() ) {
            return $desc;
        }

        $nonce   = sanitize_text_field( wp_unslash( $_GET['rank_booster_preview'] ) );
        $post_id = get_queried_object_id();

        if ( ! $post_id || ! wp_verify_nonce( $nonce, 'rank_booster_preview_' . $post_id ) ) {
            return $desc;
        }

        $draft_meta = get_post_meta( $post_id, self::META_META_KEY, true );
        return $draft_meta ? $draft_meta : $desc;
    }

    /**
     * Fallback: output meta description tag if no SEO plugin is active.
     */
    public static function maybe_output_meta_description() {
        if ( ! isset( $_GET['rank_booster_preview'] ) || ! is_singular() ) {
            return;
        }

        // Skip if Yoast or RankMath handles it
        if ( defined( 'WPSEO_VERSION' ) || class_exists( 'RankMath' ) ) {
            return;
        }

        $nonce   = sanitize_text_field( wp_unslash( $_GET['rank_booster_preview'] ) );
        $post_id = get_queried_object_id();

        if ( ! $post_id || ! wp_verify_nonce( $nonce, 'rank_booster_preview_' . $post_id ) ) {
            return;
        }

        $draft_meta = get_post_meta( $post_id, self::META_META_KEY, true );
        if ( $draft_meta ) {
            echo '<meta name="description" content="' . esc_attr( $draft_meta ) . '" />' . "\n";
        }
    }

    /* ═══════════════════════════════════════════════
       Elementor preview — swap data in render pipeline
       ═══════════════════════════════════════════════ */

    /**
     * Filter Elementor's builder_content_data to swap in the preview version.
     *
     * This fires inside Elementor's get_builder_content() right before rendering,
     * giving us the parsed elements array. Much more reliable than intercepting
     * get_post_metadata, which can be bypassed by internal Elementor caching.
     *
     * The preview data is pre-computed in generate_preview_url() and stored
     * as JSON in a separate meta key.
     *
     * @param array $data    Parsed Elementor elements array.
     * @param int   $post_id Post ID being rendered.
     * @return array
     */
    public static function filter_elementor_frontend_data( $data, $post_id ) {
        $nonce = sanitize_text_field( wp_unslash( $_GET['rank_booster_preview'] ) );
        if ( ! wp_verify_nonce( $nonce, 'rank_booster_preview_' . $post_id ) ) {
            return $data;
        }

        $preview_json = get_post_meta( $post_id, self::META_ELEMENTOR_PREVIEW, true );
        if ( empty( $preview_json ) ) {
            return $data;
        }

        $preview_data = is_array( $preview_json ) ? $preview_json : json_decode( $preview_json, true );
        if ( ! is_array( $preview_data ) ) {
            return $data;
        }

        // Elementor caches rendered HTML in _elementor_element_cache meta.
        // Delete it NOW so print_elements() renders our modified data fresh
        // instead of serving the cached live version.
        delete_post_meta( $post_id, '_elementor_element_cache' );

        // After the page is fully sent, delete the cache AGAIN to remove the
        // poisoned cache that Elementor just stored from our preview render.
        // This ensures subsequent live page loads re-render from real _elementor_data.
        $pid = $post_id;
        add_action( 'shutdown', function () use ( $pid ) {
            delete_post_meta( $pid, '_elementor_element_cache' );
        } );

        return $preview_data;
    }

    /* ═══════════════════════════════════════════════
       Generate preview URL for a job
       ═══════════════════════════════════════════════ */

    /**
     * Replace the first <h1>...</h1> tag in HTML/shortcode content with new text.
     *
     * Used for Divi / WPBakery / Classic / Gutenberg where the visual H1 lives
     * inside post_content rather than in a separate data structure.
     *
     * @param string $content    HTML/shortcode content.
     * @param string $new_h1     New H1 text (plain text, no HTML).
     * @return string Content with H1 replaced, or original if no H1 found.
     */
    public static function inject_h1_into_content( $content, $new_h1 ) {
        if ( empty( $new_h1 ) || empty( $content ) ) {
            return $content;
        }
        // Match the first <h1> tag (with any attributes) and its inner content.
        $pattern = '/(<h1[^>]*>)(.*?)(<\/h1>)/is';
        $replacement = '${1}' . $new_h1 . '${3}';
        $result = preg_replace( $pattern, $replacement, $content, 1 );
        return $result !== null ? $result : $content;
    }

    /**
     * Write draft to post meta and return a nonced preview URL.
     *
     * @param object $job DB row from rank_booster_jobs.
     * @return string Preview URL.
     */
    public static function generate_preview_url( $job ) {
        $post_id = (int) $job->post_id;

        // For non-Elementor/Breakdance/CustomFields editors, the visual H1 lives
        // inside post_content. If the API improved the H1 (draft_title), inject
        // it into the draft content so preview and push-live show the new H1.
        $draft_content = $job->draft_content;
        if ( ! empty( $draft_content ) && ! empty( $job->draft_title ) ) {
            $non_slot_editors = array( 'divi', 'wpbakery', 'gutenberg', 'classic' );
            if ( in_array( $job->editor_type, $non_slot_editors, true ) ) {
                $draft_content = self::inject_h1_into_content( $draft_content, $job->draft_title );
            }
        }

        // Write draft content to post meta for the filter to pick up
        if ( ! empty( $draft_content ) ) {
            update_post_meta( $post_id, self::META_KEY, $draft_content );
        }
        if ( ! empty( $job->draft_title ) ) {
            update_post_meta( $post_id, self::META_TITLE_KEY, $job->draft_title );
        }
        if ( ! empty( $job->draft_title_tag ) ) {
            update_post_meta( $post_id, self::META_TITLE_TAG_KEY, $job->draft_title_tag );
        }
        if ( ! empty( $job->draft_meta ) ) {
            update_post_meta( $post_id, self::META_META_KEY, $job->draft_meta );
        }

        // Elementor: pre-compute modified _elementor_data for the preview filter.
        // This builds the data in memory now (admin context) so the frontend filter
        // can just read and return it — no Elementor class needed on the frontend.
        if ( $job->editor_type === 'elementor' && ! empty( $job->draft_content ) && class_exists( 'Rank_Booster_Elementor' ) ) {
            $preview_json = Rank_Booster_Elementor::build_preview_data(
                $post_id,
                $job->draft_content,
                ! empty( $job->draft_title ) ? $job->draft_title : ''
            );
            if ( $preview_json ) {
                update_post_meta( $post_id, self::META_ELEMENTOR_PREVIEW, wp_slash( $preview_json ) );
            }
        }

        // Breakdance: pre-compute the modified _breakdance_data tree for preview.
        if ( $job->editor_type === 'breakdance' && ! empty( $job->draft_content ) && class_exists( 'Rank_Booster_Breakdance' ) ) {
            $preview_json = Rank_Booster_Breakdance::build_preview_data( $post_id, $job->draft_content, self::faq_summary_position( $job ) );
            if ( $preview_json ) {
                update_post_meta( $post_id, self::META_BREAKDANCE_PREVIEW, wp_slash( $preview_json ) );
            }
        }

        // Custom fields: store the field_key => rewritten value map for the
        // preview meta filter to serve.
        if ( $job->editor_type === 'customfields' && ! empty( $job->draft_content ) && class_exists( 'Rank_Booster_CustomFields' ) ) {
            $field_map = Rank_Booster_CustomFields::build_field_updates( $post_id, $job->draft_content, self::faq_summary_position( $job ) );
            if ( ! empty( $field_map ) ) {
                update_post_meta( $post_id, self::META_CUSTOMFIELDS_PREVIEW, wp_slash( wp_json_encode( $field_map ) ) );
            }
        }

        // Generate nonce (valid 24 hours by default)
        $nonce = wp_create_nonce( 'rank_booster_preview_' . $post_id );

        // Store nonce in job for reference
        Rank_Booster_DB::update_job( $job->id, array( 'preview_nonce' => $nonce ) );

        return add_query_arg( 'rank_booster_preview', $nonce, get_permalink( $post_id ) );
    }

    /* ═══════════════════════════════════════════════
       Push live — apply draft to the actual post
       ═══════════════════════════════════════════════ */

    /**
     * Apply the draft from a job to the live post.
     *
     * @param int $job_id
     * @return array|WP_Error
     */
    public static function apply_draft( $job_id ) {
        $job = Rank_Booster_DB::get_job( $job_id );
        if ( ! $job ) {
            return new WP_Error( 'no_job', 'Job not found.' );
        }

        // Terms (taxonomy archives) have their own write path — they don't have
        // post_content, and may have title/meta-only drafts (no draft_content).
        if ( $job->object_type === 'term' ) {
            return self::apply_term_draft( $job );
        }

        if ( empty( $job->draft_content ) ) {
            return new WP_Error( 'no_draft', 'No draft content available.' );
        }

        $post_id = (int) $job->post_id;
        $post    = get_post( $post_id );
        if ( ! $post ) {
            return new WP_Error( 'no_post', 'Post not found.' );
        }

        // Preserve current live version in revision history
        wp_save_post_revision( $post_id );

        // ── Elementor: re-inject text slots into _elementor_data ──────────────
        if ( $job->editor_type === 'elementor' ) {
            $slots    = Rank_Booster_Elementor::html_to_slots( $job->draft_content );
            $non_slot = Rank_Booster_Elementor::extract_non_slot_html( $job->draft_content );

            // If the entire draft is non-slot HTML (e.g. add_faq / quick_summary returned
            // plain HTML because the LLM didn't preserve slot wrappers), handle gracefully.
            if ( empty( $slots ) && empty( $non_slot ) ) {
                return new WP_Error( 'elementor_parse_failed', 'Could not parse Elementor slot HTML from draft.' );
            }

            // Apply slot-based updates (text edits to existing widgets)
            if ( ! empty( $slots ) ) {
                // The API's improve_header check returns the improved H1 in draft_title
                // but does NOT update it inside post_content_html (the slots). So we
                // find the H1 heading widget slot key and overwrite it with draft_title
                // before applying all slots — ensuring the improved H1 is used.
                if ( ! empty( $job->draft_title ) ) {
                    $h1_slot = Rank_Booster_Elementor::get_h1_slot( $post_id );
                    if ( $h1_slot ) {
                        $slots[ $h1_slot['slot_key'] ] = $job->draft_title;
                    }
                }

                Rank_Booster_Elementor::apply_text_slots( $post_id, $slots );
            }

            // Inject any non-slot HTML (add_faq → append, quick_summary → prepend).
            // Determine position: check selected_checks first; fall back to content heuristic
            // (FAQ heading → append, anything else → prepend).
            if ( ! empty( $non_slot ) ) {
                $selected    = $job->selected_checks ? json_decode( $job->selected_checks, true ) : array();
                $has_summary = in_array( 'quick_summary', $selected, true );
                $has_faq     = in_array( 'add_faq', $selected, true );

                // Heuristic fallback for automatic mode (selected_checks is null)
                if ( ! $has_summary && ! $has_faq ) {
                    $lower    = strtolower( $non_slot );
                    $has_faq  = strpos( $lower, 'faq' ) !== false
                             || strpos( $lower, 'frequently asked' ) !== false;
                    $has_summary = ! $has_faq;
                }

                $position = ( $has_faq && ! $has_summary ) ? 'append' : 'prepend';
                Rank_Booster_Elementor::inject_html_widget( $post_id, $non_slot, $position );
            }

            // For Elementor, the H1 lives inside _elementor_data (heading widget) —
            // it was updated via the slot map above.
            // We do NOT update post_title because Elementor typically hides the WP title.

            // Optionally update publish date
            if ( $job->update_publish_date ) {
                $now     = current_time( 'mysql' );
                $now_gmt = current_time( 'mysql', true );
                wp_update_post( array(
                    'ID'               => $post_id,
                    'post_date'        => $now,
                    'post_date_gmt'    => $now_gmt,
                    'post_modified'    => $now,
                    'post_modified_gmt' => $now_gmt,
                ) );
            }
        } elseif ( $job->editor_type === 'breakdance' ) {
            // ── Breakdance: apply slots + any appended FAQ/summary to the tree ──
            $applied = Rank_Booster_Breakdance::apply_draft( $post_id, $job->draft_content, self::faq_summary_position( $job ) );
            if ( ! $applied ) {
                // Don't abort the whole push — title/meta below can still apply.
                Rank_Booster_DB::add_log_entry(
                    $job->id,
                    '⚠️ Breakdance: the AI draft produced no applicable changes. '
                    . 'Content left unchanged; title/meta still applied.'
                );
            } else {
                Rank_Booster_DB::add_log_entry( $job->id, '✓ Breakdance: applied ' . (int) $applied . ' change(s) to the layout.' );
            }

            // The H1/title for Breakdance lives inside the tree (handled as a
            // slot when it qualifies as prose). We do not touch post_title.
            if ( $job->update_publish_date ) {
                $now     = current_time( 'mysql' );
                $now_gmt = current_time( 'mysql', true );
                wp_update_post( array(
                    'ID'                => $post_id,
                    'post_date'         => $now,
                    'post_date_gmt'     => $now_gmt,
                    'post_modified'     => $now,
                    'post_modified_gmt' => $now_gmt,
                ) );
            }
        } elseif ( $job->editor_type === 'customfields' ) {
            // ── Custom fields / ACF: write rewrites + appended FAQ/summary ──────
            $applied = Rank_Booster_CustomFields::apply_draft( $post_id, $job->draft_content, self::faq_summary_position( $job ) );
            if ( ! $applied ) {
                Rank_Booster_DB::add_log_entry(
                    $job->id,
                    '⚠️ Custom fields: the AI draft produced no applicable field updates. '
                    . 'Fields left unchanged; title/meta still applied.'
                );
            }

            if ( $job->update_publish_date ) {
                $now     = current_time( 'mysql' );
                $now_gmt = current_time( 'mysql', true );
                wp_update_post( array(
                    'ID'                => $post_id,
                    'post_date'         => $now,
                    'post_date_gmt'     => $now_gmt,
                    'post_modified'     => $now,
                    'post_modified_gmt' => $now_gmt,
                ) );
            }
        } else {
            // ── Classic / Gutenberg / Divi / WPBakery: replace post_content ─────
            $draft_content = $job->draft_content;

            // The API returns improved H1 in draft_title but does NOT update it
            // inside post_content_html. Inject it so the visual H1 is correct.
            if ( ! empty( $draft_content ) && ! empty( $job->draft_title ) ) {
                $draft_content = self::inject_h1_into_content( $draft_content, $job->draft_title );
            }

            $update_args = array(
                'ID'           => $post_id,
                'post_content' => $draft_content,
            );

            if ( ! empty( $job->draft_title ) ) {
                $update_args['post_title'] = $job->draft_title;
            }

            if ( $job->update_publish_date ) {
                $now     = current_time( 'mysql' );
                $now_gmt = current_time( 'mysql', true );

                $update_args['post_date']         = $now;
                $update_args['post_date_gmt']     = $now_gmt;
                $update_args['post_modified']      = $now;
                $update_args['post_modified_gmt']  = $now_gmt;
            }

            $result = wp_update_post( $update_args, true );

            if ( is_wp_error( $result ) ) {
                return $result;
            }
        }

        // Write the optimized SEO title tag + meta description to the active
        // supported SEO plugin (Yoast / Rank Math / SEOPress / SureRank /
        // The SEO Framework / Slim SEO). No-op when no supported plugin is active.
        if ( ! empty( $job->draft_title_tag ) || ! empty( $job->draft_meta ) ) {
            Rank_Booster::write_seo_meta(
                $post_id,
                ! empty( $job->draft_title_tag ) ? $job->draft_title_tag : '',
                ! empty( $job->draft_meta ) ? $job->draft_meta : ''
            );
        }

        // Clean up preview & draft meta
        delete_post_meta( $post_id, self::META_KEY );
        delete_post_meta( $post_id, self::META_TITLE_KEY );
        delete_post_meta( $post_id, self::META_TITLE_TAG_KEY );
        delete_post_meta( $post_id, self::META_META_KEY );
        delete_post_meta( $post_id, self::META_ELEMENTOR_PREVIEW );
        delete_post_meta( $post_id, self::META_BREAKDANCE_PREVIEW );
        delete_post_meta( $post_id, self::META_CUSTOMFIELDS_PREVIEW );
        delete_post_meta( $post_id, Rank_Booster_Elementor::META_DRAFT );
        delete_post_meta( $post_id, Rank_Booster_Elementor::META_DRAFT_PENDING );
        delete_post_meta( $post_id, Rank_Booster_Divi::META_DRAFT );
        delete_post_meta( $post_id, Rank_Booster_Divi::META_DRAFT_PENDING );
        delete_post_meta( $post_id, Rank_Booster_WPBakery::META_DRAFT );
        delete_post_meta( $post_id, Rank_Booster_WPBakery::META_DRAFT_PENDING );
        delete_post_meta( $post_id, Rank_Booster_Breakdance::META_DRAFT );
        delete_post_meta( $post_id, Rank_Booster_Breakdance::META_DRAFT_PENDING );
        delete_post_meta( $post_id, '_rank_booster_classic_draft_pending' );

        // Update job status
        Rank_Booster_DB::update_job( $job_id, array(
            'status'         => 'pushed_live',
            'pushed_live_at' => current_time( 'mysql' ),
        ) );

        Rank_Booster_DB::add_log_entry( $job_id, '🚀 Pushed live! Post updated.' );

        return array(
            'success'  => true,
            'post_url' => get_permalink( $post_id ),
            'post_id'  => $post_id,
        );
    }

    /**
     * Push a term (taxonomy archive) draft live.
     *
     * Writes the optimized SEO title tag + meta description to the active SEO
     * plugin, and — only when a content rewrite was requested and produced
     * content — updates the term description.
     *
     * @param object $job Job DB row (object_type = 'term', post_id = term ID).
     * @return array|WP_Error
     */
    private static function apply_term_draft( $job ) {
        $term_id = (int) $job->post_id;
        $term    = get_term( $term_id );
        if ( ! $term || is_wp_error( $term ) ) {
            return new WP_Error( 'no_term', 'Term not found.' );
        }

        // SEO title tag + meta description.
        Rank_Booster::write_seo_meta(
            $term_id,
            ! empty( $job->draft_title_tag ) ? $job->draft_title_tag : '',
            ! empty( $job->draft_meta ) ? $job->draft_meta : '',
            'term',
            $term->taxonomy
        );

        // Term description (the editable archive content).
        if ( ! empty( $job->draft_content ) ) {
            wp_update_term( $term_id, $term->taxonomy, array(
                'description' => $job->draft_content,
            ) );
        }

        // H1 / term-name rewrite is never published — we don't rename categories.
        // Flag it in the log so the user knows that part of the draft was skipped.
        if ( ! empty( $job->draft_h1 ) || ! empty( $job->draft_title ) ) {
            Rank_Booster_DB::add_log_entry( $job->id, 'ℹ️ H1 (category name) change was not published — renaming category pages is not supported.' );
        }

        Rank_Booster_DB::update_job( $job->id, array(
            'status'         => 'pushed_live',
            'pushed_live_at' => current_time( 'mysql' ),
        ) );
        Rank_Booster_DB::add_log_entry( $job->id, '🚀 Pushed live! Category page updated.' );

        return array(
            'success'  => true,
            'post_url' => get_term_link( $term ),
            'post_id'  => $term_id,
        );
    }
}
