package com.android.trisolarisserver.component import com.fasterxml.jackson.databind.ObjectMapper import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import org.springframework.web.client.RestTemplate import org.springframework.web.util.UriComponentsBuilder @Component class GoogleGeocodingClient( private val restTemplate: RestTemplate, private val objectMapper: ObjectMapper, @Value("\${google.maps.apiKey:}") private val apiKey: String, @Value("\${google.maps.geocode.baseUrl:https://maps.googleapis.com/maps/api/geocode/json}") private val baseUrl: String ) { private val logger = LoggerFactory.getLogger(GoogleGeocodingClient::class.java) fun resolveCityState(pinCode: String): GeocodeResult { if (apiKey.isBlank()) { return GeocodeResult(null, null, "NO_API_KEY") } return try { val url = UriComponentsBuilder.fromUriString(baseUrl) .queryParam("components", "postal_code:$pinCode|country:IN") .queryParam("region", "IN") .queryParam("key", apiKey) .toUriString() val primary = fetch(url) if (primary.status == "OK") { return primary } if (primary.status == "ZERO_RESULTS") { val fallbackUrl = UriComponentsBuilder.fromUriString(baseUrl) .queryParam("address", "$pinCode India") .queryParam("region", "IN") .queryParam("key", apiKey) .toUriString() val fallback = fetch(fallbackUrl) if (fallback.resolvedCityState != null) return fallback return primary } primary } catch (ex: Exception) { logger.warn("Geocoding failed: {}", ex.message) GeocodeResult(null, null, "ERROR") } } private fun fetch(url: String): GeocodeResult { val response = restTemplate.getForEntity(url, String::class.java) val body = response.body ?: return GeocodeResult(null, null, "EMPTY_BODY") val status = parseStatus(body) val parsed = if (status == "OK") parseCityState(body) else null return GeocodeResult(parsed, body, status ?: "UNKNOWN_STATUS") } private fun parseStatus(body: String): String? { return objectMapper.readTree(body).path("status").asText(null) } private fun parseCityState(body: String): String? { val root = objectMapper.readTree(body) val results = root.path("results") if (!results.isArray || results.isEmpty) return null val resultNode = results.firstOrNull { node -> node.path("types").any { it.asText(null) == "postal_code" } } ?: results.first() val components = resultNode.path("address_components") if (!components.isArray) return null var city: String? = null var admin2: String? = null var state: String? = null for (comp in components) { val types = comp.path("types").mapNotNull { it.asText(null) }.toSet() when { "locality" in types -> city = comp.path("long_name").asText(null) ?: city "postal_town" in types -> if (city == null) { city = comp.path("long_name").asText(null) } "sublocality" in types -> if (city == null) { city = comp.path("long_name").asText(null) } "administrative_area_level_2" in types -> admin2 = comp.path("long_name").asText(null) ?: admin2 "administrative_area_level_1" in types -> state = comp.path("long_name").asText(null) ?: state } } val preferredCity = admin2?.trim()?.ifBlank { null } ?: city?.trim()?.ifBlank { null } if (preferredCity == null && state.isNullOrBlank()) return null return listOfNotNull(preferredCity, state?.trim()?.ifBlank { null }).joinToString(", ") } } data class GeocodeResult( val resolvedCityState: String?, val rawResponse: String?, val status: String? ) data class PincodeLookupResult( val resolvedCityState: String?, val rawResponse: String?, val status: String?, val source: String, val errorMessage: String? = null, val requestUrl: String? = null )