package com.fieldlabel.collector.service

import android.content.Context
import android.content.Intent
import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.speech.RecognitionListener
import android.speech.RecognizerIntent
import android.speech.SpeechRecognizer
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton

// ── ハンズフリーコマンド ─────────────────────────────────────────────────────
sealed class HfCommand {
    /** 「新しい地点」: 新しい地点セッションを開始 */
    object NewLocation : HfCommand()
    /** 「標本」: 現在のセッションに標本を追加して印刷 */
    object AddSpecimen : HfCommand()
    /** 「写真」: カメラを起動（撮影後は手動タグ付けダイアログ） */
    object OpenCamera : HfCommand()
    /** 「地点写真」: カメラを起動し撮影後に地点へ自動タグ付け */
    object OpenCameraForLocation : HfCommand()
    /** 「標本写真」: カメラを起動し撮影後に最新標本へ自動タグ付け */
    object OpenCameraForSpecimen : HfCommand()
    /** 「備考 [テキスト]」: 次の標本に備考を付ける */
    data class SetNotes(val text: String) : HfCommand()
    /** 「状況」: 現在の地点IDと標本数をアナウンス */
    object AnnounceStatus : HfCommand()
    /** 「今何番」: 次の標本番号をアナウンス */
    object AnnounceSpecimenNumber : HfCommand()
    /** 「いまどこ」: 採集地点からの現在地距離・方角をアナウンス */
    object AnnounceDistance : HfCommand()
    /** 「ライト」: フラッシュライトをトグル */
    object ToggleFlashlight : HfCommand()
    /** 「タイマー N分」: N 分のタイマーをセット（ViewModel 側で管理） */
    data class StartTimer(val minutes: Int) : HfCommand()
    /** 「残り時間」: タイマーの残り時間をアナウンス */
    object AnnounceTimerRemaining : HfCommand()
    /** 「終了」: ハンズフリーセッションを終了 */
    object EndSession : HfCommand()
    /** 「繰り返し」: 最後の音声メッセージを再読み上げ */
    object Repeat : HfCommand()
    /** 「録画」: 動画録画を開始する（録画終了後は UI ボタンで停止） */
    object StartVideoRecording : HfCommand()
    /** 「録音」: 音声録音を開始する（録音終了後は UI ボタンで停止） */
    object StartAudioRecording : HfCommand()
    /** 「フィールド名 値」: カスタムフィールドに値を入力する */
    data class SetCustomField(val fieldName: String, val value: String) : HfCommand()
    /** 「ログスタート」: 経路ログの記録を開始する */
    object StartRouteLog : HfCommand()
    /** 「ログストップ」: 経路ログの記録を停止する */
    object StopRouteLog : HfCommand()
}

// ── 会話状態 ──────────────────────────────────────────────────────────────────
enum class ConvState {
    IDLE,       // ハンズフリー非アクティブ
    WAKE_WAIT,  // ウェイクワード待ち
    CMD_WAIT,   // コマンド待ち
    PROCESSING, // コマンド処理中
    SPEAKING,   // TTS 発話中
    NOTES_WAIT, // 備考テキスト待ち
    TIMER_WAIT  // タイマー分数入力待ち
}

// ── HandsFreeManager ─────────────────────────────────────────────────────────
/**
 * ハンズフリーモードのコアマネージャー。
 * - Android SpeechRecognizer (オフライン可) を使ったウェイクワード + コマンド認識
 * - TextToSpeech による音声フィードバック
 * - UI はこのクラスの [commands] SharedFlow を collect してコマンドを受け取る
 *
 * ⚠ SpeechRecognizer はメインスレッドでのみ利用可能。
 *    start()/stop() もメインスレッドから呼ぶこと。
 */
@Singleton
class HandsFreeManager @Inject constructor(
    @ApplicationContext private val context: Context
) {
    companion object {
        /**
         * TTS 発話終了後、マイクを起動するまでの待機時間（ms）。
         * スピーカー音がマイクに入る（エコーフィードバック）を防ぐ。
         * 端末・環境によって変わるが 600ms が経験的に安定値。
         */
        private const val POST_TTS_DELAY_MS = 700L

        /**
         * onResults / onError コールバック後にミュートを解除するまでの待機時間（ms）。
         * 認識成功音・終了音はコールバックの直後に遅延再生されるため、
         * その再生が完全に終わるまでミュートを維持する必要がある。
         */
        private const val BEEP_MUTE_TAIL_MS = 1500L
    }
    // ── 公開 State ──────────────────────────────────────────────────────────
    private val _convState = MutableStateFlow(ConvState.IDLE)
    val convState: StateFlow<ConvState> = _convState.asStateFlow()

    private val _commands = MutableSharedFlow<HfCommand>(extraBufferCapacity = 8)
    val commands: SharedFlow<HfCommand> = _commands.asSharedFlow()

    /** 最後に認識したテキスト（デバッグ / UI 表示用） */
    private val _lastText = MutableStateFlow("")
    val lastText: StateFlow<String> = _lastText.asStateFlow()

    // ── 設定（CollectionViewModel が DataStore から読んで更新する） ─────────
    var wakeWord: String          = "れこちゃん"
    var language: String          = "ja"          // "ja" | "en"
    var voiceResponseEnabled: Boolean = true
    /** true: ウェイクワードを要求してからコマンドを受け付ける（デフォルト） */
    var requireWakeWord: Boolean  = true
    /** カスタムフィールドのラベル一覧（CollectionViewModel が同期する）。音声マッチングに使用。 */
    var customFieldNames: List<String> = emptyList()

    // ── 内部状態 ─────────────────────────────────────────────────────────────
    private var recognizer: SpeechRecognizer? = null
    private var tts: TextToSpeech? = null
    private var ttsReady = false
    private var lastSpoken = ""
    private var pendingNotes = ""   // notes 待ち状態で蓄積するテキスト
    private val mainHandler = Handler(Looper.getMainLooper())
    private var isRunning = false
    /**
     * TTS 発話中のフラグ。
     * SpeechRecognizer の onError / onResults が SPEAKING 中に発火しても
     * 無視するためのガード。
     */
    private var isSpeaking = false

    /**
     * カメラアプリ使用中のフラグ。
     * カメラが前面にある間は onError からの認識再起動を遮断する。
     * [pauseForCamera] でセット、[clearCameraFlag] でクリアする。
     */
    private var isCameraOpen = false

    /** ミュート解除の遅延 Runnable（次の startListening で上書きされる前にキャンセルするため保持）*/
    private var unmuteRunnable: Runnable? = null

    /** AudioManager（認識ビープ音ミュート用） */
    private val audioManager: AudioManager by lazy {
        context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    }

    /**
     * SpeechRecognizer が自動再生する開始音・終了音を消音する。
     * startListening() の直前に呼ぶ。
     * MODIFY_AUDIO_SETTINGS パーミッションが必要（AndroidManifest に宣言済み）。
     *
     * 端末により STREAM_RING と STREAM_SYSTEM のどちらかで鳴るため両方ミュートする。
     */
    private fun muteRecognitionBeeps() {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                audioManager.adjustStreamVolume(AudioManager.STREAM_RING,   AudioManager.ADJUST_MUTE, 0)
                audioManager.adjustStreamVolume(AudioManager.STREAM_SYSTEM, AudioManager.ADJUST_MUTE, 0)
            }
        } catch (_: Exception) {}
    }

    /** [muteRecognitionBeeps] で消音したストリームを元に戻す。 */
    private fun unmuteRecognitionBeeps() {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                audioManager.adjustStreamVolume(AudioManager.STREAM_RING,   AudioManager.ADJUST_UNMUTE, 0)
                audioManager.adjustStreamVolume(AudioManager.STREAM_SYSTEM, AudioManager.ADJUST_UNMUTE, 0)
            }
        } catch (_: Exception) {}
    }

    /**
     * BEEP_MUTE_TAIL_MS 後にミュート解除する。
     * 前回の解除 Runnable をキャンセルしてから新たにスケジュールするため、
     * 次の startListening() が割り込んでも二重解除にならない。
     */
    private fun scheduleUnmute() {
        unmuteRunnable?.let { mainHandler.removeCallbacks(it) }
        val r = Runnable { unmuteRecognitionBeeps() }
        unmuteRunnable = r
        mainHandler.postDelayed(r, BEEP_MUTE_TAIL_MS)
    }

    // ── TTS 初期化 ────────────────────────────────────────────────────────────
    private fun initTts() {
        if (tts != null) return
        tts = TextToSpeech(context) { status ->
            ttsReady = (status == TextToSpeech.SUCCESS)
            if (ttsReady) applyTtsLanguage()
        }
        tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
            override fun onStart(utteranceId: String?) {
                isSpeaking = true
            }
            override fun onDone(utteranceId: String?) {
                isSpeaking = false
                // TTS 音声がスピーカーから完全に消えるまで少し待ってから聴き始める。
                // 発話音をマイクが拾う（エコーフィードバック）を防ぐための遅延。
                mainHandler.postDelayed({
                    // SPEAKING 状態のときのみ次の待機状態へ遷移する。
                    // カスタム onDone コールバックを使う場合（タイマー待ち、備考待ちなど）は
                    // そちらが状態を管理するので SPEAKING にはならない。
                    if (_convState.value == ConvState.SPEAKING) {
                        // requireWakeWord に応じて待機状態を切り替える
                        _convState.value = if (requireWakeWord) ConvState.WAKE_WAIT else ConvState.CMD_WAIT
                        rebuildRecognizerAndListen()
                    }
                }, POST_TTS_DELAY_MS)
            }
            @Deprecated("Deprecated in Java")
            override fun onError(utteranceId: String?) {
                isSpeaking = false
            }
        })
    }

    private fun applyTtsLanguage() {
        val locale = if (language == "ja") Locale.JAPANESE else Locale.ENGLISH
        tts?.language = locale
    }

    // ── SpeechRecognizer 初期化 ────────────────────────────────────────────
    private fun buildRecognizer(): SpeechRecognizer {
        val r = SpeechRecognizer.createSpeechRecognizer(context)
        r.setRecognitionListener(object : RecognitionListener {
            override fun onReadyForSpeech(params: Bundle?) {}
            override fun onBeginningOfSpeech() {}
            override fun onRmsChanged(rmsdB: Float) {}
            override fun onBufferReceived(buffer: ByteArray?) {}
            override fun onPartialResults(partialResults: Bundle?) {}
            override fun onEvent(eventType: Int, params: Bundle?) {}

            override fun onResults(results: Bundle?) {
                // 認識成功音はコールバック後に遅延再生されるため、
                // 十分な余裕を持ってからミュート解除する
                scheduleUnmute()
                // TTS 発話中に拾った音声は捨てる（エコーフィードバック対策）
                if (isSpeaking) return
                val matches = results
                    ?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
                    ?: return
                val text = matches.firstOrNull()?.trim() ?: return
                _lastText.value = text
                handleRecognizedText(text)
            }

            override fun onError(error: Int) {
                // 終了音はコールバック後に遅延再生されるため、
                // 十分な余裕を持ってからミュート解除する
                scheduleUnmute()
                // TTS 発話中は無視（エコーフィードバック対策）
                if (isSpeaking) return
                // カメラアプリ使用中は再起動しない（受付音ループを防ぐ）
                if (isCameraOpen) return
                // 認識できなかった / タイムアウト → 再試行
                if (isRunning && _convState.value != ConvState.IDLE
                              && _convState.value != ConvState.SPEAKING) {
                    mainHandler.postDelayed({ startListening() }, 400)
                }
            }

            override fun onEndOfSpeech() {}
        })
        return r
    }

    private fun buildRecognizerIntent(): Intent {
        val locale = if (language == "ja") "ja-JP" else "en-US"
        return Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
            putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
            putExtra(RecognizerIntent.EXTRA_LANGUAGE, locale)
            putExtra(RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE, locale)
            putExtra(RecognizerIntent.EXTRA_ONLY_RETURN_LANGUAGE_PREFERENCE, false)
            putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, false)
            putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 3)
        }
    }

    // ── 公開 API ──────────────────────────────────────────────────────────────
    /** ハンズフリーを開始する（メインスレッドから呼ぶ）。 */
    fun start() {
        if (isRunning) return
        isRunning = true
        initTts()
        val initialState = if (requireWakeWord) ConvState.WAKE_WAIT else ConvState.CMD_WAIT
        _convState.value = initialState
        // 開始アナウンスを発話後、TTS 完了後に待機状態へ移行して聴き始める
        val startMsg = if (requireWakeWord) {
            if (language == "ja") "ハンズフリーモードを開始しました。ウェイクワードを話してください。"
            else "Hands-free mode started. Say the wake word."
        } else {
            if (language == "ja") "ハンズフリーモードを開始しました。コマンドを話してください。"
            else "Hands-free mode started. Say a command."
        }
        speak(startMsg) {
            if (isRunning) {
                _convState.value = if (requireWakeWord) ConvState.WAKE_WAIT else ConvState.CMD_WAIT
                mainHandler.postDelayed({ rebuildRecognizerAndListen() }, POST_TTS_DELAY_MS)
            }
        }
    }

    /** ハンズフリーを停止する（メインスレッドから呼ぶ）。 */
    fun stop() {
        if (!isRunning) return
        isRunning = false
        // ミュート解除の遅延 Runnable もキャンセル
        unmuteRunnable?.let { mainHandler.removeCallbacks(it) }
        unmuteRunnable = null
        recognizer?.stopListening()
        recognizer?.destroy()
        recognizer = null
        tts?.stop()
        _convState.value = ConvState.IDLE
        _lastText.value = ""
        // 念のためミュート解除（stop 時に onError が呼ばれない場合の保険）
        unmuteRecognitionBeeps()
    }

    /** TTS で発話する。[onDone] は発話完了後に呼ばれる（null なら CMD_WAIT へ遷移）。 */
    fun speak(text: String, onDone: (() -> Unit)? = null) {
        lastSpoken = text
        if (!voiceResponseEnabled || !ttsReady) {
            onDone?.invoke() ?: run {
                if (isRunning) {
                    _convState.value = ConvState.CMD_WAIT
                    mainHandler.postDelayed({ rebuildRecognizerAndListen() }, POST_TTS_DELAY_MS)
                }
            }
            return
        }
        // マイクを先に止めてから発話開始（stopListening だけでなく destroy して内部バッファもクリア）
        recognizer?.destroy()
        recognizer = null
        isSpeaking = true
        _convState.value = ConvState.SPEAKING
        applyTtsLanguage()
        if (onDone != null) {
            // カスタム完了ハンドラーがある場合はリスナーを上書き
            tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
                override fun onStart(utteranceId: String?) {}
                override fun onDone(utteranceId: String?) {
                    isSpeaking = false  // ← isSpeaking を必ずクリアしてから呼び出す
                    mainHandler.post { onDone() }
                    // デフォルトリスナーを復元
                    restoreDefaultTtsListener()
                }
                @Deprecated("Deprecated in Java")
                override fun onError(utteranceId: String?) {
                    isSpeaking = false  // ← エラー時も必ずクリア
                    mainHandler.post { onDone() }
                }
            })
        }
        tts?.speak(text, TextToSpeech.QUEUE_FLUSH, null, "hf_${System.currentTimeMillis()}")
    }

    private fun restoreDefaultTtsListener() {
        tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
            override fun onStart(utteranceId: String?) { isSpeaking = true }
            override fun onDone(utteranceId: String?) {
                isSpeaking = false
                mainHandler.postDelayed({
                    if (_convState.value == ConvState.SPEAKING) {
                        _convState.value = if (requireWakeWord) ConvState.WAKE_WAIT else ConvState.CMD_WAIT
                        rebuildRecognizerAndListen()
                    }
                }, POST_TTS_DELAY_MS)
            }
            @Deprecated("Deprecated in Java")
            override fun onError(utteranceId: String?) { isSpeaking = false }
        })
    }

    /**
     * 既存の Recognizer を破棄して新たに作り直し、すぐに聴き始める。
     * TTS のエコーがバッファに残らないよう destroy → create → start の順で実行する。
     */
    private fun rebuildRecognizerAndListen() {
        if (!isRunning) return
        mainHandler.post {
            recognizer?.destroy()
            recognizer = buildRecognizer()
            startListening()
        }
    }

    /**
     * カメラアプリを起動する直前に呼ぶ。
     * Recognizer を破棄してカメラフラグをセットし、
     * バックグラウンド中に onError ループが鳴らないようにする。
     * メインスレッドから呼ぶこと。
     */
    fun pauseForCamera() {
        isCameraOpen = true
        recognizer?.destroy()
        recognizer = null
    }

    /**
     * カメラアプリから戻ったあと（保存完了 or キャンセル後）に呼ぶ。
     * カメラフラグをクリアして、続けて speakAndResume や CMD_WAIT 復帰を行えるようにする。
     */
    fun clearCameraFlag() {
        isCameraOpen = false
    }

    /**
     * 発話後に必ず CMD_WAIT へ遷移してリスニングを再開するショートカット。
     * コマンド処理失敗・条件不満足のときに PROCESSING から復帰するために使う。
     */
    fun speakAndResume(text: String) {
        speak(text) {
            if (isRunning) {
                // requireWakeWord に応じてウェイクワード待ちかコマンド待ちに戻る
                _convState.value = if (requireWakeWord) ConvState.WAKE_WAIT else ConvState.CMD_WAIT
                mainHandler.postDelayed({ rebuildRecognizerAndListen() }, POST_TTS_DELAY_MS)
            }
        }
    }

    // ── 通知 API（CollectionViewModel から呼ぶ） ──────────────────────────────
    /** 新しい地点セッションが開始されたことを通知する。 */
    fun notifyLocationStarted(locationId: String) {
        if (!isRunning) return
        speak(if (language == "ja") "地点 ${locationId} を開始しました。標本と言って標本を追加してください。"
              else "Location $locationId started. Say 'specimen' to add a specimen.")
    }

    /** 標本が追加・印刷されたことを通知する。 */
    fun notifySpecimenAdded(specimenNumber: Int, printed: Boolean) {
        if (!isRunning) return
        val msg = if (language == "ja") {
            val printPart = if (printed) "印刷しました。" else "保存しました。"
            "標本 ${specimenNumber} 番を$printPart"
        } else {
            val printPart = if (printed) "printed." else "saved."
            "Specimen $specimenNumber $printPart"
        }
        speak(msg)
    }

    /** 現在の状況をアナウンスする。 */
    fun notifyStatus(locationId: String?, specimenCount: Int) {
        if (!isRunning) return
        val msg = if (language == "ja") {
            if (locationId != null) "現在の地点は ${locationId}、標本数は ${specimenCount} 件です。"
            else "現在、地点は未設定です。「新しい地点」と言って開始してください。"
        } else {
            if (locationId != null) "Current location: $locationId, specimens: $specimenCount."
            else "No location set. Say 'new location' to start."
        }
        speak(msg)
    }

    // ── 内部 ─────────────────────────────────────────────────────────────────
    private fun startListening() {
        if (!isRunning) return
        mainHandler.post {
            try {
                // 前回の遅延ミュート解除をキャンセルして確実に再ミュートする
                unmuteRunnable?.let { mainHandler.removeCallbacks(it) }
                unmuteRunnable = null
                // 開始音・終了音を消音してから認識開始
                // （scheduleUnmute で BEEP_MUTE_TAIL_MS 後に解除）
                muteRecognitionBeeps()
                recognizer?.startListening(buildRecognizerIntent())
            } catch (_: Exception) {
                // SpeechRecognizer が無い端末などは無視
                unmuteRecognitionBeeps()
            }
        }
    }

    private fun handleRecognizedText(text: String) {
        val lower = text.lowercase(Locale.getDefault())

        when (_convState.value) {

            ConvState.WAKE_WAIT -> {
                if (!requireWakeWord) {
                    // ウェイクワード不要モード: 認識したテキストをそのままコマンドとして処理
                    processCommand(lower, text)
                    return
                }
                val wakeWordLower = wakeWord.lowercase()
                if (lower.contains(wakeWordLower)) {
                    // ウェイクワード検出 → 同一発話にコマンドが含まれるか確認
                    val wakeIdx   = lower.indexOf(wakeWordLower)
                    val afterWake = lower.substring(wakeIdx + wakeWordLower.length).trim()

                    if (afterWake.isNotBlank() && hasCommandKeyword(afterWake)) {
                        // 例：「うんち標本」→ ウェイクワード確認なしに直接コマンド実行
                        _convState.value = ConvState.CMD_WAIT
                        processCommand(afterWake, afterWake)
                    } else {
                        // コマンドなし → 短い確認音声 + 即座にマイク起動
                        speakWakeWordAck()
                    }
                } else {
                    startListening()  // ウェイクワードでなければ無音で聴き直す
                }
            }

            ConvState.CMD_WAIT -> {
                processCommand(lower, text)
            }

            ConvState.NOTES_WAIT -> {
                // "終了" で備考入力をキャンセル
                if (matchesAny(lower, listOf("終了", "キャンセル", "end", "cancel"))) {
                    pendingNotes = ""
                    _convState.value = ConvState.CMD_WAIT
                    speak(if (language == "ja") "備考をキャンセルしました。" else "Notes cancelled.")
                } else {
                    pendingNotes = text
                    _commands.tryEmit(HfCommand.SetNotes(text))
                    _convState.value = ConvState.PROCESSING
                    speak(if (language == "ja") "備考を設定しました。「標本」と言って追加してください。"
                          else "Notes set. Say 'specimen' to add.")
                }
            }

            ConvState.TIMER_WAIT -> {
                val minutes = extractNumber(lower)
                if (minutes != null && minutes in 1..180) {
                    // タイマー管理は ViewModel 側で行う（HF オフ時も UI で使えるようにするため）
                    _convState.value = ConvState.PROCESSING
                    _commands.tryEmit(HfCommand.StartTimer(minutes))
                } else {
                    // 無効な入力 → 再プロンプト（TIMER_WAIT を維持）
                    speak(
                        if (language == "ja") "分数を教えてください。例えば「5分」のように話してください。"
                        else "Please say the number of minutes, for example 'five minutes'."
                    ) {
                        if (isRunning) {
                            _convState.value = ConvState.TIMER_WAIT
                            mainHandler.postDelayed({ rebuildRecognizerAndListen() }, POST_TTS_DELAY_MS)
                        }
                    }
                }
            }

            // SPEAKING / PROCESSING / IDLE 時は何もしない（onResults の isSpeaking ガードで
            // 通常はここに到達しないが、万一ここに来ても startListening() を呼ばない）
            else -> { /* no-op */ }
        }
    }

    private fun processCommand(lower: String, original: String) {
        when {
            // 新しい地点
            matchesAny(lower, listOf("新しい地点", "新規地点", "new location", "new point")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.NewLocation)
            }

            // 今何番？（標本番号確認）— 「状況」より先にチェック（"何番" の部分一致に注意）
            matchesAny(lower, listOf("今何番", "いま何番", "次何番", "なん番", "what number", "which number")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.AnnounceSpecimenNumber)
            }

            // いまどこ？（現在地確認）
            matchesAny(lower, listOf("いまどこ", "今どこ", "いま何処", "現在地", "where am i", "where are we")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.AnnounceDistance)
            }

            // 残り時間確認（タイマーより先にチェック）
            matchesAny(lower, listOf("残り時間", "あと何分", "タイマー残り", "残り何分", "timer remaining", "time left", "how much time")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.AnnounceTimerRemaining)
            }

            // タイマー（分数入力待ちに移行）
            matchesAny(lower, listOf("タイマー", "timer", "計る", "はかる")) -> {
                speak(if (language == "ja") "何分測りますか？" else "How many minutes?") {
                    if (isRunning) {
                        _convState.value = ConvState.TIMER_WAIT
                        mainHandler.postDelayed({ rebuildRecognizerAndListen() }, POST_TTS_DELAY_MS)
                    }
                }
            }

            // 地点写真（ダイアログなしで地点に紐づけ）— 「写真」より先にチェック
            matchesAny(lower, listOf("地点写真", "地点の写真", "location photo")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.OpenCameraForLocation)
            }

            // 標本写真（ダイアログなしで最新標本に紐づけ）— 「標本」「写真」より先にチェック
            matchesAny(lower, listOf("標本写真", "標本の写真", "specimen photo")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.OpenCameraForSpecimen)
            }

            // 標本追加
            matchesAny(lower, listOf("標本", "追加", "specimen", "add")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.AddSpecimen)
            }

            // 録画（動画録画を開始）— 「写真」より先にチェック
            matchesAny(lower, listOf("録画", "動画録画", "video recording", "record video")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.StartVideoRecording)
            }

            // 録音（音声録音を開始）— 「写真」より先にチェック
            matchesAny(lower, listOf("録音", "音声録音", "audio recording", "record audio")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.StartAudioRecording)
            }

            // 写真撮影（手動タグ付けダイアログあり）
            matchesAny(lower, listOf("写真", "撮影", "photo", "camera", "picture")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.OpenCamera)
            }

            // 備考
            lower.startsWith("備考") || lower.startsWith("メモ") || lower.startsWith("notes") -> {
                val noteText = lower
                    .removePrefix("備考").removePrefix("メモ").removePrefix("notes")
                    .trim()
                if (noteText.isNotEmpty()) {
                    _commands.tryEmit(HfCommand.SetNotes(noteText))
                    speak(if (language == "ja") "備考を設定しました。" else "Notes set.")
                } else {
                    // カスタムコールバックで TTS 後に NOTES_WAIT へ遷移（speak() が SPEAKING で上書きするため）
                    speak(if (language == "ja") "備考を話してください。" else "Please say your notes.") {
                        if (isRunning) {
                            _convState.value = ConvState.NOTES_WAIT
                            mainHandler.postDelayed({ rebuildRecognizerAndListen() }, POST_TTS_DELAY_MS)
                        }
                    }
                }
            }

            // 状況確認
            matchesAny(lower, listOf("状況", "何番", "status", "how many")) -> {
                _commands.tryEmit(HfCommand.AnnounceStatus)
            }

            // ライト（フラッシュライトトグル）
            matchesAny(lower, listOf("ライト", "フラッシュ", "懐中電灯", "light", "flashlight", "torch")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.ToggleFlashlight)
            }

            // 繰り返し
            matchesAny(lower, listOf("繰り返し", "もう一度", "repeat", "again")) -> {
                speak(lastSpoken)
            }

            // ログスタート（「ストップ」を含む「ログストップ」と区別するため「終了」より先にチェック）
            matchesAny(lower, listOf("ログスタート", "ログ開始", "log start", "start log")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.StartRouteLog)
            }

            // ログストップ（「ストップ」が終了コマンドと衝突するため「終了」より先にチェック）
            matchesAny(lower, listOf("ログストップ", "ログ停止", "ログ終了", "log stop", "stop log")) -> {
                _convState.value = ConvState.PROCESSING
                _commands.tryEmit(HfCommand.StopRouteLog)
            }

            // 終了
            matchesAny(lower, listOf("終了", "ストップ", "end", "stop", "finish")) -> {
                _commands.tryEmit(HfCommand.EndSession)
            }

            // カスタムフィールド: "フィールドラベル 値" の形式（例: "体重 12.5"）
            customFieldNames.isNotEmpty() && customFieldNames.any { lower.startsWith(it.lowercase()) } -> {
                val fieldName = customFieldNames.first { lower.startsWith(it.lowercase()) }
                val value = lower.removePrefix(fieldName.lowercase()).trim()
                if (value.isNotEmpty()) {
                    _convState.value = ConvState.PROCESSING
                    _commands.tryEmit(HfCommand.SetCustomField(fieldName, value))
                } else {
                    // フィールド名のみ → 無音で再試行
                    _convState.value = ConvState.CMD_WAIT
                    mainHandler.postDelayed({ rebuildRecognizerAndListen() }, 400)
                }
            }

            // コマンドとして認識できなかった → 無音で CMD_WAIT に戻る
            // （背景会話などの誤検知時にも余計な音声・ビープを出さない）
            else -> {
                _convState.value = ConvState.CMD_WAIT
                mainHandler.postDelayed({ rebuildRecognizerAndListen() }, 400)
            }
        }
    }

    /**
     * 認識テキストから数値（分数など）を抽出する。
     * ① アラビア数字 → ② 和語・漢数字 → ③ 英語 の順で試みる。
     */
    private fun extractNumber(text: String): Int? {
        // ① アラビア数字（ASR は通常これを返す）
        val digitMatch = Regex("\\d+").find(text)
        if (digitMatch != null) return digitMatch.value.toIntOrNull()

        // ② 和語・漢数字（長い表現を先に試す）
        val kanjiMap = linkedMapOf(
            "にじゅう" to 20, "さんじゅう" to 30, "よんじゅう" to 40,
            "ごじゅう"  to 50, "ろくじゅう" to 60, "ななじゅう" to 70,
            "はちじゅう" to 80, "きゅうじゅう" to 90,
            "二十" to 20, "三十" to 30, "四十" to 40,
            "五十" to 50, "六十" to 60, "七十" to 70,
            "八十" to 80, "九十" to 90,
            "じゅう" to 10, "十" to 10,
            "いち" to 1, "に" to 2, "さん" to 3, "よん" to 4, "ご" to 5,
            "ろく" to 6, "なな" to 7, "しち" to 7, "はち" to 8, "きゅう" to 9,
            "一" to 1, "二" to 2, "三" to 3, "四" to 4, "五" to 5,
            "六" to 6, "七" to 7, "八" to 8, "九" to 9
        )
        val lower = text.lowercase()
        for ((word, num) in kanjiMap) {
            if (lower.contains(word)) return num
        }

        // ③ 英語
        val englishMap = mapOf(
            "sixty" to 60, "fifty" to 50, "forty" to 40, "thirty" to 30, "twenty" to 20,
            "fifteen" to 15, "fourteen" to 14, "thirteen" to 13, "twelve" to 12, "eleven" to 11,
            "ten" to 10, "nine" to 9, "eight" to 8, "seven" to 7, "six" to 6,
            "five" to 5, "four" to 4, "three" to 3, "two" to 2, "one" to 1
        )
        for ((word, num) in englishMap) {
            if (lower.contains(word)) return num
        }
        return null
    }

    private fun matchesAny(lower: String, keywords: List<String>): Boolean =
        keywords.any { lower.contains(it) }

    /**
     * 音声テキストに既知のコマンドキーワードが含まれるか確認する。
     * ウェイクワードと同時発話されたコマンドの有無を判断するために使う。
     */
    private fun hasCommandKeyword(text: String): Boolean {
        val allKeywords = listOf(
            "新しい地点", "新規地点", "new location", "new point",
            "今何番", "いま何番", "次何番", "なん番", "what number", "which number",
            "いまどこ", "今どこ", "現在地", "where am i",
            "残り時間", "あと何分", "残り何分", "timer remaining", "time left",
            "タイマー", "timer", "計る",
            "地点写真", "location photo",
            "標本写真", "specimen photo",
            "標本", "追加", "specimen", "add",
            "録画", "動画録画", "video recording", "record video",
            "録音", "音声録音", "audio recording", "record audio",
            "写真", "撮影", "photo", "camera", "picture",
            "備考", "メモ", "notes",
            "状況", "何番", "status",
            "ライト", "フラッシュ", "light", "flashlight",
            "繰り返し", "もう一度", "repeat",
            "ログスタート", "ログ開始", "log start", "start log",
            "ログストップ", "ログ停止", "ログ終了", "log stop", "stop log",
            "終了", "ストップ", "end", "stop"
        ) + customFieldNames
        return allKeywords.any { text.contains(it) }
    }

    /**
     * ウェイクワード確認の短い応答。
     * "はい" だけを発話し、TTS 開始後すぐにマイクを起動することで
     * 「はい、どうぞ」が終わるのを待たずにコマンドを受け付ける。
     */
    private fun speakWakeWordAck() {
        val ackText = if (language == "ja") "はい" else "Yes"
        lastSpoken  = ackText

        if (!voiceResponseEnabled || !ttsReady) {
            // 音声フィードバックなし → 即座に CMD_WAIT へ
            _convState.value = ConvState.CMD_WAIT
            mainHandler.postDelayed({ rebuildRecognizerAndListen() }, POST_TTS_DELAY_MS)
            return
        }

        recognizer?.destroy()
        recognizer  = null
        isSpeaking  = true
        _convState.value = ConvState.SPEAKING
        applyTtsLanguage()

        tts?.setOnUtteranceProgressListener(object : android.speech.tts.UtteranceProgressListener() {
            override fun onStart(utteranceId: String?) {
                isSpeaking = true
                // TTS 開始から 300ms 後にマイクを起動する。
                // 「はい」と言い終わる前後のタイミングでマイクが用意できるため、
                // 「はい（pause）」を待たずにユーザーが即座にコマンドを話し始められる。
                mainHandler.postDelayed({
                    if (isRunning) {
                        isSpeaking          = false
                        _convState.value    = ConvState.CMD_WAIT
                        rebuildRecognizerAndListen()
                        restoreDefaultTtsListener()
                    }
                }, 300L)
            }
            override fun onDone(utteranceId: String?) {
                // onStart で既にマイクを起動しているので通常はここに来ない
                isSpeaking = false
                restoreDefaultTtsListener()
            }
            @Deprecated("Deprecated in Java")
            override fun onError(utteranceId: String?) {
                isSpeaking = false
                if (isRunning) {
                    _convState.value = ConvState.CMD_WAIT
                    mainHandler.postDelayed({ rebuildRecognizerAndListen() }, POST_TTS_DELAY_MS)
                }
                restoreDefaultTtsListener()
            }
        })
        tts?.speak(ackText, TextToSpeech.QUEUE_FLUSH, null, "hf_wake_${System.currentTimeMillis()}")
    }

    fun destroy() {
        stop()
        unmuteRecognitionBeeps()
        tts?.shutdown()
        tts = null
    }
}
