<?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'
                ],
            ],
        ]);
    }

    /**
     * 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) {
            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
        $response = wp_remote_post($url, [
            'headers' => [
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type'  => 'application/json',
            ],
            'body'    => wp_json_encode($payload),
            'timeout' => 60,
        ]);

        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);

        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]
            );
        }

    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
        $user_id = get_current_user_id();
        if (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 ) {
            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 (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);
        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();
