<?php
/**
 * AI Voice/TTS manager.
 */

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

class AIAutotool_AI_Voice_Manager {
    private static $instance = null;

    private $option_provider = 'aiautotool_tts_provider';
    private $option_voice = 'aiautotool_tts_voice';
    private $option_model = 'aiautotool_tts_model';
    private $option_usage = 'aiautotool_tts_usage_stats';

    public static function get_instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    public function get_supported_providers() {
        return array(
            'openai' => array(
                'name' => 'OpenAI',
                'default_model' => 'tts-1',
                'default_voice' => 'alloy',
                'models' => array(
                    'tts-1' => 'TTS 1',
                    'tts-1-hd' => 'TTS 1 HD',
                    'gpt-4o-mini-tts' => 'GPT-4o Mini TTS',
                ),
                'voices' => array(
                    'alloy' => 'Alloy',
                    'ash' => 'Ash',
                    'ballad' => 'Ballad',
                    'coral' => 'Coral',
                    'echo' => 'Echo',
                    'fable' => 'Fable',
                    'nova' => 'Nova',
                    'onyx' => 'Onyx',
                    'sage' => 'Sage',
                    'shimmer' => 'Shimmer',
                ),
                'formats' => array('mp3'),
                'status' => 'ready',
            ),
            'elevenlabs' => array(
                'name' => 'ElevenLabs',
                'default_model' => 'eleven_multilingual_v2',
                'default_voice' => '21m00Tcm4TlvDq8ikWAM',
                'models' => array(
                    'eleven_multilingual_v2' => 'Eleven Multilingual v2',
                    'eleven_turbo_v2_5' => 'Eleven Turbo v2.5',
                    'eleven_flash_v2_5' => 'Eleven Flash v2.5',
                    'eleven_monolingual_v1' => 'Eleven Monolingual v1',
                ),
                'voices' => array(
                    '21m00Tcm4TlvDq8ikWAM' => 'Rachel',
                    'EXAVITQu4vr4xnSDxMaL' => 'Bella',
                    'ErXwobaYiN019PkySvjV' => 'Antoni',
                    'MF3mGyEYCl7XYWbV9V6O' => 'Elli',
                    'TxGEqnHWrfWFTfGW9XjX' => 'Josh',
                    'VR6AewLTigWG4xSOukaG' => 'Arnold',
                    'pNInz6obpgDQGcFmaJgB' => 'Adam',
                    'yoZ06aMxZJJ28mfd3POQ' => 'Sam',
                ),
                'formats' => array('mp3'),
                'status' => 'ready',
            ),
            'google_tts' => array('name' => 'Google TTS', 'status' => 'planned'),
            'azure_speech' => array('name' => 'Azure Speech', 'status' => 'planned'),
            'amazon_polly' => array('name' => 'Amazon Polly', 'status' => 'planned'),
            'minimax_tts' => array('name' => 'Minimax TTS', 'status' => 'planned'),
            'fish_audio' => array('name' => 'Fish Audio', 'status' => 'planned'),
            'playht' => array('name' => 'PlayHT', 'status' => 'planned'),
            'cartesia' => array('name' => 'Cartesia', 'status' => 'planned'),
            'edge_tts' => array('name' => 'Edge TTS', 'status' => 'planned'),
            'kokoro' => array('name' => 'Kokoro', 'status' => 'planned'),
            'xtts_self_hosted' => array('name' => 'XTTS Self Hosted', 'status' => 'planned'),
        );
    }

    public function get_config() {
        $provider = get_option($this->option_provider, 'openai');
        $providers = $this->get_supported_providers();
        if (!isset($providers[$provider])) {
            $provider = 'openai';
        }

        return array(
            'provider' => $provider,
            'model' => get_option($this->option_model, isset($providers[$provider]['default_model']) ? $providers[$provider]['default_model'] : ''),
            'voice' => get_option($this->option_voice, isset($providers[$provider]['default_voice']) ? $providers[$provider]['default_voice'] : ''),
            'providers' => $providers,
            'usage' => $this->get_usage_stats(),
        );
    }

    public function get_usage_stats() {
        $stats = get_option($this->option_usage, array());
        $defaults = array(
            'characters' => 0,
            'audio_count' => 0,
            'audio_seconds' => 0,
            'estimated_cost' => 0,
            'month' => gmdate('Y-m'),
        );

        if (!is_array($stats) || (isset($stats['month']) && $stats['month'] !== gmdate('Y-m'))) {
            return $defaults;
        }

        return array_merge($defaults, $stats);
    }

    public function generate_from_post($post_id, $options = array()) {
        $post = get_post($post_id);
        if (!$post) {
            return array('success' => false, 'error' => __('Post not found', 'ai-auto-tool'));
        }

        $text = $this->build_post_text($post, $options);
        if ($text === '') {
            return array('success' => false, 'error' => __('Post has no readable text for TTS', 'ai-auto-tool'));
        }

        $options['post_id'] = $post_id;
        $options['title'] = get_the_title($post);

        return $this->generate_audio($text, $options);
    }

    public function generate_audio($text, $options = array()) {
        $text = $this->normalize_text($text, $options);
        if ($text === '') {
            return array('success' => false, 'error' => __('Text is empty', 'ai-auto-tool'));
        }

        $provider = isset($options['provider']) ? sanitize_key($options['provider']) : get_option($this->option_provider, 'openai');
        $providers = $this->get_supported_providers();
        if (empty($providers[$provider]) || $providers[$provider]['status'] !== 'ready') {
            return array('success' => false, 'error' => __('Selected TTS provider is not ready yet', 'ai-auto-tool'));
        }

        $model = isset($options['model']) ? sanitize_text_field($options['model']) : get_option($this->option_model, $providers[$provider]['default_model']);
        $voice = isset($options['voice']) ? sanitize_text_field($options['voice']) : get_option($this->option_voice, $providers[$provider]['default_voice']);
        $format = isset($options['format']) ? sanitize_key($options['format']) : 'mp3';

        $chunks = $this->split_text_for_provider($text, $provider);
        $audio_parts = array();

        foreach ($chunks as $chunk_index => $chunk) {
            if ($provider === 'openai') {
                $result = $this->generate_with_openai($chunk, $model, $voice, $format);
            } elseif ($provider === 'elevenlabs') {
                $result = $this->generate_with_elevenlabs($chunk, $model, $voice);
            } else {
                $result = array('success' => false, 'error' => __('Unsupported TTS provider', 'ai-auto-tool'));
            }

            if (empty($result['success'])) {
                $result['error'] = sprintf(__('TTS failed at part %1$d/%2$d: %3$s', 'ai-auto-tool'), $chunk_index + 1, count($chunks), isset($result['error']) ? $result['error'] : __('Unknown error', 'ai-auto-tool'));
                return $result;
            }

            $audio_parts[] = $result['audio'];
        }

        $title = isset($options['title']) ? sanitize_text_field($options['title']) : __('Article narration', 'ai-auto-tool');
        $combined_audio = $this->combine_audio_parts($audio_parts, $format, $title);
        if (empty($combined_audio['success'])) {
            return $combined_audio;
        }

        $saved = $this->save_audio_bytes($combined_audio['audio'], $title, $format, isset($options['post_id']) ? (int) $options['post_id'] : 0);
        if (empty($saved['success'])) {
            return $saved;
        }

        $this->record_usage($provider, strlen($text), $saved['bytes']);

        if (!empty($options['post_id'])) {
            update_post_meta((int) $options['post_id'], '_aiautotool_tts_audio', array(
                'attachment_id' => $saved['attachment_id'],
                'url' => $saved['url'],
                'provider' => $provider,
                'model' => $model,
                'voice' => $voice,
                'characters' => strlen($text),
                'bytes' => $saved['bytes'],
                'created_at' => current_time('mysql'),
            ));
        }

        return array_merge($saved, array(
            'provider' => $provider,
            'model' => $model,
            'voice' => $voice,
            'characters' => strlen($text),
            'parts' => count($chunks),
        ));
    }

    private function split_text_for_provider($text, $provider) {
        $limit = $provider === 'openai' ? 3000 : 4500;
        if (function_exists('mb_strlen') && mb_strlen($text, 'UTF-8') <= $limit) {
            return array($text);
        }

        if (!function_exists('mb_strlen')) {
            return str_split($text, $limit);
        }

        $sentences = preg_split('/(?<=[\.\!\?\n。！？])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
        if (empty($sentences)) {
            $sentences = array($text);
        }

        $chunks = array();
        $current = '';

        foreach ($sentences as $sentence) {
            $sentence = trim($sentence);
            if ($sentence === '') {
                continue;
            }

            if (mb_strlen($sentence, 'UTF-8') > $limit) {
                if ($current !== '') {
                    $chunks[] = trim($current);
                    $current = '';
                }

                $offset = 0;
                $length = mb_strlen($sentence, 'UTF-8');
                while ($offset < $length) {
                    $chunks[] = mb_substr($sentence, $offset, $limit, 'UTF-8');
                    $offset += $limit;
                }
                continue;
            }

            $candidate = $current === '' ? $sentence : $current . ' ' . $sentence;
            if (mb_strlen($candidate, 'UTF-8') > $limit) {
                $chunks[] = trim($current);
                $current = $sentence;
            } else {
                $current = $candidate;
            }
        }

        if ($current !== '') {
            $chunks[] = trim($current);
        }

        return array_values(array_filter($chunks));
    }

    private function combine_audio_parts($audio_parts, $format, $title) {
        if (count($audio_parts) === 1) {
            return array('success' => true, 'audio' => $audio_parts[0]);
        }

        if ($format !== 'mp3') {
            return array('success' => false, 'error' => __('Long-form TTS currently supports MP3 output only.', 'ai-auto-tool'));
        }

        $ffmpeg = $this->find_ffmpeg_binary();
        if (empty($ffmpeg)) {
            return array('success' => false, 'error' => __('Long article was split into multiple TTS parts, but ffmpeg is not available to merge MP3 files.', 'ai-auto-tool'));
        }

        $upload_dir = wp_upload_dir();
        if (!empty($upload_dir['error'])) {
            return array('success' => false, 'error' => $upload_dir['error']);
        }

        $tmp_dir = trailingslashit($upload_dir['basedir']) . 'aitool/tmp';
        if (!wp_mkdir_p($tmp_dir)) {
            return array('success' => false, 'error' => __('Could not create temporary audio directory', 'ai-auto-tool'));
        }

        $token = wp_generate_password(8, false, false);
        $concat_file = trailingslashit($tmp_dir) . 'tts-' . $token . '.txt';
        $part_files = array();

        foreach ($audio_parts as $index => $audio) {
            $part_file = trailingslashit($tmp_dir) . 'tts-' . $token . '-' . $index . '.mp3';
            if (false === file_put_contents($part_file, $audio)) {
                $this->cleanup_files(array_merge($part_files, array($concat_file)));
                return array('success' => false, 'error' => __('Could not write temporary audio part', 'ai-auto-tool'));
            }
            $part_files[] = $part_file;
        }

        $concat_lines = array_map(function($file) {
            return "file '" . str_replace("'", "'\\''", $file) . "'";
        }, $part_files);
        file_put_contents($concat_file, implode("\n", $concat_lines));

        $output_file = trailingslashit($tmp_dir) . sanitize_file_name(sanitize_title($title) . '-' . $token . '.mp3');
        $command = escapeshellarg($ffmpeg) . ' -y -f concat -safe 0 -i ' . escapeshellarg($concat_file) . ' -c copy ' . escapeshellarg($output_file) . ' 2>&1';
        exec($command, $output, $code);

        $this->cleanup_files(array_merge($part_files, array($concat_file)));

        if ($code !== 0 || !file_exists($output_file) || filesize($output_file) <= 0) {
            @unlink($output_file);
            return array('success' => false, 'error' => __('Could not merge MP3 parts with ffmpeg: ', 'ai-auto-tool') . implode("\n", array_slice($output, -3)));
        }

        $audio = file_get_contents($output_file);
        @unlink($output_file);

        return array('success' => true, 'audio' => $audio);
    }

    private function find_ffmpeg_binary() {
        $candidates = array('/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg', '/usr/bin/ffmpeg', 'ffmpeg');
        foreach ($candidates as $candidate) {
            if ($candidate === 'ffmpeg') {
                $path = trim((string) shell_exec('command -v ffmpeg 2>/dev/null'));
                if ($path !== '') {
                    return $path;
                }
                continue;
            }

            if (is_executable($candidate)) {
                return $candidate;
            }
        }

        return '';
    }

    private function cleanup_files($files) {
        foreach ($files as $file) {
            if ($file && file_exists($file)) {
                @unlink($file);
            }
        }
    }

    private function generate_with_openai($text, $model, $voice, $format) {
        $api_key = $this->get_provider_key('openai');
        if (empty($api_key)) {
            return array('success' => false, 'error' => __('OpenAI API key is not configured', 'ai-auto-tool'));
        }

        $response = wp_remote_post('https://api.openai.com/v1/audio/speech', array(
            'headers' => array(
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type' => 'application/json',
            ),
            'body' => wp_json_encode(array(
                'model' => $model ?: 'tts-1',
                'voice' => $voice ?: 'alloy',
                'input' => $text,
                'response_format' => $format ?: 'mp3',
            )),
            'timeout' => 90,
        ));

        return $this->parse_binary_tts_response($response, 'OpenAI');
    }

    private function generate_with_elevenlabs($text, $model, $voice) {
        $api_key = $this->get_provider_key('elevenlabs');
        if (empty($api_key)) {
            return array('success' => false, 'error' => __('ElevenLabs API key is not configured', 'ai-auto-tool'));
        }

        $voice = $voice ?: '21m00Tcm4TlvDq8ikWAM';
        $response = wp_remote_post('https://api.elevenlabs.io/v1/text-to-speech/' . rawurlencode($voice), array(
            'headers' => array(
                'xi-api-key' => $api_key,
                'Content-Type' => 'application/json',
            ),
            'body' => wp_json_encode(array(
                'text' => $text,
                'model_id' => $model ?: 'eleven_multilingual_v2',
            )),
            'timeout' => 90,
        ));

        return $this->parse_binary_tts_response($response, 'ElevenLabs');
    }

    private function parse_binary_tts_response($response, $provider_name) {
        if (is_wp_error($response)) {
            return array('success' => false, 'error' => $response->get_error_message());
        }

        $code = wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);
        if ($code < 200 || $code >= 300) {
            $json = json_decode($body, true);
            $message = isset($json['error']['message']) ? $json['error']['message'] : (isset($json['detail']['message']) ? $json['detail']['message'] : $body);
            return array('success' => false, 'error' => $provider_name . ' TTS error: ' . wp_strip_all_tags($message));
        }

        if (empty($body)) {
            return array('success' => false, 'error' => $provider_name . ' returned empty audio');
        }

        return array('success' => true, 'audio' => $body);
    }

    private function save_audio_bytes($audio, $title, $format, $post_id = 0) {
        $upload_dir = wp_upload_dir();
        if (!empty($upload_dir['error'])) {
            return array('success' => false, 'error' => $upload_dir['error']);
        }

        $subdir = 'aitool/audio';
        $dir = trailingslashit($upload_dir['basedir']) . $subdir;
        if (!wp_mkdir_p($dir)) {
            return array('success' => false, 'error' => __('Could not create audio upload directory', 'ai-auto-tool'));
        }

        $ext = in_array($format, array('mp3', 'wav', 'm4a'), true) ? $format : 'mp3';
        $filename = sanitize_file_name(sanitize_title($title) . '-' . wp_generate_password(8, false, false) . '.' . $ext);
        $file = trailingslashit($dir) . $filename;

        if (false === file_put_contents($file, $audio)) {
            return array('success' => false, 'error' => __('Could not write audio file', 'ai-auto-tool'));
        }

        $filetype = wp_check_filetype($filename);
        $attachment_id = wp_insert_attachment(array(
            'guid' => trailingslashit($upload_dir['baseurl']) . $subdir . '/' . $filename,
            'post_mime_type' => $filetype['type'] ?: 'audio/mpeg',
            'post_title' => sanitize_text_field($title),
            'post_content' => '',
            'post_status' => 'inherit',
        ), $file, $post_id);

        if (is_wp_error($attachment_id)) {
            @unlink($file);
            return array('success' => false, 'error' => $attachment_id->get_error_message());
        }

        require_once ABSPATH . 'wp-admin/includes/image.php';
        wp_update_attachment_metadata($attachment_id, wp_generate_attachment_metadata($attachment_id, $file));

        return array(
            'success' => true,
            'attachment_id' => $attachment_id,
            'url' => wp_get_attachment_url($attachment_id),
            'path' => $file,
            'bytes' => filesize($file),
        );
    }

    private function build_post_text($post, $options) {
        $parts = array();

        if (!isset($options['read_title']) || !empty($options['read_title'])) {
            $parts[] = get_the_title($post);
        }

        if (!empty($post->post_excerpt)) {
            $parts[] = $post->post_excerpt;
        }

        $parts[] = $post->post_content;

        return $this->normalize_text(implode("\n\n", array_filter($parts)), $options);
    }

    public function normalize_text($text, $options = array()) {
        $text = strip_shortcodes((string) $text);
        $text = preg_replace('#<script\b[^>]*>.*?</script>#is', ' ', $text);
        $text = preg_replace('#<style\b[^>]*>.*?</style>#is', ' ', $text);
        $text = preg_replace('#<pre\b[^>]*>.*?</pre>#is', ' ', $text);
        $text = preg_replace('#<code\b[^>]*>.*?</code>#is', ' ', $text);
        $text = wp_strip_all_tags($text, true);

        if (!empty($options['skip_urls'])) {
            $text = preg_replace('~https?://\S+~i', ' ', $text);
        }

        $text = html_entity_decode($text, ENT_QUOTES, get_bloginfo('charset') ?: 'UTF-8');
        $text = preg_replace('/\s+/u', ' ', $text);

        return trim($text);
    }

    private function get_provider_key($provider) {
        if (class_exists('AIAutotool_AI_Provider_API_Keys')) {
            return AIAutotool_AI_Provider_API_Keys::get_provider_key($provider);
        }

        return '';
    }

    private function record_usage($provider, $characters, $bytes) {
        $stats = $this->get_usage_stats();
        $stats['characters'] += (int) $characters;
        $stats['audio_count'] += 1;
        $stats['audio_seconds'] += max(1, (int) ceil($characters / 14));
        $stats['estimated_cost'] += $this->estimate_cost($provider, $characters);
        $stats['month'] = gmdate('Y-m');

        update_option($this->option_usage, $stats, false);
    }

    private function estimate_cost($provider, $characters) {
        $per_million = array(
            'openai' => 15.00,
            'elevenlabs' => 30.00,
        );

        $rate = isset($per_million[$provider]) ? $per_million[$provider] : 0;
        return round(($characters / 1000000) * $rate, 6);
    }
}
