<?php
/**
 * Data Upload Class
 *
 * @package SAAI
 */

namespace SAAI\Classes\Memory;

use SAAI\Abstracts\Abstract_Class;
use SAAI\Traits\Singleton_Trait;
use SAAI\Helpers\Connect;
use SAAI\Helpers\Helper;
use SAAI\Classes\Plan_Manager;
use SAAI\Classes\Notification;
use SAAI\REST\Chat\Chat_Post_Types;

defined( 'ABSPATH' ) || exit;

/**
 * Data Upload Class
 */
class Data_Upload extends Abstract_Class {

    use Singleton_Trait;

    const POST_DATA_UPLOAD_HOOK           = 'saai_post_data_upload';
    const POST_AI_MEMORY_ENABLED_META     = 'saai_post_ai_memory_enabled';
    const POST_DATA_UPLOAD_HASH_META      = 'saai_post_data_upload_hash';
    const POST_DATA_UPLOAD_TIMESTAMP_META = 'saai_post_data_upload_timestamp';
    const PENDING_POST_DATA_UPLOAD_META   = 'saai_post_data_upload_pending';
    const POST_DATA_CURRENT_HASH_META     = 'saai_post_data_current_hash';
    const POSTS_PER_BATCH                 = 10;

    const POST_DATA_DELETE_HOOK      = 'saai_post_data_delete';
    const POST_DATA_DELETE_QUEUE_KEY = 'saai_post_data_delete_queue';
    const POSTS_DELETE_PER_BATCH     = 10;

    const POST_DATA_UPLOAD_RETRY_COUNT_META = 'saai_post_data_upload_retry_count';
    const MAX_UPLOAD_RETRIES                = 3;
    const UPLOAD_IN_PROGRESS_FLAG_KEY       = 'saai_post_data_upload_in_progress';

    /**
     * Run the class
     */
    public function run() {

        add_action( self::POST_DATA_UPLOAD_HOOK, array( $this, 'process_post_data_upload' ) );
        add_action( self::POST_DATA_DELETE_HOOK, array( $this, 'process_post_data_deletion' ) );

        add_action( 'admin_notices', array( $this, 'display_progress_notice' ) );
        add_action( 'admin_notices', array( $this, 'display_vector_availability_notice' ) );
        add_action( 'saai_admin_notices', array( $this, 'display_progress_notice' ) );
        add_action( 'saai_admin_notices', array( $this, 'display_vector_availability_notice' ) );

        $post_types = Helper::get_post_types_with_ai_memory_enabled();
        foreach ( $post_types as $post_type ) {
            /***************************************************************************
             * Update post hash when post is saved
             ***************************************************************************
             *
             * This is to update the post hash when the post is saved. We skip product
             * post type as we handle it separately.
             */
            if ( 'product' === $post_type ) {
                continue;
            }
            add_action( 'save_post_' . $post_type, array( $this, 'update_post_hash' ), 10, 2 );
        }
        // Hook into product save events to update current hash.
        add_action( 'woocommerce_update_product', array( $this, 'update_post_hash' ), 10, 2 );
        add_action( 'woocommerce_new_product', array( $this, 'update_post_hash' ), 10, 2 );

        // Hook into product deletion and AI memory disabling.
        add_action( 'before_delete_post', array( $this, 'remove_post_data_from_ai_memory' ), 10, 1 );
        add_action( 'saai_post_ai_memory_disabled', array( $this, 'remove_post_data_from_ai_memory' ), 10, 2 );

        // AJAX handler for retrying product upload.
        add_action( 'wp_ajax_saai_retry_upload_to_ai_memory', array( $this, 'ajax_retry_upload_to_ai_memory' ) );

        // Hook into Heartbeat API to ensure upload scheduling works on restrictive hosts.
        add_filter( 'heartbeat_received', array( $this, 'maybe_schedule_upload_via_heartbeat' ), 10, 2 );
    }

    /**
     * Schedule post data upload process
     *
     * @since 1.1.2
     *
     * @param string   $post_type The post type to schedule the upload for.
     * @param null|int $timestamp Optional timestamp to schedule the upload for.
     * @return void
     */
    public function schedule_post_data_upload( $post_type, $timestamp = null ) {

        // Prevent duplicate async scheduling to avoid concurrent executions.
        if ( ! $timestamp && as_has_scheduled_action( self::POST_DATA_UPLOAD_HOOK, array( $post_type ) ) ) {
            return;
        }

        if ( $timestamp ) {
            as_schedule_single_action( $timestamp, self::POST_DATA_UPLOAD_HOOK, array( $post_type ) );
            return;
        }

        // If no timestamp is provided, enqueue the async action.
        as_enqueue_async_action( self::POST_DATA_UPLOAD_HOOK, array( $post_type ) );
    }

    /**
     * Maybe schedule posts data upload if not already and only when it's not scheduled yet.
     *
     * @since 1.1.2
     *
     * @param string   $post_type The post type to schedule the upload for.
     * @param null|int $timestamp Optional timestamp to schedule the upload for.
     *
     * @return void
     */
    public function maybe_schedule_post_data_upload( $post_type, $timestamp = null ) {
        if ( ! as_has_scheduled_action( self::POST_DATA_UPLOAD_HOOK, array( $post_type ) ) ) {
            $this->schedule_post_data_upload( $post_type, $timestamp );
        }
    }

    /**
     * Update product hash when product is saved
     *
     * @param int                  $post_id Post ID.
     * @param \WP_Post|\WC_Product $post Post object.
     */
    public function update_post_hash( $post_id, $post ) {
        $post_type = get_post_type( $post_id );
        // Get the current hash before updating.
        $old_hash = Helper::get_post_meta( $post_id, self::POST_DATA_CURRENT_HASH_META );
        $new_hash = $this->get_post_data_hash( $post );

        Helper::update_post_meta( $post_id, self::POST_DATA_CURRENT_HASH_META, $new_hash );

        $has_hash_meta   = Helper::get_post_meta( $post_id, self::POST_DATA_UPLOAD_HASH_META );
        $is_new_post     = ( 'product' === $post_type && doing_action( 'woocommerce_new_product' ) ) || ! $has_hash_meta;
        $is_post_changed = ! empty( $old_hash ) && $old_hash !== $new_hash;

        // Schedule upload for the next day if:
        // 1. It's a new post/product, OR
        // 2. It's an existing post/product that has changed.
        if ( $is_new_post || $is_post_changed ) {
            // Set upload in progress flag when scheduling upload.
            set_transient( self::UPLOAD_IN_PROGRESS_FLAG_KEY, true, HOUR_IN_SECONDS );
            $this->maybe_schedule_post_data_upload( $post_type, time() + HOUR_IN_SECONDS );
        }
    }

    /**
     * Process single post data upload.
     *
     * @param \WP_Post|\WC_Product $post The post object.
     * @return bool True on success, false on failure.
     */
    protected function process_single_post_data_upload( $post ) {
        $data_id    = $post instanceof \WC_Product ? $post->get_id() : $post->ID;
        $is_enabled = Helper::get_post_meta( $data_id, self::POST_AI_MEMORY_ENABLED_META );
        $logger     = wc_get_logger();

        // If AI memory is disabled explicitly, skip the upload.
        if ( 'no' === $is_enabled ) {
            $logger->info(
                sprintf(
                    'Skipping post data upload to AI memory (post ID: %s): AI memory is disabled',
                    $data_id
                ),
                array(
                    'source' => 'storeagent-ai-for-woocommerce',
                )
            );
            return false;
        }

        // Set pending upload flag.
        Helper::update_post_meta( $data_id, self::PENDING_POST_DATA_UPLOAD_META, 'yes' );

        // Prepare post data.
        $post_data = $this->prepare_post_data( $post );
        // Upload to vector endpoint.
        $response = $this->upload_post_data_to_vector( $data_id, $post_data );

        if ( is_wp_error( $response ) ) {
            $retry_count = (int) Helper::get_post_meta( $data_id, self::POST_DATA_UPLOAD_RETRY_COUNT_META );
            if ( $retry_count >= self::MAX_UPLOAD_RETRIES ) {
                $logger->error(
                    sprintf(
                        'Post data upload permanently failed after %d attempts (post ID: %s): %s. AI memory disabled.',
                        $retry_count + 1,  // Show total attempts instead of retry count.
                        $data_id,
                        $response->get_error_message()
                    ),
                    array(
                        'source' => 'storeagent-ai-for-woocommerce',
                    )
                );
                if ( $post instanceof \WC_Product ) {
                    $post->update_meta_data( self::POST_AI_MEMORY_ENABLED_META, 'no' );
                    $post->delete_meta_data( self::PENDING_POST_DATA_UPLOAD_META );
                    $post->delete_meta_data( self::POST_DATA_UPLOAD_RETRY_COUNT_META );
                    $post->save_meta_data();
                } else {
                    Helper::update_post_meta( $data_id, self::POST_AI_MEMORY_ENABLED_META, 'no' );
                    delete_post_meta( $data_id, self::PENDING_POST_DATA_UPLOAD_META );
                    delete_post_meta( $data_id, self::POST_DATA_UPLOAD_RETRY_COUNT_META );
                }
            } else {
                // Increment retry count.
                Helper::update_post_meta( $data_id, self::POST_DATA_UPLOAD_RETRY_COUNT_META, $retry_count + 1 );

                $logger->error(
                    sprintf(
                        'Failed to upload post data to AI memory (post ID: %s, retry %d/%d): %s',
                        $data_id,
                        $retry_count + 1,
                        self::MAX_UPLOAD_RETRIES,
                        $response->get_error_message()
                    ),
                    array(
                        'source' => 'storeagent-ai-for-woocommerce',
                    )
                );
                // Remove pending upload flag.
                Helper::update_post_meta( $data_id, self::PENDING_POST_DATA_UPLOAD_META, '' );
            }

            return false;
        }

        // Set hash and timestamp.
        $post_data_hash = $this->get_post_data_hash( $post );
        if ( $post instanceof \WC_Product ) {
            // Remove pending flag.
            $post->delete_meta_data( self::PENDING_POST_DATA_UPLOAD_META );
            $post->save_meta_data();
        } else {
            // Remove pending flag.
            delete_post_meta( $data_id, self::PENDING_POST_DATA_UPLOAD_META );
        }

        Helper::update_post_meta( $data_id, self::POST_DATA_UPLOAD_HASH_META, $post_data_hash );
        Helper::update_post_meta( $data_id, self::POST_DATA_CURRENT_HASH_META, $post_data_hash );
        Helper::update_post_meta( $data_id, self::POST_DATA_UPLOAD_TIMESTAMP_META, time() );
        Helper::update_post_meta( $data_id, self::POST_AI_MEMORY_ENABLED_META, 'yes' );
        Helper::update_post_meta( $data_id, self::POST_DATA_UPLOAD_RETRY_COUNT_META, 0 );

        return true;
    }

    /**
     * Process post data upload
     *
     * @param string $post_type The post type to process the upload for.
     * @return array Array of processed post IDs.
     */
    public function process_post_data_upload( $post_type ) {
        // Get posts that need to be processed.
        $posts        = $this->get_posts_for_upload( $post_type );
        $plan_manager = Plan_Manager::instance();

        $check = $plan_manager->fetch_subscription_plans();

        // If there is an error fetching subscription plans, log the error and return.
        if ( is_wp_error( $check ) ) {
            wc_get_logger()->error(
                sprintf(
                    'Failed to process "%s" data upload: %s',
                    $post_type,
                    $check->get_error_message()
                ),
                array( 'source' => 'storeagent-ai-for-woocommerce' )
            );
        }

        // Check if vector upload is allowed.
        $is_upload_allowed = Plan_Manager::instance()->is_vector_upload_allowed();

        /**
         * If either there are no posts to upload or vector upload is not allowed anymore (limit has been reached)
         * send completion email and return.
         */
        if ( empty( $posts ) || ! $is_upload_allowed ) {
            // Note: Flag clearing is handled by checking all post types together,
            // not per post type, to avoid clearing when one post type finishes
            // while others are still processing.
            $this->maybe_send_setup_completion_email( $post_type );
            return array();
        }

        // Set upload in progress flag when we start processing posts.
        set_transient( self::UPLOAD_IN_PROGRESS_FLAG_KEY, true, HOUR_IN_SECONDS );

        $processed_posts = array();
        foreach ( $posts as $post ) {
            if ( $this->process_single_post_data_upload( $post ) ) {
                $processed_posts[] = $post instanceof \WC_Product ? $post->get_id() : $post->ID;
            }
        }

        // Schedule the next batch only if there are more products to process and upload is still allowed.
        $remaining_products = $this->get_posts_for_upload( $post_type, 'count' );
        if ( $remaining_products > 0 && $is_upload_allowed ) {
            $this->schedule_post_data_upload( $post_type );
        }

        return $processed_posts;
    }

    /**
     * Get the meta query for posts that need to be uploaded.
     *
     * @return array The meta query array.
     */
    protected function get_posts_upload_meta_query() {
        return array(
            'relation' => 'AND',
            array(
                'relation' => 'OR',
                array(
                    'key'     => self::POST_DATA_UPLOAD_HASH_META,
                    'compare' => 'NOT EXISTS',
                ),
                array(
                    'key'     => self::POST_DATA_UPLOAD_HASH_META,
                    'value'   => '',
                    'compare' => '=',
                ),
            ),
            array(
                'relation' => 'OR',
                array(
                    'key'     => self::PENDING_POST_DATA_UPLOAD_META,
                    'compare' => 'NOT EXISTS',
                ),
                array(
                    'key'     => self::PENDING_POST_DATA_UPLOAD_META,
                    'value'   => '',
                    'compare' => '=',
                ),
                array(
                    'key'     => self::PENDING_POST_DATA_UPLOAD_META,
                    'value'   => 'yes',
                    'compare' => '=',
                ),
            ),
            array(
                'relation' => 'OR',
                array(
                    'key'     => self::POST_AI_MEMORY_ENABLED_META,
                    'compare' => 'NOT EXISTS',
                ),
                array(
                    'key'     => self::POST_AI_MEMORY_ENABLED_META,
                    'value'   => 'yes',
                    'compare' => '=',
                ),
            ),
        );
    }

    /**
     * Get posts that need to be uploaded (both new and updated posts).
     *
     * @param string $post_type The post type to get the posts for.
     * @param string $return_type The type of data to return. Can be 'posts' or 'count'.
     * @return array|int Array of WP_Post objects if return_type is 'posts', or count if 'count'.
     */
    public function get_posts_for_upload( $post_type, $return_type = 'posts' ) {
        global $wpdb;

        $post_ids        = array();
        $posts_per_batch = $this->get_posts_per_batch();

        // Get posts that need initial upload (no hash or empty hash).
        $new_posts_args = array(
            'post_type'      => $post_type,
            'posts_per_page' => $posts_per_batch,
            'post_status'    => 'publish',
            'meta_query'     => $this->get_posts_upload_meta_query(),
            'fields'         => 'ids',
        );

        $new_posts_query = new \WP_Query( $new_posts_args );
        if ( $new_posts_query->have_posts() ) {
            $post_ids = array_merge( $post_ids, $new_posts_query->posts );
        }
        wp_reset_postdata();

        // Get posts that have been updated since last upload (if we have room for more).
        // Include updated posts even during setup progress, but only if we have remaining slots
        // after processing new posts. This ensures updates are processed after initial uploads.
        $remaining_slots = $posts_per_batch - count( $post_ids );
        if ( $remaining_slots > 0 ) {
            $current_hash_meta  = self::POST_DATA_CURRENT_HASH_META;
            $uploaded_hash_meta = self::POST_DATA_UPLOAD_HASH_META;

            $prepared_sql = $wpdb->prepare(
                "SELECT p.ID
                 FROM {$wpdb->posts} p
                 INNER JOIN {$wpdb->postmeta} current_hash 
                    ON p.ID = current_hash.post_id 
                    AND current_hash.meta_key = %s
                 INNER JOIN {$wpdb->postmeta} uploaded_hash 
                    ON p.ID = uploaded_hash.post_id 
                    AND uploaded_hash.meta_key = %s
                 WHERE p.post_type = %s
                 AND p.post_status = 'publish'
                 AND current_hash.meta_value != uploaded_hash.meta_value
                 LIMIT %d",
                $current_hash_meta,
                $uploaded_hash_meta,
                $post_type,
                $remaining_slots
            );

            $updated_post_ids = $wpdb->get_col( $prepared_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            if ( $updated_post_ids ) {
                $post_ids = array_merge( $post_ids, $updated_post_ids );
            }
        }

        // Remove duplicates and limit to batch size.
        $post_ids = array_unique( $post_ids );
        $post_ids = array_slice( $post_ids, 0, $posts_per_batch );

        if ( 'count' === $return_type ) {
            return count( $post_ids );
        }

        // Convert IDs to WP_Post objects.
        $posts = array();
        foreach ( $post_ids as $post_id ) {
            $post = 'product' === $post_type ? wc_get_product( $post_id ) : get_post( $post_id );
            if ( $post ) {
                $posts[] = $post;
            }
        }

        return $posts;
    }

    /**
     * Prepare post data for upload.
     *
     * @param \WP_Post|\WC_Product $post The post object.
     * @return array The prepared post data.
     */
    protected function prepare_post_data( $post ) {
        $product = is_object( $post ) && $post instanceof \WC_Product ? $post : null;
        $post_id = $product ? $product->get_id() : $post->ID;

        $post_type = get_post_type( $post_id );
        $settings  = get_option( Chat_Post_Types::OPTION_KEY, array() );
        $meta_keys = isset( $settings[ $post_type ]['meta_keys'] ) ? $settings[ $post_type ]['meta_keys'] : array();

        $taxonomy_terms = apply_filters( 'saai_prepare_post_data_taxonomy_terms', array(), $post_id, $post_type );
        if ( empty( $taxonomy_terms ) ) {
            if ( $product ) {
                $taxonomy_name = 'product_cat';
                $terms         = wp_get_post_terms( $product->get_id(), $taxonomy_name, array( 'fields' => 'names' ) );
                $taxonomy      = get_taxonomy( $taxonomy_name );
                if ( $taxonomy && ! empty( $terms ) ) {
                    $taxonomy_terms[ $taxonomy->labels->name ] = $terms;
                }
            } else {
                $taxonomy_name = 'category';
                $terms         = wp_get_post_terms( $post_id, $taxonomy_name, array( 'fields' => 'names' ) );
                $taxonomy      = get_taxonomy( $taxonomy_name );
                if ( $taxonomy && ! empty( $terms ) ) {
                    $taxonomy_terms[ $taxonomy->labels->name ] = $terms;
                }
            }
        }

        $metadata = apply_filters( 'saai_prepare_post_data_metadata', array(), $post_id, $post_type );
        if ( empty( $metadata ) ) {
            if ( ! empty( $meta_keys ) ) {
                foreach ( $meta_keys as $meta_key ) {
                    $metadata[ $meta_key ] = Helper::get_post_meta( $post_id, $meta_key, true );
                }
            }
        }

        try {
            if ( $product ) {
                $data       = array(
                    'post_type'    => 'product',
                    'product_data' => array(
                        'name'              => $product->get_name(),
                        'product_url'       => get_permalink( $product->get_id() ),
                        'short_description' => $product->get_short_description(),
                        'description'       => $product->get_description(),
                        'price'             => $product->get_price(),
                        'image'             => wp_get_attachment_url( $product->get_image_id() ),
                        'category'          => wp_get_post_terms( $product->get_id(), 'product_cat', array( 'fields' => 'names' ) ),
                        'tags'              => wp_get_post_terms( $product->get_id(), 'product_tag', array( 'fields' => 'names' ) ),
                        'attributes'        => array(),
                    ),
                    'metadata'     => $metadata,
                );
                $attributes = $product->get_attributes();
                foreach ( $attributes as $attribute ) {
                    // Validate that the attribute is a valid WC_Product_Attribute object.
                    if ( ! $attribute || ! is_object( $attribute ) ) {
                        continue;
                    }

                    // Ensure the attribute has the required methods.
                    if ( ! method_exists( $attribute, 'get_name' ) || ! method_exists( $attribute, 'get_options' ) ) {
                        continue;
                    }

                    $data['product_data']['attributes'][] = array(
                        'name'  => $attribute->get_name(),
                        'value' => $attribute->get_options(),
                    );
                }
            } else {
                $data = array(
                    'title'          => get_the_title( $post_id ),
                    'content'        => get_the_content( null, false, $post ),
                    'post_type'      => get_post_type( $post_id ),
                    'taxonomy_terms' => $taxonomy_terms,
                    'metadata'       => $metadata,
                );
            }

            return apply_filters( 'saai_prepare_post_data', $data, $post_id, $post_type );
        } catch ( \Exception $e ) {
            // Log the error and return basic post data to prevent fatal errors.
            wc_get_logger()->warning(
                sprintf(
                    'Error preparing post data for upload (post ID: %s): %s',
                    $post_id,
                    $e->getMessage()
                ),
                array( 'source' => 'storeagent-ai-for-woocommerce' )
            );

            // Return basic post data as fallback.
            return array(
                'title'    => get_the_title( $post_id ),
                'post_url' => get_permalink( $post_id ),
            );
        }
    }

    /**
     * Get the hash of the post data.
     *
     * @param \WP_Post|\WC_Product $post The post object.
     * @return string The hash of the post data.
     */
    protected function get_post_data_hash( $post ) {
        $post_data = $this->prepare_post_data( $post );
        return md5( wp_json_encode( $post_data ) );
    }

    /**
     * Upload post data to vector endpoint.
     *
     * @param int   $post_id   The post ID.
     * @param array $post_data The post data.
     * @return true|\WP_Error True on success, WP_Error on failure.
     */
    protected function upload_post_data_to_vector( $post_id, $post_data ) {
        $headers = array(
            'Content-Type'  => 'application/json',
            'X-Origin'      => wp_parse_url( get_home_url(), PHP_URL_HOST ),
            'Authorization' => 'Basic ' . base64_encode( Connect::get_consumer_key() . ':' . Connect::get_consumer_secret() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
        );

        $response = wp_remote_post(
            Helper::get_storeagent_base_url( 'api' ) . '/storeagent/v1/memory/upload-content',
            array(
                'headers' => $headers,
                'body'    => wp_json_encode(
                    array_merge(
                        array(
                            'data_id' => $post_id,
                        ),
                        $post_data
                    )
                ),
                'timeout' => 30,
            )
        );

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

        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        $code = wp_remote_retrieve_response_code( $response );

        if ( $code > 299 ) {
            return new \WP_Error(
                'storeagent_upload_post_data_error',
                isset( $body['message'] ) ? $body['message'] : __( 'Something went wrong while uploading post data to AI memory.', 'storeagent-ai-for-woocommerce' ),
                $body
            );
        }

        return true;
    }

    /**
     * Calculate the upload progress.
     *
     * @return int The upload progress.
     */
    public function calculate_upload_progress() {
        $progress_data = $this->get_detailed_upload_progress();
        return $progress_data['percentage'];
    }

    /**
     * Get detailed upload progress information.
     *
     * @return array Array containing detailed progress information.
     */
    public function get_detailed_upload_progress() {
        $post_types   = Helper::get_post_types_with_ai_memory_enabled();
        $plan_manager = Plan_Manager::instance();
        $limit        = $plan_manager->get_plan_limit( 'vector_items' );

        // Get ALL posts that should be uploaded (regardless of upload status).
        // This gives us the fixed total that shouldn't change during the process.
        $total_args = array(
            'post_type'      => $post_types,
            'posts_per_page' => -1,
            'fields'         => 'ids',
            'post_status'    => 'publish',
            'meta_query'     => array(
                array(
                    'relation' => 'OR',
                    array(
                        'key'     => self::POST_AI_MEMORY_ENABLED_META,
                        'compare' => 'NOT EXISTS',
                    ),
                    array(
                        'key'     => self::POST_AI_MEMORY_ENABLED_META,
                        'value'   => 'yes',
                        'compare' => '=',
                    ),
                ),
            ),
        );

        $total_query = new \WP_Query( $total_args );
        $total       = $total_query->found_posts;
        $total       = $limit > 0 ? min( $total, $limit ) : $total;

        // Count how many posts have been successfully uploaded (have upload hash).
        // Must match the same criteria as total_args to ensure accurate progress calculation.
        $processed_args = array(
            'post_type'      => $post_types,
            'posts_per_page' => -1,
            'fields'         => 'ids',
            'post_status'    => 'publish',
            'meta_query'     => array(
                'relation' => 'AND',
                array(
                    'key'     => self::POST_DATA_UPLOAD_HASH_META,
                    'compare' => 'EXISTS',
                ),
                array(
                    'key'     => self::POST_DATA_UPLOAD_HASH_META,
                    'value'   => '',
                    'compare' => '!=',
                ),
                array(
                    'relation' => 'OR',
                    array(
                        'key'     => self::POST_AI_MEMORY_ENABLED_META,
                        'compare' => 'NOT EXISTS',
                    ),
                    array(
                        'key'     => self::POST_AI_MEMORY_ENABLED_META,
                        'value'   => 'yes',
                        'compare' => '=',
                    ),
                ),
            ),
        );

        $processed_query = new \WP_Query( $processed_args );
        $processed       = $processed_query->found_posts;

        // Safety clamp: Ensure processed doesn't exceed total (shouldn't happen with matching criteria,
        // but protects against edge cases or race conditions).
        $processed = min( $processed, $total );

        $remaining  = $total - $processed;
        $percentage = $total > 0 ? ( $processed / $total ) * 100 : 0;

        // Calculate time estimates.
        $time_estimates = $this->calculate_time_estimates( $processed, $remaining );

        return array(
            'processed'        => $processed,
            'total'            => $total,
            'remaining'        => $remaining,
            'percentage'       => $percentage,
            'time_elapsed'     => $time_estimates['elapsed'],
            'time_remaining'   => $time_estimates['remaining'],
            'items_per_minute' => $time_estimates['items_per_minute'],
        );
    }

    /**
     * Calculate time estimates for upload progress.
     *
     * @param int $processed Number of items processed.
     * @param int $remaining Number of items remaining.
     * @return array Array containing time estimates.
     */
    protected function calculate_time_estimates( $processed, $remaining ) {
        $estimates = array(
            'elapsed'          => 0,
            'remaining'        => 0,
            'items_per_minute' => 0,
        );

        if ( 0 === $processed ) {
            return $estimates;
        }

        // Get the timestamp of the first uploaded product to calculate elapsed time.
        $first_upload_time = $this->get_first_upload_timestamp();
        if ( ! $first_upload_time ) {
            return $estimates;
        }

        $elapsed_seconds      = time() - $first_upload_time;
        $estimates['elapsed'] = $elapsed_seconds;

        if ( $elapsed_seconds > 0 ) {
            $items_per_second              = $processed / $elapsed_seconds;
            $estimates['items_per_minute'] = $items_per_second * 60;

            if ( $remaining > 0 && $items_per_second > 0 ) {
                $estimates['remaining'] = $remaining / $items_per_second;
            }
        }

        return $estimates;
    }

    /**
     * Get the timestamp of the first uploaded product.
     *
     * @return int|null The timestamp of the first upload, or null if not found.
     */
    protected function get_first_upload_timestamp() {
        global $wpdb;

        $meta_key = self::POST_DATA_UPLOAD_TIMESTAMP_META;

        $prepared_sql = $wpdb->prepare(
            "SELECT MIN(CAST(meta_value AS UNSIGNED)) as first_upload
             FROM {$wpdb->postmeta}
             WHERE meta_key = %s
             AND meta_value != ''
             AND meta_value != '0'",
            $meta_key
        );

        $result = $wpdb->get_var( $prepared_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

        return $result ? (int) $result : null;
    }

    /**
     * Format duration in seconds to human-readable format.
     *
     * @param int $seconds Duration in seconds.
     * @return string Human-readable duration.
     */
    public function format_duration( $seconds ) {
        if ( $seconds < 60 ) {
            // translators: %d represents the number of seconds.
            return sprintf( _n( '%d second', '%d seconds', $seconds, 'storeagent-ai-for-woocommerce' ), $seconds );
        } elseif ( $seconds < 3600 ) {
            $minutes = round( $seconds / 60 );
            // translators: %d represents the number of minutes.
            return sprintf( _n( '%d minute', '%d minutes', $minutes, 'storeagent-ai-for-woocommerce' ), $minutes );
        } else {
            $hours = round( $seconds / 3600 );
            // translators: %d represents the number of hours.
            return sprintf( _n( '%d hour', '%d hours', $hours, 'storeagent-ai-for-woocommerce' ), $hours );
        }
    }

    /**
     * Get per-post-type index status using efficient direct DB queries.
     *
     * @since 1.1.4
     *
     * @param array $post_types Array of post type slugs. If empty, uses all enabled post types.
     * @return array Associative array keyed by post type slug with index stats.
     */
    public function get_per_post_type_index_status( $post_types = array() ) {
        global $wpdb;

        if ( empty( $post_types ) ) {
            $post_types = Helper::get_post_types_with_ai_memory_enabled();
        }

        $result = array();

        foreach ( $post_types as $post_type ) {
            // Total: published posts with memory enabled ('yes' OR meta not exists).
            $total = (int) $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT COUNT(DISTINCT p.ID)
                    FROM {$wpdb->posts} p
                    LEFT JOIN {$wpdb->postmeta} pm_enabled
                        ON p.ID = pm_enabled.post_id AND pm_enabled.meta_key = %s
                    WHERE p.post_type = %s
                    AND p.post_status = 'publish'
                    AND (pm_enabled.meta_value = 'yes' OR pm_enabled.meta_id IS NULL)",
                    self::POST_AI_MEMORY_ENABLED_META,
                    $post_type
                )
            );

            // Indexed: posts with non-empty upload hash.
            $indexed = (int) $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT COUNT(DISTINCT p.ID)
                    FROM {$wpdb->posts} p
                    INNER JOIN {$wpdb->postmeta} pm_hash
                        ON p.ID = pm_hash.post_id AND pm_hash.meta_key = %s
                    LEFT JOIN {$wpdb->postmeta} pm_enabled
                        ON p.ID = pm_enabled.post_id AND pm_enabled.meta_key = %s
                    WHERE p.post_type = %s
                    AND p.post_status = 'publish'
                    AND pm_hash.meta_value != ''
                    AND (pm_enabled.meta_value = 'yes' OR pm_enabled.meta_id IS NULL)",
                    self::POST_DATA_UPLOAD_HASH_META,
                    self::POST_AI_MEMORY_ENABLED_META,
                    $post_type
                )
            );

            // Failed: posts with retry count >= MAX_UPLOAD_RETRIES.
            $failed = (int) $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT COUNT(DISTINCT p.ID)
                    FROM {$wpdb->posts} p
                    INNER JOIN {$wpdb->postmeta} pm_retry
                        ON p.ID = pm_retry.post_id AND pm_retry.meta_key = %s
                    LEFT JOIN {$wpdb->postmeta} pm_enabled
                        ON p.ID = pm_enabled.post_id AND pm_enabled.meta_key = %s
                    WHERE p.post_type = %s
                    AND p.post_status = 'publish'
                    AND CAST(pm_retry.meta_value AS UNSIGNED) >= %d
                    AND (pm_enabled.meta_value = 'yes' OR pm_enabled.meta_id IS NULL)",
                    self::POST_DATA_UPLOAD_RETRY_COUNT_META,
                    self::POST_AI_MEMORY_ENABLED_META,
                    $post_type,
                    self::MAX_UPLOAD_RETRIES
                )
            );

            // Last indexed timestamp (only for memory-enabled posts, consistent with other queries).
            $last_indexed_raw = $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT MAX(CAST(pm_ts.meta_value AS UNSIGNED))
                    FROM {$wpdb->posts} p
                    INNER JOIN {$wpdb->postmeta} pm_ts
                        ON p.ID = pm_ts.post_id AND pm_ts.meta_key = %s
                    LEFT JOIN {$wpdb->postmeta} pm_enabled
                        ON p.ID = pm_enabled.post_id AND pm_enabled.meta_key = %s
                    WHERE p.post_type = %s
                    AND p.post_status = 'publish'
                    AND pm_ts.meta_value != ''
                    AND pm_ts.meta_value != '0'
                    AND (pm_enabled.meta_value = 'yes' OR pm_enabled.meta_id IS NULL)",
                    self::POST_DATA_UPLOAD_TIMESTAMP_META,
                    self::POST_AI_MEMORY_ENABLED_META,
                    $post_type
                )
            );

            $pending    = max( 0, $total - $indexed - $failed );
            $percentage = $total > 0 ? round( ( $indexed / $total ) * 100, 1 ) : 0;
            $is_syncing = as_has_scheduled_action( self::POST_DATA_UPLOAD_HOOK, array( $post_type ) );

            $last_indexed = null;
            if ( $last_indexed_raw ) {
                $last_indexed = gmdate( 'c', (int) $last_indexed_raw );
            }

            $result[ $post_type ] = array(
                'total'        => $total,
                'indexed'      => $indexed,
                'failed'       => $failed,
                'pending'      => $pending,
                'percentage'   => $percentage,
                'last_indexed' => $last_indexed,
                'is_syncing'   => (bool) $is_syncing,
            );
        }

        return $result;
    }

    /**
     * Display the progress notice.
     */
    public function display_progress_notice() {
        // Only show progress notice if the option is enabled.
        if ( ! Create_Memory::instance()->should_show_setup_progress() ) {
            return;
        }

        $progress_data = $this->get_detailed_upload_progress();

        /***************************************************************************
         * If the progress data is empty, don't show the notice.
         ***************************************************************************
         *
         * This is to prevent the notice from being shown if the progress data is
         * empty. This can happen if we have enabled post types with AI memory, but
         * no posts are actually enabled for AI memory (e.g. excluded posts).
         */
        $maybe_empty_data = array_filter( $progress_data );
        if ( empty( $maybe_empty_data ) ) {
            return;
        }

        $progress      = $progress_data['percentage'];
        $has_scheduled = as_has_scheduled_action( self::POST_DATA_UPLOAD_HOOK );

        // If the progress is 100% and there is no scheduled upload, send the setup completion email.
        if ( 100 === $progress && ! $has_scheduled ) {
            $this->maybe_send_setup_completion_email();
            // Don't show the notice when upload is complete.
            return;
        }

        include SAAI_TEMPLATES_DIR . 'admin/ai-memory/ai-memory-upload-progress-notice.php';
    }

    /**
     * Display vector availability notice.
     * Shows when there are available vector limits and unuploaded products,
     * but no pending upload schedule exists.
     */
    public function display_vector_availability_notice() {
        // Don't show if chat setup is not yet started or we're still showing setup progress.
        if ( ! Create_Memory::instance()->is_memory_created() || Create_Memory::instance()->should_show_setup_progress() ) {
            return;
        }

        // Don't show if there's already a scheduled upload.
        if ( as_has_scheduled_action( self::POST_DATA_UPLOAD_HOOK ) ) {
            return;
        }

        $has_enabled_post_types = Helper::get_post_types_with_ai_memory_enabled();
        if ( empty( $has_enabled_post_types ) ) {
            return;
        }

        // Check if vector uploads are allowed.
        $plan_manager = Plan_Manager::instance();
        if ( ! $plan_manager->is_vector_allowed() || ! $plan_manager->is_vector_upload_allowed() ) {
            return;
        }

        // Check if there are products that need to be uploaded.
        $post_types  = Helper::get_post_types_with_ai_memory_enabled();
        $posts_count = 0;
        foreach ( $post_types as $post_type ) {
            $posts_count += $this->get_posts_for_upload( $post_type, 'count' );
        }

        if ( 0 === $posts_count ) {
            return;
        }

        // Get counts for display.
        $posts_to_show = min( $posts_count, $plan_manager->get_plan_limit( 'vector_items' ) );

        // Display the notice.
        include SAAI_TEMPLATES_DIR . 'admin/ai-memory/ai-memory-vector-availability-notice.php';
    }

    /**
     * Check if setup is complete and send email if needed
     *
     * @param string $post_type The post type to send the email for.
     */
    protected function maybe_send_setup_completion_email( $post_type = null ) {
        $create_memory_instance = Create_Memory::instance();

        // Only send email if setup progress tracking is enabled and email hasn't been sent yet.
        if ( $create_memory_instance->should_show_setup_progress() && ! $create_memory_instance->is_setup_email_sent() ) {
            // Count uploaded posts to include in email.
            $uploaded_posts_count = $this->count_uploaded_posts( $post_type );

            // Send the completion email.
            if ( $create_memory_instance->send_setup_completion_email( $uploaded_posts_count ) ) {
                // Mark email as sent and disable progress tracking.
                $create_memory_instance->mark_setup_email_sent();
            }

            // Create a success notification about the upload completion. This will be shown in the dashboard.
            $this->create_upload_completion_notification( $uploaded_posts_count );

        }

        // Disable setup progress tracking.
        update_option( Create_Memory::SHOW_SETUP_PROGRESS_OPTION, 'no' );
    }

    /**
     * Create a notification about upload completion
     *
     * @param int $uploaded_posts_count Number of posts uploaded.
     */
    protected function create_upload_completion_notification( $uploaded_posts_count ) {
        $notification_instance = Notification::instance();

        if ( $uploaded_posts_count > 0 ) {
            // Create success notification for successful upload.
            $title = __( 'AI Memory Setup Complete!', 'storeagent-ai-for-woocommerce' );

            $message = sprintf(
                /* translators: %1$d: number of posts uploaded */
                _n(
                    'Successfully uploaded %1$d post to AI memory. Your StoreAgent is now ready to provide intelligent customer support!',
                    'Successfully uploaded %1$d posts to AI memory. Your StoreAgent is now ready to provide intelligent customer support!',
                    $uploaded_posts_count,
                    'storeagent-ai-for-woocommerce'
                ),
                $uploaded_posts_count
            );

            $extra_data = array(
                'uploaded_count' => $uploaded_posts_count,
                'setup_date'     => current_time( 'mysql' ),
            );

            $notification_instance->create_success_notification(
                $title,
                $message,
                '',
                null,
                $extra_data
            );
        } else {
            // Create info notification if no products were uploaded.
            $title   = __( 'AI Memory Setup Complete', 'storeagent-ai-for-woocommerce' );
            $message = sprintf(
                __( 'AI memory setup process is complete, but no posts were uploaded. You can enable AI memory for individual posts from the posts page.', 'storeagent-ai-for-woocommerce' )
            );

            $notification_instance->create_info_notification(
                $title,
                $message,
                'AI Memory Setup'
            );
        }
    }

    /**
     * Count the number of posts that have been uploaded to AI memory
     *
     * @param string|array|null $post_type The post type(s) to count the uploaded posts for.
     * @return int Number of uploaded posts.
     */
    protected function count_uploaded_posts( $post_type = null ) {

        if ( ! $post_type ) {
            $post_types = Helper::get_post_types_with_ai_memory_enabled();
        } else {
            $post_types = is_array( $post_type ) ? $post_type : array( $post_type );
        }

        $args = array(
            'post_type'      => $post_types,
            'posts_per_page' => -1,
            'fields'         => 'ids',
            'post_status'    => 'publish',
            'meta_query'     => array(
                array(
                    'key'     => self::POST_DATA_UPLOAD_HASH_META,
                    'compare' => 'EXISTS',
                ),
                array(
                    'key'     => self::POST_DATA_UPLOAD_HASH_META,
                    'value'   => '',
                    'compare' => '!=',
                ),
            ),
        );

        $query = new \WP_Query( $args );
        return $query->found_posts;
    }

    /**
     * Schedule post data deletion from AI memory.
     *
     * @param array    $post_ids  Array of post IDs to delete from AI memory.
     * @param string   $post_type The post type for which to schedule the deletion.
     * @param int|null $timestamp Optional timestamp to schedule the deletion for.
     */
    public function schedule_post_data_deletion( $post_ids, $post_type, $timestamp = null ) {
        // Ensure we have an array of post IDs.
        if ( ! is_array( $post_ids ) ) {
            $post_ids = array( $post_ids );
        }

        // Remove any invalid IDs and ensure uniqueness.
        $post_ids = array_unique( array_filter( array_map( 'absint', $post_ids ) ) );

        // If we have post IDs to add, merge them with existing queue.
        if ( ! empty( $post_ids ) ) {
            // Get existing queue from transient.
            $existing_queue = get_transient( self::POST_DATA_DELETE_QUEUE_KEY . '_' . $post_type );
            if ( ! is_array( $existing_queue ) ) {
                $existing_queue = array();
            }

            // Merge new IDs with existing queue.
            $merged_queue = array_unique( array_merge( $existing_queue, $post_ids ) );

            // Store updated queue in transient (expire in 2 hours to be safe).
            set_transient( self::POST_DATA_DELETE_QUEUE_KEY . '_' . $post_type, $merged_queue, 2 * HOUR_IN_SECONDS );
        }

        // Schedule action if not already scheduled.
        if ( ! as_has_scheduled_action( self::POST_DATA_DELETE_HOOK, array( $post_type ) ) ) {
            if ( $timestamp ) {
                as_schedule_single_action( $timestamp, self::POST_DATA_DELETE_HOOK, array( $post_type ) );
            } else {
                as_enqueue_async_action( self::POST_DATA_DELETE_HOOK, array( $post_type ) );
            }
        }
    }

    /**
     * Handle AI memory disabled - schedule deletion from AI memory.
     *
     * @param int    $post_id The post ID for which AI memory was disabled.
     * @param string $post_type The post type for which AI memory was disabled.
     */
    public function remove_post_data_from_ai_memory( $post_id, $post_type = null ) {
        if ( ! $post_type ) {
            $post_type = get_post_type( $post_id );
        }

        $post = 'product' === $post_type ? wc_get_product( $post_id ) : get_post( $post_id );

        if ( ! $post ) {
            return;
        }

        // Check if the post has AI memory data that needs to be deleted.
        $upload_hash = Helper::get_post_meta( $post_id, self::POST_DATA_UPLOAD_HASH_META );

        if ( ! empty( $upload_hash ) ) {
            // Schedule deletion for the next day.
            $this->schedule_post_data_deletion( array( $post_id ), $post_type, time() + HOUR_IN_SECONDS );
        }
    }

    /**
     * Process post data deletion from AI memory.
     * Reads post IDs from transient queue and processes them in batches.
     *
     * @param string $post_type The post type for which to process the deletion.
     * @return array Array of successfully processed post IDs.
     */
    public function process_post_data_deletion( $post_type ) {
        // Get post IDs from transient queue.
        $post_ids = get_transient( self::POST_DATA_DELETE_QUEUE_KEY . '_' . $post_type );

        if ( ! is_array( $post_ids ) || empty( $post_ids ) ) {
            return array();
        }

        // Remove any invalid IDs.
        $post_ids = array_filter( array_map( 'absint', $post_ids ) );

        if ( empty( $post_ids ) ) {
            delete_transient( self::POST_DATA_DELETE_QUEUE_KEY . '_' . $post_type );
            return array();
        }

        // Process only batch size number of posts.
        $batch_posts     = array_slice( $post_ids, 0, $this->get_posts_delete_per_batch() );
        $remaining_posts = array_slice( $post_ids, $this->get_posts_delete_per_batch() );
        $logger          = wc_get_logger();

        $processed_posts = array();

        foreach ( $batch_posts as $post_id ) {
            $post = 'product' === $post_type ? wc_get_product( $post_id ) : get_post( $post_id );

            // Validate post when it still exists to ensure that the AI memory is disabled for it.
            if ( $post ) {
                $upload_hash = Helper::get_post_meta( $post_id, self::POST_DATA_UPLOAD_HASH_META, true );
                $is_enabled  = Helper::get_post_meta( $post_id, self::POST_AI_MEMORY_ENABLED_META, true );

                // Skip if the post data is not uploaded or AI memory is NOT disabled for the post.
                if ( empty( $upload_hash ) || 'no' !== $is_enabled ) {
                    $logger->info(
                        sprintf(
                            'Skipping post data deletion from AI memory (post ID: %s, hash: %s, enabled: %s)',
                            $post_id,
                            $upload_hash,
                            $is_enabled
                        ),
                        array(
                            'source' => 'storeagent-ai-for-woocommerce',
                        )
                    );
                    continue;
                }
            }

            // Delete post data from vector endpoint.
            $response = $this->delete_post_data_from_vector( $post_id );

            if ( is_wp_error( $response ) ) {
                $logger->error(
                    sprintf(
                        'Failed to delete post data from AI memory (post ID: %s): %s',
                        $post_id,
                        $response->get_error_message()
                    ),
                    array(
                        'source' => 'storeagent-ai-for-woocommerce',
                    )
                );
                continue;
            }

            // Clean up metadata if post still exists.
            if ( $post ) {
                Helper::delete_post_meta( $post_id, self::POST_DATA_UPLOAD_HASH_META );
                Helper::delete_post_meta( $post_id, self::POST_DATA_UPLOAD_TIMESTAMP_META );
                Helper::delete_post_meta( $post_id, self::POST_DATA_CURRENT_HASH_META );
            }

            $processed_posts[] = $post_id;
        }

        // Handle remaining posts.
        if ( ! empty( $remaining_posts ) ) {
            // Update transient with remaining posts.
            set_transient( self::POST_DATA_DELETE_QUEUE_KEY . '_' . $post_type, $remaining_posts, 2 * HOUR_IN_SECONDS );

            // Schedule another async batch to process remaining posts.
            $this->schedule_post_data_deletion( array(), $post_type ); // Empty array triggers async without adding new IDs.
        } else {
            // Clear the transient queue when all posts are processed.
            delete_transient( self::POST_DATA_DELETE_QUEUE_KEY . '_' . $post_type );
        }

        return $processed_posts;
    }

    /**
     * Delete post data from vector endpoint.
     *
     * @param int $post_id The post ID.
     * @return true|\WP_Error True on success, WP_Error on failure.
     */
    protected function delete_post_data_from_vector( $post_id ) {
        $headers = array(
            'Content-Type'  => 'application/json',
            'X-Origin'      => wp_parse_url( get_home_url(), PHP_URL_HOST ),
            'Authorization' => 'Basic ' . base64_encode( Connect::get_consumer_key() . ':' . Connect::get_consumer_secret() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
        );

        $response = wp_remote_request(
            Helper::get_storeagent_base_url( 'api' ) . '/storeagent/v1/memory/data',
            array(
                'method'  => 'DELETE',
                'headers' => $headers,
                'body'    => wp_json_encode(
                    array(
                        'data_id' => $post_id,
                    )
                ),
                'timeout' => 30,
            )
        );

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

        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        $code = wp_remote_retrieve_response_code( $response );

        if ( $code > 299 ) {
            return new \WP_Error(
                'storeagent_delete_post_data_error',
                isset( $body['message'] ) ? $body['message'] : __( 'Something went wrong while deleting post data from AI memory.', 'storeagent-ai-for-woocommerce' ),
                $body
            );
        }

        return true;
    }

    /**
     * Get the number of available vector slots.
     *
     * @return int Number of available vector slots.
     */
    protected function get_available_vector_count() {
        $plan_manager = Plan_Manager::instance();
        $limit        = $plan_manager->get_plan_limit( 'vector_items' );
        $used         = $plan_manager->get_usage_count( 'vector_items' );

        // If limit is 0, it might mean unlimited.
        if ( 0 === $limit ) {
            return PHP_INT_MAX;
        }

        return max( 0, $limit - $used );
    }

    /**
     * Check if all post types have finished uploading.
     *
     * @since 1.1.2
     *
     * @return bool True if all post types are done, false otherwise.
     */
    protected function are_all_post_types_done() {
        $post_types = Helper::get_post_types_with_ai_memory_enabled();

        if ( empty( $post_types ) ) {
            return true;
        }

        // Check if any post type still has posts to upload.
        foreach ( $post_types as $post_type ) {
            $remaining_count = $this->get_posts_for_upload( $post_type, 'count' );
            if ( $remaining_count > 0 ) {
                return false;
            }
        }

        return true;
    }

    /**
     * Schedule uploads for all enabled post types.
     * Shared method for rescheduling uploads.
     *
     * @since 1.1.2
     *
     * @param bool $stagger Whether to stagger the uploads with delays.
     * @return void
     */
    protected function schedule_uploads_for_enabled_post_types( $stagger = false ) {
        $settings = get_option( Chat_Post_Types::OPTION_KEY, array() );
        $minute   = 0;

        // Set upload in progress flag when scheduling uploads.
        set_transient( self::UPLOAD_IN_PROGRESS_FLAG_KEY, true, HOUR_IN_SECONDS );

        foreach ( $settings as $post_type => $config ) {
            if ( 'yes' === $config['enabled'] ) {
                $timestamp = $stagger ? time() + ( MINUTE_IN_SECONDS * 20 * $minute ) : null;
                $this->schedule_post_data_upload( $post_type, $timestamp );
                ++$minute;
            }
        }
    }

    /**
     * Maybe schedule upload via Heartbeat API.
     * This is a fallback mechanism for hosting providers that don't allow
     * creating async schedules after running process_post_data_upload.
     *
     * @since 1.1.2
     *
     * @param array $response Heartbeat response data.
     * @param array $data     Heartbeat data received.
     * @return array Heartbeat response data.
     */
    public function maybe_schedule_upload_via_heartbeat( $response, $data ) {
        // Only run in admin area.
        if ( ! is_admin() ) {
            return $response;
        }

        // Only check if upload is in progress.
        if ( ! get_transient( self::UPLOAD_IN_PROGRESS_FLAG_KEY ) ) {
            return $response;
        }

        // Check if there's already a scheduled action.
        if ( as_has_scheduled_action( self::POST_DATA_UPLOAD_HOOK ) ) {
            return $response;
        }

        // Check if vector upload is allowed.
        $plan_manager      = Plan_Manager::instance();
        $is_upload_allowed = $plan_manager->is_vector_upload_allowed();

        if ( ! $is_upload_allowed ) {
            // Clear upload in progress flag when upload is not allowed.
            delete_transient( self::UPLOAD_IN_PROGRESS_FLAG_KEY );
            return $response;
        }

        // Check if all post types are done uploading.
        $all_done = $this->are_all_post_types_done();

        if ( $all_done ) {
            // Clear upload in progress flag when all post types are done.
            delete_transient( self::UPLOAD_IN_PROGRESS_FLAG_KEY );
            return $response;
        }

        // Use get_detailed_upload_progress to check if there are remaining posts to upload.
        $progress_data = $this->get_detailed_upload_progress();

        // Reschedule if processing is lower than total (meaning there are still posts to upload).
        if ( $progress_data['processed'] < $progress_data['total'] ) {
            $this->schedule_uploads_for_enabled_post_types();
        } else {
            // Clear upload in progress flag when all posts are processed.
            delete_transient( self::UPLOAD_IN_PROGRESS_FLAG_KEY );
        }

        return $response;
    }

    /**
     * AJAX handler for retrying product upload.
     *
     * @since 1.0.7
     * @return void
     */
    public function ajax_retry_upload_to_ai_memory() {
        // Verify nonce.
        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'saai_retry_upload_to_ai_memory' ) ) {
            wp_send_json_error( array( 'message' => __( 'Security check failed.', 'storeagent-ai-for-woocommerce' ) ) );
        }

        // Check user capabilities.
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            wp_send_json_error( array( 'message' => __( 'You don\'t have permission to perform this action.', 'storeagent-ai-for-woocommerce' ) ) );
        }

        // Check if already scheduled.
        // If already scheduled, return success since the goal is achieved (upload will happen).
        if ( as_has_scheduled_action( self::POST_DATA_UPLOAD_HOOK ) ) {
            wp_send_json_success( array( 'message' => __( 'Product upload is already scheduled.', 'storeagent-ai-for-woocommerce' ) ) );
            return;
        }

        // Schedule the upload with staggered delays.
        $this->schedule_uploads_for_enabled_post_types( true );

        // Send success response.
        wp_send_json_success( array( 'message' => __( 'Product upload scheduled.', 'storeagent-ai-for-woocommerce' ) ) );
    }

    /**
     * Get the number of posts per upload batch.
     *
     * @return int The number of posts per batch.
     */
    protected function get_posts_per_batch() {
        /**
         * Filter the number of posts per upload batch.
         *
         * @since 1.3.1
         *
         * @param int $batch_size The number of posts per batch. Default POSTS_PER_BATCH constant value.
         */
        return max( 1, absint( apply_filters( 'saai_posts_per_batch', self::POSTS_PER_BATCH ) ) );
    }

    /**
     * Get the number of posts per delete batch.
     *
     * @return int The number of posts per delete batch.
     */
    protected function get_posts_delete_per_batch() {
        /**
         * Filter the number of posts per delete batch.
         *
         * @since 1.3.1
         *
         * @param int $batch_size The number of posts per delete batch. Default POSTS_DELETE_PER_BATCH constant value.
         */
        return max( 1, absint( apply_filters( 'saai_posts_delete_per_batch', self::POSTS_DELETE_PER_BATCH ) ) );
    }
}
