package com.fieldlabel.collector.service

import com.fieldlabel.collector.data.local.GeocodeCacheDao
import com.fieldlabel.collector.data.local.GeocodeCacheEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.ResponseBody
import org.json.JSONException
import org.json.JSONObject
import retrofit2.HttpException
import retrofit2.http.GET
import retrofit2.http.Query
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.roundToInt

// ---- Nominatim API ----
interface NominatimApi {
    @GET("reverse")
    suspend fun reverse(
        @Query("lat")             lat: Double,
        @Query("lon")             lon: Double,
        @Query("format")          format: String,       // 必ず "json" を渡すこと
        @Query("zoom")            zoom: Int,             // 16 = 街区・集落レベル
        @Query("accept-language") lang: String,          // "ja"
        @Query("email")           email: String          // Nominatim 利用規約: 連絡先メール必須
    ): ResponseBody
}

// ---- 逆ジオコーディング結果 ----
sealed class GeocodingResult {
    /** 住所取得成功 */
    data class Success(val address: String) : GeocodingResult()

    /** 取得失敗（code をUIに表示してデバッグに使う） */
    sealed class Failure : GeocodingResult() {
        abstract val code: String
        abstract val detail: String?

        /** DNS 解決失敗 / インターネット未接続 */
        data class NetworkUnreachable(override val detail: String? = null) : Failure() {
            override val code = "NET_UNREACHABLE"
        }
        /** 接続タイムアウト / 読み取りタイムアウト */
        data class Timeout(override val detail: String? = null) : Failure() {
            override val code = "TIMEOUT"
        }
        /** HTTP エラー（例: 429 レート超過, 500 サーバーエラー） */
        data class HttpError(val status: Int, override val detail: String? = null) : Failure() {
            override val code = "HTTP_$status"
        }
        /** JSON パース失敗 */
        data class ParseError(override val detail: String? = null) : Failure() {
            override val code = "PARSE_ERROR"
        }
        /** API は正常応答したが住所データが空（Nominatim が "error" フィールドを返した等） */
        data class EmptyResult(override val detail: String? = null) : Failure() {
            override val code = "EMPTY_RESULT"
        }
        /** その他の例外 */
        data class Unknown(override val detail: String? = null) : Failure() {
            override val code = "UNKNOWN"
        }
    }
}

// ---- 定数 ----
/** Nominatim 利用規約で要求される連絡先メールアドレス */
private const val NOMINATIM_EMAIL = "kutu92xjoo@gmail.com"

// ---- サービス本体 ----
@Singleton
class GeocodingService @Inject constructor(
    private val api: NominatimApi,
    private val cacheDao: GeocodeCacheDao
) {
    /**
     * 緯度経度から逆ジオコーディング結果を返す。
     * キャッシュヒット → GeocodingResult.Success
     * API 成功      → GeocodingResult.Success
     * 各種エラー   → GeocodingResult.Failure.XXX（code フィールドにコード）
     */
    suspend fun reverseGeocode(lat: Double, lon: Double): GeocodingResult =
        withContext(Dispatchers.IO) {
            val key = cacheKey(lat, lon)

            // キャッシュチェック
            cacheDao.get(key)?.address?.let { return@withContext GeocodingResult.Success(it) }

            // API 呼び出し
            try {
                val body    = api.reverse(lat, lon, "json", 16, "ja", NOMINATIM_EMAIL)
                val jsonStr = body.string()
                android.util.Log.d("GeocodingService", "raw(500): ${jsonStr.take(500)}")

                val json = try {
                    JSONObject(jsonStr)
                } catch (e: JSONException) {
                    android.util.Log.w("GeocodingService", "JSON parse error: ${e.message}")
                    return@withContext GeocodingResult.Failure.ParseError(e.message)
                }

                // Nominatim が {"error": "..."} を返す場合
                val nominatimError = json.optString("error").ifBlank { null }
                if (nominatimError != null) {
                    android.util.Log.w("GeocodingService", "Nominatim error field: $nominatimError")
                    return@withContext GeocodingResult.Failure.EmptyResult(nominatimError)
                }

                val displayName = json.optString("display_name").ifBlank { null }
                val addressJson = json.optJSONObject("address")
                val address     = buildAddress(addressJson) ?: displayName

                android.util.Log.d("GeocodingService", "resolved: $address")

                if (address == null) {
                    return@withContext GeocodingResult.Failure.EmptyResult(
                        "display_name=null, address=${addressJson?.toString()?.take(200)}"
                    )
                }

                cacheDao.insert(GeocodeCacheEntity(key = key, address = address))
                cacheDao.evictOld(System.currentTimeMillis() - 30L * 24 * 3600 * 1000)
                GeocodingResult.Success(address)

            } catch (e: UnknownHostException) {
                android.util.Log.w("GeocodingService", "NET_UNREACHABLE: ${e.message}")
                GeocodingResult.Failure.NetworkUnreachable(e.message)
            } catch (e: SocketTimeoutException) {
                android.util.Log.w("GeocodingService", "TIMEOUT: ${e.message}")
                GeocodingResult.Failure.Timeout(e.message)
            } catch (e: HttpException) {
                val errBody = try { e.response()?.errorBody()?.string()?.take(300) } catch (_: Exception) { null }
                android.util.Log.w("GeocodingService", "HTTP_${e.code()}: ${e.message()} | body=$errBody")
                GeocodingResult.Failure.HttpError(e.code(), "${e.message()} | $errBody")
            } catch (e: java.io.IOException) {
                android.util.Log.w("GeocodingService", "IO_ERROR → NET_UNREACHABLE: ${e.message}")
                GeocodingResult.Failure.NetworkUnreachable(e.message)
            } catch (e: Exception) {
                android.util.Log.w(
                    "GeocodingService",
                    "UNKNOWN ${e.javaClass.simpleName}: ${e.message}"
                )
                GeocodingResult.Failure.Unknown("${e.javaClass.simpleName}: ${e.message}")
            }
        }

    /**
     * Nominatim の address オブジェクトから日本向け住所文字列を構築する。
     *
     * 都市部:  state + city + cityDistrict/suburb + neighbourhood
     * 政令市:  state + city + borough + suburb/neighbourhood
     * 農村部:  state + county + town/village + hamlet/allotments
     * 山間部:  state + county + village + hamlet/isolatedDwelling
     */
    private fun buildAddress(addr: JSONObject?): String? {
        if (addr == null) return null

        fun opt(key: String): String? = addr.optString(key).ifBlank { null }

        val state            = opt("state")
        val county           = opt("county")
        val city             = opt("city") ?: opt("town") ?: opt("village") ?: opt("municipality")
        val borough          = opt("borough")
        val cityDistrict     = opt("city_district")
        val suburb           = opt("suburb")
        val quarter          = opt("quarter")
        val neighbourhood    = opt("neighbourhood")
        val hamlet           = opt("hamlet")
        val allotments       = opt("allotments")
        val isolatedDwelling = opt("isolated_dwelling")

        android.util.Log.d(
            "GeocodingService",
            "addr: state=$state county=$county city=$city borough=$borough " +
            "cityDistrict=$cityDistrict suburb=$suburb quarter=$quarter " +
            "neighbourhood=$neighbourhood hamlet=$hamlet allotments=$allotments " +
            "isolatedDwelling=$isolatedDwelling"
        )

        return buildString {
            state?.let { append(it) }

            if (city != null) {
                if (county != null && city != county) append(county)
                append(city)
            } else {
                county?.let { append(it) }
            }

            borough?.let { append(it) }

            val districtLevel = cityDistrict ?: suburb ?: quarter
            districtLevel?.let { append(it) }

            val localLevel = neighbourhood ?: hamlet ?: allotments ?: isolatedDwelling
            if (localLevel != null && localLevel != districtLevel) {
                append(localLevel)
            }
        }.ifBlank { null }
    }

    /** 緯度経度を小数点4桁に丸めてキャッシュキーにする（約11m精度） */
    private fun cacheKey(lat: Double, lon: Double): String {
        val la = (lat * 10000).roundToInt() / 10000.0
        val lo = (lon * 10000).roundToInt() / 10000.0
        return "${la}_${lo}"
    }
}
