<?php
/**
 * Speaking_AI_Evaluation
 *
 * @package Spacetree/lib/proqyz/speaking
 * @since   2.2.0
 */

defined('ABSPATH') || exit;



class Speaking_AI_Evaluation {

    /**
     * Version.
     *
     * @var string
     */
    public $version = '2.2.0';

    /**
     * Current review id.
     *
     * @var int|null
     */
    public $review_id = null;

    protected static $_instance = null;

    /**
     * Table names.
     *
     * @var string
     */
    public $table_scores;
    public $table_progress_submissions;
    public $table_progress_submissions_meta;
    public $table_submissions_versioning;
    public $table_progress_meta_submissions;
    public $table_submissions_review;
    public $table_submissions_review_meta;

    /**
     * Main instance.
     *
     * @return Speaking_AI_Evaluation
     */
    public static function instance() {
        if (is_null(self::$_instance)) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    /**
     * Constructor.
     */
    public function __construct() {
        global 
            $table_proqyz_v3_progress_submissions_score,
            $table_proqyz_v3_progress_submissions,
            $table_proqyz_v3_progress_submissions_meta,
            $table_proqyz_v3_submissions_versioning,
            $table_proqyz_v3_progress_meta_submissions,
            $table_proqyz_v3_submissions_review,
            $table_proqyz_v3_submissions_review_meta;

        // Initialize table names
        $this->table_scores = $table_proqyz_v3_progress_submissions_score;
        $this->table_progress_submissions = $table_proqyz_v3_progress_submissions;
        $this->table_progress_submissions_meta = $table_proqyz_v3_progress_submissions_meta;
        $this->table_submissions_versioning = $table_proqyz_v3_submissions_versioning;
        $this->table_progress_meta_submissions = $table_proqyz_v3_progress_meta_submissions;
        $this->table_submissions_review = $table_proqyz_v3_submissions_review;
        $this->table_submissions_review_meta = $table_proqyz_v3_submissions_review_meta;

        $this->register_rest_api_routes();
    }

    /**
     * Register REST routes.
     */
    public function register_rest_api_routes() {
        add_action('rest_api_init', [$this, 'register_speaking_evaluation_route']);
    }

    /**
     * Register speaking AI evaluation route.
     */
    public function register_speaking_evaluation_route() {
        register_rest_route('spacetree/proqyz', '/speaking/ai/evaluation', [
            'methods'  => 'POST',
            'callback' => [$this, 'handle_ai_evaluation_request'],
            'permission_callback' => '__return_true',
        ]);
        register_rest_route('spacetree/proqyz', '/speaking/ai/evaluation/run', [
            'methods'  => 'POST',
            'callback' => [$this, 'handle_ai_evaluation_run'],
            'permission_callback' => '__return_true',
        ]);
    }

    

    /**
     * stage: 1 (ajax called from frontend) ------------------------------------------------------------- 1
     * Handle AI evaluation request.
     *
     * @param WP_REST_Request $request Request.
     * @return WP_REST_Response
     */
    public function handle_ai_evaluation_request($request) {
        ignore_user_abort(true);
        set_time_limit(0);

        try {
            $response = $this->process_ai_evaluation_request($request);
            if ($response instanceof WP_REST_Response) {
                return $response;
            }

            return new WP_REST_Response([
                'success' => (object) [
                    'schema' => $response,
                ],
            ], 200);
        } catch (\Throwable $throwable) {
            if (function_exists('error_log')) {
                error_log('PROQYZ_SPEAKING_AI exception: ' . $throwable->getMessage() . '\n' . $throwable->getTraceAsString());
            }

            return new WP_REST_Response([
                'error' => (object) [
                    'message' => 'Internal server error during speaking AI evaluation.',
                    'code'    => 'PROQYZ_SPEAKING_AI::exception',
                    'detail'  => $throwable->getMessage(),
                ],
            ], 200);
        }
    }

    /**
     * stage: 2 (ajax called from frontend) ------------------------------------------------------------- 2
     */
    protected function process_ai_evaluation_request($request) {
        ignore_user_abort(true);
        set_time_limit(0);

        # adding custom log hook here
        ST_ProQyz_Debug_Log::info( 'Inside process_ai_evaluation_request' );


        $result_id  = isset($request['result_id']) ? sanitize_text_field($request['result_id']) : '';
        $context    = isset($request['context']) ? sanitize_text_field($request['context']) : 'student';
        $force      = isset($request['force']) ? rest_sanitize_boolean($request['force']) : false;

        if (empty($result_id)) {
            return new WP_REST_Response([
                'error' => (object) [
                    'message' => 'Missing required parameters',
                    'code'    => 'PROQYZ_SPEAKING_AI::missing_params',
                ],
            ], 200);
        }

        global $wpdb,
        $ST_OPTION_st_proqyz__open_ai_endpoint,
        $ST_OPTION_st_proqyz__open_ai_modal,
        $ST_OPTION_st_proqyz__open_ai_enable,
        $ST_OPTION_st_proqyz__open_ai_apikey;

        // keys
        $openai_endpoint = get_option($ST_OPTION_st_proqyz__open_ai_endpoint, 'https://api.openai.com/v1/chat/completions');
        $openai_model    = get_option($ST_OPTION_st_proqyz__open_ai_modal, 'gpt-4o-mini');
        $openai_enabled  = (bool) get_option($ST_OPTION_st_proqyz__open_ai_enable, false);
        $openai_apikey   = get_option($ST_OPTION_st_proqyz__open_ai_apikey, '');


        if (!$openai_enabled || empty($openai_apikey)) {
            return new WP_REST_Response([
                'error' => (object) [
                    'message' => 'OpenAI evaluation is disabled or API key not configured.',
                    'code'    => 'PROQYZ_SPEAKING_AI::disabled',
                ],
            ], 200);
        }

        $get_result = $wpdb->get_row($wpdb->prepare(
            "SELECT `user_id`, `api_id`, `quiz_category`, `status`
             FROM {$this->table_progress_submissions}
             WHERE `ID` = %d
             LIMIT 1",
            $result_id
        ));

        if (!$get_result) {
            return new WP_REST_Response([
                'error' => (object) [
                    'message' => 'Result not found.',
                    'code'    => 'PROQYZ_SPEAKING_AI::result_missing',
                ],
            ], 200);
        }

        if ($get_result->quiz_category !== 'ielts-speaking') {
            return new WP_REST_Response([
                'error' => (object) [
                    'message' => 'Result does not belong to IELTS Speaking category.',
                    'code'    => 'PROQYZ_SPEAKING_AI::invalid_category',
                ],
            ], 200);
        }

        $student_id = (int) $get_result->user_id;
        $quiz_id    = $get_result->api_id;

        # adding custom log hook here
        ST_ProQyz_Debug_Log::info( 'Loading quiz payload' );
        // load the quiz and its metadata
        $quiz_payload = $this->load_quiz_payload($result_id);
        if (is_wp_error($quiz_payload)) {
            return new WP_REST_Response([
                'error' => (object) [
                    'message' => $quiz_payload->get_error_message(),
                    'code'    => 'PROQYZ_SPEAKING_AI::quiz_meta_invalid',
                ],
            ], 200);
        }

        # adding custom log hook here
        ST_ProQyz_Debug_Log::info( 'Building task schema' );
        # build the task schema
        $schema = $this->build_task_schema($quiz_payload, $quiz_id, $result_id, $student_id, $openai_apikey, $force);
        if (isset($schema['error'])) {
            return new WP_REST_Response([
                'error' => (object) $schema['error'],
            ], 200);
        }

        # adding custom log hook here
        ST_ProQyz_Debug_Log::info( 'Running AI evaluation' );
        return $this->run_ai_evaluation([
            'schema'          => $schema,
            'student_id'      => $student_id,
            'result_id'       => $result_id,
            'quiz_id'         => $quiz_id,
            'context'         => $context,
            'force'           => $force,
            'openai_endpoint' => $openai_endpoint,
            'openai_model'    => $openai_model,
            'openai_apikey'   => $openai_apikey,
        ]);
    }

    /**
     * stage: 3 --------------------------------------------------------------------------- 3
     * Execute AI evaluation using prepared schema.
     */
    protected function run_ai_evaluation($args) {
        global $wpdb;

        $schema          = isset($args['schema']) ? (array) $args['schema'] : [];
        $student_id      = (int) ($args['student_id'] ?? 0);
        $result_id       = (int) ($args['result_id'] ?? 0);
        $quiz_id         = isset($args['quiz_id']) ? $args['quiz_id'] : '';
        $context         = isset($args['context']) ? $args['context'] : 'student';
        $force           = !empty($args['force']);
        $openai_endpoint = $args['openai_endpoint'] ?? '';
        // $openai_model    = $args['openai_model'] ?? '';
        $openai_model    = 'gpt-4o-mini';
        $openai_apikey   = $args['openai_apikey'] ?? '';

        if (empty($schema) || !$student_id || !$result_id) {
            return new WP_REST_Response([
                'error' => (object) [
                    'message' => 'No speaking data available for evaluation.',
                    'code'    => 'PROQYZ_SPEAKING_AI::empty_schema',
                ],
            ], 200);
        }

        $review_id = $this->ensure_review_record($result_id, $student_id);

        if (!$review_id) {
            return new WP_REST_Response([
                'error' => (object) [
                    'message' => 'Failed to initialise review for AI evaluation.',
                    'code'    => 'PROQYZ_SPEAKING_AI::review_init_failed',
                ],
            ], 200);
        }

        $this->review_id = $review_id;

        $current_status = $this->get_review_meta_value($review_id, 'review-status');

        if (!$force && in_array($current_status, ['started', 'processing'], true)) {
            return new WP_REST_Response([
                'success' => (object) [
                    'message' => 'AI speaking evaluation is already processing. Please wait.',
                    'status'  => $current_status,
                ],
            ], 200);
        }

        if ($force) {
            $this->update_review_meta($review_id, 'review-status', 'started');
            # remove reason
            $wpdb->delete($this->table_submissions_review_meta, [
                'meta_key' => 're-evaluation',
                'review_id' => $review_id
            ]);
            $this->log_review_message($review_id, 'info', 'Force re-run requested', [
                'context' => $context,
            ]);
        } else {
            # remove reason
            $wpdb->delete($this->table_submissions_review_meta, [
                'meta_key' => 're-evaluation',
                'review_id' => $review_id
            ]);
            $this->update_review_meta($review_id, 'review-status', 'started');
        }

        # adding custom log hook here
        ST_ProQyz_Debug_Log::info( 'Building prompt messages' );
        $messages = $this->build_prompt_messages_from_schema($schema);
        ST_ProQyz_Debug_Log::info( 'Building prompt messages end' );

        // Store prompt and run context for background worker, then trigger worker and return "started" (no wait for OpenAI).
        $this->update_review_meta($review_id, 'prompt', $messages);
        $this->update_review_meta($review_id, 'run_context', [
            'result_id'  => $result_id,
            'student_id' => $student_id,
            'schema'     => $schema,
        ]);
        $this->update_review_meta($review_id, 'review-status', 'processing');

        $run_key = hash_hmac('sha256', (string) $review_id, wp_salt('auth'));

        // Run the evaluation worker after we send the response. Non-blocking wp_remote_post
        // to the same site often never hits the endpoint (script ends before request completes,
        // or server cannot resolve local hostname / SSL). Using shutdown runs the handler in-process.
        $self = $this;
        add_action('shutdown', function () use ($self, $review_id, $run_key) {
            $request = new WP_REST_Request('POST', '/spacetree/proqyz/speaking/ai/evaluation/run');
            $request->set_param('review_id', $review_id);
            $request->set_param('run_key', $run_key);
            $self->handle_ai_evaluation_run($request);
        }, 0);

        ST_ProQyz_Debug_Log::info( 'scheduled AI evaluation run (shutdown)' );

        return new WP_REST_Response([
            'success' => (object) [
                'message' => 'Evaluation started. You can leave this tab; we will notify you when complete.',
                'started'  => true,
            ],
        ], 200);
    }

    /**
     * stage: 4 (background worker) --------------------------------------------------------------------------- 4
     * Background worker: run OpenAI request and save response. Called internally (fire-and-forget).
     *
     * @param WP_REST_Request $request Must contain review_id and run_key (HMAC for verification).
     * @return WP_REST_Response
     */
    public function handle_ai_evaluation_run($request) {
        ignore_user_abort(true);
        set_time_limit(0);

        # adding custom log hook here
        ST_ProQyz_Debug_Log::info( 'Inside handle_ai_evaluation_run' );

        $review_id = isset($request['review_id']) ? absint($request['review_id']) : 0;
        $run_key   = isset($request['run_key']) ? $request['run_key'] : '';

        if (!$review_id || !$run_key) {
            return new WP_REST_Response(['error' => (object) ['message' => 'Missing review_id or run_key']], 200);
        }

        $expected_key = hash_hmac('sha256', (string) $review_id, wp_salt('auth'));
        if (!hash_equals($expected_key, $run_key)) {
            return new WP_REST_Response(['error' => (object) ['message' => 'Invalid run_key']], 200);
        }

        global $wpdb;
        global $ST_OPTION_st_proqyz__open_ai_endpoint, $ST_OPTION_st_proqyz__open_ai_modal, $ST_OPTION_st_proqyz__open_ai_apikey;

        $this->review_id = $review_id;

        $messages = $this->get_review_meta_value($review_id, 'prompt');
        if (empty($messages) || !is_array($messages)) {
            $this->mark_review_failed('Background worker: prompt not found', []);
            return new WP_REST_Response(['error' => (object) ['message' => 'Prompt not found']], 200);
        }

        $run_context = $this->get_review_meta_value($review_id, 'run_context');
        if (empty($run_context) || empty($run_context['result_id']) || empty($run_context['student_id'])) {
            $this->mark_review_failed('Background worker: run_context not found', []);
            return new WP_REST_Response(['error' => (object) ['message' => 'Run context not found']], 200);
        }

        $result_id  = (int) $run_context['result_id'];
        $student_id = (int) $run_context['student_id'];
        $schema     = isset($run_context['schema']) ? $run_context['schema'] : [];

        $openai_endpoint = get_option($ST_OPTION_st_proqyz__open_ai_endpoint, 'https://api.openai.com/v1/chat/completions');
        $openai_model    = 'gpt-4o-mini';
        $openai_apikey   = get_option($ST_OPTION_st_proqyz__open_ai_apikey, '');
        $config          = (object) [
            'endpoint' => $openai_endpoint,
            'model'    => $openai_model,
            'apikey'   => $openai_apikey,
        ];

        # adding custom log hook here
        ST_ProQyz_Debug_Log::info( 'Making OpenAI conversation request' );
        $ai_response = $this->openai_conversation_request($config, $messages);
        # adding custom log hook here
        ST_ProQyz_Debug_Log::info( 'OpenAI conversation request made' );



        if (isset($ai_response->error)) {
            $this->mark_review_failed(
                $ai_response->error->message ?? 'AI evaluation failed.',
                ['context' => $ai_response->error ?? []]
            );
            return new WP_REST_Response(['error' => $ai_response->error], 200);
        }

        $raw_body = $ai_response->success->body ?? '';
        $decoded  = $this->decode_ai_payload($raw_body);

        if (!$decoded || empty($decoded['success']['response'])) {
            $this->mark_review_failed('AI response parsing failed.', ['raw' => $raw_body]);
            return new WP_REST_Response([
                'error' => (object) [
                    'message' => 'AI response parsing failed.',
                    'code'    => 'PROQYZ_SPEAKING_AI::invalid_payload',
                ],
            ], 200);
        }

        $payload = $decoded['success']['response'];
        $scores  = $this->extract_scores_from_ai_payload($payload);

        if (empty($scores)) {
            $scores = [1, 1, 1, 1];
        }

        $feedback_text = $this->compose_human_readable_feedback($payload);

        $this->persist_review_results(
            $result_id,
            $scores,
            $decoded,
            $feedback_text,
            $student_id,
            $schema
        );

        return new WP_REST_Response([
            'success' => (object) ['message' => 'Speaking evaluation completed in background'],
        ], 200);
    }

    /**
     * returns meta_key: quiz 
     * table: $table_proqyz_progress_submissions_meta
     * format: unserialized json object
     */
    protected function load_quiz_payload($result_id) {
        global $wpdb;

        $submission_quiz_version_id_meta = $wpdb->get_row($wpdb->prepare(
            "SELECT `meta_value`
            FROM {$this->table_progress_submissions_meta}
            WHERE `submission_id` = %d
            AND `meta_key` = 'quiz_version_id'
            LIMIT 1",
            $result_id
        ));

        if (!$submission_quiz_version_id_meta) {
            return new WP_Error('speaking_ai_quiz_version_id_missing', 'Failed to load quiz version id metadata for evaluation.');
        }

        $quiz_version_id = $submission_quiz_version_id_meta->meta_value;
        # get quiz from versioning table 
        $quiz_version_meta = $wpdb->get_row($wpdb->prepare(
            "SELECT `meta_value`
            FROM {$this->table_submissions_versioning}
            WHERE `version_id` = %d
            AND `meta_key` = 'quiz'
            LIMIT 1",
            $quiz_version_id
        ));

        if (!$quiz_version_meta) {
            return new WP_Error('speaking_ai_quiz_version_meta_missing', 'Failed to load quiz version meta for evaluation.');
        }


        $quiz_payload = maybe_unserialize($quiz_version_meta->meta_value);
        if (!is_object($quiz_payload)) {
            return new WP_Error('speaking_ai_quiz_invalid', 'Quiz metadata is invalid.');
        }

        return $quiz_payload;
    }

    /**
     * Build task schema.
     *
     * @param object $meta_table Meta table.
     * @param string $table_proqyz_progress_meta_submissions Table name.
     * @param string $quiz_id Quiz id.
     * @param int    $result_id Result id.
     * @param string $openai_apikey OpenAI API key.
     * @return array
     */
    protected function build_task_schema($quiz_payload, $quiz_id, $result_id, $student_id, $openai_apikey, $force = false) {
        global $wpdb;
        $schema = [];

        $sections  = isset($quiz_payload->sections) ? (array) $quiz_payload->sections : [];
        $questions = isset($quiz_payload->questions) ? (array) $quiz_payload->questions : [];

        $question_map = [];
        foreach ($questions as $question) {
            if (isset($question->_id)) {
                $question_map[$question->_id] = $question;
            }
        }

        foreach ($sections as $section) {
            if (!isset($section->_id)) {
                continue;
            }

            $section_id = $section->_id;
            $record = $wpdb->get_row($wpdb->prepare(
                "SELECT *
                FROM {$this->table_progress_meta_submissions}
                WHERE `quiz_id` = %s
                AND `section_id` = %s
                AND `session_id` = %d
                AND `status` = 1
                LIMIT 1",
                $quiz_id,
                $section_id,
                $result_id
            ));

            if (!$record) {
                continue;
            }

            $question_ids = array_filter(array_map('trim', explode(',', (string) $record->question_ids)));
            $question_count = count($question_ids);
            $interval_string = (string) $record->question_intervals;
            $intervals       = $this->prepare_intervals($interval_string, $question_count);

            $audio_info = maybe_unserialize($record->json);
            $user_answer = '';
            $audio_absolute_path = null;
            $final_response = null;
            $response_error = null;
            $question_transcripts = $question_count > 0 ? array_fill(0, $question_count, '') : [];
            $question_segments_map = [];
            $question_primary_segment_map = [];
            $cache_key = $this->get_transcription_meta_key($result_id, $section_id);
            $audio_hash = null;
            $cached_transcription = null;
            $response_from_cache = false;

            if (is_object($audio_info) || is_array($audio_info)) {
                $audio_info = (object) $audio_info;
                if (!empty($audio_info->location) && !empty($audio_info->filename)) {
                    $path = ltrim($audio_info->location, '/');
                    $user_answer = trailingslashit(site_url()) . ltrim($path . '/' . $audio_info->filename, '/');
                    $audio_absolute_path = $this->resolve_audio_path($audio_info->location, $audio_info->filename);
                }
            }

            if ($audio_absolute_path && file_exists($audio_absolute_path)) {
                $audio_hash = $this->generate_audio_hash($audio_absolute_path);

                if ($force) {
                    $this->purge_transcription_cache($student_id, $cache_key);
                } else {
                    $cached_transcription = $this->maybe_get_cached_transcription($student_id, $cache_key, $audio_hash);
                    if (!empty($cached_transcription['response'])) {
                        $final_response = $cached_transcription['response'];
                        $response_from_cache = true;
                    }
                }

                if (!$final_response) {
                    try {
                        $transcription = $this->transcribe_audio($audio_absolute_path, $openai_apikey);
                        if (isset($transcription['error'])) {
                            $response_error = (object) $transcription['error'];
                        } else {
                            $final_response = $transcription;
                            $this->save_transcription_cache($student_id, $cache_key, $audio_hash, $final_response);
                            $response_from_cache = false;
                        }
                    } catch (\Throwable $e) {
                        throw $e;
                    }
                }
            }

            $questions_schema = [];
            $response_words = [];
            if (!empty($final_response) && is_array($final_response) && !empty($final_response['words'])) {
                $response_words = $final_response['words'];
            }

            foreach ($question_ids as $index => $question_id) {
                $question_obj = isset($question_map[$question_id]) ? (object) $question_map[$question_id] : null;
                $start_time = isset($intervals[$index]) ? (float) $intervals[$index] : null;
                $end_time   = isset($intervals[$index + 1]) ? (float) $intervals[$index + 1] : null;
                $word_slice = $this->collect_words_for_interval($response_words, $start_time, $end_time);
                $transcript_text = $this->generate_question_transcript($word_slice);
                $timing_analysis = $this->analyze_question_timing($word_slice, $start_time, $end_time);
                $transcript_html = $this->generate_question_transcript_with_hesitations($word_slice, $timing_analysis['hesitation_markers']);
                $question_timing = $timing_analysis;
                unset($question_timing['hesitation_markers']);

                $questions_schema[] = [
                    'question_id' => $question_id,
                    'question'    => $question_obj ? (string) ($question_obj->title ?? '') : '',
                    'words'       => $word_slice,
                    'transcript'  => $transcript_text,
                    'transcript_html' => $transcript_html,
                    'interval'    => [
                        'start' => $start_time,
                        'end'   => $end_time,
                    ],
                    'timing' => $question_timing,
                ];
            }

            $overall_transcript = '';
            if (!empty($final_response['text'])) {
                $overall_transcript = (string) $final_response['text'];
            } elseif (!empty($final_response['words'])) {
                $overall_transcript = $this->generate_question_transcript($final_response['words']);
            }

            $schema[] = [
                'task_id'   => $section_id,
                'task_key'  => isset($section->order) ? 'part-' . $section->order : $section_id,
                'task'      => isset($section->title) ? (string) $section->title : '',
                'questions' => $questions_schema,
                'user_answer' => $user_answer,
                'user_transcribe' => '',
                'final_response' => $final_response,
                'overall_transcript' => $overall_transcript,
                'response_cached' => $response_from_cache,
                'response_error' => $response_error,
                'intervals' => $intervals,
            ];
        }

        return $schema;
    }

    

    /**
     * Build the meta key for caching transcriptions.
     */
    protected function get_transcription_meta_key($result_id, $section_id) {
        $section_fragment = sanitize_key((string) $section_id);
        return sprintf('proqyz_speaking_ai_transcription_%d_%s', (int) $result_id, $section_fragment ?: 'section');
    }

    /**
     * Generate a deterministic hash for the audio file.
     */
    protected function generate_audio_hash($file_path) {
        if (!file_exists($file_path)) {
            return null;
        }

        $hash = md5_file($file_path);
        if (!$hash) {
            $hash = sha1_file($file_path);
        }

        return $hash ?: null;
    }

    /**
     * Retrieve cached transcription data.
     */
    protected function maybe_get_cached_transcription($student_id, $meta_key, $audio_hash) {
        if (!$student_id || !$meta_key || !$audio_hash) {
            return null;
        }

        $raw = get_user_meta($student_id, $meta_key, true);
        if (empty($raw)) {
            return null;
        }

        $payload = is_string($raw) ? json_decode($raw, true) : (array) $raw;
        if (!is_array($payload)) {
            return null;
        }

        if (empty($payload['hash']) || $payload['hash'] !== $audio_hash) {
            return null;
        }

        if (empty($payload['response'])) {
            return null;
        }

        return $payload;
    }

    /**
     * Cache transcription payload.
     */
    protected function save_transcription_cache($student_id, $meta_key, $audio_hash, $response) {
        if (!$student_id || !$meta_key || !$audio_hash || empty($response)) {
            return;
        }

        $payload = [
            'hash'       => $audio_hash,
            'response'   => $response,
            'stored_at'  => time(),
            'version'    => $this->version,
        ];

        update_user_meta($student_id, $meta_key, $payload);
    }

    /**
     * Purge cached transcription for a section.
     */
    protected function purge_transcription_cache($student_id, $meta_key) {
        if (!$student_id || !$meta_key) {
            return;
        }

        delete_user_meta($student_id, $meta_key);
    }

    /**
     * Filter word timestamps belonging to a question interval.
     *
     * @param array $words
     * @param float|null $start
     * @param float|null $end
     * @return array
     */
    protected function collect_words_for_interval($words, $start, $end) {
        if (empty($words) || $start === null || $end === null) {
            return [];
        }

        if ($end < $start) {
            $tmp = $start;
            $start = $end;
            $end = $tmp;
        }

        $slice = [];
        foreach ($words as $word) {
            $word_start = isset($word['start']) ? (float) $word['start'] : null;
            $word_end   = isset($word['end']) ? (float) $word['end'] : null;

            if ($word_start === null) {
                continue;
            }

            $in_interval = ($word_start >= $start && $word_start < $end);

            if (!$in_interval && $word_end !== null) {
                $in_interval = ($word_end > $start && $word_end <= $end);
            }

            if ($in_interval) {
                $slice[] = $word;
            }
        }

        return $slice;
    }

    /**
     * Analyse pauses and hesitations for a question's response.
     */
    protected function analyze_question_timing($words, $question_start, $question_end) {
        $threshold = 1.5; // seconds to treat as hesitation
        $first_word_start = null;
        $last_word_end = $question_start;
        $previous_end = $question_start;
        $hesitation_markers = [];

        if (is_array($words) && !empty($words)) {
            foreach ($words as $index => $word) {
                $word_start = isset($word['start']) ? (float) $word['start'] : null;
                $word_end   = isset($word['end']) ? (float) $word['end'] : $word_start;

                if ($first_word_start === null && $word_start !== null) {
                    $first_word_start = $word_start;
                }

                if ($word_start !== null && $previous_end !== null) {
                    $gap = $word_start - $previous_end;
                    if ($gap >= $threshold) {
                        $hesitation_markers[$index] = [
                            'gap_start'  => round($previous_end, 2),
                            'gap_end'    => round($word_start, 2),
                            'duration'   => round($gap, 2),
                            'after_word' => $word['word'] ?? '',
                            'before_word'=> $words[$index - 1]['word'] ?? '',
                        ];
                    }
                }

                if ($word_end !== null) {
                    $previous_end = $word_end;
                    $last_word_end = $word_end;
                } elseif ($word_start !== null) {
                    $previous_end = $word_start;
                }
            }
        }

        $pre_pause = 0;
        if ($first_word_start !== null && $question_start !== null) {
            $pre_pause = max(0, $first_word_start - $question_start);
        }

        $response_duration = 0;
        if ($first_word_start !== null && $last_word_end !== null) {
            $response_duration = max(0, $last_word_end - $first_word_start);
        } elseif ($question_start !== null && $question_end !== null) {
            $response_duration = max(0, $question_end - $question_start);
        }

        return [
            'pre_response_pause' => round($pre_pause, 2),
            'response_duration'  => round($response_duration, 2),
            'word_count'         => is_array($words) ? count($words) : 0,
            'hesitations'        => array_values($hesitation_markers),
            'hesitation_markers' => $hesitation_markers,
        ];
    }

    /**
     * Build transcript HTML with hesitation markers inserted.
     */
    protected function generate_question_transcript_with_hesitations($words, $markers) {
        if (empty($words) || !is_array($words)) {
            return '';
        }

        $parts = [];
        foreach ($words as $index => $word) {
            if (!empty($markers[$index])) {
                $marker = $markers[$index];
                $duration = isset($marker['duration']) ? (float) $marker['duration'] : null;
                $label = $duration
                    ? sprintf(__('Pause %.1fs', 'spacetree'), $duration)
                    : __('Pause', 'spacetree');
                $parts[] = sprintf(
                    '<mark class="ai-hesitation" data-duration="%s" title="%s">%s</mark>',
                    $duration !== null ? esc_attr($duration) : '',
                    esc_attr($label),
                    esc_html($label)
                );
            }

            $token = isset($word['word']) ? $word['word'] : '';
            $parts[] = esc_html($token);
        }

        return implode(' ', array_filter($parts, static function ($fragment) {
            return $fragment !== '';
        }));
    }

    /**
     * Ensure review record exists.
     */
    protected function ensure_review_record($result_id, $student_id) {
        global $wpdb;

        $existing = $wpdb->get_row($wpdb->prepare(
            "SELECT `ID`
             FROM {$this->table_submissions_review}
             WHERE `result_id` = %d
               AND `user_id`   = %d
               AND `status`    = 1

             LIMIT 1",
            $result_id,
            $student_id
        ));

        if ($existing) {
            return (int) $existing->ID;
        }

        $inserted = $wpdb->insert($this->table_submissions_review, [
            'result_id'  => $result_id,
            'user_id'    => $student_id,
            'section_id' => '',
            'status'     => 1,
        ]);

        return $inserted ? (int) $wpdb->insert_id : null;
    }

    /**
     * Update or insert review meta.
     */
    protected function update_review_meta($review_id, $key, $value) {
        global $wpdb;

        $existing = $wpdb->get_row($wpdb->prepare(
            "SELECT `ID`
             FROM {$this->table_submissions_review_meta}
             WHERE `review_id` = %d
               AND `meta_key` = %s
               AND `meta_status` = 1
             LIMIT 1",
            $review_id,
            $key
        ));

        if ($existing) {
            $wpdb->update(
                $this->table_submissions_review_meta,
                ['meta_value' => maybe_serialize($value)],
                ['ID' => $existing->ID]
            );
            return (int) $existing->ID;
        }

        $wpdb->insert($this->table_submissions_review_meta, [
            'review_id'   => $review_id,
            'meta_key'    => $key,
            'meta_value'  => maybe_serialize($value),
            'meta_status' => 1,
        ]);

        return (int) $wpdb->insert_id;
    }

    /**
     * Retrieve review meta value.
     */
    protected function get_review_meta_value($review_id, $key) {
        global $wpdb;

        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT `meta_value`
             FROM {$this->table_submissions_review_meta}
             WHERE `review_id` = %d
               AND `meta_key` = %s
               AND `meta_status` = 1
             LIMIT 1",
            $review_id,
            $key
        ));

        if (!$row) {
            return null;
        }

        return maybe_unserialize($row->meta_value);
    }

    /**
     * Log review message.
     */
    protected function log_review_message($review_id, $type, $message, $data = []) {
        $log_entry = [
            'type'    => $type,
            'message' => $message,
            'data'    => $data,
            'time'    => time(),
        ];

        $this->update_review_meta($review_id, 'review-log', $log_entry);
    }

    /**
     * Mark review as failed.
     */
    protected function mark_review_failed($error_message, $extra = []) {
        $this->update_review_meta($this->review_id, 'review-status', 'failed');
        $this->log_review_message($this->review_id, 'error', $error_message, $extra);
    }

    /**
     * Persist review results.
     */
    protected function persist_review_results(
        $result_id,
        $scores,
        $raw_ai_payload,
        $feedback_text,
        $student_id,
        $schema_summary = []
    ) {
        global $wpdb;

        $this->update_review_meta($this->review_id, 'review-feedback-enable', 1);
        $this->update_review_meta($this->review_id, 'review-feedback-content', $feedback_text);
        $this->update_review_meta($this->review_id, 'review-scores', wp_json_encode($scores));
        $this->update_review_meta($this->review_id, 'ai-review-content', wp_json_encode($raw_ai_payload));
        if (!empty($schema_summary)) {
            $this->update_review_meta($this->review_id, 'ai-review-schema', wp_json_encode($this->summarize_schema_for_storage($schema_summary)));
        }
        $this->update_review_meta($this->review_id, 'review-status', 'completed');
        $this->update_review_meta($this->review_id, 'reviewer', -1);

        $wpdb->update($this->table_progress_submissions, [
            'review'        => 1,
            'date_modified' => time(),
        ], [
            'ID' => $result_id,
        ]);

        apply_filters('proqyz_quiz__on_evaluation_speaking', [
            'user_id'     => $student_id,
            'reviewer_id' => -1,
            'result_id'   => $result_id,
        ]);
    }

    /**
     * Upsert submission meta.
     */
    protected function upsert_submission_meta($table, $submission_id, $meta_key, $meta_value) {
        global $wpdb;

        $exists = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*)
             FROM {$table}
             WHERE `submission_id` = %d
               AND `meta_key` = %s",
            $submission_id,
            $meta_key
        ));

        if ($exists) {
            $wpdb->update(
                $table,
                ['meta_value' => $meta_value],
                [
                    'submission_id' => $submission_id,
                    'meta_key'      => $meta_key,
                ]
            );
        } else {
            $wpdb->insert(
                $table,
                [
                    'submission_id' => $submission_id,
                    'meta_key'      => $meta_key,
                    'meta_value'    => $meta_value,
                    'created_at'    => time(),
                ]
            );
        }
    }
    /**
     * Upsert submission score.
     */
    protected function upsert_submission_score($submission_id, $student_id, $score_data) {
        global $wpdb;
        
        $exists = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*)
             FROM {$this->table_scores}
             WHERE `submission_id` = %d
               AND `user_id` = %d",
            $submission_id,
            $student_id
        ));

        if ($exists) {
            $wpdb->update($this->table_scores, [
                'score_data' => $score_data,
                'updated_at' => time(),
            ], [
                'submission_id' => $submission_id,
                'user_id' => $student_id,
            ]);
        } else {
            $wpdb->insert($this->table_scores, [
                'submission_id' => $submission_id,
                'user_id' => $student_id,
                'score_data' => $score_data,
                'created_at' => time(),
            ]);
        }
    }

    /**
     * Build prompt messages from schema.
     */
    protected function build_prompt_messages_from_schema($schema) {
        ST_ProQyz_Debug_Log::info( 'Inside build_prompt_messages_from_schema: start' );
        $system_prompt = <<<PROMPT
        You are an experienced IELTS Speaking examiner. Evaluate the candidate strictly using official IELTS Speaking band descriptors across Fluency & Coherence, Lexical Resource, Grammatical Range & Accuracy, and Pronunciation. Provide an Overall band, per-part summary, highlights, and actionable suggestions. You must also analyse the provided transcripts and highlight notable grammar/pronunciation issues by returning the exact text snippets that require correction.

        EVALUATION CRITERIA (All equally weighted):

        1. **Fluency and Coherence** - Ability to speak at length without noticeable effort, self-correction, logical organization
        2. **Lexical Resource** - Range of vocabulary, appropriateness, ability to paraphrase, precision
        3. **Grammatical Range and Accuracy** - Variety of structures, complexity, accuracy, error-free sentences
        4. **Pronunciation** - Clarity, rhythm, stress, intonation, individual sounds

        IMPORTANT NOTES:
        - You are evaluating a TRANSCRIPT (text version of audio), so pronunciation assessment is limited
        - For pronunciation, focus on: clarity of expression (evident from transcript), likely stress patterns, and overall intelligibility
        - Be strict but fair - use official IELTS descriptors
        - Use 0.5 increments (6.0, 6.5, 7.0, etc.)
        - Overall band is the average of 4 criteria, rounded to nearest 0.5
        - Provide 2-4 strengths and weaknesses per criterion
        - Give 5-7 specific improvement suggestions

        Timing metadata is provided for every question:
        - `timing.pre_response_pause` → silence before the candidate speaks (seconds)
        - `timing.response_duration` → speaking length for that question
        - `timing.hesitations` → pauses of 1.5s+ between words, with gap duration
        Reference long pauses and repeated hesitations explicitly in Fluency & Coherence feedback/band rationales.

        Rules for transcript-issues:
        - You MUST analyse and return transcript-issues for ALL parts: Part 1 (Introduction), Part 2 (Topic), and Part 3 (Discussion). Do not skip Part 1.
        - Part 1 has many short Q&A answers; still identify grammar, vocabulary, and pronunciation issues where present (e.g. tense errors, article misuse, wrong word choice, unclear phrasing). Aim for at least 1-3 critical issues per part when present.
        - Use the exact snippet from the candidate transcript so it can be highlighted.
        - Provide at most 5 critical issues per part (so up to 15 total across 3 parts). Prioritise the most impactful errors.
        - Always include "question_id" with the EXACT question_id string from the candidate payload (e.g. from "questions[].question_id") so each issue is linked to the correct question.
        - Always include "error_types": an array of objects that describe every mistake detected in that snippet.
        - Each error_types item must be an object with "type" and "suggestions" (array of rewritten examples or pronunciation tips).
        - Allowed type values: grammar, vocabulary, fluency, pronunciation, coherence, content, structure, filler-words, delivery, general.
        - Suggestions must be concise, natural English rewrites (max 3 per type) so they can be surfaced directly in the UI.

        Return ONLY the JSON object in this structure (no Markdown fences or extra commentary):

        {
            "success": {
                "format": "json",
                "response": {
                "fluency-coherence": { "feedback": "...", "band": "1-9", "strengths": ["Strength 1", "Strength 2"], "weaknesses": ["Weakness 1", "Weakness 2"] },
                "lexical-resource": { "feedback": "...", "band": "1-9", "strengths": ["Strength 1", "Strength 2"],"weaknesses": ["Weakness 1", "Weakness 2"] },
                "grammatical-range-and-accuracy": { "feedback": "...", "band": "1-9", "strengths": ["Strength 1", "Strength 2"],"weaknesses": ["Weakness 1", "Weakness 2"] },
                "pronunciation": { "feedback": "...", "band": "1-9", "strengths": ["Strength 1", "Strength 2"],"weaknesses": ["Weakness 1", "Weakness 2"] },
                "overall": { "feedback": "...", "band": "1-9", "strengths": ["Strength 1", "Strength 2"],"weaknesses": ["Weakness 1", "Weakness 2"] },
                "per-part": [
                    { "part": "Part 1", "summary": "...", "band": "1-9", "suggestions": ["...", "..."] }
                ],
                "highlights": ["Strength 1", "Strength 2"],
                "transcript-issues": [
                    { "question_id": "USE_EXACT_QUESTION_ID_FROM_PART1", "text": "Exact snippet", "issue": "Description", "error_types": [{ "type": "grammar", "suggestions": ["Improved phrasing"] }] },
                    { "question_id": "USE_EXACT_QUESTION_ID_FROM_PART2_OR_PART3", "text": "Exact snippet", "issue": "Description", "error_types": [{ "type": "vocabulary", "suggestions": ["Better word"] }] }
                ]
                }
            }
        }
        PROMPT;

        $parts_payload = [];
        foreach ($schema as $index => $part) {
            $final_response = isset($part['final_response']) ? $part['final_response'] : null;
            $overall_transcript = '';

            if (is_array($final_response) && !empty($final_response['text'])) {
                $overall_transcript = (string) $final_response['text'];
            } elseif (is_array($final_response) && !empty($final_response['words'])) {
                $overall_transcript = $this->generate_question_transcript($final_response['words']);
            }

            $questions_payload = [];
            foreach ($part['questions'] as $q_index => $question) {
                $transcript = $question['transcript'] ?? $this->generate_question_transcript($question['words'] ?? []);
                $questions_payload[] = [
                    'question_number'     => $q_index + 1,
                    'question_id'         => $question['question_id'] ?? '',
                    'question_title'      => $question['question'] ?? '',
                    'question_interval'   => $question['interval'] ?? [],
                    'candidate_transcript'=> $transcript,
                    'timing'              => $question['timing'] ?? null,
                ];
            }

            $parts_payload[] = [
                'part_number'        => $index + 1,
                'title'              => $part['task'] ?? ('Part ' . ($index + 1)),
                'instructions'       => wp_strip_all_tags($part['instructions'] ?? ''),
                'overall_transcript' => $overall_transcript,
                'questions'          => $questions_payload,
            ];
        }

        $user_payload = wp_json_encode([
            'candidate' => [
                'parts' => $parts_payload,
            ],
        ]);

        ST_ProQyz_Debug_Log::info( 'Inside build_prompt_messages_from_schema: end' );
        return [
            (object) ['role' => 'system', 'content' => $system_prompt],
            (object) ['role' => 'user', 'content' => $user_payload],
        ];
    }

    /**
     * Generate transcript sentence from words.
     */
    protected function generate_question_transcript($words) {
        if (empty($words) || !is_array($words)) {
            return '';
        }

        $tokens = [];
        foreach ($words as $entry) {
            if (!empty($entry['word'])) {
                $tokens[] = $entry['word'];
            }
        }

        return trim(preg_replace('/\s+/', ' ', implode(' ', $tokens)));
    }

    /**
     * Submit chat completion request.
     */
    protected function openai_conversation_request($config, $messages) {
        $this->update_review_meta($this->review_id, 'prompt', $messages);

        $payload = [
            'model'       => $config->model,
            'messages'    => $messages,
            'temperature' => 0.2,
            'max_tokens'  => 2048,
        ];

        $response = wp_remote_post($config->endpoint, [
            'headers' => [
                'Authorization' => 'Bearer ' . $config->apikey,
                'Content-Type'  => 'application/json',
            ],
            'body'    => wp_json_encode($payload),
            'timeout' => 120,
        ]);

        if (is_wp_error($response)) {
            return (object) [
                'error' => (object) [
                    'message' => $response->get_error_message(),
                    'code'    => 'PROQYZ_SPEAKING_AI::chat_request_failed',
                ],
            ];
        }

        $code = wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);

        $this->update_review_meta($this->review_id, 'response', $body);

        if ($code !== 200) {
            return (object) [
                'error' => (object) [
                    'message' => 'Chat completion request failed.',
                    'detail'  => $body,
                    'code'    => 'PROQYZ_SPEAKING_AI::chat_http_' . $code,
                ],
            ];
        }

        $decoded = json_decode($body, true);
        if (!$decoded || !isset($decoded['choices'][0]['message']['content'])) {
            return (object) [
                'error' => (object) [
                    'message' => 'Chat completion response invalid.',
                    'code'    => 'PROQYZ_SPEAKING_AI::chat_invalid_response',
                ],
            ];
        }

        return (object) [
            'success' => (object) [
                'message' => 'Response from AI',
                'body'    => $decoded['choices'][0]['message']['content'],
            ],
        ];
    }

    /**
     * Decode AI payload, stripping code fences if necessary.
     */
    protected function decode_ai_payload($body) {
        if (!is_string($body)) {
            return null;
        }

        $body = trim($body);
        $body = preg_replace('/```(?:json)?/i', '', $body);
        $body = trim($body);

        $decoded = json_decode($body, true);

        if (!$decoded) {
            return null;
        }

        return $decoded;
    }

    /**
     * Extract numeric band scores from payload.
     */
    protected function extract_scores_from_ai_payload($payload) {
        $keys = [
            'fluency-coherence',
            'lexical-resource',
            'grammatical-range-and-accuracy',
            'pronunciation',
        ];

        $scores = [];
        foreach ($keys as $key) {
            if (!isset($payload[$key]['band'])) {
                return [];
            }
            $scores[] = $this->normalize_band_score($payload[$key]['band']);
        }

        return $scores;
    }

    /**
     * Normalise a band score string/number.
     */
    protected function normalize_band_score($value) {
        $band = (float) preg_replace('/[^0-9\.]/', '', (string) $value);
        if ($band < 1) {
            $band = 1;
        }
        if ($band > 9) {
            $band = 9;
        }
        return (int) round($band);
    }

    /**
     * Compose readable feedback summary.
     */
    protected function compose_human_readable_feedback($payload) {
        $lines = [];

        if (isset($payload['overall']['band'])) {
            $lines[] = 'Overall Band: ' . $payload['overall']['band'];
        }

        if (!empty($payload['overall']['feedback'])) {
            $lines[] = '';
            $lines[] = 'Overall Feedback:';
            $lines[] = trim($payload['overall']['feedback']);
        }

        $category_map = [
            'fluency-coherence'              => 'Fluency & Coherence',
            'lexical-resource'               => 'Lexical Resource',
            'grammatical-range-and-accuracy' => 'Grammatical Range & Accuracy',
            'pronunciation'                  => 'Pronunciation',
        ];

        foreach ($category_map as $key => $label) {
            if (!empty($payload[$key]['feedback'])) {
                $lines[] = '';
                $lines[] = sprintf('%s (Band %s):', $label, $payload[$key]['band'] ?? '-');
                $lines[] = trim($payload[$key]['feedback']);
            }
        }

        if (!empty($payload['per-part']) && is_array($payload['per-part'])) {
            $lines[] = '';
            $lines[] = 'Per-part Summary:';
            foreach ($payload['per-part'] as $part) {
                $lines[] = '';
                $part_title = isset($part['part']) ? $part['part'] : 'Part';
                $lines[] = $part_title . ':';
                if (!empty($part['summary'])) {
                    $lines[] = trim($part['summary']);
                }
                if (!empty($part['suggestions']) && is_array($part['suggestions'])) {
                    $lines[] = 'Suggestions:';
                    foreach ($part['suggestions'] as $suggestion) {
                        $lines[] = '- ' . trim($suggestion);
                    }
                }
            }
        }

        if (!empty($payload['highlights']) && is_array($payload['highlights'])) {
            $lines[] = '';
            $lines[] = 'Highlights:';
            foreach ($payload['highlights'] as $highlight) {
                $lines[] = '- ' . trim($highlight);
            }
        }

        return implode("\n", array_map('trim', $lines));
    }

    /**
     * Reduce schema payload for storage.
     */
    protected function summarize_schema_for_storage($schema) {
        $summary = [];

        foreach ((array) $schema as $part) {
            $part_summary = [
                'task_id'            => $part['task_id'] ?? '',
                'task'               => $part['task'] ?? '',
                'overall_transcript' => $part['overall_transcript'] ?? '',
                'questions'          => [],
            ];

            if (!empty($part['questions']) && is_array($part['questions'])) {
                foreach ($part['questions'] as $question) {
                    $part_summary['questions'][] = [
                        'question_id' => $question['question_id'] ?? '',
                        'question'    => $question['question'] ?? '',
                        'interval'    => $question['interval'] ?? [],
                        'transcript'  => $question['transcript'] ?? '',
                        'transcript_html' => $question['transcript_html'] ?? '',
                        'timing'      => $question['timing'] ?? [],
                    ];
                }
            }

            $summary[] = $part_summary;
        }

        return $summary;
    }

    /**
     * Transcribe audio using OpenAI Whisper.
     */
    public function transcribe_audio($file_path, $apikey) {
        if (!file_exists($file_path)) {
            return [
                'error' => [
                    'message' => 'Recording file not found for transcription.',
                    'code'    => 'PROQYZ_SPEAKING_AI::transcribe_missing_file',
                ],
            ];
        }

        $mime = wp_check_filetype($file_path);
        $mime_type = $mime && $mime['type'] ? $mime['type'] : 'audio/wav';

        $curl = curl_init();

        $post_fields = [
            'file'            => new CURLFile($file_path, $mime_type, basename($file_path)),
            'model'           => 'whisper-1',
            'response_format' => 'verbose_json',
            'temperature'     => 0,
            'timestamp_granularities[]' => 'word',
        ];

        curl_setopt_array($curl, [
            CURLOPT_URL            => 'https://api.openai.com/v1/audio/transcriptions',
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $post_fields,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'Authorization: Bearer ' . $apikey,
            ],
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false
        ]);

        $body = curl_exec($curl);
        $curl_error = curl_error($curl);
        $http_code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);

        if ($curl_error) {
            return [
                'error' => [
                    'message' => $curl_error,
                    'code'    => 'PROQYZ_SPEAKING_AI::transcribe_curl_error',
                ],
            ];
        }

        if ($http_code !== 200) {
            return [
                'error' => [
                    'message' => 'Transcription request failed.',
                    'detail'  => mb_substr($body, 0, 500),
                    'code'    => 'PROQYZ_SPEAKING_AI::transcribe_http_' . $http_code,
                ],
            ];
        }

        $decoded = json_decode($body, true);

        return $decoded;
    }

    /**
     * Resolve audio path.
     */
    protected function resolve_audio_path($location, $filename) {
        $location = ltrim($location, '/');
        $root = trailingslashit(ABSPATH);
        $path = wp_normalize_path($root . $location . '/' . $filename);
        return $path;
    }

    /**
     * Prepare intervals. - used
     */
    protected function prepare_intervals($interval_string, $question_count, $fallback_duration = null) {
        $intervals = array_values(array_filter(array_map('floatval', explode(',', $interval_string)), static function ($value) {
            return $value >= 0;
        }));

        if (count($intervals) < $question_count + 1) {
            $approx_step = $fallback_duration && $question_count > 0 ? $fallback_duration / $question_count : 60;
            $intervals = [];
            for ($i = 0; $i <= $question_count; $i++) {
                $intervals[] = round($approx_step * $i, 2);
            }
        }

        return $intervals;
    }




    public static function date(?string $content = null, $params = []) { return date($content, $params); }










    
}

return Speaking_AI_Evaluation::instance();




