GigaProjects

← Back to GigaWeather

WeatherDetailActivity.kt

package com.gigaprojects.gigaweather

import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.gigaprojects.gigaweather.data.LocationDatabase
import com.gigaprojects.gigaweather.data.LocationEntity
import com.gigaprojects.gigaweather.ui.theme.GigaWeatherTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import java.util.*

private const val FUTURE_FORECAST_DAYS = 7
private const val FORECAST_REQUEST_DAYS = 8

class WeatherDetailActivity : ComponentActivity() {
    private val requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { _ -> }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        hideSystemBars()
        checkNotificationPermission()
        
        val name = intent.getStringExtra("name") ?: getString(R.string.unknown_location)
        val lat = intent.getDoubleExtra("lat", 0.0)
        val lon = intent.getDoubleExtra("lon", 0.0)

        setContent {
            val sharedPreferences = remember { getSharedPreferences("geo_weather_prefs", Context.MODE_PRIVATE) }
            val useSystemTheme = sharedPreferences.collectAsState(key = "use_system_theme", defaultValue = true)
            val darkModeEnabled = sharedPreferences.collectAsState(key = "dark_mode_enabled", defaultValue = false)
            val dynamicColor = sharedPreferences.collectAsState(key = "dynamic_color", defaultValue = true)
            
            val darkTheme = if (useSystemTheme.value) isSystemInDarkTheme() else darkModeEnabled.value
            
            GigaWeatherTheme(darkTheme = darkTheme, dynamicColor = dynamicColor.value) {
                WeatherDetailScreen(
                    name = name,
                    lat = lat,
                    lon = lon,
                    onBack = { finish() }
                )
            }
        }
    }

    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        if (hasFocus) {
            hideSystemBars()
        }
    }

    private fun hideSystemBars() {
        val windowInsetsController =
            WindowCompat.getInsetsController(window, window.decorView)
        windowInsetsController.systemBarsBehavior =
            WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
        windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
    }

    private fun checkNotificationPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
                requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WeatherDetailScreen(
    name: String,
    lat: Double,
    lon: Double,
    onBack: () -> Unit
) {
    val scope = rememberCoroutineScope()
    val context = LocalContext.current
    val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
    val db = remember { LocationDatabase.getDatabase(context) }
    val sharedPreferences = remember { context.getSharedPreferences("geo_weather_prefs", Context.MODE_PRIVATE) }
    
    val tempUnit by sharedPreferences.collectStringAsState("temp_unit", "celsius")
    val windUnit by sharedPreferences.collectStringAsState("wind_unit", "kmh")
    val weatherProvider by sharedPreferences.collectStringAsState("weather_provider", "open_meteo")
    val weatherApiKey by sharedPreferences.collectStringAsState("weather_api_key", "")
    val qweatherApiKey by sharedPreferences.collectStringAsState("qweather_api_key", "")

    var weatherJson by remember { mutableStateOf<String?>(null) }
    var aqiJson by remember { mutableStateOf<String?>(null) }
    var moonPhaseName by remember { mutableStateOf<String?>(null) }
    var forecastList by remember { mutableStateOf<List<DailyForecast>>(emptyList()) }
    var hourlyForecastList by remember { mutableStateOf<List<HourlyForecast>>(emptyList()) }
    var historicalData by remember { mutableStateOf<List<DailyForecast>>(emptyList()) }
    var errorMessage by remember { mutableStateOf<String?>(null) }
    var isRefreshing by remember { mutableStateOf(false) }
    
    var selectedDayIndex by remember { mutableStateOf(-1) }

    suspend fun refreshWeatherData(forceRefresh: Boolean = false) {
        try {
            isRefreshing = true
            val entity = withContext(Dispatchers.IO) { db.locationDao().findByCoordinates(lat, lon) }
            val currentTime = System.currentTimeMillis()
            
            val lastUpdated = entity?.lastUpdated
            val dataAgeMinutes = if (lastUpdated != null) (currentTime - lastUpdated) / (1000 * 60) else Long.MAX_VALUE
            val cachedWeatherData = entity?.weatherData
            val cacheHasPrecipitation = cachedWeatherData?.let { hasPrecipitationData(it) } ?: false
            val cacheHasFutureForecast = cachedWeatherData?.let {
                hasFutureForecastDays(it, FUTURE_FORECAST_DAYS)
            } ?: false

            var json: String? = null
            var aqiJsonResponse: String? = null
            val aqiUrl = "https://air-quality-api.open-meteo.com/v1/air-quality?latitude=$lat&longitude=$lon&hourly=european_aqi&timezone=auto"

            if (!forceRefresh && cachedWeatherData != null && cacheHasPrecipitation && cacheHasFutureForecast && dataAgeMinutes < 30) {
                json = cachedWeatherData
                weatherJson = json
            } else {
                val url = if (weatherProvider == "weatherapi" && weatherApiKey.isNotEmpty()) {
                    "https://api.weatherapi.com/v1/forecast.json?key=$weatherApiKey&q=$lat,$lon&days=$FORECAST_REQUEST_DAYS&aqi=yes"
                } else {
                    "https://api.open-meteo.com/v1/forecast?latitude=$lat&longitude=$lon&current=temperature_2m,weather_code,wind_speed_10m&hourly=temperature_2m,weather_code,relative_humidity_2m,pressure_msl,apparent_temperature,precipitation,precipitation_probability&daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,precipitation_sum,precipitation_probability_max,wind_speed_10m_max&timezone=auto&forecast_days=$FORECAST_REQUEST_DAYS"
                }
                
                val histUrl = "https://archive-api.open-meteo.com/v1/archive?latitude=$lat&longitude=$lon&start_date=${getYesterdayDate(-7)}&end_date=${getYesterdayDate(0)}&daily=temperature_2m_max,temperature_2m_min&timezone=auto"
                
                json = withContext(Dispatchers.IO) { httpGet(url) }
                
                try {
                    val histJson = withContext(Dispatchers.IO) { httpGet(histUrl) }
                    historicalData = parseForecastData(histJson)
                } catch (_: Exception) {}

                if (qweatherApiKey.isNotEmpty()) {
                    try {
                        val moonUrl = "https://devapi.qweather.com/v7/astronomy/moon?location=$lon,$lat&key=$qweatherApiKey"
                        val mq = withContext(Dispatchers.IO) { httpGet(moonUrl) }
                        val obj = JSONObject(mq).getJSONArray("moonPhase").getJSONObject(0)
                        moonPhaseName = obj.optString("name", null)
                    } catch (_: Exception) {}
                }
            }

            aqiJsonResponse = withContext(Dispatchers.IO) { try { httpGet(aqiUrl) } catch (e: Exception) { null } }

            if (weatherProvider == "open_meteo") {
                entity?.copy(weatherData = json, lastUpdated = currentTime)?.let {
                    withContext(Dispatchers.IO) { db.locationDao().updateLocation(it) }
                }
            }

            weatherJson = json
            aqiJson = aqiJsonResponse
            if (json != null) {
                if (weatherProvider == "weatherapi") {
                    parseWeatherApiData(json).let {
                        forecastList = it.first
                        hourlyForecastList = it.second
                    }
                } else {
                    forecastList = parseForecastData(
                        json = json,
                        futureOnly = true,
                        limit = FUTURE_FORECAST_DAYS
                    )
                    hourlyForecastList = parseHourlyForecastData(json)
                }
            }
            errorMessage = null
        } catch (e: Exception) {
            errorMessage = e.message ?: context.getString(R.string.error_loading_weather)
        } finally {
            isRefreshing = false
        }
    }

    LaunchedEffect(Unit) { refreshWeatherData() }

    Scaffold(
        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
        topBar = {
            TopAppBar(
                title = { Text(name) },
                navigationIcon = {
                    IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_nav_desc)) }
                },
                actions = {
                    IconButton(onClick = { scope.launch { refreshWeatherData(true) } }, enabled = !isRefreshing) {
                        if (isRefreshing) CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
                        else Icon(Icons.Default.Refresh, contentDescription = stringResource(R.string.refresh_nav_desc))
                    }
                },
                scrollBehavior = scrollBehavior
            )
        }
    ) { innerPadding ->
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            contentPadding = innerPadding,
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            if (errorMessage != null) {
                item {
                    Text(errorMessage!!, color = MaterialTheme.colorScheme.error, modifier = Modifier.padding(16.dp))
                }
            }

            weatherJson?.let { json ->
                val (temp, weatherCode) = if (weatherProvider == "weatherapi") {
                    val current = JSONObject(json).getJSONObject("current")
                    current.getDouble("temp_c") to 0
                } else {
                    val weatherObj = JSONObject(json)
                    val current = weatherObj.optJSONObject("current")
                    val legacyCurrent = weatherObj.optJSONObject("current_weather")

                    if (current != null) {
                        current.getDouble("temperature_2m") to current.getInt("weather_code")
                    } else {
                        val fallbackCurrent = legacyCurrent ?: JSONObject()
                        fallbackCurrent.getDouble("temperature") to fallbackCurrent.getInt("weathercode")
                    }
                }
                
                val displayTemp = if (tempUnit == "fahrenheit") (temp * 9/5 + 32).toInt() else temp.toInt()

                item {
                    val tempSuffixC = stringResource(R.string.temp_c_suffix)
                    val tempSuffixF = stringResource(R.string.temp_f_suffix)
                    val tempSuffix = if (tempUnit == "fahrenheit") tempSuffixF else tempSuffixC

                    Column(
                        modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        Icon(
                            painter = painterResource(id = WeatherIconMapper.getWeatherIcon(weatherCode)),
                            contentDescription = null,
                            modifier = Modifier.size(120.dp),
                            tint = Color.Unspecified
                        )
                        Text("$displayTemp$tempSuffix", style = MaterialTheme.typography.displayLarge, fontWeight = FontWeight.Bold)
                        Text(WeatherCodes.getDescription(weatherCode, context), style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
                    }
                }

                item {
                    WeatherDetailsGrid(JSONObject(json), aqiJson, tempUnit, windUnit, weatherProvider)
                }

                item {
                    HourlyForecastSection(hourlyForecastList, tempUnit)
                }

                item {
                    Column(modifier = Modifier.padding(horizontal = 16.dp)) {
                        Text(stringResource(R.string.forecast_7day_label), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
                        Spacer(Modifier.height(8.dp))
                        forecastList.forEachIndexed { index, forecast ->
                            ForecastItemRow(
                                forecast = forecast,
                                tempUnit = tempUnit,
                                isSelected = selectedDayIndex == index,
                                onClick = { selectedDayIndex = if (selectedDayIndex == index) -1 else index }
                            )
                        }
                    }
                }

                if (moonPhaseName != null) {
                    item {
                        Card(
                            modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
                            colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f))
                        ) {
                            Row(
                                modifier = Modifier.padding(16.dp),
                                verticalAlignment = Alignment.CenterVertically
                            ) {
                                Text(text = "๐ŸŒ™", fontSize = 24.sp)
                                Spacer(Modifier.width(16.dp))
                                Column {
                                    Text(text = stringResource(R.string.MoonPhaseTXT), style = MaterialTheme.typography.labelMedium)
                                    Text(text = moonPhaseName ?: "", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
                                }
                            }
                        }
                    }
                }

                if (historicalData.isNotEmpty()) {
                    item {
                        HistoricalTrendsSection(historicalData, tempUnit)
                    }
                }
                
                item { Spacer(Modifier.height(32.dp)) }
            }

            if (weatherJson == null && !isRefreshing) {
                item { Box(Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } }
            }
        }
    }
}

@Composable
fun ForecastItemRow(
    forecast: DailyForecast,
    tempUnit: String,
    isSelected: Boolean,
    onClick: () -> Unit
) {
    val tempSuffix = stringResource(R.string.temp_deg_suffix)
    val displayMax = if (tempUnit == "fahrenheit") (forecast.tempMax * 9/5 + 32).toInt() else forecast.tempMax.toInt()
    val displayMin = if (tempUnit == "fahrenheit") (forecast.tempMin * 9/5 + 32).toInt() else forecast.tempMin.toInt()
    val rainText = formatRainAmount(forecast.precipitationMm)
    val precipitationText = if (forecast.precipitationChance > 0) {
        "$rainText ยท ${forecast.precipitationChance}%"
    } else {
        rainText
    }

    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp)
            .clickable { onClick() },
        colors = CardDefaults.cardColors(
            containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent
        )
    ) {
        Column {
            ListItem(
                headlineContent = { Text(forecast.date) },
                supportingContent = {
                    Column {
                        Text(WeatherCodes.getDescription(forecast.weatherCode, LocalContext.current))
                        Text(
                            text = stringResource(R.string.rain_amount_label, precipitationText),
                            style = MaterialTheme.typography.bodySmall,
                            color = MaterialTheme.colorScheme.onSurfaceVariant
                        )
                    }
                },
                trailingContent = { Text("$displayMax$tempSuffix / $displayMin$tempSuffix", fontWeight = FontWeight.Bold) },
                leadingContent = { 
                    Icon(
                        painter = painterResource(WeatherIconMapper.getWeatherIcon(forecast.weatherCode)), 
                        contentDescription = null, 
                        modifier = Modifier.size(32.dp), 
                        tint = Color.Unspecified
                    ) 
                },
                colors = ListItemDefaults.colors(containerColor = Color.Transparent)
            )
            AnimatedVisibility(visible = isSelected) {
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(start = 64.dp, end = 16.dp, bottom = 12.dp),
                    horizontalArrangement = Arrangement.SpaceBetween
                ) {
                    DetailItemSmall(label = stringResource(R.string.trend_max), value = "$displayMax$tempSuffix")
                    DetailItemSmall(label = stringResource(R.string.trend_min), value = "$displayMin$tempSuffix")
                    DetailItemSmall(label = stringResource(R.string.rain_label), value = precipitationText)
                }
            }
        }
    }
}

@Composable
fun DetailItemSmall(label: String, value: String) {
    Column {
        Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
        Text(value, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium)
    }
}

@Composable
fun HistoricalTrendsSection(data: List<DailyForecast>, tempUnit: String) {
    Column(modifier = Modifier.padding(horizontal = 16.dp)) {
        Text(stringResource(R.string.historical_trends_label), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
        Spacer(Modifier.height(12.dp))
        Card(
            modifier = Modifier.fillMaxWidth(),
            colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
        ) {
            Column(modifier = Modifier.padding(16.dp)) {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(150.dp)
                        .padding(top = 8.dp)
                ) {
                    Canvas(modifier = Modifier.fillMaxSize()) {
                        val width = size.width
                        val height = size.height
                        val points = data.size
                        if (points < 2) return@Canvas

                        val maxTemp = data.maxOf { it.tempMax }.toFloat()
                        val minTemp = data.minOf { it.tempMin }.toFloat()
                        val range = (maxTemp - minTemp).coerceAtLeast(1f)

                        val pathMax = Path()
                        val pathMin = Path()

                        data.forEachIndexed { index, forecast ->
                            val x = index * (width / (points - 1))
                            val yMax = height - ((forecast.tempMax.toFloat() - minTemp) / range * height)
                            val yMin = height - ((forecast.tempMin.toFloat() - minTemp) / range * height)

                            if (index == 0) {
                                pathMax.moveTo(x, yMax)
                                pathMin.moveTo(x, yMin)
                            } else {
                                pathMax.lineTo(x, yMax)
                                pathMin.lineTo(x, yMin)
                            }
                        }

                        drawPath(pathMax, color = Color.Red, style = Stroke(width = 3.dp.toPx()))
                        drawPath(pathMin, color = Color.Blue, style = Stroke(width = 3.dp.toPx()))
                    }
                }
                Spacer(Modifier.height(8.dp))
                Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                    Text(stringResource(R.string.trend_min), color = Color.Blue, style = MaterialTheme.typography.labelSmall)
                    Text(stringResource(R.string.trend_max), color = Color.Red, style = MaterialTheme.typography.labelSmall)
                }
            }
        }
    }
}

@Composable
fun WeatherDetailsGrid(weatherObj: JSONObject, aqiJson: String?, tempUnit: String, windUnit: String, provider: String) {
    val context = LocalContext.current
    val (wind, feelsLike, humidity) = if (provider == "weatherapi") {
        val current = weatherObj.getJSONObject("current")
        Triple(current.getDouble("wind_kph"), current.getDouble("feelslike_c"), current.getInt("humidity"))
    } else {
        val current = weatherObj.optJSONObject("current")
        val legacyCurrent = weatherObj.optJSONObject("current_weather")
        val hourly = weatherObj.optJSONObject("hourly")
        val currentIndex = if (hourly != null) getCurrentHourIndex(hourly.getJSONArray("time")) else -1
        val currentTemp = current?.optDouble("temperature_2m")
            ?: legacyCurrent?.getDouble("temperature")
            ?: 0.0
        val windVal = current?.optDouble("wind_speed_10m")
            ?: legacyCurrent?.getDouble("windspeed")
            ?: 0.0
        val humidityValues = hourly?.optJSONArray("relative_humidity_2m")
            ?: hourly?.optJSONArray("relativehumidity_2m")
        val feelsVal = if (currentIndex >= 0) {
            hourly?.optJSONArray("apparent_temperature")?.optDouble(currentIndex, currentTemp) ?: currentTemp
        } else {
            currentTemp
        }
        val humVal = if (currentIndex >= 0) humidityValues?.optInt(currentIndex, 0) ?: 0 else 0
        Triple(windVal, feelsVal, humVal)
    }

    val displayWind = if (windUnit == "mph") (wind * 0.621371).toInt() else wind.toInt()
    val windSuffix = if (windUnit == "mph") stringResource(R.string.wind_mph_suffix) else stringResource(R.string.wind_kmh_suffix)

    val displayFeelsLike = if (tempUnit == "fahrenheit") (feelsLike * 9/5 + 32).toInt() else feelsLike.toInt()
    val tempSuffix = if (tempUnit == "fahrenheit") stringResource(R.string.temp_f_suffix) else stringResource(R.string.temp_c_suffix)
    val humiditySuffix = stringResource(R.string.humidity_suffix)
    val europeanAqi = parseCurrentEuropeanAqi(aqiJson)

    Card(
        modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
    ) {
        Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
                DetailItem(label = stringResource(R.string.wind_label), value = "$displayWind$windSuffix")
                DetailItem(label = stringResource(R.string.feels_like_label), value = "$displayFeelsLike$tempSuffix")
                DetailItem(label = stringResource(R.string.humidity_label), value = "$humidity$humiditySuffix")
            }
            if (weatherObj.has("daily")) {
                val daily = weatherObj.getJSONObject("daily")
                val sunrise = formatTime(daily.getJSONArray("sunrise").getString(0))
                val sunset = formatTime(daily.getJSONArray("sunset").getString(0))
                HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
                Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
                    DetailItem(label = stringResource(R.string.sunrise_label), value = sunrise)
                    DetailItem(label = stringResource(R.string.sunset_label), value = sunset)
                    if (europeanAqi != null) {
                        DetailItem(
                            label = stringResource(R.string.air_label),
                            value = europeanAqi.toInt().toString(),
                            valueColor = getEuropeanAqiColor(europeanAqi)
                        )
                    }
                }
            }
        }
    }
}

@Composable
fun DetailItem(label: String, value: String, valueColor: Color = MaterialTheme.colorScheme.onSurface) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text(label, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
        Text(value, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = valueColor)
    }
}

@Composable
fun HourlyForecastSection(list: List<HourlyForecast>, tempUnit: String) {
    val tempSuffix = if (tempUnit == "fahrenheit") stringResource(R.string.temp_f_suffix) else stringResource(R.string.temp_c_suffix)
    Column(modifier = Modifier.padding(horizontal = 16.dp)) {
        Text(stringResource(R.string.hourly_label), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
        Spacer(Modifier.height(8.dp))
        LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            items(list) { forecast ->
                val displayTemp = if (tempUnit == "fahrenheit") (forecast.temp * 9/5 + 32).toInt() else forecast.temp.toInt()
                Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHighest)) {
                    Column(modifier = Modifier.padding(12.dp), horizontalAlignment = Alignment.CenterHorizontally) {
                        Text(forecast.time, style = MaterialTheme.typography.labelMedium)
                        Icon(painter = painterResource(WeatherIconMapper.getWeatherIcon(forecast.weatherCode)), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.Unspecified)
                        Text("$displayTemp$tempSuffix", style = MaterialTheme.typography.titleSmall)
                        Text(
                            text = stringResource(R.string.rain_amount_label, formatRainAmount(forecast.precipitationMm)),
                            style = MaterialTheme.typography.labelSmall,
                            color = MaterialTheme.colorScheme.primary
                        )
                    }
                }
            }
        }
    }
}

private fun httpGet(urlString: String): String {
    val url = URL(urlString)
    val c = url.openConnection() as HttpURLConnection
    c.setRequestProperty("User-Agent", "GigaWeatherApp")
    c.connectTimeout = 12000
    c.readTimeout = 12000
    BufferedReader(InputStreamReader(c.inputStream, StandardCharsets.UTF_8)).use { reader ->
        val sb = StringBuilder()
        var line: String?
        while (reader.readLine().also { line = it } != null) sb.append(line)
        return sb.toString()
    }
}

fun hasPrecipitationData(json: String): Boolean {
    return try {
        val obj = JSONObject(json)
        val hourly = obj.optJSONObject("hourly")
        val daily = obj.optJSONObject("daily")

        hourly?.has("precipitation") == true && daily?.has("precipitation_sum") == true
    } catch (_: Exception) {
        false
    }
}

fun hasFutureForecastDays(json: String, requiredDays: Int): Boolean {
    return try {
        val obj = JSONObject(json)
        val daily = obj.optJSONObject("daily") ?: return false
        val times = daily.optJSONArray("time") ?: return false
        val todayKey = getTodayKey(getResponseTimeZone(obj))
        var futureDays = 0

        for (i in 0 until times.length()) {
            if (times.getString(i) > todayKey) {
                futureDays += 1
            }
        }

        futureDays >= requiredDays
    } catch (_: Exception) {
        false
    }
}

fun getResponseTimeZone(obj: JSONObject): TimeZone {
    val timeZoneName = obj.optString("timezone", "")
    if (timeZoneName.isBlank()) return TimeZone.getDefault()

    val timeZone = TimeZone.getTimeZone(timeZoneName)
    if (timeZone.id == "GMT" && timeZoneName != "GMT") return TimeZone.getDefault()

    return timeZone
}

fun getTodayKey(timeZone: TimeZone): String {
    return SimpleDateFormat("yyyy-MM-dd", Locale.US).apply {
        this.timeZone = timeZone
    }.format(Date())
}

fun getYesterdayDate(daysAgo: Int): String {
    val cal = Calendar.getInstance()
    cal.add(Calendar.DAY_OF_YEAR, -daysAgo)
    return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(cal.time)
}

data class DailyForecast(
    val date: String,
    val tempMax: Double,
    val tempMin: Double,
    val weatherCode: Int,
    val precipitationMm: Double = 0.0,
    val precipitationChance: Int = 0
)

data class HourlyForecast(
    val time: String,
    val temp: Double,
    val weatherCode: Int,
    val precipitationMm: Double = 0.0,
    val precipitationChance: Int = 0
)

fun parseForecastData(
    json: String,
    futureOnly: Boolean = false,
    limit: Int? = null
): List<DailyForecast> {
    val list = mutableListOf<DailyForecast>()
    try {
        val obj = JSONObject(json)
        val daily = obj.getJSONObject("daily")
        val times = daily.getJSONArray("time")
        val tMax = daily.getJSONArray("temperature_2m_max")
        val tMin = daily.getJSONArray("temperature_2m_min")
        val codes = daily.optJSONArray("weather_code") ?: daily.optJSONArray("weathercode")
        val precipitation = if (daily.has("precipitation_sum")) daily.getJSONArray("precipitation_sum") else null
        val precipitationChance = if (daily.has("precipitation_probability_max")) daily.getJSONArray("precipitation_probability_max") else null
        val responseTimeZone = getResponseTimeZone(obj)
        val todayKey = getTodayKey(responseTimeZone)
        val df = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).apply {
            timeZone = responseTimeZone
        }
        val outF = SimpleDateFormat("EEE, dd. MMM", Locale.getDefault()).apply {
            timeZone = responseTimeZone
        }
        for (i in 0 until times.length()) {
            val dateText = times.getString(i)
            if (futureOnly && dateText <= todayKey) continue

            val date = df.parse(dateText)
            list.add(
                DailyForecast(
                    date = outF.format(date ?: Date()),
                    tempMax = tMax.getDouble(i),
                    tempMin = tMin.getDouble(i),
                    weatherCode = codes?.getInt(i) ?: 0,
                    precipitationMm = precipitation?.optDouble(i, 0.0) ?: 0.0,
                    precipitationChance = precipitationChance?.optInt(i, 0) ?: 0
                )
            )

            if (limit != null && list.size >= limit) break
        }
    } catch (_: Exception) {}
    return list
}

fun parseHourlyForecastData(json: String): List<HourlyForecast> {
    val list = mutableListOf<HourlyForecast>()
    try {
        val hourly = JSONObject(json).getJSONObject("hourly")
        val times = hourly.getJSONArray("time")
        val temps = hourly.getJSONArray("temperature_2m")
        val codes = hourly.optJSONArray("weather_code") ?: hourly.getJSONArray("weathercode")
        val precipitation = if (hourly.has("precipitation")) hourly.getJSONArray("precipitation") else null
        val precipitationChance = if (hourly.has("precipitation_probability")) hourly.getJSONArray("precipitation_probability") else null
        val inF = SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.getDefault())
        val outF = SimpleDateFormat("HH:mm", Locale.getDefault())
        val now = Calendar.getInstance()
        for (i in 0 until times.length()) {
            val date = inF.parse(times.getString(i)) ?: continue
            if (date.after(now.time) && list.size < 24) {
                list.add(
                    HourlyForecast(
                        time = outF.format(date),
                        temp = temps.getDouble(i),
                        weatherCode = codes.getInt(i),
                        precipitationMm = precipitation?.optDouble(i, 0.0) ?: 0.0,
                        precipitationChance = precipitationChance?.optInt(i, 0) ?: 0
                    )
                )
            }
        }
    } catch (_: Exception) {}
    return list
}

fun parseWeatherApiData(json: String): Pair<List<DailyForecast>, List<HourlyForecast>> {
    val dailyList = mutableListOf<DailyForecast>()
    val hourlyList = mutableListOf<HourlyForecast>()
    try {
        val obj = JSONObject(json)
        val forecast = obj.getJSONObject("forecast").getJSONArray("forecastday")
        val outF = SimpleDateFormat("EEE, dd. MMM", Locale.getDefault())
        val df = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
        val todayKey = obj.optJSONObject("location")
            ?.optString("localtime", "")
            ?.takeIf { it.length >= 10 }
            ?.take(10)
            ?: SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
        
        for (i in 0 until forecast.length()) {
            val day = forecast.getJSONObject(i)
            val astro = day.getJSONObject("day")
            val dateText = day.getString("date")
            
            if (i == 0) {
                val hours = day.getJSONArray("hour")
                val now = System.currentTimeMillis()
                for (j in 0 until hours.length()) {
                    val h = hours.getJSONObject(j)
                    if (h.getLong("time_epoch") * 1000 > now && hourlyList.size < 24) {
                        val time = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(h.getLong("time_epoch") * 1000))
                        hourlyList.add(
                            HourlyForecast(
                                time = time,
                                temp = h.getDouble("temp_c"),
                                weatherCode = 0,
                                precipitationMm = h.optDouble("precip_mm", 0.0),
                                precipitationChance = h.optInt("chance_of_rain", 0)
                            )
                        )
                    }
                }
            }

            if (dateText <= todayKey) continue
            if (dailyList.size >= FUTURE_FORECAST_DAYS) continue

            val date = df.parse(dateText)
            dailyList.add(
                DailyForecast(
                    date = outF.format(date ?: Date()),
                    tempMax = astro.getDouble("maxtemp_c"),
                    tempMin = astro.getDouble("mintemp_c"),
                    weatherCode = 0,
                    precipitationMm = astro.optDouble("totalprecip_mm", 0.0),
                    precipitationChance = astro.optInt("daily_chance_of_rain", 0)
                )
            )
        }
    } catch (_: Exception) {}
    return dailyList to hourlyList
}

fun getCurrentHourIndex(timesArray: JSONArray): Int {
    val now = Calendar.getInstance()
    val currentHour = now.get(Calendar.HOUR_OF_DAY)
    for (i in 0 until timesArray.length()) {
        val date = SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.getDefault()).parse(timesArray.getString(i)) ?: continue
        val cal = Calendar.getInstance().apply { time = date }
        if (cal.get(Calendar.HOUR_OF_DAY) == currentHour && cal.get(Calendar.DAY_OF_YEAR) == now.get(Calendar.DAY_OF_YEAR)) return i
    }
    return 0
}

fun parseCurrentEuropeanAqi(aqiJson: String?): Double? {
    if (aqiJson == null) return null

    return try {
        val hourly = JSONObject(aqiJson).getJSONObject("hourly")
        val times = hourly.getJSONArray("time")
        val values = hourly.getJSONArray("european_aqi")
        val currentIndex = getCurrentHourIndex(times)

        values.optDouble(currentIndex).takeIf { !it.isNaN() }
    } catch (_: Exception) {
        null
    }
}

fun getEuropeanAqiColor(aqi: Double): Color {
    if (aqi <= 20.0) return Color(0xFF2E7D32)
    if (aqi <= 40.0) return Color(0xFF7CB342)
    if (aqi <= 60.0) return Color(0xFFF9A825)
    if (aqi <= 80.0) return Color(0xFFEF6C00)
    if (aqi <= 100.0) return Color(0xFFC62828)
    return Color(0xFF6A1B9A)
}

fun formatTime(timeString: String): String {
    return try {
        val date = SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.getDefault()).parse(timeString)
        SimpleDateFormat("HH:mm", Locale.getDefault()).format(date ?: Date())
    } catch (_: Exception) { timeString.takeLast(5) }
}

fun formatRainAmount(amountMm: Double): String {
    return if (amountMm < 0.1) {
        "0 mm"
    } else {
        String.format(Locale.getDefault(), "%.1f mm", amountMm)
    }
}