package com.fieldlabel.collector.data

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import com.fieldlabel.collector.data.local.CollectionRecordDao
import com.fieldlabel.collector.data.local.CollectionRecordEntity
import com.fieldlabel.collector.data.local.CustomFieldDefDao
import com.fieldlabel.collector.data.local.CustomFieldDefEntity
import com.fieldlabel.collector.data.local.CustomFieldValueDao
import com.fieldlabel.collector.data.local.CustomFieldValueEntity
import com.fieldlabel.collector.data.local.SpecimenPhotoDao
import com.fieldlabel.collector.data.local.SpecimenPhotoEntity
import com.fieldlabel.collector.data.remote.FirestoreDataSource
import com.fieldlabel.collector.domain.model.CollectionRecord
import com.fieldlabel.collector.domain.model.CustomFieldDef
import com.fieldlabel.collector.domain.model.CustomFieldValue
import com.fieldlabel.collector.domain.model.LocationSession
import com.fieldlabel.collector.domain.model.SpecimenPhoto
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.Year
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class CollectionRepository @Inject constructor(
    private val dao: CollectionRecordDao,
    private val photoDao: SpecimenPhotoDao,
    private val customFieldDefDao: CustomFieldDefDao,
    private val customFieldValueDao: CustomFieldValueDao,
    private val firestore: FirestoreDataSource,
    private val dataStore: DataStore<Preferences>
) {
    // ---- DataStore キー ----
    companion object {
        val KEY_COLLECTOR_NAME     = stringPreferencesKey("collector_name")
        val KEY_TAPE_WIDTH         = intPreferencesKey("tape_width_mm")
        val KEY_PRINTER_MAC        = stringPreferencesKey("printer_mac_address")   // 旧: MACアドレス保存用（後方互換）
        val KEY_PRINTER_LOCAL_NAME = stringPreferencesKey("printer_local_name")    // 新: BLE ローカルネーム
        val KEY_PRINTER_MODEL      = stringPreferencesKey("printer_model")
        val KEY_LOC_YEAR              = stringPreferencesKey("loc_counter_year")
        val KEY_LOC_COUNTER           = intPreferencesKey("loc_counter_value")
        // 地点ID命名規則
        val KEY_LOC_PREFIX               = stringPreferencesKey("loc_id_prefix")
        val KEY_LOC_SEPARATOR            = stringPreferencesKey("loc_id_separator")
        val KEY_LOC_INCLUDE_YEAR         = booleanPreferencesKey("loc_id_include_year")
        val KEY_LOC_YEAR_DIGITS          = stringPreferencesKey("loc_id_year_digits")        // "YYYY"|"YY"
        val KEY_LOC_COUNTER_DIGITS       = intPreferencesKey("loc_id_counter_digits")        // 3|4|5
        val KEY_LOC_COUNTER_RESET_YEARLY = booleanPreferencesKey("loc_id_reset_yearly")
        // ラベル・印刷設定
        val KEY_PRINT_MODE_IMMEDIATE  = booleanPreferencesKey("print_mode_immediate")
        val KEY_LABEL_PORTRAIT        = booleanPreferencesKey("label_portrait")         // 旧キー・後方互換
        val KEY_LABEL_FONT_SCALE      = floatPreferencesKey("label_font_scale")
        val KEY_LABEL_LAYOUT          = stringPreferencesKey("label_layout")            // "compact"|"qr_right"|"column"|"portrait"
        val KEY_LABEL_LENGTH_MM       = intPreferencesKey("label_length_mm")            // 30-150
        val KEY_LABEL_FONT_FAMILY     = stringPreferencesKey("label_font_family")
        val KEY_LABEL_FONT_BOLD       = booleanPreferencesKey("label_font_bold")
        val KEY_LABEL_SHOW_QR         = booleanPreferencesKey("label_show_qr")
        val KEY_LABEL_SHOW_DATE       = booleanPreferencesKey("label_show_date")
        val KEY_LABEL_SHOW_LOCATION   = booleanPreferencesKey("label_show_location")
        val KEY_LABEL_SHOW_NOTES      = booleanPreferencesKey("label_show_notes")
        val KEY_LABEL_SHOW_COLLECTOR    = booleanPreferencesKey("label_show_collector")
        val KEY_LABEL_SHOW_COORDINATES  = booleanPreferencesKey("label_show_coordinates")
        val KEY_CUT_MODE              = stringPreferencesKey("cut_mode")                // "auto"|"half"|"none"
        // 要素ごとの文字スケール（ベースサイズに対する倍率）
        val KEY_ELEM_ID_SCALE         = floatPreferencesKey("elem_id_scale")
        val KEY_ELEM_DATE_SCALE       = floatPreferencesKey("elem_date_scale")
        val KEY_ELEM_LOCATION_SCALE   = floatPreferencesKey("elem_location_scale")
        val KEY_ELEM_COLLECTOR_SCALE  = floatPreferencesKey("elem_collector_scale")
        val KEY_ELEM_NOTES_SCALE      = floatPreferencesKey("elem_notes_scale")
        // 要素ごとの占有比（セクション配置での幅の相対比率）
        val KEY_ELEM_ID_WEIGHT         = floatPreferencesKey("elem_id_weight")
        val KEY_ELEM_DATE_WEIGHT       = floatPreferencesKey("elem_date_weight")
        val KEY_ELEM_LOCATION_WEIGHT   = floatPreferencesKey("elem_location_weight")
        val KEY_ELEM_COLLECTOR_WEIGHT  = floatPreferencesKey("elem_collector_weight")
        val KEY_ELEM_NOTES_WEIGHT      = floatPreferencesKey("elem_notes_weight")
        val KEY_ELEM_QR_WEIGHT         = floatPreferencesKey("elem_qr_weight")
        // マップ設定
        val KEY_MAP_TILE_SOURCE  = stringPreferencesKey("map_tile_source")   // "MAPNIK"|"TOPO"|"SATELLITE"
        val KEY_MAP_PIN_STYLE    = stringPreferencesKey("map_pin_style")     // "default"|"circle"|"star"|"diamond"
        // 表示設定
        val KEY_SHOW_THUMBNAILS  = booleanPreferencesKey("show_thumbnails")  // HistoryScreen でサムネイルを表示するか
        // ハンズフリー設定
        val KEY_HF_ENABLED           = booleanPreferencesKey("hf_enabled")            // ハンズフリーモード有効
        val KEY_HF_SCREEN_LOCK       = booleanPreferencesKey("hf_screen_lock")      // ハンズフリー中の画面操作ロック
        val KEY_HF_VOICE_RESPONSE    = booleanPreferencesKey("hf_voice_response")   // 音声フィードバック有効
        val KEY_HF_WAKE_WORD         = stringPreferencesKey("hf_wake_word")         // ウェイクワード
        val KEY_HF_LANGUAGE          = stringPreferencesKey("hf_language")          // "ja" | "en"
        val KEY_HF_REQUIRE_WAKE_WORD = booleanPreferencesKey("hf_require_wake_word") // ウェイクワード要求
        // チューブ径設定（QRコード曲率補正用）
        // null (0f) = フラット面。実際の値は mm 単位の外径。
        val KEY_TUBE_DIAMETER_DEFAULT = floatPreferencesKey("tube_diameter_default") // デフォルト径 (0=フラット)
        val KEY_TUBE_PRESET_1         = floatPreferencesKey("tube_preset_1")         // クイックボタン1
        val KEY_TUBE_PRESET_2         = floatPreferencesKey("tube_preset_2")         // クイックボタン2
        val KEY_TUBE_PRESET_3         = floatPreferencesKey("tube_preset_3")         // クイックボタン3
    }

    // ---- 全レコード・写真購読 ----
    val allRecords: Flow<List<CollectionRecord>> =
        dao.getAllFlow().map { list -> list.map { it.toDomain() } }

    /** 全レコードを一回取得する（インポート重複チェック用） */
    suspend fun getAllRecordsOnce(): List<CollectionRecord> =
        dao.getAll().map { it.toDomain() }

    val recentRecords: Flow<List<CollectionRecord>> =
        dao.getRecentFlow(50).map { list -> list.map { it.toDomain() } }

    /** 地点IDに紐づく写真のFlowを返す（UI観察用） */
    fun getPhotosForLocationFlow(locationId: String): Flow<List<SpecimenPhoto>> =
        photoDao.getByLocationIdFlow(locationId).map { list -> list.map { it.toDomain() } }

    /** 地点IDに紐づく写真を一回取得する */
    suspend fun getPhotosByLocationId(locationId: String): List<SpecimenPhoto> =
        photoDao.getByLocationId(locationId).map { it.toDomain() }

    /**
     * 地点ごとの最初の写真を購読する（サムネイル表示用）。
     * Map<locationId, SpecimenPhoto?> — 写真なしの地点は null 値を持つわけではなく単にキーが存在しない。
     */
    val firstPhotoByLocation: Flow<Map<String, SpecimenPhoto>> =
        photoDao.getAllPhotosFlow().map { list ->
            list
                .groupBy { it.locationId }
                .mapValues { (_, photos) -> photos.minByOrNull { it.takenAt }!!.toDomain() }
        }

    // ---- 設定値 ----
    val collectorName: Flow<String> = dataStore.data.map {
        it[KEY_COLLECTOR_NAME] ?: ""
    }
    val tapeWidthMm: Flow<Int> = dataStore.data.map {
        it[KEY_TAPE_WIDTH] ?: 36
    }

    val printerMac: Flow<String> = dataStore.data.map {
        it[KEY_PRINTER_MAC] ?: ""
    }

    /**
     * BLE ローカルネーム（例: "PT-P710BT"）。
     * 未設定の場合は旧 KEY_PRINTER_MAC の値にフォールバックする（移行用）。
     */
    val printerLocalName: Flow<String> = dataStore.data.map { prefs ->
        prefs[KEY_PRINTER_LOCAL_NAME]
            ?: prefs[KEY_PRINTER_MAC]
            ?: ""
    }

    val printerModel: Flow<String> = dataStore.data.map {
        it[KEY_PRINTER_MODEL] ?: "PT_P910BT"
    }
    val printModeImmediate: Flow<Boolean> = dataStore.data.map { it[KEY_PRINT_MODE_IMMEDIATE] ?: false }
    val labelPortrait: Flow<Boolean>      = dataStore.data.map { it[KEY_LABEL_PORTRAIT] ?: false }
    val labelFontScale: Flow<Float>       = dataStore.data.map { it[KEY_LABEL_FONT_SCALE] ?: 0.6f }
    // 旧レイアウト値を新レイアウト値にマイグレーションする
    val labelLayout: Flow<String>         = dataStore.data.map {
        when (val raw = it[KEY_LABEL_LAYOUT] ?: "auto_h") {
            "compact", "qr_right" -> "simple_h"
            "column"              -> "column_h"
            "portrait"            -> "simple_v"
            else                  -> raw   // "simple_h"|"column_h"|"simple_v"|"column_v"
        }
    }
    val labelLengthMm: Flow<Int>          = dataStore.data.map { it[KEY_LABEL_LENGTH_MM] ?: 70 }
    val labelFontFamily: Flow<String>     = dataStore.data.map { it[KEY_LABEL_FONT_FAMILY] ?: "" }
    val labelFontBold:   Flow<Boolean>    = dataStore.data.map { it[KEY_LABEL_FONT_BOLD]   ?: false }
    val labelShowQr: Flow<Boolean>        = dataStore.data.map { it[KEY_LABEL_SHOW_QR] ?: true }
    val labelShowDate: Flow<Boolean>      = dataStore.data.map { it[KEY_LABEL_SHOW_DATE] ?: true }
    val labelShowLocation: Flow<Boolean>  = dataStore.data.map { it[KEY_LABEL_SHOW_LOCATION] ?: true }
    val labelShowNotes: Flow<Boolean>     = dataStore.data.map { it[KEY_LABEL_SHOW_NOTES] ?: true }
    val cutMode: Flow<String>             = dataStore.data.map { it[KEY_CUT_MODE] ?: "half" }
    val labelShowCollector:    Flow<Boolean> = dataStore.data.map { it[KEY_LABEL_SHOW_COLLECTOR]    ?: true  }
    val labelShowCoordinates:  Flow<Boolean> = dataStore.data.map { it[KEY_LABEL_SHOW_COORDINATES]  ?: true  }
    // マップ設定
    val mapTileSource: Flow<String>  = dataStore.data.map { it[KEY_MAP_TILE_SOURCE] ?: "MAPNIK"  }
    val mapPinStyle:   Flow<String>  = dataStore.data.map { it[KEY_MAP_PIN_STYLE]   ?: "default" }
    // 表示設定
    val showThumbnails: Flow<Boolean> = dataStore.data.map { it[KEY_SHOW_THUMBNAILS] ?: true }
    // ハンズフリー設定
    val hfEnabled:          Flow<Boolean> = dataStore.data.map { it[KEY_HF_ENABLED]           ?: false         }
    val hfScreenLock:       Flow<Boolean> = dataStore.data.map { it[KEY_HF_SCREEN_LOCK]      ?: true          }
    val hfVoiceResponse:    Flow<Boolean> = dataStore.data.map { it[KEY_HF_VOICE_RESPONSE]   ?: true          }
    val hfWakeWord:         Flow<String>  = dataStore.data.map { it[KEY_HF_WAKE_WORD]        ?: "猫"       }
    val hfLanguage:         Flow<String>  = dataStore.data.map { it[KEY_HF_LANGUAGE]         ?: "ja"          }
    val hfRequireWakeWord:  Flow<Boolean> = dataStore.data.map { it[KEY_HF_REQUIRE_WAKE_WORD] ?: true         }
    // チューブ径設定（0f = フラット面）
    val tubeDiameterDefault: Flow<Float> = dataStore.data.map { it[KEY_TUBE_DIAMETER_DEFAULT] ?: 0f }
    val tubePreset1: Flow<Float>         = dataStore.data.map { it[KEY_TUBE_PRESET_1] ?: 10f }
    val tubePreset2: Flow<Float>         = dataStore.data.map { it[KEY_TUBE_PRESET_2] ?: 16f }
    val tubePreset3: Flow<Float>         = dataStore.data.map { it[KEY_TUBE_PRESET_3] ?: 25f }
    // 要素ごとの文字スケール・占有比
    val elemIdScale:         Flow<Float>  = dataStore.data.map { it[KEY_ELEM_ID_SCALE]         ?: 1.2f  }
    val elemIdWeight:        Flow<Float>  = dataStore.data.map { it[KEY_ELEM_ID_WEIGHT]        ?: 1.5f  }
    val elemDateScale:       Flow<Float>  = dataStore.data.map { it[KEY_ELEM_DATE_SCALE]       ?: 0.85f }
    val elemDateWeight:      Flow<Float>  = dataStore.data.map { it[KEY_ELEM_DATE_WEIGHT]      ?: 1.0f  }
    val elemLocationScale:   Flow<Float>  = dataStore.data.map { it[KEY_ELEM_LOCATION_SCALE]   ?: 0.85f }
    val elemLocationWeight:  Flow<Float>  = dataStore.data.map { it[KEY_ELEM_LOCATION_WEIGHT]  ?: 1.2f  }
    val elemCollectorScale:  Flow<Float>  = dataStore.data.map { it[KEY_ELEM_COLLECTOR_SCALE]  ?: 0.85f }
    val elemCollectorWeight: Flow<Float>  = dataStore.data.map { it[KEY_ELEM_COLLECTOR_WEIGHT] ?: 1.0f  }
    val elemNotesScale:      Flow<Float>  = dataStore.data.map { it[KEY_ELEM_NOTES_SCALE]      ?: 0.8f  }
    val elemNotesWeight:     Flow<Float>  = dataStore.data.map { it[KEY_ELEM_NOTES_WEIGHT]     ?: 1.0f  }
    val elemQrWeight:        Flow<Float>  = dataStore.data.map { it[KEY_ELEM_QR_WEIGHT]        ?: 1.5f  }

    // 地点ID命名規則
    val locIdPrefix:             Flow<String>  = dataStore.data.map { it[KEY_LOC_PREFIX]               ?: "LOC" }
    val locIdSeparator:          Flow<String>  = dataStore.data.map { it[KEY_LOC_SEPARATOR]             ?: "-"   }
    val locIdIncludeYear:        Flow<Boolean> = dataStore.data.map { it[KEY_LOC_INCLUDE_YEAR]          ?: true  }
    val locIdYearDigits:         Flow<String>  = dataStore.data.map { it[KEY_LOC_YEAR_DIGITS]           ?: "YYYY"}
    val locIdCounterDigits:      Flow<Int>     = dataStore.data.map { it[KEY_LOC_COUNTER_DIGITS]        ?: 4     }
    val locIdCounterResetYearly: Flow<Boolean> = dataStore.data.map { it[KEY_LOC_COUNTER_RESET_YEARLY]  ?: true  }

    suspend fun saveCollectorName(name: String) {
        dataStore.edit { it[KEY_COLLECTOR_NAME] = name }
    }

    suspend fun saveTapeWidth(mm: Int) {
        dataStore.edit { it[KEY_TAPE_WIDTH] = mm }
    }

    suspend fun savePrinterMac(mac: String) {
        dataStore.edit { it[KEY_PRINTER_MAC] = mac }
    }

    /** BLE ローカルネームを保存する */
    suspend fun savePrinterLocalName(name: String) {
        dataStore.edit { it[KEY_PRINTER_LOCAL_NAME] = name }
    }

    suspend fun savePrinterModel(model: String) {
        dataStore.edit { it[KEY_PRINTER_MODEL] = model }
    }
    suspend fun savePrintModeImmediate(immediate: Boolean) {
        dataStore.edit { it[KEY_PRINT_MODE_IMMEDIATE] = immediate }
    }
    suspend fun saveLabelPortrait(portrait: Boolean) {
        dataStore.edit { it[KEY_LABEL_PORTRAIT] = portrait }
    }
    suspend fun saveLabelFontScale(scale: Float) {
        dataStore.edit { it[KEY_LABEL_FONT_SCALE] = scale }
    }
    suspend fun saveLabelLayout(layout: String)         { dataStore.edit { it[KEY_LABEL_LAYOUT] = layout } }
    suspend fun saveLabelLengthMm(mm: Int)              { dataStore.edit { it[KEY_LABEL_LENGTH_MM] = mm } }
    suspend fun saveLabelFontFamily(family: String)     { dataStore.edit { it[KEY_LABEL_FONT_FAMILY] = family } }
    suspend fun saveLabelFontBold(bold: Boolean)        { dataStore.edit { it[KEY_LABEL_FONT_BOLD]   = bold   } }
    suspend fun saveLabelShowQr(show: Boolean)          { dataStore.edit { it[KEY_LABEL_SHOW_QR] = show } }
    suspend fun saveLabelShowDate(show: Boolean)        { dataStore.edit { it[KEY_LABEL_SHOW_DATE] = show } }
    suspend fun saveLabelShowLocation(show: Boolean)    { dataStore.edit { it[KEY_LABEL_SHOW_LOCATION] = show } }
    suspend fun saveLabelShowNotes(show: Boolean)       { dataStore.edit { it[KEY_LABEL_SHOW_NOTES] = show } }
    suspend fun saveCutMode(mode: String)               { dataStore.edit { it[KEY_CUT_MODE] = mode } }
    suspend fun saveLabelShowCollector(show: Boolean)    { dataStore.edit { it[KEY_LABEL_SHOW_COLLECTOR]    = show } }
    suspend fun saveLabelShowCoordinates(show: Boolean)  { dataStore.edit { it[KEY_LABEL_SHOW_COORDINATES]  = show } }
    suspend fun saveElemIdScale(v: Float)         { dataStore.edit { it[KEY_ELEM_ID_SCALE]         = v } }
    suspend fun saveElemIdWeight(v: Float)        { dataStore.edit { it[KEY_ELEM_ID_WEIGHT]        = v } }
    suspend fun saveElemDateScale(v: Float)       { dataStore.edit { it[KEY_ELEM_DATE_SCALE]       = v } }
    suspend fun saveElemDateWeight(v: Float)      { dataStore.edit { it[KEY_ELEM_DATE_WEIGHT]      = v } }
    suspend fun saveElemLocationScale(v: Float)   { dataStore.edit { it[KEY_ELEM_LOCATION_SCALE]   = v } }
    suspend fun saveElemLocationWeight(v: Float)  { dataStore.edit { it[KEY_ELEM_LOCATION_WEIGHT]  = v } }
    suspend fun saveElemCollectorScale(v: Float)  { dataStore.edit { it[KEY_ELEM_COLLECTOR_SCALE]  = v } }
    suspend fun saveElemCollectorWeight(v: Float) { dataStore.edit { it[KEY_ELEM_COLLECTOR_WEIGHT] = v } }
    suspend fun saveElemNotesScale(v: Float)      { dataStore.edit { it[KEY_ELEM_NOTES_SCALE]      = v } }
    suspend fun saveElemNotesWeight(v: Float)     { dataStore.edit { it[KEY_ELEM_NOTES_WEIGHT]     = v } }
    suspend fun saveElemQrWeight(v: Float)        { dataStore.edit { it[KEY_ELEM_QR_WEIGHT]        = v } }

    // 地点ID命名規則
    suspend fun saveLocIdPrefix(v: String)              { dataStore.edit { it[KEY_LOC_PREFIX]               = v } }
    suspend fun saveLocIdSeparator(v: String)           { dataStore.edit { it[KEY_LOC_SEPARATOR]             = v } }
    suspend fun saveLocIdIncludeYear(v: Boolean)        { dataStore.edit { it[KEY_LOC_INCLUDE_YEAR]          = v } }
    suspend fun saveLocIdYearDigits(v: String)          { dataStore.edit { it[KEY_LOC_YEAR_DIGITS]           = v } }
    suspend fun saveLocIdCounterDigits(v: Int)          { dataStore.edit { it[KEY_LOC_COUNTER_DIGITS]        = v } }
    suspend fun saveLocIdCounterResetYearly(v: Boolean) { dataStore.edit { it[KEY_LOC_COUNTER_RESET_YEARLY]  = v } }

    // マップ・表示設定
    suspend fun saveMapTileSource(source: String) { dataStore.edit { it[KEY_MAP_TILE_SOURCE] = source } }
    suspend fun saveMapPinStyle(style: String)    { dataStore.edit { it[KEY_MAP_PIN_STYLE]   = style  } }
    suspend fun saveShowThumbnails(show: Boolean) { dataStore.edit { it[KEY_SHOW_THUMBNAILS] = show   } }
    // ハンズフリー設定
    suspend fun saveHfEnabled(v: Boolean)          { dataStore.edit { it[KEY_HF_ENABLED]           = v } }
    suspend fun saveHfScreenLock(v: Boolean)       { dataStore.edit { it[KEY_HF_SCREEN_LOCK]      = v } }
    suspend fun saveHfVoiceResponse(v: Boolean)    { dataStore.edit { it[KEY_HF_VOICE_RESPONSE]   = v } }
    suspend fun saveHfWakeWord(v: String)          { dataStore.edit { it[KEY_HF_WAKE_WORD]        = v } }
    suspend fun saveHfLanguage(v: String)          { dataStore.edit { it[KEY_HF_LANGUAGE]         = v } }
    suspend fun saveHfRequireWakeWord(v: Boolean)  { dataStore.edit { it[KEY_HF_REQUIRE_WAKE_WORD] = v } }
    // チューブ径設定
    suspend fun saveTubeDiameterDefault(v: Float)  { dataStore.edit { it[KEY_TUBE_DIAMETER_DEFAULT] = v } }
    suspend fun saveTubePreset1(v: Float)          { dataStore.edit { it[KEY_TUBE_PRESET_1] = v } }
    suspend fun saveTubePreset2(v: Float)          { dataStore.edit { it[KEY_TUBE_PRESET_2] = v } }
    suspend fun saveTubePreset3(v: Float)          { dataStore.edit { it[KEY_TUBE_PRESET_3] = v } }

    // ---- ID 採番 ----
    /**
     * 新しい地点IDを採番する。
     * 命名規則は DataStore の設定値（プレフィックス・区切り文字・年包含・桁数など）に従う。
     */
    suspend fun nextLocationId(): String {
        val prefs            = dataStore.data.first()
        val currentYear      = Year.now().value
        val currentYearStr   = currentYear.toString()
        val prefix           = (prefs[KEY_LOC_PREFIX]               ?: "LOC").trim().ifBlank { "LOC" }
        val separator        =  prefs[KEY_LOC_SEPARATOR]             ?: "-"
        val includeYear      =  prefs[KEY_LOC_INCLUDE_YEAR]          ?: true
        val yearDigits       =  prefs[KEY_LOC_YEAR_DIGITS]           ?: "YYYY"
        val counterDigits    =  prefs[KEY_LOC_COUNTER_DIGITS]        ?: 4
        val resetYearly      =  prefs[KEY_LOC_COUNTER_RESET_YEARLY]  ?: true
        val savedYear        =  prefs[KEY_LOC_YEAR]                  ?: ""
        val counter = when {
            includeYear && resetYearly && savedYear != currentYearStr -> 1
            else -> (prefs[KEY_LOC_COUNTER] ?: 0) + 1
        }
        dataStore.edit { p ->
            p[KEY_LOC_YEAR]    = currentYearStr
            p[KEY_LOC_COUNTER] = counter
        }
        return buildLocationIdString(prefix, separator, includeYear, yearDigits, counterDigits, counter, currentYear)
    }

    /** 地点ID文字列を組み立てる（UI プレビューや採番共用） */
    fun buildLocationIdString(
        prefix: String, separator: String,
        includeYear: Boolean, yearDigits: String,
        counterDigits: Int, counter: Int, year: Int
    ): String {
        val yearStr    = if (yearDigits == "YY") (year % 100).toString().padStart(2, '0')
                         else year.toString()
        val counterStr = counter.toString().padStart(counterDigits, '0')
        return if (includeYear) "$prefix$separator$yearStr$separator$counterStr"
               else             "$prefix$separator$counterStr"
    }

    /** 標本IDを生成する。形式: {locationId}{separator}{NN} */
    fun buildSpecimenId(locationId: String, specimenNumber: Int, separator: String = "-"): String =
        "$locationId$separator%02d".format(specimenNumber)

    // ---- CRUD ----
    suspend fun saveRecord(record: CollectionRecord) {
        dao.insert(CollectionRecordEntity.fromDomain(record))
        // オンラインなら即時Firestoreへ同期
        try {
            firestore.upsert(record)
            dao.markSynced(record.id)
        } catch (_: Exception) {
            // オフライン時は SyncWorker が後で同期する
        }
    }

    suspend fun updateRecord(record: CollectionRecord) {
        dao.update(CollectionRecordEntity.fromDomain(record.copy(synced = false)))
        try {
            firestore.upsert(record)
            dao.markSynced(record.id)
        } catch (_: Exception) {}
    }

    suspend fun getById(id: String): CollectionRecord? =
        dao.getById(id)?.toDomain()

    suspend fun deleteRecord(record: CollectionRecord) {
        dao.delete(CollectionRecordEntity.fromDomain(record))
    }

    /** 指定地点IDに属する全標本を採集番号順で返す */
    suspend fun getByLocationId(locationId: String): List<CollectionRecord> =
        dao.getByLocationId(locationId).map { it.toDomain() }

    /** 指定地点の全標本と写真を削除する */
    suspend fun deleteByLocationId(locationId: String) {
        dao.deleteByLocationId(locationId)
        photoDao.deleteByLocationId(locationId)
    }

    /** 指定地点の全標本の採集者名を一括更新する */
    suspend fun updateLocationCollector(locationId: String, collectorName: String) {
        dao.updateCollectorByLocationId(locationId, collectorName)
    }

    // ---- 写真 CRUD ----
    /** 写真レコードを保存する */
    suspend fun savePhoto(photo: SpecimenPhoto) {
        photoDao.insert(SpecimenPhotoEntity.fromDomain(photo))
    }

    /** 写真レコードと物理ファイルを削除する */
    suspend fun deletePhoto(photo: SpecimenPhoto) {
        photoDao.delete(SpecimenPhotoEntity.fromDomain(photo))
        java.io.File(photo.filePath).delete()
    }

    /** 写真に紐づく標本IDを変更する（null = 地点全体） */
    suspend fun updatePhotoTag(photoId: String, specimenId: String?) {
        photoDao.updateTag(photoId, specimenId)
    }

    /** 地点IDに紐づく写真・動画・音声をメディア種別でフィルタして返す */
    fun getMediaByTypeFlow(locationId: String, mediaType: String): kotlinx.coroutines.flow.Flow<List<SpecimenPhoto>> =
        photoDao.getByLocationIdAndTypeFlow(locationId, mediaType).map { list -> list.map { it.toDomain() } }

    // ---- カスタムフィールド定義 CRUD ----

    val allCustomFieldDefs: kotlinx.coroutines.flow.Flow<List<CustomFieldDef>> =
        customFieldDefDao.getAllFlow().map { list -> list.map { it.toDomain() } }

    suspend fun saveCustomFieldDef(def: CustomFieldDef) {
        customFieldDefDao.insert(CustomFieldDefEntity.fromDomain(def))
    }

    suspend fun deleteCustomFieldDef(def: CustomFieldDef) {
        customFieldDefDao.deleteById(def.id)
        customFieldValueDao.deleteByFieldDefId(def.id)
    }

    suspend fun getAllCustomFieldDefsOnce(): List<CustomFieldDef> =
        customFieldDefDao.getAll().map { it.toDomain() }

    // ---- カスタムフィールド値 CRUD ----

    fun getCustomFieldValuesForLocationFlow(locationId: String): kotlinx.coroutines.flow.Flow<List<CustomFieldValue>> =
        customFieldValueDao.getByLocationIdFlow(locationId).map { list -> list.map { it.toDomain() } }

    fun getCustomFieldValuesForSpecimenFlow(specimenId: String): kotlinx.coroutines.flow.Flow<List<CustomFieldValue>> =
        customFieldValueDao.getBySpecimenIdFlow(specimenId).map { list -> list.map { it.toDomain() } }

    suspend fun upsertSpecimenFieldValue(fieldDefId: String, locationId: String, specimenId: String, value: String) {
        customFieldValueDao.upsertSpecimenValue(
            newId      = java.util.UUID.randomUUID().toString(),
            fieldDefId = fieldDefId,
            locationId = locationId,
            specimenId = specimenId,
            value      = value
        )
    }

    suspend fun upsertLocationFieldValue(fieldDefId: String, locationId: String, value: String) {
        customFieldValueDao.upsertLocationValue(
            newId      = java.util.UUID.randomUUID().toString(),
            fieldDefId = fieldDefId,
            locationId = locationId,
            value      = value
        )
    }

    suspend fun getCustomFieldValuesForLocation(locationId: String): List<CustomFieldValue> =
        customFieldValueDao.getByLocationId(locationId).map { it.toDomain() }

    suspend fun getAllCustomFieldValuesOnce(): List<CustomFieldValue> =
        customFieldValueDao.getAll().map { it.toDomain() }

    // ---- 同期 ----
    /** WorkManager から呼ばれる：未同期レコードを一括アップロード */
    suspend fun syncUnsynced() {
        val unsynced = dao.getUnsynced().map { it.toDomain() }
        if (unsynced.isEmpty()) return
        firestore.ensureSignedIn()
        firestore.batchUpsert(unsynced)
        unsynced.forEach { dao.markSynced(it.id) }
    }

    // ---- 地点セッション生成 ----
    suspend fun createLocationSession(
        latitude: Double,
        longitude: Double,
        altitude: Double?,
        address: String?,
        collectorName: String
    ): LocationSession {
        val locationId = nextLocationId()
        return LocationSession(
            locationId = locationId,
            latitude = latitude,
            longitude = longitude,
            altitude = altitude,
            address = address,
            collectionDate = LocalDate.now(),
            collectorName = collectorName
        )
    }

    /** セッション内で次の標本レコードを生成して保存する（あとで一括印刷モード用） */
    suspend fun addSpecimenToSession(
        session: LocationSession,
        notes: String,
        tubeDiameterMm: Float? = null
    ): CollectionRecord {
        val record = buildSpecimenRecord(session, notes, tubeDiameterMm)
        finalizeSpecimen(session, record)
        return record
    }

    /**
     * 標本レコードを生成するが、カウンターの更新も DB への保存も行わない。
     * 随時印刷モードで「印刷成功後のみ確定する」ために使う。
     */
    suspend fun buildSpecimenRecord(
        session: LocationSession,
        notes: String,
        tubeDiameterMm: Float? = null
    ): CollectionRecord {
        val specimenNo = session.nextSpecimenNumber
        val separator  = dataStore.data.first()[KEY_LOC_SEPARATOR] ?: "-"
        val specimenId = buildSpecimenId(session.locationId, specimenNo, separator)
        // 採集日は「保存・印刷ボタンを押した日（= 今日）」を使う。
        // 同一セッションを複数日にまたいで継続しても、各標本に正しい採集日が記録される。
        return CollectionRecord(
            id             = UUID.randomUUID().toString(),
            locationId     = session.locationId,
            specimenNumber = specimenNo,
            specimenId     = specimenId,
            collectionDate = LocalDate.now(),
            latitude       = session.latitude,
            longitude      = session.longitude,
            altitude       = session.altitude,
            address        = session.address,
            collectorName  = session.collectorName,
            notes          = notes,
            createdAt      = LocalDateTime.now(),
            tubeDiameterMm = tubeDiameterMm
        )
    }

    /**
     * 標本を確定する: セッションのカウンターをインクリメントして DB に保存する。
     * [buildSpecimenRecord] で仮生成したレコードを印刷成功後にのみ呼ぶ。
     */
    suspend fun finalizeSpecimen(session: LocationSession, record: CollectionRecord) {
        session.nextSpecimenNumber++
        saveRecord(record)
    }

}
