<?php
/**
 * Trait Niwa_AI_Ajax_Handlers
 *
 * Codex OAuth, index/embeddings, and simple AJAX handlers
 * extracted from class-niwa-ai.php.
 *
 * @package Niwa_AI
 */

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

trait Niwa_AI_Ajax_Handlers {

    public function handle_codex_oauth_start() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( 'Forbidden' );
        }

        check_admin_referer( 'niwa_ai_codex_oauth_start', 'niwa_ai_codex_oauth_start_nonce' );

        if ( ! $this->auth_manager ) {
            $this->redirect_to_admin_with_oauth_notice( 'error', 'Auth manager is not initialized.' );
            return;
        }

        // Reconnect should start from a clean local OAuth state so the panel
        // switches into the device-flow UI instead of continuing to display
        // the previous connection as active.
        if ( method_exists( $this->auth_manager, 'is_oauth_connected' ) && $this->auth_manager->is_oauth_connected() ) {
            $this->auth_manager->clear_oauth_credentials();
        }

        $start = $this->auth_manager->start_codex_device_flow( get_current_user_id() );
        if ( empty( $start['success'] ) ) {
            $error_msg = isset( $start['error'] ) ? (string) $start['error'] : 'Unable to start OAuth verification.';

            // Persist the error so it stays visible in the OAuth panel after redirect.
            $this->auth_manager->set_oauth_last_error( $error_msg );

            $this->redirect_to_admin_with_oauth_notice( 'error', $error_msg );
            return;
        }

        $this->redirect_to_admin_with_oauth_notice( 'device_started' );
    }

    public function handle_codex_oauth_callback() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( 'Forbidden' );
        }

        if ( ! $this->auth_manager ) {
            $this->redirect_to_admin_with_oauth_notice( 'error', 'Auth manager is not initialized.' );
            return;
        }

        $code = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : '';
        $state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : '';
        $redirect_uri = admin_url( 'admin-post.php?action=niwa_ai_codex_oauth_callback' );
        $result = $this->auth_manager->complete_codex_oauth(
            array(
                'code' => $code,
                'state' => $state,
                'redirect_uri' => $redirect_uri,
                'user_id' => get_current_user_id(),
            )
        );

        if ( empty( $result['success'] ) ) {
            $this->redirect_to_admin_with_oauth_notice(
                'error',
                isset( $result['error'] ) ? (string) $result['error'] : 'OAuth failed.'
            );
            return;
        }

        if ( function_exists( 'niwa_ai_queue_admin_autonomy_poll_restart' ) ) {
            niwa_ai_queue_admin_autonomy_poll_restart( 'oauth-connected' );
        }

        $this->redirect_to_admin_with_oauth_notice( 'connected' );
    }

    public function handle_codex_oauth_disconnect() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( 'Forbidden' );
        }

        check_admin_referer( 'niwa_ai_codex_oauth_disconnect', 'niwa_ai_codex_oauth_disconnect_nonce' );

        if ( $this->auth_manager ) {
            $this->auth_manager->clear_oauth_credentials();
        }
        if ( function_exists( 'niwa_ai_queue_admin_autonomy_poll_restart' ) ) {
            niwa_ai_queue_admin_autonomy_poll_restart( 'oauth-disconnected' );
        }

        $this->redirect_to_admin_with_oauth_notice( 'disconnected' );
    }

    public function ajax_codex_oauth_poll() {
        check_ajax_referer( 'niwa_ai_codex_oauth_poll', 'nonce' );

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error(
                array(
                    'message' => 'Forbidden',
                ),
                403
            );
        }

        if ( ! $this->auth_manager ) {
            wp_send_json_error(
                array(
                    'message' => 'Auth manager is not initialized.',
                ),
                500
            );
        }

        $result = $this->auth_manager->poll_codex_device_flow( get_current_user_id() );
        if ( ! empty( $result['success'] ) ) {
            if ( function_exists( 'niwa_ai_queue_admin_autonomy_poll_restart' ) ) {
                niwa_ai_queue_admin_autonomy_poll_restart( 'oauth-connected' );
            }
            $redirect_args = array(
                'page'       => isset( $_REQUEST['page'] ) ? sanitize_key( wp_unslash( $_REQUEST['page'] ) ) : 'niwa-ai',
                'niwa_oauth' => 'connected',
            );
            wp_send_json_success(
                array(
                    'connected' => true,
                    'pending' => false,
                    'message' => 'Codex OAuth connected.',
                    'redirect_url' => add_query_arg( $redirect_args, admin_url( 'admin.php' ) ),
                )
            );
        }

        if ( ! empty( $result['pending'] ) ) {
            wp_send_json_success(
                array(
                    'connected' => false,
                    'pending' => true,
                    'retry_after' => isset( $result['retry_after'] ) ? max( 1, intval( $result['retry_after'] ) ) : 5,
                    'message' => isset( $result['message'] ) ? (string) $result['message'] : 'Waiting for authorization in browser.',
                )
            );
        }

        wp_send_json_error(
            array(
                'message' => isset( $result['error'] ) ? (string) $result['error'] : 'OAuth failed.',
            ),
            400
        );
    }

    /**
     * Vraca svez `niwa_ai_chat_nonce` ulogovanim admin korisnicima.
     *
     * Koristi se iz Niwa admin chat klijenta na dva nacina:
     *   1) Periodicni heartbeat (svakih ~10 min) koji preventivno osvezava nonce
     *      dok je admin tab vidljiv, tako da nonce nikad ne istekne tokom rada.
     *   2) Reactive recovery — kad chat detektuje HTML response (npr. WP nonce
     *      expired ili WAF redirect), pokusava jednom retry sa svezim nonce-om
     *      pre nego sto pribegne `window.location.reload()`.
     *
     * Auth: koristi WP cookie sesiju, ne zahteva nonce (jer je svrha endpoint-a
     * upravo da resi nonce expiry problem). Ogranicen na ulogovane korisnike sa
     * `read` cap-om i validnom user sesijom. Ne otkriva nikakve podatke osim
     * novog nonce stringa.
     *
     * @return void
     */
    public function ajax_admin_refresh_nonce() {
        if ( ! is_user_logged_in() ) {
            wp_send_json_error( array( 'error' => 'not_logged_in' ), 401 );
        }

        $user_id = get_current_user_id();
        if ( ! $user_id ) {
            wp_send_json_error( array( 'error' => 'no_user' ), 401 );
        }

        // Minimalan capability check — bilo koji ulogovani korisnik koji moze
        // da vidi admin moze da osvezi chat nonce za sebe. Nemoguce je dobiti
        // tudji nonce jer wp_create_nonce vezuje za current user.
        $scope        = isset( $_POST['scope'] ) ? sanitize_key( wp_unslash( $_POST['scope'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $required_cap = 'admin_autonomy' === $scope ? 'manage_options' : 'read';
        if ( ! current_user_can( $required_cap ) ) {
            wp_send_json_error( array( 'error' => 'forbidden' ), 403 );
        }

        $nonce_action = 'admin_autonomy' === $scope ? 'niwa_ai_admin_autonomy_chat_nonce' : 'niwa_ai_chat_nonce';

        wp_send_json_success(
            array(
                'nonce'      => wp_create_nonce( $nonce_action ),
                'expires_in' => intval( apply_filters( 'nonce_life', DAY_IN_SECONDS ) ),
            )
        );
    }

    private function redirect_to_admin_with_oauth_notice( $status, $message = '' ) {
        $args = array(
            'page' => 'niwa-ai',
            'niwa_oauth' => sanitize_key( (string) $status ),
        );
        if ( '' !== $message ) {
            $args['niwa_oauth_message'] = sanitize_text_field( (string) $message );
        }
        wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) );
        exit;
    }

    public function ajax_save_contact() {
        check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );

        // Security: honeypot + rate limit.
        $security = Niwa_AI_Security_Service::validate_email_submission();
        if ( ! $security['allowed'] ) {
            $reason = isset( $security['reason'] ) ? $security['reason'] : 'blocked';
            $msg    = 'rate_limit_exceeded' === $reason
                ? 'Too many attempts. Please try again later.'
                : 'Request blocked.';
            wp_send_json_error(
                array(
                    'error'   => $msg,
                    'message' => $msg,
                )
            );
        }

        Niwa_AI_Security_Service::increment_rate_limit();

        if ( $this->contact_service ) {
            $this->contact_service->ensure_table_schema();
        }

        $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
        $name  = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
        $phone = isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '';
        $source = isset( $_POST['source'] ) ? sanitize_key( wp_unslash( $_POST['source'] ) ) : 'chat';
        if ( ! in_array( $source, array( 'chat', 'cb_code_email_hunt' ), true ) ) {
            $source = 'chat';
        }

        // Phone-only mode support: allow empty email when phone_capture_mode is phone_only.
        $lead_opts          = class_exists( 'Niwa_AI_Lead_Capture_Settings' ) ? Niwa_AI_Lead_Capture_Settings::get_settings() : array();
        $phone_capture_mode = isset( $lead_opts['phone_capture_mode'] ) ? $lead_opts['phone_capture_mode'] : 'email_phone';

        if ( 'phone_only' === $phone_capture_mode && empty( $email ) && ! empty( $phone ) ) {
            // Skip email validation — phone-only mode.
        } else {
            // Email validation: format + disposable domain check.
            $email_check = $this->email_validator->validate( $email );
            if ( ! $email_check['valid'] ) {
                $msg = isset( $email_check['message'] ) ? $email_check['message'] : 'Invalid email address.';
                wp_send_json_error(
                    array(
                        'error'   => $msg,
                        'message' => $msg,
                    )
                );
            }
        }

        $result = $this->contact_service->save_contact( $email, $name, $phone, $source );

        if ( ! empty( $result['success'] ) ) {
            wp_send_json_success( $result );
        } else {
            $error = '';
            if ( is_array( $result ) ) {
                if ( ! empty( $result['message'] ) ) {
                    $error = (string) $result['message'];
                } elseif ( ! empty( $result['error'] ) ) {
                    $error = (string) $result['error'];
                }
            }
            if ( '' === $error ) {
                $error = 'Failed to save contact.';
            }
            wp_send_json_error(
                array(
                    'error'   => $error,
                    'message' => $error,
                )
            );
        }
    }

    public function handle_run_full_index_and_embeddings() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( 'Forbidden' );
        }

        check_admin_referer( 'niwa_ai_run_full_index_and_embeddings', 'niwa_ai_run_full_index_and_embeddings_nonce' );

        if ( $this->content_store ) {
            $this->content_store->ensure_tables();
            // True full rebuild: clear previous index scope before re-queueing.
            $this->reset_full_index_scope();
        }

        $queue = $this->build_index_queue();
        $total = count( $queue );
        update_option(
            Niwa_AI_Constants::OPT_INDEX_PROGRESS,
            array(
                'total' => $total,
                'processed' => 0,
                'status' => 'running',
                'embeddings_started' => 0,
                'started_at' => time(),
                'updated_at' => time(),
            ),
            false
        );
        update_option( Niwa_AI_Constants::OPT_INDEX_QUEUE, $queue, false );
        delete_option( Niwa_AI_Constants::OPT_EMBEDDING_LAST_ERROR );
        update_option( Niwa_AI_Constants::OPT_EMBEDDING_KICK, 0, false );

        if ( ! wp_next_scheduled( 'niwa_ai_process_index_queue' ) ) {
            wp_schedule_single_event( time() + Niwa_AI_Constants::INDEX_QUEUE_DELAY, 'niwa_ai_process_index_queue' );
        }
        $this->dispatch_index_background_worker( 'start' );

        $url = add_query_arg(
            array(
                'page' => 'niwa-ai',
                'niwa_full_embed' => 'done',
            ),
            admin_url( 'admin.php' )
        );

        wp_safe_redirect( $url );
        exit;
    }

    public function handle_process_index_queue() {
        $processed = $this->process_index_queue_batch( Niwa_AI_Constants::INDEX_QUEUE_BATCH_SIZE );
        $progress = get_option( Niwa_AI_Constants::OPT_INDEX_PROGRESS, array() );
        $status = isset( $progress['status'] ) ? (string) $progress['status'] : 'idle';

        if ( 'done' !== $status && ! wp_next_scheduled( 'niwa_ai_process_index_queue' ) ) {
            wp_schedule_single_event( time() + Niwa_AI_Constants::INDEX_QUEUE_DELAY, 'niwa_ai_process_index_queue' );
            $this->dispatch_index_background_worker( 'cron-chain' );
        }
        if ( 'done' === $status ) {
            $this->maybe_start_embeddings();
        }
    }

    private function build_index_queue() {
        $queue = array();
        $types = $this->get_full_index_post_types();
        foreach ( $types as $type ) {
            $ids = get_posts(
                array(
                    'post_type' => $type,
                    'post_status' => $this->get_full_index_post_statuses( $type ),
                    'posts_per_page' => -1,
                    'fields' => 'ids',
                    'no_found_rows' => true,
                )
            );
            foreach ( $ids as $id ) {
                $queue[] = array(
                    'id' => intval( $id ),
                    'type' => $type,
                );
            }
        }
        $queue[] = array(
            'id' => 0,
            'type' => 'site_knowledge',
        );
        return $queue;
    }

    private function process_index_queue_batch( $batch_size = null ) {
        if ( null === $batch_size ) {
            $batch_size = Niwa_AI_Constants::INDEX_QUEUE_BATCH_SIZE;
        }
        $queue = get_option( Niwa_AI_Constants::OPT_INDEX_QUEUE, array() );
        if ( empty( $queue ) || ! is_array( $queue ) ) {
            $this->mark_index_done();
            return 0;
        }

        $batch = array_splice( $queue, 0, max( 1, intval( $batch_size ) ) );
        $processed = 0;
        foreach ( $batch as $item ) {
            $post_id = isset( $item['id'] ) ? intval( $item['id'] ) : 0;
            $post_type = isset( $item['type'] ) ? (string) $item['type'] : '';
            if ( '' !== $post_type && $this->index_worker ) {
                if ( $post_id > 0 || 'site_knowledge' === $post_type ) {
                    $this->index_worker->handle_upsert( $post_id, $post_type, true );
                    $processed++;
                }
            }
        }

        update_option( Niwa_AI_Constants::OPT_INDEX_QUEUE, $queue, false );

        $progress = get_option( Niwa_AI_Constants::OPT_INDEX_PROGRESS, array() );
        $progress['processed'] = isset( $progress['processed'] ) ? intval( $progress['processed'] ) + $processed : $processed;
        $progress['updated_at'] = time();
        update_option( Niwa_AI_Constants::OPT_INDEX_PROGRESS, $progress, false );

        if ( empty( $queue ) ) {
            $this->mark_index_done();
        }

        return $processed;
    }

    private function get_full_index_post_types() {
        $types = array( 'product', 'page', 'post', 'elementor_library', 'wp_template', 'wp_template_part', 'wp_block', 'wp_navigation' );
        return apply_filters( 'niwa_ai_index_post_types', $types );
    }

    private function get_full_index_post_statuses( $post_type ) {
        $template_like = array( 'elementor_library', 'wp_template', 'wp_template_part', 'wp_block', 'wp_navigation' );
        if ( in_array( (string) $post_type, $template_like, true ) ) {
            // Theme builder artifacts can be private/inherit depending on builder/theme.
            return array( 'publish', 'private', 'inherit', 'draft' );
        }
        return 'publish';
    }

    private function reset_full_index_scope() {
        if ( ! $this->content_store ) {
            return;
        }
        foreach ( $this->get_full_index_post_types() as $type ) {
            $this->content_store->delete_by_post_type( $type );
        }
        $this->content_store->delete_by_post_type( 'site_knowledge' );
    }

    private function mark_index_done() {
        $progress = get_option( Niwa_AI_Constants::OPT_INDEX_PROGRESS, array() );
        $progress['status'] = 'done';
        $progress['finished_at'] = time();
        if ( isset( $progress['total'] ) ) {
            $progress['processed'] = intval( $progress['total'] );
        }
        update_option( Niwa_AI_Constants::OPT_INDEX_PROGRESS, $progress, false );

        // Invalidate cached overviews so they rebuild with fresh indexed data.
        delete_transient( 'niwa_ai_store_overview' );
        delete_transient( 'niwa_ai_site_knowledge_blob' );
        delete_transient( 'niwa_ai_name_to_id_map' );

        $this->maybe_start_embeddings();
    }

    private function maybe_start_embeddings() {
        if ( ! $this->memory_embedder ) {
            return;
        }

        $progress = get_option( Niwa_AI_Constants::OPT_INDEX_PROGRESS, array() );
        $status = isset( $progress['status'] ) ? (string) $progress['status'] : 'idle';
        if ( 'done' !== $status ) {
            return;
        }

        if ( ! empty( $progress['embeddings_started'] ) ) {
            return;
        }

        $progress['embeddings_started'] = time();
        update_option( Niwa_AI_Constants::OPT_INDEX_PROGRESS, $progress, false );

        $this->memory_embedder->process_pending( Niwa_AI_Constants::EMBEDDING_INITIAL_BATCH_SIZE );
        if ( ! wp_next_scheduled( 'niwa_ai_process_memory_embeddings' ) ) {
            wp_schedule_single_event( time() + Niwa_AI_Constants::EMBEDDING_QUEUE_DELAY, 'niwa_ai_process_memory_embeddings' );
        }
        $this->dispatch_index_background_worker( 'embeddings-start' );
    }

    public function handle_process_memory_embeddings() {
        if ( ! $this->memory_embedder ) {
            return;
        }

        $this->memory_embedder->process_pending( Niwa_AI_Constants::EMBEDDING_BATCH_SIZE );

        $stats = $this->content_store ? $this->content_store->get_embedding_stats() : array();
        $pending = isset( $stats['pending'] ) ? intval( $stats['pending'] ) : 0;
        if ( $pending > 0 && ! wp_next_scheduled( 'niwa_ai_process_memory_embeddings' ) ) {
            wp_schedule_single_event( time() + Niwa_AI_Constants::EMBEDDING_QUEUE_DELAY, 'niwa_ai_process_memory_embeddings' );
            $this->dispatch_index_background_worker( 'embedding-chain' );
        }
    }

    public function handle_index_background_worker() {
        ignore_user_abort( true );
        if ( function_exists( 'set_time_limit' ) ) {
            @set_time_limit( 120 );
        }

        if ( ! $this->verify_index_worker_signature() ) {
            status_header( 403 );
            echo 'forbidden';
            exit;
        }

        $lock_token = $this->acquire_named_option_lock( 'index_background_worker', 1, 120 );
        if ( '' === $lock_token ) {
            status_header( 200 );
            echo 'busy';
            exit;
        }

        $has_more = false;
        try {
            $has_more = $this->run_index_background_slice();
        } finally {
            $this->release_named_option_lock( 'index_background_worker', $lock_token );
        }

        if ( $has_more ) {
            $this->dispatch_index_background_worker( 'chain' );
        }

        status_header( 200 );
        echo 'ok';
        exit;
    }

    private function run_index_background_slice() {
        $has_more = false;
        $progress = get_option( Niwa_AI_Constants::OPT_INDEX_PROGRESS, array() );
        $status   = isset( $progress['status'] ) ? (string) $progress['status'] : 'idle';

        if ( 'running' === $status ) {
            $queue = get_option( Niwa_AI_Constants::OPT_INDEX_QUEUE, array() );
            if ( ! empty( $queue ) && is_array( $queue ) ) {
                $this->process_index_queue_batch( Niwa_AI_Constants::INDEX_QUEUE_BATCH_SIZE );
            }

            $progress = get_option( Niwa_AI_Constants::OPT_INDEX_PROGRESS, array() );
            $status   = isset( $progress['status'] ) ? (string) $progress['status'] : $status;
            if ( 'running' === $status ) {
                $this->schedule_index_background_work( 'index-chain' );
                return true;
            }
        }

        if ( 'done' === $status ) {
            $this->maybe_start_embeddings();
            if ( $this->memory_embedder ) {
                $this->memory_embedder->process_pending( Niwa_AI_Constants::EMBEDDING_BATCH_SIZE );
            }

            $stats   = $this->content_store ? $this->content_store->get_embedding_stats() : array();
            $pending = isset( $stats['pending'] ) ? intval( $stats['pending'] ) : 0;
            if ( $pending > 0 ) {
                $this->schedule_embedding_background_work( 'embedding-chain' );
                $has_more = true;
            }
        }

        return $has_more;
    }

    private function schedule_index_background_work( $reason ) {
        if ( ! wp_next_scheduled( 'niwa_ai_process_index_queue' ) ) {
            wp_schedule_single_event( time() + Niwa_AI_Constants::INDEX_QUEUE_DELAY, 'niwa_ai_process_index_queue' );
        }
    }

    private function schedule_embedding_background_work( $reason ) {
        if ( ! wp_next_scheduled( 'niwa_ai_process_memory_embeddings' ) ) {
            wp_schedule_single_event( time() + Niwa_AI_Constants::EMBEDDING_QUEUE_DELAY, 'niwa_ai_process_memory_embeddings' );
        }
    }

    private function dispatch_index_background_worker( $reason = 'queue' ) {
        $secret = $this->get_index_worker_secret();
        $ts     = time();
        $sig    = hash_hmac( 'sha256', 'index_background_worker|' . $ts, $secret );

        wp_remote_post(
            add_query_arg(
                array(
                    'action' => 'niwa_ai_index_background_worker',
                    'reason' => sanitize_key( $reason ),
                ),
                admin_url( 'admin-post.php' )
            ),
            array(
                'timeout'   => 1,
                'blocking'  => false,
				'sslverify' => true,
                'body'      => array(
                    'niwa_ts'  => $ts,
                    'niwa_sig' => $sig,
                ),
            )
        );
    }

    private function verify_index_worker_signature() {
        $ts  = isset( $_POST['niwa_ts'] ) ? intval( $_POST['niwa_ts'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification
        $sig = isset( $_POST['niwa_sig'] ) ? sanitize_text_field( wp_unslash( $_POST['niwa_sig'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
        if ( 0 === $ts || '' === $sig || abs( time() - $ts ) > 120 ) {
            return false;
        }

        $expected = hash_hmac( 'sha256', 'index_background_worker|' . $ts, $this->get_index_worker_secret() );
        return hash_equals( $expected, $sig );
    }

    private function get_index_worker_secret() {
        $secret = get_option( Niwa_AI_Constants::OPT_INDEX_WORKER_SECRET, '' );
        if ( is_string( $secret ) && '' !== $secret ) {
            return $secret;
        }

        $secret = wp_generate_password( 32, true, true );
        update_option( Niwa_AI_Constants::OPT_INDEX_WORKER_SECRET, $secret, false );
        return $secret;
    }

    public function register_cron_schedules( $schedules ) {
        if ( ! isset( $schedules['niwa_ai_thirty_seconds'] ) ) {
            $schedules['niwa_ai_thirty_seconds'] = array(
                'interval' => 30,
                'display'  => 'Niwa AI every 30 seconds',
            );
        }
        if ( ! isset( $schedules['niwa_ai_five_minutes'] ) ) {
            $schedules['niwa_ai_five_minutes'] = array(
                'interval' => 300,
                'display'  => 'Niwa AI every 5 minutes',
            );
        }
        if ( ! isset( $schedules['niwa_ai_one_minute'] ) ) {
            $schedules['niwa_ai_one_minute'] = array(
                'interval' => 60,
                'display'  => 'Niwa AI every 1 minute',
            );
        }
        return $schedules;
    }

    public function ajax_embeddings_status() {
        check_ajax_referer( 'niwa_ai_embeddings_status', 'nonce' );

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error(
                array(
                    'error' => 'Forbidden',
                    'message' => 'Forbidden',
                ),
                403
            );
        }

        $stats = $this->content_store ? $this->content_store->get_embedding_stats() : array(
            'total' => 0,
            'done' => 0,
            'pending' => 0,
        );

        $index_progress = get_option( Niwa_AI_Constants::OPT_INDEX_PROGRESS, array() );
        $index_total = isset( $index_progress['total'] ) ? intval( $index_progress['total'] ) : 0;
        $index_done = isset( $index_progress['processed'] ) ? intval( $index_progress['processed'] ) : 0;
        $index_status = isset( $index_progress['status'] ) ? (string) $index_progress['status'] : 'idle';
        if ( $index_total <= 0 && $this->content_store ) {
            $index_total = $this->content_store->count_by_type( 'product' )
                + $this->content_store->count_by_type( 'page' )
                + $this->content_store->count_by_type( 'post' )
                + $this->content_store->count_by_type( 'elementor_library' )
                + $this->content_store->count_by_type( 'wp_template' )
                + $this->content_store->count_by_type( 'wp_template_part' )
                + $this->content_store->count_by_type( 'wp_block' )
                + $this->content_store->count_by_type( 'wp_navigation' )
                + $this->content_store->count_by_type( 'site_knowledge' );
            $index_done = $index_total;
            $index_status = 'done';
        } elseif ( 'done' === $index_status ) {
            $index_done = $index_total;
        }

        if ( 'running' === $index_status ) {
            $queue = get_option( Niwa_AI_Constants::OPT_INDEX_QUEUE, array() );
            if ( ! empty( $queue ) && is_array( $queue ) ) {
                $this->schedule_index_background_work( 'status-heal' );
                $this->dispatch_index_background_worker( 'status-heal' );
            }
        }

        $total = isset( $stats['total'] ) ? intval( $stats['total'] ) : 0;
        $done = isset( $stats['done'] ) ? intval( $stats['done'] ) : 0;
        $pending = isset( $stats['pending'] ) ? intval( $stats['pending'] ) : max( 0, $total - $done );

        if ( 'done' === $index_status && $pending > 0 && $this->memory_embedder ) {
            $kick_at = intval( get_option( Niwa_AI_Constants::OPT_EMBEDDING_KICK, 0 ) );
            if ( time() - $kick_at > 20 ) {
                $this->schedule_embedding_background_work( 'status-heal' );
                $this->dispatch_index_background_worker( 'status-heal' );
                update_option( Niwa_AI_Constants::OPT_EMBEDDING_KICK, time(), false );
                $stats = $this->content_store ? $this->content_store->get_embedding_stats() : $stats;
                $total = isset( $stats['total'] ) ? intval( $stats['total'] ) : $total;
                $done = isset( $stats['done'] ) ? intval( $stats['done'] ) : $done;
                $pending = isset( $stats['pending'] ) ? intval( $stats['pending'] ) : max( 0, $total - $done );
            }
        }

        $overall_total = $index_total + $total;
        $overall_done = $index_done + $done;
        $percent = $overall_total > 0 ? min( 100, max( 0, round( ( $overall_done / $overall_total ) * 100 ) ) ) : 0;

        wp_send_json_success(
            array(
                'stats' => $stats,
                'percent' => $percent,
                'index_total' => $index_total,
                'index_done' => $index_done,
                'index_status' => $index_status,
            )
        );
    }

    public function ajax_handle_name() {
        check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );

        $name_raw = isset( $_POST['name_raw'] ) ? sanitize_text_field( wp_unslash( $_POST['name_raw'] ) ) : '';
        $template = isset( $_POST['template'] ) ? sanitize_text_field( wp_unslash( $_POST['template'] ) ) : 'Nice to meet you, {name}!';

        $result = $this->name_processor->process( $name_raw, $template );

        wp_send_json( $result );
    }

    public function ajax_add_to_cart() {
        check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );

        $variation_attributes = array();
        if ( isset( $_POST['variation_attributes'] ) ) {
            $raw_attributes = wp_unslash( $_POST['variation_attributes'] );
            if ( is_string( $raw_attributes ) ) {
                $decoded = json_decode( $raw_attributes, true );
                if ( is_array( $decoded ) ) {
                    $variation_attributes = $decoded;
                }
            } elseif ( is_array( $raw_attributes ) ) {
                $variation_attributes = $raw_attributes;
            }
        }
        foreach ( $_POST as $key => $value ) {
            $key = (string) $key;
            if ( 0 === strpos( $key, 'attribute_' ) && ! is_array( $value ) ) {
                $variation_attributes[ $key ] = sanitize_text_field( wp_unslash( $value ) );
            }
        }

        $args = array(
            'product_id'           => isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0,
            'variation_id'         => isset( $_POST['variation_id'] ) ? absint( $_POST['variation_id'] ) : 0,
            'product_query'        => isset( $_POST['product_query'] ) ? sanitize_text_field( wp_unslash( $_POST['product_query'] ) ) : '',
            'quantity'             => isset( $_POST['qty'] ) ? max( 1, absint( $_POST['qty'] ) ) : ( isset( $_POST['quantity'] ) ? max( 1, absint( $_POST['quantity'] ) ) : 1 ),
            'variation_attributes' => $variation_attributes,
        );

		$incoming_session = isset( $_POST['niwa_ai_session'] ) ? sanitize_text_field( wp_unslash( $_POST['niwa_ai_session'] ) ) : '';
		$context = array(
            'recommended'  => ! empty( $_POST['niwa_ai_recommended'] ),
            'source'       => isset( $_POST['niwa_ai_source'] ) ? sanitize_text_field( wp_unslash( $_POST['niwa_ai_source'] ) ) : 'niwa_card',
            'cart_context' => isset( $_POST['niwa_ai_context'] ) ? sanitize_text_field( wp_unslash( $_POST['niwa_ai_context'] ) ) : 'recommendation',
			'session'      => Niwa_AI_Session_Utils::resolve_verified_session( true, $incoming_session ),
            'page_url'     => isset( $_POST['niwa_ai_page_url'] ) ? esc_url_raw( wp_unslash( $_POST['niwa_ai_page_url'] ) ) : home_url( '/' ),
        );

        $service = class_exists( 'Niwa_AI_Cart_Tool_Service' ) ? new Niwa_AI_Cart_Tool_Service() : null;
        if ( ! $service ) {
            wp_send_json( array( 'success' => false, 'message' => 'Cart service is not available.' ) );
            return;
        }

        wp_send_json( $service->add_product( $args, $context ) );
    }

    private function build_cart_fragments_payload() {
        if ( ! class_exists( 'WooCommerce' ) || ! function_exists( 'WC' ) || ! WC()->cart ) {
            return array(
                'fragments' => array(),
                'cart_hash' => '',
            );
        }

        ob_start();
        woocommerce_mini_cart();
        $mini_cart = ob_get_clean();

        $fragments = apply_filters(
            'woocommerce_add_to_cart_fragments',
            array(
                'div.widget_shopping_cart_content' => '<div class="widget_shopping_cart_content">' . $mini_cart . '</div>',
            )
        );

        return array(
            'fragments' => is_array( $fragments ) ? $fragments : array(),
            'cart_hash' => WC()->cart->get_cart_hash(),
        );
    }

    public function ajax_get_product_details() {
        check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );

        if ( ! class_exists( 'WooCommerce' ) || ! function_exists( 'wc_get_product' ) ) {
            wp_send_json( array( 'success' => false, 'message' => 'WooCommerce is not active.' ) );
            return;
        }

        $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
        $builder    = class_exists( 'Niwa_AI_Card_Builder' ) ? new Niwa_AI_Card_Builder() : null;
        $details    = $builder ? $builder->build_details_from_id( $product_id ) : array();

        if ( empty( $details ) ) {
            wp_send_json( array( 'success' => false, 'message' => 'Product not found.' ) );
            return;
        }

        wp_send_json( array(
            'success' => true,
            'product' => $details,
        ) );
    }

    public function ajax_clear_cleanup_flag() {
        check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );

        delete_option( Niwa_AI_Constants::OPT_CLEANUP_LOCALSTORAGE );

        wp_send_json( array( 'success' => true ) );
    }
}
