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

/**
 * Class Podcastmaker_API_Gemini
 *
 * Google Gemini API との通信を処理するクラス。
 */
class Podcastmaker_API_Gemini
{

	/**
	 * APIキー。
	 *
	 * @var string
	 */
	private $api_key;

	/**
	 * コンストラクタ。
	 *
	 * @param string $api_key Gemini APIキー。
	 */
	public function __construct($api_key)
	{
		$this->api_key = $api_key;
	}

	/**
	 * Gemini API から利用可能なモデルを取得する。
	 *
	 * @return array|WP_Error 成功時はモデルの配列、失敗時は WP_Error。
	 */
	public function get_models()
	{
		if (empty($this->api_key)) {
			return new WP_Error('missing_api_key', __('API key is missing.', 'quikvox-ai'));
		}

		// $url: モデル取得用のAPIエンドポイントURL
		$url = 'https://generativelanguage.googleapis.com/v1beta/models?key=' . $this->api_key;

		// $response: APIリクエストのレスポンス
		$response = wp_remote_get($url);

		if (is_wp_error($response)) {
			return $response;
		}

		// $response_code: HTTPステータスコード
		$response_code = wp_remote_retrieve_response_code($response);
		if (200 !== $response_code) {
			$response_body = wp_remote_retrieve_body($response);
			$decoded_body = json_decode($response_body, true);
			$error_message = '';

			if (is_array($decoded_body)) {
				if (!empty($decoded_body['error']['message'])) {
					$error_message = (string) $decoded_body['error']['message'];
				}
				elseif (!empty($decoded_body['error_description'])) {
					$error_message = (string) $decoded_body['error_description'];
				}
			}

			if ($error_message === '') {
				$error_message = $response_body !== '' ? $response_body : __('Unknown error.', 'quikvox-ai');
			}

			return new WP_Error(
				'api_error',
				sprintf(
					/* translators: 1: HTTP status code, 2: error message */
					__('Failed to fetch models: %1$d - %2$s', 'quikvox-ai'),
					$response_code,
					$error_message
				)
			);
		}

		// $body: レスポンスボディ
		$body = wp_remote_retrieve_body($response);
		// $data: デコードされたJSONデータ
		$data = json_decode($body, true);

		if (empty($data['models'])) {
			return array();
		}

		// $models: 整形されたモデル名の配列
		$models = array();
		foreach ($data['models'] as $model) {
			// generateContent をサポートしているモデルのみをフィルタリング
			if (isset($model['supportedGenerationMethods']) && in_array('generateContent', $model['supportedGenerationMethods'], true)) {
				// $model['name']: モデルのフルネーム (例: models/gemini-pro)
				$models[] = str_replace('models/', '', $model['name']);
			}
		}

		return $models;
	}

	/**
	 * Gemini TTS で利用可能な音声を取得する。
	 * 現在はプレビューモデルの既知の音声の静的リストを返す。
	 * 
	 * @return array 音声情報の配列。
	 */
	public function get_voices()
	{
		// Gemini TTS で現在サポートされている音声のリスト
		// 公式情報源から取得した最新の音声情報
		return array(
			// Male voices
				array('voice_id' => 'Charon', 'name' => 'Charon', 'gender' => 'Male', 'characteristics' => 'Warm, deep, informative', 'type' => 'Standard'),
				array('voice_id' => 'Fenrir', 'name' => 'Fenrir', 'gender' => 'Male', 'characteristics' => 'Friendly, clear, excitable', 'type' => 'Standard'),
				array('voice_id' => 'Achird', 'name' => 'Achird', 'gender' => 'Male', 'characteristics' => 'Friendly', 'type' => 'Standard'),
				array('voice_id' => 'Algenib', 'name' => 'Algenib', 'gender' => 'Male', 'characteristics' => 'Gravelly', 'type' => 'Standard'),
				array('voice_id' => 'Algieba', 'name' => 'Algieba', 'gender' => 'Male', 'characteristics' => 'Smooth', 'type' => 'Standard'),
				array('voice_id' => 'Alnilam', 'name' => 'Alnilam', 'gender' => 'Male', 'characteristics' => 'Firm', 'type' => 'Standard'),
				array('voice_id' => 'Enceladus', 'name' => 'Enceladus', 'gender' => 'Male', 'characteristics' => 'Breathy', 'type' => 'Standard'),
				array('voice_id' => 'Iapetus', 'name' => 'Iapetus', 'gender' => 'Male', 'characteristics' => 'Clear', 'type' => 'Standard'),

			// Female voices
				array('voice_id' => 'Aoede', 'name' => 'Aoede', 'gender' => 'Female', 'characteristics' => 'Clear, conversational', 'type' => 'Standard'),
				array('voice_id' => 'Achernar', 'name' => 'Achernar', 'gender' => 'Female', 'characteristics' => 'Clear, soft, mid-range', 'type' => 'Standard'),
				array('voice_id' => 'Autonoe', 'name' => 'Autonoe', 'gender' => 'Female', 'characteristics' => 'Bright', 'type' => 'Standard'),
				array('voice_id' => 'Callirrhoe', 'name' => 'Callirrhoe', 'gender' => 'Female', 'characteristics' => 'Easy-going', 'type' => 'Standard'),
				array('voice_id' => 'Despina', 'name' => 'Despina', 'gender' => 'Female', 'characteristics' => 'Smooth, warm, inviting', 'type' => 'Standard'),
				array('voice_id' => 'Erinome', 'name' => 'Erinome', 'gender' => 'Female', 'characteristics' => 'Clear', 'type' => 'Standard'),
				array('voice_id' => 'Gacrux', 'name' => 'Gacrux', 'gender' => 'Female', 'characteristics' => 'Mature', 'type' => 'Standard'),
		);
	}

	/**
	 * Gemini API を使用してコンテンツ (テキスト) を生成する。
	 * 
	 * @param string $model モデル名 (例: 'gemini-2.5-pro')。
	 * @param string $prompt プロンプトテキスト。
	 * @return string|WP_Error 生成されたテキストまたは WP_Error。
	 */
	public function generate_content($model, $prompt)
	{
		if (empty($this->api_key)) {
			return new WP_Error('missing_api_key', __('API key is missing.', 'quikvox-ai'));
		}

		// $url: コンテンツ生成用のAPIエンドポイントURL
		$url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $this->api_key;

		// $body: リクエストボディの構造配列
		$body = array(
			'contents' => array(
					array(
					'parts' => array(
							array('text' => $prompt)
					)
				)
			)
		);

		// $args: wp_remote_post 用の引数配列
		$args = array(
			'body' => json_encode($body),
			'headers' => array('Content-Type' => 'application/json'),
			'timeout' => 60, // 生成のためにタイムアウトを延長
			'method' => 'POST',
			'data_format' => 'body',
		);

		// $response: APIリクエストのレスポンス
		$response = wp_remote_post($url, $args);

		if (is_wp_error($response)) {
			return $response;
		}

		// $response_code: HTTPステータスコード
		$response_code = wp_remote_retrieve_response_code($response);
		if (200 !== $response_code) {
			// $error_message: エラーメッセージ
			$error_message = wp_remote_retrieve_body($response);
			/* translators: 1: HTTP response code, 2: error message. */
			return new WP_Error('api_error', sprintf(__('Failed to generate content: %1$d - %2$s', 'quikvox-ai'), $response_code, $error_message));
		}

		// $response_body: レスポンスボディ
		$response_body = wp_remote_retrieve_body($response);
		// $data: デコードされたJSONデータ
		$data = json_decode($response_body, true);

		if (isset($data['candidates'][0]['content']['parts'][0]['text'])) {
			return $data['candidates'][0]['content']['parts'][0]['text'];
		}

		return new WP_Error('api_error', __('No content was generated.', 'quikvox-ai'));
	}

	/**
	 * Gemini TTS モデルを使用して音声を生成する。
	 * タイムアウトを防ぐため、長いテキストをチャンク分割して処理する。
	 * 
	 * @param string $text 話させるテキスト。
	 * @param string $voice_config 音声設定 (名前/ID)。
	 * @param string $model TTSに使用するモデルID。
	 * @param string $voice_direction Voice direction instructions for the generated speech.
	 * @return string|WP_Error 音声バイナリデータ (WAV) または WP_Error。
	 */
	public function generate_speech($text, $voice_config = 'Charon', $model = 'gemini-2.5-flash-preview-tts', $voice_direction = '')
	{
		if (empty($this->api_key)) {
			return new WP_Error('missing_api_key', __('API key is missing.', 'quikvox-ai'));
		}

		// テキストをチャンクに分割 (Google AI Studio の Gemini TTS は文字数ベースで制御)
		// 1500文字程度を目安に分割してタイムアウトを回避
		$chunks = $this->split_text_into_chunks($text, 1500, $voice_direction);
		if (empty($chunks)) {
			return new WP_Error('empty_text', __('The text is empty.', 'quikvox-ai'));
		}

		// $url: 音声生成用のAPIエンドポイントURL
		$url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $this->api_key;

		$combined_pcm_data = '';

		foreach ($chunks as $i => $chunk) {
			$prompt_text = $this->build_single_speaker_prompt($chunk, $voice_direction);
			// Gemini TTS 用のペイロード
			$payload_array = array(
				'contents' => array(
						array(
						'parts' => array(
								array('text' => $prompt_text)
						)
					)
				),
				'generationConfig' => array(
					'responseModalities' => array('AUDIO'),
					'speechConfig' => array(
						'voiceConfig' => array(
							'prebuiltVoiceConfig' => array(
								'voiceName' => $voice_config
							)
						)
					)
				)
			);

			$args = array(
				'body' => json_encode($payload_array),
				'headers' => array('Content-Type' => 'application/json'),
				'timeout' => 120, // チャンク単位でも長めに設定
				'method' => 'POST',
			);

			$response = wp_remote_post($url, $args);

			if (is_wp_error($response)) {
				return $response;
			}

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

			if (200 !== $response_code) {
				/* translators: 1: current chunk number, 2: total chunk count, 3: HTTP response code, 4: response body. */
				return new WP_Error('api_error', sprintf(__('Gemini TTS error (chunk %1$d/%2$d, %3$d): %4$s', 'quikvox-ai'), $i + 1, count($chunks), $response_code, $body));
			}

			$data = json_decode($body, true);
			$chunk_pcm = '';

			if (isset($data['candidates'][0]['content']['parts']) && is_array($data['candidates'][0]['content']['parts'])) {
				foreach ($data['candidates'][0]['content']['parts'] as $part) {
					$inline = isset($part['inlineData']) ? $part['inlineData'] : (isset($part['inline_data']) ? $part['inline_data'] : null);
					if ($inline && isset($inline['data'])) {
						$chunk_pcm .= base64_decode($inline['data']);
					}
				}
			}

			if (empty($chunk_pcm)) {
				/* translators: %d is the current chunk number. */
				return new WP_Error('api_error', sprintf(__('Audio data was not found for chunk %d.', 'quikvox-ai'), $i + 1));
			}

			$combined_pcm_data .= $chunk_pcm;
		}

		if (empty($combined_pcm_data)) {
			return new WP_Error('api_error', __('No audio data was generated.', 'quikvox-ai'));
		}

		// 全ての PCM データを結合した後、最後に WAV ヘッダーを付与
		return $this->add_wav_header($combined_pcm_data);
	}

	/**
	 * Build the single-speaker prompt text for Gemini TTS.
	 *
	 * @param string $text The script text for a single voice.
	 * @param string $voice_direction Voice direction instructions.
	 * @return string
	 */
	private function build_single_speaker_prompt($text, $voice_direction = '')
	{
		$normalized_text = trim((string) $text);
		$normalized_voice_direction = trim((string) $voice_direction);

		if ($normalized_voice_direction === '') {
			return $normalized_text;
		}

		return "Voice Direction:\n" . $normalized_voice_direction . "\n\nTTS the following script in a single voice:\n" . $normalized_text;
	}

	/**
	 * Generate multi-speaker speech using Gemini TTS models.
	 *
	 * @param array<int, array<string, string>> $turns Parsed dialogue turns.
	 * @param array<string, string> $speaker_map Voice mapping keyed by speaker name.
	 * @param string $model TTS model ID.
	 * @return string|WP_Error
	 */
	public function generate_multi_speaker_speech($turns, $speaker_map, $model = 'gemini-2.5-flash-preview-tts', $voice_direction = '')
	{
		if (empty($this->api_key)) {
			return new WP_Error('missing_api_key', __('API key is missing.', 'quikvox-ai'));
		}

		$chunks = $this->split_turns_into_chunks($turns, $speaker_map, $voice_direction, 3200);
		if (empty($chunks)) {
			return new WP_Error('empty_text', __('The conversation text for Multi Speaker is empty.', 'quikvox-ai'));
		}

		$url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $this->api_key;
		$combined_pcm_data = '';

		foreach ($chunks as $chunk_index => $chunk_turns) {
			$prompt_text = $this->build_multi_speaker_prompt($chunk_turns, $speaker_map, $voice_direction);
			$speaker_voice_configs = $this->build_speaker_voice_configs($speaker_map);

			$payload_array = array(
				'contents' => array(
					array(
						'parts' => array(
							array('text' => $prompt_text),
						),
					),
				),
				'generationConfig' => array(
					'responseModalities' => array('AUDIO'),
					'speechConfig' => array(
						'multiSpeakerVoiceConfig' => array(
							'speakerVoiceConfigs' => $speaker_voice_configs,
						),
					),
				),
			);

			$args = array(
				'body' => wp_json_encode($payload_array),
				'headers' => array('Content-Type' => 'application/json'),
				'timeout' => 120,
				'method' => 'POST',
			);

			$response = wp_remote_post($url, $args);
			if (is_wp_error($response)) {
				return $response;
			}

			$response_code = wp_remote_retrieve_response_code($response);
			$body = wp_remote_retrieve_body($response);
			if (200 !== $response_code) {
				/* translators: 1: current chunk number, 2: total chunk count, 3: HTTP response code, 4: response body. */
				return new WP_Error('api_error', sprintf(__('Gemini multi-speaker TTS error (chunk %1$d/%2$d, %3$d): %4$s', 'quikvox-ai'), $chunk_index + 1, count($chunks), $response_code, $body));
			}

			$data = json_decode($body, true);
			$chunk_pcm = '';

			if (isset($data['candidates'][0]['content']['parts']) && is_array($data['candidates'][0]['content']['parts'])) {
				foreach ($data['candidates'][0]['content']['parts'] as $part) {
					$inline = isset($part['inlineData']) ? $part['inlineData'] : (isset($part['inline_data']) ? $part['inline_data'] : null);
					if ($inline && isset($inline['data'])) {
						$chunk_pcm .= base64_decode($inline['data']);
					}
				}
			}

			if (empty($chunk_pcm)) {
				return new WP_Error('api_error', __('Multi-speaker audio data was not found.', 'quikvox-ai'));
			}

			$combined_pcm_data .= $chunk_pcm;
		}

		if (empty($combined_pcm_data)) {
			return new WP_Error('api_error', __('No multi-speaker audio data was generated.', 'quikvox-ai'));
		}

		return $this->add_wav_header($combined_pcm_data);
	}

	/**
	 * Generate a single multi-speaker chunk while applying the voice direction prompt.
	 *
	 * @param array<int, array<string, string>> $turns Parsed dialogue turns for one chunk.
	 * @param array<string, string>             $speaker_map Voice mapping keyed by speaker.
	 * @param string                            $model The TTS model.
	 * @param string                            $voice_direction The voice direction text.
	 * @return string|WP_Error
	 */
	public function generate_multi_speaker_chunk_speech($turns, $speaker_map, $model = 'gemini-2.5-flash-preview-tts', $voice_direction = '')
	{
		return $this->generate_multi_speaker_speech($turns, $speaker_map, $model, $voice_direction);
	}

	/**
	 * テキストを指定文字数以下のチャンクに分割する。
	 * 文の区切り (。！？!\n) で分割を試みる。
	 *
	 * @param string $text 分隔するテキスト。
	 * @param int    $max_chars チャンクあたりの最大文字数。
	 * @return array テキストチャンクの配列。
	 */
	private function split_text_into_chunks($text, $max_chars = 1500, $voice_direction = '')
	{
		$text = trim($text);
		if (empty($text)) {
			return array();
		}

		$prompt_overhead = mb_strlen($this->build_single_speaker_prompt('', $voice_direction), 'UTF-8');
		$content_budget = max(200, $max_chars - $prompt_overhead);

		if (mb_strlen($this->build_single_speaker_prompt($text, $voice_direction), 'UTF-8') <= $max_chars) {
			return array($text);
		}

		// 文区切りで分割
		$sentences = preg_split('/(。|！|？|!|\?|\n)+/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);

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

		foreach ($sentences as $piece) {
			if (mb_strlen($this->build_single_speaker_prompt($current_chunk . $piece, $voice_direction), 'UTF-8') <= $max_chars) {
				$current_chunk .= $piece;
			}
			else {
				if (!empty(trim($current_chunk))) {
					$chunks[] = trim($current_chunk);
				}

				if (mb_strlen($this->build_single_speaker_prompt($piece, $voice_direction), 'UTF-8') > $max_chars) {
					// この文が単独で制限を超える場合、強制分割
					$sub_chunks = $this->force_split_by_chars($piece, $content_budget);
					foreach ($sub_chunks as $sc) {
						$chunks[] = $sc;
					}
					$current_chunk = '';
				}
				else {
					$current_chunk = $piece;
				}
			}
		}

		if (!empty(trim($current_chunk))) {
			$chunks[] = trim($current_chunk);
		}

		return $chunks;
	}

	/**
	 * Split parsed dialogue turns into prompt-sized chunks while preserving turns.
	 *
	 * @param array<int, array<string, string>> $turns Parsed dialogue turns.
	 * @param int $max_bytes Maximum bytes per chunk.
	 * @return array<int, array<int, array<string, string>>>
	 */
	private function split_turns_into_chunks($turns, $speaker_map, $voice_direction = '', $max_total_bytes = 3200)
	{
		$chunks = array();
		$current_chunk = array();
		$voice_direction = is_string($voice_direction) ? trim($voice_direction) : '';

		foreach ($turns as $turn) {
			$speaker_name = isset($turn['speaker']) ? trim((string) $turn['speaker']) : '';
			$turn_text = isset($turn['text']) ? trim((string) $turn['text']) : '';
			if ($speaker_name === '' || $turn_text === '') {
				continue;
			}

			$single_turn_payload_bytes = $this->estimate_multi_speaker_prompt_bytes(array($turn), $speaker_map, $voice_direction);

			if ($single_turn_payload_bytes > $max_total_bytes) {
				if (!empty($current_chunk)) {
					$chunks[] = $current_chunk;
					$current_chunk = array();
				}

				$split_turns = $this->split_turn_text_by_bytes($speaker_name, $turn_text, $speaker_map, max(600, $max_total_bytes - $this->estimate_multi_speaker_prompt_bytes(array(), $speaker_map, $voice_direction) - 32));
				foreach ($split_turns as $split_turn) {
					$chunks[] = array($split_turn);
				}
				continue;
			}

			$candidate_chunk = $current_chunk;
			$candidate_chunk[] = $turn;
			if ($this->estimate_multi_speaker_prompt_bytes($candidate_chunk, $speaker_map, $voice_direction) > $max_total_bytes) {
				$chunks[] = $current_chunk;
				$current_chunk = array($turn);
				continue;
			}

			$current_chunk[] = $turn;
		}

		if (!empty($current_chunk)) {
			$chunks[] = $current_chunk;
		}

		return $chunks;
	}

	/**
	 * Split a single oversized turn into multiple turn fragments.
	 *
	 * @param string $speaker_name Speaker name.
	 * @param string $turn_text Turn text.
	 * @param int $max_bytes Maximum bytes per fragment.
	 * @return array<int, array<string, string>>
	 */
	private function split_turn_text_by_bytes($speaker_name, $turn_text, $speaker_map, $max_bytes)
	{
		$prefix = $this->format_multi_speaker_prompt_label($speaker_name, $speaker_map) . ': ';
		$available_bytes = max(200, $max_bytes - strlen($prefix));
		$segments = array();
		$offset = 0;
		$text_bytes = strlen($turn_text);

		while ($offset < $text_bytes) {
			$segment = trim((string) mb_strcut($turn_text, $offset, $available_bytes, 'UTF-8'));
			if ($segment === '') {
				break;
			}

			$segments[] = array(
				'speaker' => $speaker_name,
				'text' => $segment,
			);
			$offset += strlen($segment);
		}

		return $segments;
	}

	/**
	 * Build the multi-speaker prompt string expected by Gemini TTS.
	 *
	 * @param array<int, array<string, string>> $turns Parsed dialogue turns.
	 * @return string
	 */
	private function build_multi_speaker_prompt($turns, $speaker_map, $voice_direction = '')
	{
		$speaker_names = array();
		$lines = array();

		foreach ($turns as $turn) {
			$speaker_name = isset($turn['speaker']) ? trim((string) $turn['speaker']) : '';
			$turn_text = isset($turn['text']) ? trim((string) $turn['text']) : '';
			if ($speaker_name === '' || $turn_text === '') {
				continue;
			}

			$display_label = $this->format_multi_speaker_prompt_label($speaker_name, $speaker_map);
			if (!in_array($display_label, $speaker_names, true)) {
				$speaker_names[] = $display_label;
			}
			$lines[] = $display_label . ': ' . $turn_text;
		}

		$voice_direction_block = '';
		$voice_direction = is_string($voice_direction) ? trim($voice_direction) : '';
		if ($voice_direction !== '') {
			$voice_direction_block = "Voice Direction:\n" . $voice_direction . "\n\n";
		}

		return $voice_direction_block . 'TTS the following conversation between ' . implode(' and ', $speaker_names) . ":\n" . implode("\n", $lines);
	}

	/**
	 * Estimate the UTF-8 byte size for a multi-speaker prompt.
	 *
	 * @param array<int, array<string, string>> $turns Parsed dialogue turns.
	 * @param string                            $voice_direction The voice direction text.
	 * @return int
	 */
	private function estimate_multi_speaker_prompt_bytes($turns, $speaker_map, $voice_direction = '')
	{
		return strlen($this->build_multi_speaker_prompt($turns, $speaker_map, $voice_direction));
	}

	/**
	 * Build the label used in multi-speaker TTS prompts.
	 *
	 * @param string $speaker_name Speaker name.
	 * @param array<string, string> $speaker_map Voice mapping keyed by speaker name.
	 * @return string
	 */
	private function format_multi_speaker_prompt_label($speaker_name, $speaker_map)
	{
		$normalized_speaker_name = trim((string) $speaker_name);
		if ($normalized_speaker_name === '') {
			return '';
		}

		if (!isset($speaker_map[$normalized_speaker_name]) || trim((string) $speaker_map[$normalized_speaker_name]) === '') {
			return $normalized_speaker_name;
		}

		return $normalized_speaker_name . ' (' . trim((string) $speaker_map[$normalized_speaker_name]) . ')';
	}

	/**
	 * Build the speaker voice config payload for Gemini TTS.
	 *
	 * @param array<string, string> $speaker_map Voice mapping keyed by speaker name.
	 * @return array<int, array<string, mixed>>
	 */
	private function build_speaker_voice_configs($speaker_map)
	{
		$configs = array();
		$ordered_speaker_names = array('Speaker1', 'Speaker2');
		$remaining_speaker_names = array();

		foreach (array_keys($speaker_map) as $speaker_name) {
			if (!in_array($speaker_name, $ordered_speaker_names, true)) {
				$remaining_speaker_names[] = $speaker_name;
			}
		}

		foreach (array_merge($ordered_speaker_names, $remaining_speaker_names) as $speaker_name) {
			if (!isset($speaker_map[$speaker_name])) {
				continue;
			}

			$voice_name = $speaker_map[$speaker_name];
			$configs[] = array(
				'speaker' => $speaker_name,
				'voiceConfig' => array(
					'prebuiltVoiceConfig' => array(
						'voiceName' => $voice_name,
					),
				),
			);
		}

		return $configs;
	}

	/**
	 * テキストを文字数で強制分割する (マルチバイト安全)。
	 *
	 * @param string $text テキスト。
	 * @param int    $max_chars 最大文字数。
	 * @return array チャンク配列。
	 */
	private function force_split_by_chars($text, $max_chars)
	{
		$chunks = array();
		$len = mb_strlen($text, 'UTF-8');

		for ($i = 0; $i < $len; $i += $max_chars) {
			$chunks[] = mb_substr($text, $i, $max_chars, 'UTF-8');
		}

		return $chunks;
	}

	/**
	 * Raw PCM データに WAV ヘッダーを追加する。
	 * 
	 * @param string $pcm_data Raw PCM バイナリデータ。
	 * @param int    $sample_rate サンプルレート (Hz) (Gemini のデフォルトは 24000)。
	 * @param int    $channels チャンネル数 (デフォルト 1)。
	 * @param int    $bits_per_sample サンプルあたりのビット数 (デフォルト 16)。
	 * @return string WAV ヘッダー付きのデータ。
	 */
	private function add_wav_header($pcm_data, $sample_rate = 24000, $channels = 1, $bits_per_sample = 16)
	{
		// $data_len: PCMデータの長さ
		$data_len = strlen($pcm_data);
		// $block_align: ブロックアライン
		$block_align = $channels * (int)($bits_per_sample / 8);
		// $byte_rate: バイトレート
		$byte_rate = $sample_rate * $block_align;
		// $riff_chunk_size: RIFFチャンクサイズ
		$riff_chunk_size = 36 + $data_len;

		// $header: WAVヘッダーの構築
		$header = '';
		$header .= 'RIFF'; // ChunkID
		$header .= pack('V', $riff_chunk_size); // ChunkSize
		$header .= 'WAVE'; // Format
		$header .= 'fmt '; // Subchunk1ID
		$header .= pack('V', 16); // Subchunk1Size (16 for PCM)
		$header .= pack('v', 1); // AudioFormat (1 for PCM)
		$header .= pack('v', $channels); // NumChannels
		$header .= pack('V', $sample_rate); // SampleRate
		$header .= pack('V', $byte_rate); // ByteRate
		$header .= pack('v', $block_align); // BlockAlign
		$header .= pack('v', $bits_per_sample); // BitsPerSample
		$header .= 'data'; // Subchunk2ID
		$header .= pack('V', $data_len); // Subchunk2Size

		return $header . $pcm_data;
	}
}
