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

/**
 * Class Podcastmaker_Prompt_Sets
 *
 * Handles CRUD operations for Prompt Sets.
 */
class Podcastmaker_Prompt_Sets {

	/**
	 * Option name for storing prompt sets.
	 */
	const OPTION_NAME = 'Podcastmaker_prompt_sets';

	/**
	 * Option name for storing prompt set defaults.
	 */
	const CONFIG_OPTION = 'Podcastmaker_prompt_set_config';

	/**
	 * Option name for storing base prompts.
	 */
	const BASE_PROMPTS_OPTION = 'Podcastmaker_base_prompts';

	/**
	 * Option name for storing the next available sequential ID.
	 */
	const NEXT_ID_OPTION = 'Podcastmaker_prompt_next_id';

	/**
	 * Get all prompt sets.
	 *
	 * @return array Array of prompt sets.
	 */
	public function get_all() {
		$sets = get_option( self::OPTION_NAME, array() );
		$sets = is_array( $sets ) ? $sets : array();
		$sets = $this->ensure_builtin_prompt_sets( $sets );
		$sets = $this->cleanup_obsolete_builtin_prompt_sets( $sets );

		foreach ( $sets as $prompt_set_id => $prompt_set ) {
			$sets[ $prompt_set_id ] = $this->normalize_prompt_set_record( $prompt_set_id, $prompt_set );
		}

		return $sets;
	}

	/**
	 * Get prompt set configuration values.
	 *
	 * @return array<string, string>
	 */
	public function get_config() {
		$config = get_option( self::CONFIG_OPTION, array() );

		return is_array( $config ) ? $config : array();
	}

	/**
	 * Get all base prompts.
	 *
	 * @return array<string, array<string, string>>
	 */
	public function get_base_prompts() {
		$base_prompts = get_option( self::BASE_PROMPTS_OPTION, array() );
		$base_prompts = is_array( $base_prompts ) ? $base_prompts : array();
		$base_prompts = $this->ensure_default_base_prompts( $base_prompts );

		foreach ( $base_prompts as $base_prompt_id => $base_prompt ) {
			$base_prompts[ $base_prompt_id ] = $this->normalize_base_prompt_record( $base_prompt_id, $base_prompt );
		}

		return $base_prompts;
	}

	/**
	 * Reset stored prompt data to the bundled defaults only.
	 *
	 * This removes all user-edited prompt sets, default references, and base prompt overrides.
	 *
	 * @return bool
	 */
	public function reset_to_bundled_defaults() {
		delete_option( self::OPTION_NAME );
		delete_option( self::CONFIG_OPTION );
		delete_option( self::BASE_PROMPTS_OPTION );
		delete_option( self::NEXT_ID_OPTION );

		$this->get_all();
		$this->get_base_prompts();
		update_option(
			self::CONFIG_OPTION,
			array(
				'default_single_prompt_set_id' => $this->get_first_builtin_prompt_set_id_by_speaker_type( 'single' ),
				'default_multi_prompt_set_id' => $this->get_first_builtin_prompt_set_id_by_speaker_type( 'multi' ),
			)
		);

		return true;
	}

	/**
	 * Get a specific base prompt record by language and speaker type.
	 *
	 * @param string $language The language code.
	 * @param string $speaker_type The speaker type.
	 * @return array<string, string>
	 */
	public function get_base_prompt_record( $language, $speaker_type ) {
		$base_prompt_id = $this->get_base_prompt_record_id( $language, $speaker_type );
		$base_prompts = $this->get_base_prompts();

		if ( isset( $base_prompts[ $base_prompt_id ] ) ) {
			return $base_prompts[ $base_prompt_id ];
		}

		return $this->build_default_base_prompt_record( $language, $speaker_type );
	}

	/**
	 * Get the base prompt text for the requested language and speaker type.
	 *
	 * @param string $language The language code.
	 * @param string $speaker_type The speaker type.
	 * @return string
	 */
	public function get_base_prompt_text( $language, $speaker_type ) {
		$base_prompt = $this->get_base_prompt_record( $language, $speaker_type );

		return isset( $base_prompt['prompt'] ) ? (string) $base_prompt['prompt'] : '';
	}

	/**
	 * Save a base prompt.
	 *
	 * @param string $language The language code.
	 * @param string $speaker_type The speaker type.
	 * @param string $name The display name.
	 * @param string $prompt The base prompt text.
	 * @return bool
	 */
	public function save_base_prompt( $language, $speaker_type, $name, $prompt ) {
		$normalized_language = $this->normalize_base_prompt_language( $language );
		$normalized_speaker_type = $speaker_type === 'multi' ? 'multi' : 'single';
		$base_prompt_id = $this->get_base_prompt_record_id( $normalized_language, $normalized_speaker_type );
		$base_prompts = $this->get_base_prompts();
		$existing_prompt = isset( $base_prompts[ $base_prompt_id ] ) && is_array( $base_prompts[ $base_prompt_id ] ) ? $base_prompts[ $base_prompt_id ] : array();

		$base_prompts[ $base_prompt_id ] = array(
			'language' => $normalized_language,
			'speaker_type' => $normalized_speaker_type,
			'name' => sanitize_text_field( $name ),
			'prompt' => sanitize_textarea_field( $prompt ),
			'last_updated' => current_time( 'mysql' ),
		);

		$base_prompts[ $base_prompt_id ] = $this->normalize_base_prompt_record( $base_prompt_id, $base_prompts[ $base_prompt_id ], $existing_prompt );
		update_option( self::BASE_PROMPTS_OPTION, $base_prompts );

		return true;
	}

	/**
	 * Get a specific prompt set by ID.
	 *
	 * @param string $id The prompt set ID.
	 * @return array|false The prompt set data or false if not found.
	 */
	public function get( $id ) {
		$sets = $this->get_all();
		return isset( $sets[ $id ] ) ? $sets[ $id ] : false;
	}

	/**
	 * Save a prompt set (Create or Update).
	 *
	 * @param string|null $id       The prompt set ID. If null, a new one is created.
	 * @param string      $name     The name of the prompt set.
	 * @param string      $prompt   The prompt content.
	 * @param string      $language     The language code (ja, en, zh-cn, zh-tw, ko).
	 * @param string      $speaker_type The speaker type (single, multi).
	 * @return string The ID of the saved prompt set.
	 */
	public function save( $id, $name, $prompt, $language = 'en', $speaker_type = 'single' ) {
		$sets = $this->get_all();
		$speaker_type = $speaker_type === 'multi' ? 'multi' : 'single';

		if ( empty( $id ) ) {
			$id = uniqid( 'prompt_' );
		}

		// Handle Sequential Numeric ID
		$existing_data = isset( $sets[ $id ] ) ? $sets[ $id ] : array();
		$index_id      = isset( $existing_data['index_id'] ) ? $existing_data['index_id'] : 0;

		if ( empty( $index_id ) ) {
			$index_id = (int) get_option( self::NEXT_ID_OPTION, 1 );
			update_option( self::NEXT_ID_OPTION, $index_id + 1 );
		}

		$sets[ $id ] = array(
			'index_id'     => $index_id,
			'name'         => sanitize_text_field( $name ),
			'prompt'       => sanitize_textarea_field( $prompt ),
			'language'     => sanitize_text_field( $language ),
			'speaker_type' => sanitize_text_field( $speaker_type ),
			'source_type'  => isset( $existing_data['source_type'] ) ? sanitize_key( $existing_data['source_type'] ) : 'custom',
			'source_definition_id' => isset( $existing_data['source_definition_id'] ) ? sanitize_text_field( $existing_data['source_definition_id'] ) : '',
			'last_updated' => current_time( 'mysql' ),
		);

		update_option( self::OPTION_NAME, $sets );
		$this->ensure_default_prompt_for_speaker_type( $speaker_type, $id );

		return $id;
	}

	/**
	 * Duplicate a prompt set.
	 *
	 * @param string $id The prompt set ID to duplicate.
	 * @return string|false The new ID of the duplicated prompt or false on failure.
	 */
	public function duplicate( $id ) {
		$source = $this->get( $id );
		if ( ! $source ) {
			return false;
		}

		$new_name = $source['name'] . ' (copy)';
		$speaker_type = isset( $source['speaker_type'] ) ? $source['speaker_type'] : 'single';
		return $this->save( null, $new_name, $source['prompt'], $source['language'], $speaker_type );
	}

	/**
	 * Delete a prompt set.
	 *
	 * @param string $id The prompt set ID.
	 */
	public function delete( $id ) {
		$sets = $this->get_all();
		$deleted_speaker_type = isset( $sets[ $id ]['speaker_type'] ) && $sets[ $id ]['speaker_type'] === 'multi' ? 'multi' : 'single';

		if ( isset( $sets[ $id ] ) ) {
			unset( $sets[ $id ] );
			update_option( self::OPTION_NAME, $sets );
			$this->reassign_default_prompt_after_delete( $deleted_speaker_type, $id );
		}
	}

	/**
	 * Get the default prompt set ID for the requested speaker type.
	 *
	 * @param string $speaker_type The requested speaker type.
	 * @return string
	 */
	public function get_default_prompt_set_id( $speaker_type ) {
		$normalized_speaker_type = $speaker_type === 'multi' ? 'multi' : 'single';
		$config = $this->get_config();
		$config_key = $normalized_speaker_type === 'multi' ? 'default_multi_prompt_set_id' : 'default_single_prompt_set_id';
		$default_id = isset( $config[ $config_key ] ) ? sanitize_text_field( $config[ $config_key ] ) : '';

		if ( $default_id !== '' ) {
			$existing_prompt = $this->get( $default_id );
			if ( is_array( $existing_prompt ) ) {
				$existing_type = isset( $existing_prompt['speaker_type'] ) && $existing_prompt['speaker_type'] === 'multi' ? 'multi' : 'single';
				if ( $existing_type === $normalized_speaker_type ) {
					return $default_id;
				}
			}
		}

		return $this->get_first_prompt_set_id_by_speaker_type( $normalized_speaker_type );
	}

	/**
	 * Set the default prompt set ID for a given speaker type.
	 *
	 * @param string $speaker_type The speaker type.
	 * @param string $prompt_set_id The prompt set ID.
	 * @return bool
	 */
	public function set_default_prompt_set_id( $speaker_type, $prompt_set_id ) {
		$normalized_speaker_type = $speaker_type === 'multi' ? 'multi' : 'single';
		$prompt_set_id = sanitize_text_field( $prompt_set_id );
		$prompt_set = $this->get( $prompt_set_id );

		if ( ! is_array( $prompt_set ) ) {
			return false;
		}

		$prompt_speaker_type = isset( $prompt_set['speaker_type'] ) && $prompt_set['speaker_type'] === 'multi' ? 'multi' : 'single';
		if ( $prompt_speaker_type !== $normalized_speaker_type ) {
			return false;
		}

		$config = $this->get_config();
		$config_key = $normalized_speaker_type === 'multi' ? 'default_multi_prompt_set_id' : 'default_single_prompt_set_id';
		$config[ $config_key ] = $prompt_set_id;
		update_option( self::CONFIG_OPTION, $config );

		return true;
	}

	/**
	 * Ensure the given speaker type has a valid default prompt ID.
	 *
	 * @param string $speaker_type The speaker type.
	 * @param string $fallback_id The prompt set ID that should become default if none exists.
	 * @return void
	 */
	private function ensure_default_prompt_for_speaker_type( $speaker_type, $fallback_id ) {
		$current_default = $this->get_default_prompt_set_id( $speaker_type );
		if ( $current_default !== '' ) {
			return;
		}

		$this->set_default_prompt_set_id( $speaker_type, $fallback_id );
	}

	/**
	 * Reassign default prompt after deleting a prompt set.
	 *
	 * @param string $speaker_type The deleted prompt's speaker type.
	 * @param string $deleted_id The deleted prompt ID.
	 * @return void
	 */
	private function reassign_default_prompt_after_delete( $speaker_type, $deleted_id ) {
		$normalized_speaker_type = $speaker_type === 'multi' ? 'multi' : 'single';
		$config = $this->get_config();
		$config_key = $normalized_speaker_type === 'multi' ? 'default_multi_prompt_set_id' : 'default_single_prompt_set_id';
		$current_default = isset( $config[ $config_key ] ) ? sanitize_text_field( $config[ $config_key ] ) : '';

		if ( $current_default !== sanitize_text_field( $deleted_id ) ) {
			return;
		}

		$next_default = $this->get_first_prompt_set_id_by_speaker_type( $normalized_speaker_type );
		$config[ $config_key ] = $next_default;
		update_option( self::CONFIG_OPTION, $config );
	}

	/**
	 * Get the first prompt set ID that matches the requested speaker type.
	 *
	 * @param string $speaker_type The speaker type.
	 * @return string
	 */
	private function get_first_prompt_set_id_by_speaker_type( $speaker_type ) {
		$normalized_speaker_type = $speaker_type === 'multi' ? 'multi' : 'single';
		$sets = $this->get_all();

		foreach ( $sets as $prompt_set_id => $prompt_set ) {
			$current_type = isset( $prompt_set['speaker_type'] ) && $prompt_set['speaker_type'] === 'multi' ? 'multi' : 'single';
			if ( $current_type === $normalized_speaker_type ) {
				return (string) $prompt_set_id;
			}
		}

		return '';
	}

	/**
	 * Ensure built-in prompt sets exist without overwriting user edits.
	 *
	 * @param array $existing_sets Existing saved prompt sets.
	 * @return array
	 */
	private function ensure_builtin_prompt_sets( array $existing_sets ) {
		$built_in_prompt_sets = $this->get_builtin_prompt_sets();
		$has_updates = false;
		$next_index_id = (int) get_option( self::NEXT_ID_OPTION, 1 );

		foreach ( $built_in_prompt_sets as $prompt_set_id => $prompt_set ) {
			if ( isset( $existing_sets[ $prompt_set_id ] ) ) {
				continue;
			}

			$existing_sets[ $prompt_set_id ] = array(
				'index_id'     => $next_index_id,
				'name'         => $prompt_set['name'],
				'prompt'       => $prompt_set['prompt'],
				'language'     => $prompt_set['language'],
				'speaker_type' => $prompt_set['speaker_type'],
				'source_type'  => 'builtin',
				'source_definition_id' => $prompt_set_id,
				'last_updated' => current_time( 'mysql' ),
			);
			$next_index_id++;
			$has_updates = true;
		}

		if ( $has_updates ) {
			update_option( self::OPTION_NAME, $existing_sets );
			update_option( self::NEXT_ID_OPTION, $next_index_id );
		}

		return $existing_sets;
	}

	/**
	 * Remove obsolete built-in prompt sets that are no longer shipped.
	 *
	 * @param array $existing_sets Existing saved prompt sets.
	 * @return array
	 */
	private function cleanup_obsolete_builtin_prompt_sets( array $existing_sets ) {
		$built_in_prompt_sets = $this->get_builtin_prompt_sets();
		$config = $this->get_config();
		$has_updates = false;

		foreach ( $existing_sets as $prompt_set_id => $prompt_set ) {
			$source_type = isset( $prompt_set['source_type'] ) ? sanitize_key( $prompt_set['source_type'] ) : '';
			$source_definition_id = isset( $prompt_set['source_definition_id'] ) ? sanitize_text_field( $prompt_set['source_definition_id'] ) : '';
			$is_legacy_builtin_id = strpos( (string) $prompt_set_id, 'builtin_' ) === 0;

			$should_remove = false;

			if ( $source_type === 'builtin' && $source_definition_id !== '' && ! isset( $built_in_prompt_sets[ $source_definition_id ] ) ) {
				$should_remove = true;
			} elseif ( $source_type === '' && $is_legacy_builtin_id && ! isset( $built_in_prompt_sets[ $prompt_set_id ] ) ) {
				$should_remove = true;
			}

			if ( ! $should_remove ) {
				continue;
			}

			unset( $existing_sets[ $prompt_set_id ] );
			$has_updates = true;
			$config = $this->reassign_default_prompt_in_config_after_cleanup( $config, $prompt_set_id, $prompt_set );
		}

		if ( $has_updates ) {
			update_option( self::OPTION_NAME, $existing_sets );
			update_option( self::CONFIG_OPTION, $config );
		}

		return $existing_sets;
	}

	/**
	 * Get built-in prompt set definitions.
	 *
	 * @return array<string, array<string, string>>
	 */
	private function get_builtin_prompt_sets() {
		$config_path = plugin_dir_path( dirname( __FILE__ ) ) . 'config/prompt-sets.php';
		$prompt_sets = file_exists( $config_path ) ? require $config_path : array();

		return is_array( $prompt_sets ) ? $prompt_sets : array();
	}

	/**
	 * Normalize prompt set records loaded from storage.
	 *
	 * @param string $prompt_set_id Prompt set ID.
	 * @param array  $prompt_set Prompt set record.
	 * @return array
	 */
	private function normalize_prompt_set_record( $prompt_set_id, array $prompt_set ) {
		$normalized_set = $prompt_set;

		if ( ! isset( $normalized_set['source_type'] ) ) {
			$normalized_set['source_type'] = $this->is_builtin_prompt_set_id( $prompt_set_id ) ? 'builtin' : 'custom';
		}
		$normalized_set['source_type'] = sanitize_key( $normalized_set['source_type'] );

		if ( ! isset( $normalized_set['source_definition_id'] ) ) {
			$normalized_set['source_definition_id'] = $normalized_set['source_type'] === 'builtin' ? $prompt_set_id : '';
		}
		$normalized_set['source_definition_id'] = sanitize_text_field( $normalized_set['source_definition_id'] );

		return $normalized_set;
	}

	/**
	 * Ensure the base prompt storage contains bundled defaults.
	 *
	 * @param array $existing_prompts Existing saved base prompts.
	 * @return array
	 */
	private function ensure_default_base_prompts( array $existing_prompts ) {
		$default_base_prompts = $this->get_default_base_prompt_records();
		$has_updates = false;

		foreach ( $default_base_prompts as $base_prompt_id => $base_prompt ) {
			if ( isset( $existing_prompts[ $base_prompt_id ] ) ) {
				continue;
			}

			$existing_prompts[ $base_prompt_id ] = $base_prompt;
			$has_updates = true;
		}

		if ( $has_updates ) {
			update_option( self::BASE_PROMPTS_OPTION, $existing_prompts );
		}

		return $existing_prompts;
	}

	/**
	 * Build the bundled base prompt records.
	 *
	 * @return array<string, array<string, string>>
	 */
	private function get_default_base_prompt_records() {
		return array(
			'ja_single' => array(
				'language' => 'ja',
				'speaker_type' => 'single',
				'name' => 'Japanese Base Prompt - Single',
				'prompt' => <<<'TEXT'
記事の内容をもとに、ポッドキャスト用のスクリプトを作成してください。

共通ルール:
- 元の記事の主要な事実、要点、結論が崩れないようにしてください
- 出力はスクリプト本文のみとしてください
- 各行の先頭に必ず `Host:` を付けてください
- 1つの発話を長くしすぎず、短めの単位に分けてください
- 同じ話者が続く場合も、各行ごとに `Host:` を省略せず付けてください
- 聞いて理解しやすいように、必要に応じて順序や言い回しを話し言葉向けに整えてください
TEXT,
			),
			'ja_multi' => array(
				'language' => 'ja',
				'speaker_type' => 'multi',
				'name' => 'Japanese Base Prompt - Multi',
				'prompt' => <<<'TEXT'
記事の内容をもとに、ポッドキャスト用のスクリプトを作成してください。

共通ルール:
- 元の記事の主要な事実、要点、結論が崩れないようにしてください
- 出力はスクリプト本文のみとしてください
- 各行の先頭に必ず `Host:` または `Guest:` を付けてください
- 1つの発話を長くしすぎず、短めの単位に分けてください
- 同じ話者が続く場合も、各行ごとに話者名を省略せず付けてください
- 聞いて理解しやすいように、必要に応じて順序や言い回しを話し言葉向けに整えてください
TEXT,
			),
			'en_single' => array(
				'language' => 'en',
				'speaker_type' => 'single',
				'name' => 'English Base Prompt - Single',
				'prompt' => <<<'TEXT'
Create a podcast script based on the article.

Common rules:
- Keep the main facts, points, and conclusions intact
- Output script text only
- Every line must start with `Host:`
- Keep each utterance short and easy to follow
- Repeat `Host:` on every line even if the same speaker continues
- Adjust ordering and wording so it sounds natural when spoken
TEXT,
			),
			'en_multi' => array(
				'language' => 'en',
				'speaker_type' => 'multi',
				'name' => 'English Base Prompt - Multi',
				'prompt' => <<<'TEXT'
Create a podcast script based on the article.

Common rules:
- Keep the main facts, points, and conclusions intact
- Output script text only
- Every line must start with `Host:` or `Guest:`
- Keep each utterance short and easy to follow
- Repeat the speaker label on every line
- Adjust ordering and wording so it sounds natural when spoken
TEXT,
			),
			'zh-cn_single' => array(
				'language' => 'zh-cn',
				'speaker_type' => 'single',
				'name' => 'Chinese Simplified Base Prompt - Single',
				'prompt' => <<<'TEXT'
请根据文章内容制作播客脚本。

通用规则:
- 保留原文的主要事实、要点和结论
- 仅输出脚本正文
- 每一行都必须以 `Host:` 开头
- 每次发言保持简短，便于收听
- 即使同一位说话人连续发言，也要每行重复 `Host:`
- 根据需要调整顺序和措辞，使其更适合口语表达
TEXT,
			),
			'zh-cn_multi' => array(
				'language' => 'zh-cn',
				'speaker_type' => 'multi',
				'name' => 'Chinese Simplified Base Prompt - Multi',
				'prompt' => <<<'TEXT'
请根据文章内容制作播客脚本。

通用规则:
- 保留原文的主要事实、要点和结论
- 仅输出脚本正文
- 每一行都必须以 `Host:` 或 `Guest:` 开头
- 每次发言保持简短，便于收听
- 每行都要重复说话人标签
- 根据需要调整顺序和措辞，使其更适合口语表达
TEXT,
			),
			'zh-tw_single' => array(
				'language' => 'zh-tw',
				'speaker_type' => 'single',
				'name' => 'Chinese Traditional Base Prompt - Single',
				'prompt' => <<<'TEXT'
請根據文章內容製作播客腳本。

通用規則:
- 保留原文的主要事實、要點和結論
- 僅輸出腳本正文
- 每一行都必須以 `Host:` 開頭
- 每次發言保持簡短，便於收聽
- 即使同一位說話人連續發言，也要每行重複 `Host:`
- 根據需要調整順序和措辭，使其更適合口語表達
TEXT,
			),
			'zh-tw_multi' => array(
				'language' => 'zh-tw',
				'speaker_type' => 'multi',
				'name' => 'Chinese Traditional Base Prompt - Multi',
				'prompt' => <<<'TEXT'
請根據文章內容製作播客腳本。

通用規則:
- 保留原文的主要事實、要點和結論
- 僅輸出腳本正文
- 每一行都必須以 `Host:` 或 `Guest:` 開頭
- 每次發言保持簡短，便於收聽
- 每行都要重複說話人標籤
- 根據需要調整順序和措辭，使其更適合口語表達
TEXT,
			),
			'ko_single' => array(
				'language' => 'ko',
				'speaker_type' => 'single',
				'name' => 'Korean Base Prompt - Single',
				'prompt' => <<<'TEXT'
기사 내용을 바탕으로 팟캐스트 스크립트를 작성하세요.

공통 규칙:
- 원문의 주요 사실, 핵심, 결론을 유지하세요
- 스크립트 본문만 출력하세요
- 모든 줄은 반드시 `Host:` 로 시작하세요
- 한 발화는 짧고 듣기 쉽게 나누세요
- 같은 화자가 계속 말해도 매 줄마다 `Host:` 를 반복하세요
- 말로 들었을 때 자연스럽도록 순서와 표현을 조정하세요
TEXT,
			),
			'ko_multi' => array(
				'language' => 'ko',
				'speaker_type' => 'multi',
				'name' => 'Korean Base Prompt - Multi',
				'prompt' => <<<'TEXT'
기사 내용을 바탕으로 팟캐스트 스크립트를 작성하세요.

공통 규칙:
- 원문의 주요 사실, 핵심, 결론을 유지하세요
- 스크립트 본문만 출력하세요
- 모든 줄은 반드시 `Host:` 또는 `Guest:` 로 시작하세요
- 한 발화는 짧고 듣기 쉽게 나누세요
- 같은 화자가 계속 말해도 매 줄마다 화자 라벨을 반복하세요
- 말로 들었을 때 자연스럽도록 순서와 표현을 조정하세요
TEXT,
			),
		);
	}

	/**
	 * Normalize a stored base prompt record.
	 *
	 * @param string $base_prompt_id Base prompt record ID.
	 * @param array  $base_prompt Base prompt record.
	 * @param array  $fallback_record Fallback record used when the stored data is incomplete.
	 * @return array<string, string>
	 */
	private function normalize_base_prompt_record( $base_prompt_id, array $base_prompt, array $fallback_record = array() ) {
		$default_record = $fallback_record;
		if ( empty( $default_record ) ) {
			$default_record = $this->build_default_base_prompt_record( $this->get_base_prompt_language_from_id( $base_prompt_id ), $this->get_base_prompt_speaker_type_from_id( $base_prompt_id ) );
		}

		$normalized_language = isset( $base_prompt['language'] ) && $base_prompt['language'] !== ''
			? $this->normalize_base_prompt_language( $base_prompt['language'] )
			: $this->normalize_base_prompt_language( $this->get_base_prompt_language_from_id( $base_prompt_id ) );
		$normalized_speaker_type = isset( $base_prompt['speaker_type'] ) && $base_prompt['speaker_type'] === 'multi'
			? 'multi'
			: $this->get_base_prompt_speaker_type_from_id( $base_prompt_id );
		$normalized_name = isset( $base_prompt['name'] ) && $base_prompt['name'] !== '' ? sanitize_text_field( $base_prompt['name'] ) : ( isset( $default_record['name'] ) ? $default_record['name'] : 'Base Prompt' );
		$normalized_prompt = isset( $base_prompt['prompt'] ) ? sanitize_textarea_field( $base_prompt['prompt'] ) : ( isset( $default_record['prompt'] ) ? $default_record['prompt'] : '' );
		$normalized_last_updated = isset( $base_prompt['last_updated'] ) ? sanitize_text_field( $base_prompt['last_updated'] ) : current_time( 'mysql' );

		return array(
			'language' => $normalized_language,
			'speaker_type' => $normalized_speaker_type,
			'name' => $normalized_name,
			'prompt' => $normalized_prompt,
			'last_updated' => $normalized_last_updated,
		);
	}

	/**
	 * Build a default base prompt record for the requested language and speaker type.
	 *
	 * @param string $language The language code.
	 * @param string $speaker_type The speaker type.
	 * @return array<string, string>
	 */
	private function build_default_base_prompt_record( $language, $speaker_type ) {
		$normalized_language = $this->normalize_base_prompt_language( $language );
		$normalized_speaker_type = $speaker_type === 'multi' ? 'multi' : 'single';
		$default_records = $this->get_default_base_prompt_records();
		$base_prompt_id = $this->get_base_prompt_record_id( $normalized_language, $normalized_speaker_type );

		if ( isset( $default_records[ $base_prompt_id ] ) ) {
			$record = $default_records[ $base_prompt_id ];
			$record['last_updated'] = current_time( 'mysql' );

			return $record;
		}

		return array(
			'language' => $normalized_language,
			'speaker_type' => $normalized_speaker_type,
			'name' => 'Base Prompt',
			'prompt' => '',
			'last_updated' => current_time( 'mysql' ),
		);
	}

	/**
	 * Get the storage key for a base prompt record.
	 *
	 * @param string $language The language code.
	 * @param string $speaker_type The speaker type.
	 * @return string
	 */
	private function get_base_prompt_record_id( $language, $speaker_type ) {
		return $this->normalize_base_prompt_language( $language ) . '_' . ( $speaker_type === 'multi' ? 'multi' : 'single' );
	}

	/**
	 * Extract the language code from a base prompt record ID.
	 *
	 * @param string $base_prompt_id Base prompt record ID.
	 * @return string
	 */
	private function get_base_prompt_language_from_id( $base_prompt_id ) {
		$parts = explode( '_', (string) $base_prompt_id );

		return isset( $parts[0] ) ? $this->normalize_base_prompt_language( $parts[0] ) : 'en';
	}

	/**
	 * Extract the speaker type from a base prompt record ID.
	 *
	 * @param string $base_prompt_id Base prompt record ID.
	 * @return string
	 */
	private function get_base_prompt_speaker_type_from_id( $base_prompt_id ) {
		$base_prompt_id = (string) $base_prompt_id;

		return strpos( $base_prompt_id, '_multi' ) !== false ? 'multi' : 'single';
	}

	/**
	 * Normalize a base prompt language code.
	 *
	 * @param string $language The language code.
	 * @return string
	 */
	private function normalize_base_prompt_language( $language ) {
		$normalized_language = sanitize_key( $language );
		$allowed_languages = array( 'ja', 'en', 'zh-cn', 'zh-tw', 'ko' );

		return in_array( $normalized_language, $allowed_languages, true ) ? $normalized_language : 'en';
	}

	/**
	 * Reassign default prompt IDs inside config when a built-in is removed during cleanup.
	 *
	 * @param array  $config Prompt set config.
	 * @param string $deleted_prompt_set_id Deleted prompt set ID.
	 * @param array  $deleted_prompt_set Deleted prompt set data.
	 * @return array
	 */
	private function reassign_default_prompt_in_config_after_cleanup( array $config, $deleted_prompt_set_id, array $deleted_prompt_set ) {
		$deleted_speaker_type = isset( $deleted_prompt_set['speaker_type'] ) && $deleted_prompt_set['speaker_type'] === 'multi' ? 'multi' : 'single';
		$config_key = $deleted_speaker_type === 'multi' ? 'default_multi_prompt_set_id' : 'default_single_prompt_set_id';
		$current_default = isset( $config[ $config_key ] ) ? sanitize_text_field( $config[ $config_key ] ) : '';

		if ( $current_default !== sanitize_text_field( $deleted_prompt_set_id ) ) {
			return $config;
		}

		$fallback_prompt_id = $this->get_first_builtin_prompt_set_id_by_speaker_type( $deleted_speaker_type );
		$config[ $config_key ] = $fallback_prompt_id;

		return $config;
	}

	/**
	 * Get the first bundled prompt set ID that matches the requested speaker type.
	 *
	 * @param string $speaker_type The speaker type.
	 * @return string
	 */
	private function get_first_builtin_prompt_set_id_by_speaker_type( $speaker_type ) {
		$normalized_speaker_type = $speaker_type === 'multi' ? 'multi' : 'single';
		$built_in_prompt_sets = $this->get_builtin_prompt_sets();

		foreach ( $built_in_prompt_sets as $prompt_set_id => $prompt_set ) {
			$current_type = isset( $prompt_set['speaker_type'] ) && $prompt_set['speaker_type'] === 'multi' ? 'multi' : 'single';
			if ( $current_type === $normalized_speaker_type ) {
				return (string) $prompt_set_id;
			}
		}

		return '';
	}

	/**
	 * Check whether a prompt set ID belongs to a bundled definition.
	 *
	 * @param string $prompt_set_id Prompt set ID.
	 * @return bool
	 */
	private function is_builtin_prompt_set_id( $prompt_set_id ) {
		$built_in_prompt_sets = $this->get_builtin_prompt_sets();
		return isset( $built_in_prompt_sets[ $prompt_set_id ] );
	}
}


