<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * VMO OpenAI Text-to-Speech HD
 * 
 * High-quality TTS streaming using OpenAI's latest model
 * Works via REST API proxy - no client-side API key exposure
 * 
 * @package VMO_AI_Website_Builder
 * @since 1.0.0
 */

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

class VMO_OpenAI_TTS_REST {
    
    public function __construct() {
        add_action('rest_api_init', [$this, 'register_routes']);
        // Streaming via admin-ajax to allow <audio src> without custom headers
        add_action('wp_ajax_vmo_tts_stream', [$this, 'ajax_stream_speak']);
        // AJAX fallback for fetching voices (since REST API permalinks can be flaky)
        add_action('wp_ajax_vmo_tts_voices', [$this, 'ajax_get_voices']);

        // Force flush rewrite rules if needed (Self-healing)
        if (!get_transient('vmo_tts_rest_flushed_v9_0_1')) {
            add_action('init', function() {
                flush_rewrite_rules();
                set_transient('vmo_tts_rest_flushed_v9_0_1', true, 12 * HOUR_IN_SECONDS);
            });
        }
    }

    /**
     * Register REST API routes
     */
    public function register_routes() {
        register_rest_route('vmo/v1', '/tts/speak', [
            'methods'  => 'POST',
            'permission_callback' => [$this, 'can_use_tts'],
            'callback' => [$this, 'handle_speak_request'],
            'args' => [
                'text' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_textarea_field',
                ],
                'voice' => [
                    'required' => false,
                    'type' => 'string',
                    'default' => 'ash',
                    'enum' => ['ash', 'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'],
                ],
                'format' => [
                    'required' => false,
                    'type' => 'string',
                    'default' => 'mp3',
                    'enum' => ['mp3', 'opus', 'aac', 'flac', 'wav', 'pcm'],
                ],
                'speed' => [
                    'required' => false,
                    'type' => 'number',
                    'default' => 1.0,
                    'minimum' => 0.25,
                    'maximum' => 4.0,
                ],
            ],
        ]);

        register_rest_route('vmo/v1', '/tts/voices', [
            'methods'  => 'GET',
            'permission_callback' => [$this, 'can_use_tts'],
            'callback' => [$this, 'get_available_voices'],
        ]);
    }

    /**
     * Permission check - only logged-in users can use TTS
     */
    public function can_use_tts() {
        return is_user_logged_in();
    }

    /**
     * Get available voices with descriptions
     */
    public function get_available_voices() {
        return rest_ensure_response([
            'voices' => [
                [
                    'id' => 'ash',
                    'name' => 'Ash',
                    'description' => 'Modern, confident tone tuned for consultations'
                ],
                [
                    'id' => 'alloy',
                    'name' => 'Alloy',
                    'description' => 'Neutral and balanced voice, suitable for most content'
                ],
                [
                    'id' => 'echo',
                    'name' => 'Echo',
                    'description' => 'Warm and engaging, great for storytelling'
                ],
                [
                    'id' => 'fable',
                    'name' => 'Fable',
                    'description' => 'Expressive and dynamic, perfect for creative content'
                ],
                [
                    'id' => 'onyx',
                    'name' => 'Onyx',
                    'description' => 'Deep and authoritative, ideal for professional content'
                ],
                [
                    'id' => 'nova',
                    'name' => 'Nova',
                    'description' => 'Bright and friendly, excellent for tutorials'
                ],
                [
                    'id' => 'shimmer',
                    'name' => 'Shimmer',
                    'description' => 'Clear and articulate, perfect for announcements'
                ],
            ],
        ]);
    }

    /**
     * Normalize a possible legacy/base64 TTS proxy response into raw audio bytes.
     * The QuickLaunch client can hit either the raw proxy or an older JSON proxy;
     * this keeps both paths playable and prevents JSON/base64 text from being
     * mislabeled as audio/mpeg, which causes browser DEMUXER errors.
     */
    private function normalize_audio_response_body($body, $content_type = '', $format = 'mp3') {
        $body = (string) $body;
        $content_type = strtolower((string) $content_type);
        $trimmed = ltrim(substr($body, 0, 512));

        if ($trimmed !== '' && ($trimmed[0] === '{' || $trimmed[0] === '[' || strpos($content_type, 'json') !== false || strpos($content_type, 'text/') !== false)) {
            $decoded = json_decode($body, true);
            if (is_array($decoded)) {
                $audio64 = $decoded['audio'] ?? ($decoded['data']['audio'] ?? ($decoded['audio_base64'] ?? ($decoded['data']['audio_base64'] ?? '')));
                if (is_string($audio64) && $audio64 !== '') {
                    $audio64 = preg_replace('#^data:audio/[^;]+;base64,#i', '', $audio64);
                    $raw = base64_decode($audio64, true);
                    if ($raw !== false && strlen($raw) > 128) {
                        return $raw;
                    }
                }
                $message = $decoded['message'] ?? ($decoded['error']['message'] ?? ($decoded['data']['message'] ?? 'TTS proxy returned JSON without audio data.'));
                return new WP_Error('vmo_tts_non_audio_json', (string) $message, array('status' => 502));
            }
            return new WP_Error('vmo_tts_non_audio_text', 'TTS proxy returned text/HTML instead of playable audio.', array('status' => 502));
        }

        if (strlen($body) < 128) {
            return new WP_Error('vmo_tts_empty_audio', 'TTS returned an empty or tiny audio payload.', array('status' => 502));
        }

        // Some proxy/server paths can prepend a PHP notice, BOM, or whitespace
        // before the raw audio bytes. Strip that junk before the browser sees it.
        $repaired = $this->strip_leading_junk_before_audio($body);
        if ($repaired !== $body) {
            return $repaired;
        }

        // VMO_TTS_RAW_BASE64_NORMALIZER_v2
        // Some master/client proxy responses return raw base64 but keep an audio/*
        // content-type. Decode that safely before echoing it as binary audio.
        $body_probe = preg_replace('/\s+/', '', $body);
        if (strlen($body_probe) > 256 && preg_match('#^(?:data:audio/[a-z0-9.+-]+;base64,)?[A-Za-z0-9+/]+={0,2}$#i', $body_probe)) {
            $body_probe = preg_replace('#^data:audio/[^;]+;base64,#i', '', $body_probe);
            $raw = base64_decode($body_probe, true);
            if ($raw !== false && strlen($raw) > 128) {
                $magic = substr($raw, 0, 12);
                if (strncmp($magic, 'ID3', 3) === 0 || substr($magic, 0, 2) === "\xFF\xFB" || substr($magic, 0, 2) === "\xFF\xF3" || substr($magic, 0, 2) === "\xFF\xF2" || substr($magic, 0, 4) === 'OggS' || (substr($magic, 0, 4) === 'RIFF' && substr($magic, 8, 4) === 'WAVE')) {
                    return $raw;
                }
            }
        }

        if (!$this->audio_body_has_magic($body)) {
            return new WP_Error('vmo_tts_invalid_audio_container', 'TTS payload is not a playable audio container.', array('status' => 502));
        }

        return $body;
    }

    private function audio_body_has_magic($body) {
        $body = (string) $body;
        if ($body === '') {
            return false;
        }
        if (strncmp($body, 'ID3', 3) === 0 || strncmp($body, 'OggS', 4) === 0 || strncmp($body, 'fLaC', 4) === 0) {
            return true;
        }
        if (strncmp($body, 'RIFF', 4) === 0 && substr($body, 8, 4) === 'WAVE') {
            return true;
        }
        $prefix = substr($body, 0, 2);
        return $prefix === "\xFF\xFB" || $prefix === "\xFF\xF3" || $prefix === "\xFF\xF2";
    }

    private function strip_leading_junk_before_audio($body) {
        $body = (string) $body;
        if ($body === '') {
            return $body;
        }

        $candidates = [];
        foreach ([
            strpos($body, 'ID3'),
            strpos($body, 'OggS'),
            strpos($body, 'fLaC'),
            strpos($body, 'RIFF'),
            strpos($body, "\xFF\xFB"),
            strpos($body, "\xFF\xF3"),
            strpos($body, "\xFF\xF2"),
        ] as $pos) {
            if ($pos !== false) {
                $candidates[] = (int) $pos;
            }
        }

        if (empty($candidates)) {
            return $body;
        }

        $offset = min($candidates);
        if ($offset <= 0 || $offset > 8192) {
            return $body;
        }

        return substr($body, $offset);
    }

    private function response_content_type($response) {
        $headers = wp_remote_retrieve_headers($response);
        if ($headers instanceof \WP_Http_Requests_Headers || $headers instanceof \Requests_Utility_CaseInsensitiveDictionary) {
            return (string) ($headers['content-type'] ?? '');
        }
        if (is_array($headers)) {
            return (string) ($headers['content-type'] ?? ($headers['Content-Type'] ?? ''));
        }
        return '';
    }

    /**
     * Handle TTS speak request
     * 
     * @param WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function handle_speak_request( WP_REST_Request $request ) {
        $api_key = get_option('vmo_openai_api_key');
        if ( ! $api_key && defined('VMO_OPENAI_API_KEY') ) {
            $api_key = VMO_OPENAI_API_KEY;
        }

        if (!$api_key && (!defined('VMO_CLIENT_MODE') || VMO_CLIENT_MODE !== true)) {
            return new WP_Error(
                'missing_api_key',
                'AI API key not configured',
                ['status' => 500]
            );
        }

        $text   = $request->get_param('text');
        $voice  = $request->get_param('voice') ?: 'ash';
        $format = $request->get_param('format') ?: 'mp3';
        $speed  = $request->get_param('speed') ?: 1.0;

        // Validate text length (OpenAI limit is 4096 characters)
        if (strlen($text) > 4096) {
            return new WP_Error(
                'text_too_long',
                'Text exceeds 4096 character limit',
                ['status' => 400]
            );
        }

        // Use the latest HD TTS model
        $model = 'tts-1-hd'; // High quality model
        $url   = 'https://api.openai.com/v1/audio/speech';

        $payload = [
            'model'  => $model,
            'input'  => $text,
            'voice'  => $voice,
            'response_format' => $format,
            'speed'  => floatval($speed),
        ];

        error_log('VMO TTS: Requesting speech synthesis - Voice: ' . $voice . ', Format: ' . $format);

        // Make request to OpenAI
        $tts_args = [
            'headers' => [
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type'  => 'application/json',
            ],
            'body'    => wp_json_encode($payload),
            'timeout' => 60,
            'vmo_operation' => 'tts',
        ];
        $response = function_exists('vmo_smart_remote_post')
            ? vmo_smart_remote_post($url, $tts_args)
            : wp_remote_post($url, $tts_args);

        if ( is_wp_error($response) ) {
            error_log('VMO TTS Error: ' . $response->get_error_message());
            return $response;
        }

        $code = wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);
        $response_ct = $this->response_content_type($response);

        if ($code < 200 || $code >= 300) {
            $error_data = json_decode($body, true);
            error_log('VMO TTS OpenAI Error: ' . $body);
            return new WP_Error(
                'openai_tts_error',
                'AI TTS error: ' . ($error_data['error']['message'] ?? 'Unknown error'),
                ['status' => $code, 'body' => $error_data]
            );
        }

        $normalized_body = $this->normalize_audio_response_body($body, $response_ct, $format);
        if (is_wp_error($normalized_body)) {
            error_log('VMO TTS Normalize Error: ' . $normalized_body->get_error_message());
            return $normalized_body;
        }
        $body = $normalized_body;

        error_log('VMO TTS: Successfully generated audio (' . strlen($body) . ' bytes)');

        // Determine MIME type based on format
        $mime_types = [
            'mp3'  => 'audio/mpeg',
            // OpenAI opus is typically Ogg container with Opus codec
            'opus' => 'audio/ogg; codecs=opus',
            'aac'  => 'audio/aac',
            'flac' => 'audio/flac',
            'wav'  => 'audio/wav',
            'pcm'  => 'audio/pcm',
        ];
        $mime = $mime_types[$format] ?? 'audio/mpeg';

        // Return audio directly (disable compression and ensure clean output)
        // IMPORTANT: We bypass WP REST serialization here to avoid corrupting binary payloads.
        // We emit headers + raw bytes and exit.
        // DISCARD all output buffers — ob_end_clean (not flush) to prevent
        // any buffered HTML / notices from being sent before the binary audio.
        while (ob_get_level() > 0) {
            @ob_end_clean();
        }
        if ( function_exists('nocache_headers') ) {
            nocache_headers();
        }
        if ( headers_sent($hs_file, $hs_line) ) {
            error_log("VMO TTS WARNING: Headers already sent by {$hs_file}:{$hs_line} — audio Content-Type may be wrong");
        }
        // Always attempt to set headers (even if headers_sent — PHP will warn but we need to try)
        @header_remove('Content-Encoding');
        @header('Content-Type: ' . $mime);
        @header('Content-Length: ' . strlen($body));
        @header('Cache-Control: no-cache, no-store, must-revalidate');
        @header('Pragma: no-cache');
        @header('Expires: 0');
        @header('Accept-Ranges: bytes');
        @header('Content-Disposition: inline; filename="speech.' . ($format === 'mp3' ? 'mp3' : $format) . '"');

        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- raw binary response
        echo $body; // output raw audio bytes
        exit; // prevent WP from attempting to serialize the response
    }

    /**
     * AJAX handler for getting voices (Fallback for REST API)
     */
    public function ajax_get_voices() {
        if ( ! is_user_logged_in() ) {
            wp_send_json_error(['message' => 'Unauthorized'], 401);
        }
        
        // Reuse the same logic as the REST controller
        $response = $this->get_available_voices();
        
        // Unwrap the REST response object if needed
        if ( $response instanceof WP_REST_Response ) {
            wp_send_json($response->get_data());
        } else {
            wp_send_json($response);
        }
    }

    /**
     * Stream TTS audio directly to the browser using admin-ajax
     * This enables progressive playback via <audio src="...">
     */
    public function ajax_stream_speak() {
        // Flush all output buffers first to ensure clean output
        while (ob_get_level() > 0) {
            @ob_end_clean();
        }
        
        // Helper to output error as JSON with proper headers
        $output_error = function($message, $code = 500) {
            if (!headers_sent()) {
                @header_remove('Content-Encoding');
                header('Content-Type: application/json; charset=utf-8');
                http_response_code($code);
            }
            echo json_encode(['error' => true, 'message' => $message]);
            exit;
        };
        
        if ( ! is_user_logged_in() ) {
            $output_error('Unauthorized - please log in', 401);
        }
        
        // ✅ TOKEN DEDUCTION v8.1.0: Charge for TTS voice synthesis.
        // In VMO_CLIENT_MODE the Vultr/QuickLaunch client must not do local
        // token accounting for provider APIs. It routes to the Kinsta VMO master,
        // and the master proxy is the billing authority. Local pre-deduction here
        // can falsely block brand-new hosted clients before plan/license mirrors
        // are hydrated, causing OpenAI voice to fail and browser voice fallback.
        // Master admins (manage_options) are also exempt: they are the plugin
        // operator testing their own Jarvis panel, not a billed end-user.
        $user_id = get_current_user_id();
        $is_client_mode = (defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true);
        $is_master_admin = !$is_client_mode && current_user_can('manage_options');
        if (!$is_client_mode && !$is_master_admin && function_exists('vmo_deduct_tokens_for_operation')) {
            if (!vmo_deduct_tokens_for_operation($user_id, 'tts')) {
                $output_error('Insufficient tokens for voice synthesis. Please top up your balance.', 402);
            }
        }

        $api_key = get_option('vmo_openai_api_key');
        if ( ! $api_key && defined('VMO_OPENAI_API_KEY') ) {
            $api_key = VMO_OPENAI_API_KEY;
        }
        if ( ! $api_key && (!defined('VMO_CLIENT_MODE') || VMO_CLIENT_MODE !== true) ) {
            error_log('VMO TTS Stream error: OpenAI API key not configured');
            $output_error('AI API key not configured. Please add your API key in VMO settings.', 500);
        }

        // Accept text from GET or POST (POST supports long text without URL limits)
        $text   = isset($_POST['text']) ? sanitize_textarea_field(wp_unslash($_POST['text'])) : '';
        if (!$text && isset($_GET['text'])) {
            $text = sanitize_textarea_field(wp_unslash($_GET['text']));
        }
        $voice  = isset($_REQUEST['voice']) ? sanitize_text_field(wp_unslash($_REQUEST['voice'])) : 'alloy';
        $format = isset($_REQUEST['format']) ? sanitize_text_field(wp_unslash($_REQUEST['format'])) : 'mp3';

        if ( ! $text ) {
            $output_error('Missing text parameter', 400);
        }
        
        // Validate voice
        $valid_voices = ['ash', 'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
        if (!in_array($voice, $valid_voices)) {
            $voice = 'alloy';
        }
        
        // Validate format
        $valid_formats = ['mp3', 'opus', 'aac', 'flac', 'wav', 'pcm'];
        if (!in_array($format, $valid_formats)) {
            $format = 'mp3';
        }

        // MIME type mapping
        $mime_types = [
            'mp3' => 'audio/mpeg',
            'wav' => 'audio/wav',
            'opus' => 'audio/ogg; codecs=opus',
            'aac' => 'audio/aac',
            'flac' => 'audio/flac',
            'pcm' => 'audio/pcm'
        ];
        $mime = $mime_types[$format] ?? 'audio/mpeg';

        // Prepare OpenAI request. The streaming route favors the low-latency
        // OpenAI TTS model; the REST fallback remains available below.
        $model = apply_filters('vmo_tts_stream_model', 'tts-1', $text, $voice, $format);
        $payload = [
            'model'  => $model,
            'input'  => $text,
            'voice'  => $voice,
            'response_format' => $format,
            'speed'  => 1.0,
        ];

        if (defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true && function_exists('vmo_smart_remote_post')) {
            $response = vmo_smart_remote_post('https://api.openai.com/v1/audio/speech', [
                'timeout' => 120,
                'headers' => [
                    'Authorization' => 'Bearer ' . $api_key,
                    'Content-Type'  => 'application/json',
                ],
                'body' => wp_json_encode($payload),
                'vmo_operation' => 'tts',
            ]);

            if (is_wp_error($response)) {
                error_log('VMO TTS Proxy Stream error: ' . $response->get_error_message());
                $output_error('TTS request failed: ' . $response->get_error_message(), 502);
            }

            $code = wp_remote_retrieve_response_code($response);
            if ($code < 200 || $code >= 300) {
                error_log('VMO TTS Proxy Stream error: master returned ' . $code);
                $output_error('TTS returned error ' . $code, 502);
            }

            $body = wp_remote_retrieve_body($response);
            $response_ct = $this->response_content_type($response);
            $normalized_body = $this->normalize_audio_response_body($body, $response_ct, $format);
            if (is_wp_error($normalized_body)) {
                $output_error($normalized_body->get_error_message(), 502);
            }
            $body = $normalized_body;
            if (empty($body)) {
                $output_error('TTS returned empty response', 502);
            }

            nocache_headers();
            @header_remove('Content-Encoding');
            @header('Content-Type: ' . $mime);
            @header('Content-Length: ' . strlen($body));
            @header('Cache-Control: no-cache, no-store, must-revalidate');
            @header('Pragma: no-cache');
            @header('Expires: 0');
            @header('Accept-Ranges: bytes');

            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- raw binary audio
            echo $body;
            exit;
        }

        if (function_exists('curl_init')) {
            $http_code = 0;
            $sent_audio_headers = false;
            $error_body = '';
            $ch = curl_init('https://api.openai.com/v1/audio/speech');

            if ($ch) {
                @set_time_limit(0);
                if (function_exists('apache_setenv')) {
                    @apache_setenv('no-gzip', '1');
                }

                curl_setopt_array($ch, [
                    CURLOPT_POST => true,
                    CURLOPT_RETURNTRANSFER => false,
                    CURLOPT_HEADER => false,
                    CURLOPT_HTTPHEADER => [
                        'Authorization: Bearer ' . $api_key,
                        'Content-Type: application/json',
                    ],
                    CURLOPT_POSTFIELDS => wp_json_encode($payload),
                    CURLOPT_TIMEOUT => 120,
                    CURLOPT_CONNECTTIMEOUT => 15,
                    CURLOPT_HEADERFUNCTION => function($curl, $header) use (&$http_code) {
                        if (preg_match('/^HTTP\/\S+\s+(\d+)/', $header, $matches)) {
                            $http_code = (int) $matches[1];
                        }
                        return strlen($header);
                    },
                    CURLOPT_WRITEFUNCTION => function($curl, $chunk) use (&$sent_audio_headers, &$error_body, &$http_code, $mime, $format) {
                        $length = strlen($chunk);
                        if ($http_code >= 200 && $http_code < 300) {
                            if (!$sent_audio_headers) {
                                nocache_headers();
                                @header_remove('Content-Encoding');
                                @header('Content-Type: ' . $mime);
                                @header('Cache-Control: no-cache, no-store, must-revalidate');
                                @header('Pragma: no-cache');
                                @header('Expires: 0');
                                @header('Accept-Ranges: none');
                                @header('X-Accel-Buffering: no');
                                @header('Content-Disposition: inline; filename="speech.' . ($format === 'mp3' ? 'mp3' : $format) . '"');
                                $sent_audio_headers = true;
                            }

                            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- raw binary audio chunk
                            echo $chunk;
                            if (ob_get_level() > 0) {
                                @ob_flush();
                            }
                            @flush();
                        } else {
                            $error_body .= $chunk;
                        }
                        return $length;
                    },
                ]);

                $ok = curl_exec($ch);
                $curl_errno = curl_errno($ch);
                $curl_error = curl_error($ch);
                if (!$http_code) {
                    $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
                }
                curl_close($ch);

                if ($sent_audio_headers) {
                    exit;
                }
                if (!$ok || $curl_errno) {
                    error_log('VMO TTS Stream cURL error: ' . $curl_error);
                    $output_error('TTS stream request failed: ' . $curl_error, 502);
                }
                if ($http_code < 200 || $http_code >= 300) {
                    $error_data = json_decode($error_body, true);
                    $message = is_array($error_data) ? ($error_data['error']['message'] ?? '') : '';
                    error_log('VMO TTS Stream OpenAI error: HTTP ' . $http_code . ' ' . substr($error_body, 0, 500));
                    $output_error('TTS returned error ' . $http_code . ($message ? ': ' . $message : ''), 502);
                }
                $output_error('TTS stream ended without audio.', 502);
            }
        }

        $response = wp_remote_post('https://api.openai.com/v1/audio/speech', [
            'timeout' => 120,
            'headers' => [
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type'  => 'application/json',
            ],
            'body' => wp_json_encode($payload),
        ]);

        if (is_wp_error($response)) {
            error_log('VMO TTS Stream error: ' . $response->get_error_message());
            $output_error('TTS request failed: ' . $response->get_error_message(), 502);
        }

        $code = wp_remote_retrieve_response_code($response);
        if ($code < 200 || $code >= 300) {
            error_log('VMO TTS Stream error: OpenAI returned ' . $code);
            $output_error('TTS returned error ' . $code, 502);
        }

        $body = wp_remote_retrieve_body($response);
        $response_ct = $this->response_content_type($response);
        $normalized_body = $this->normalize_audio_response_body($body, $response_ct, $format);
        if (is_wp_error($normalized_body)) {
            $output_error($normalized_body->get_error_message(), 502);
        }
        $body = $normalized_body;
        if (empty($body)) {
            $output_error('TTS returned empty response', 502);
        }

        // SUCCESS — now output audio headers and binary data
        nocache_headers();
        if ( headers_sent($hs_file, $hs_line) ) {
            error_log("VMO TTS Stream WARNING: Headers already sent by {$hs_file}:{$hs_line} — audio Content-Type may be wrong");
        }
        // Always attempt to set headers (suppress warnings if headers already sent)
        @header_remove('Content-Encoding');
        @header('Content-Type: ' . $mime);
        @header('Content-Length: ' . strlen($body));
        @header('Cache-Control: no-cache, no-store, must-revalidate');
        @header('Pragma: no-cache');
        @header('Expires: 0');
        @header('Accept-Ranges: bytes');

        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- raw binary audio
        echo $body;
        exit;
    }
}

// Initialize the TTS class
new VMO_OpenAI_TTS_REST();
