<?php

function weatherRound1(float $value): float
{
    return round($value, 1);
}

function weatherWmo(int $code): array
{
    static $codes = [
        0 => ['label' => 'Clear sky', 'icon' => '☀️'],
        1 => ['label' => 'Mainly clear', 'icon' => '🌤️'],
        2 => ['label' => 'Partly cloudy', 'icon' => '⛅'],
        3 => ['label' => 'Overcast', 'icon' => '☁️'],
        45 => ['label' => 'Foggy', 'icon' => '🌫️'],
        48 => ['label' => 'Icy fog', 'icon' => '🌫️'],
        51 => ['label' => 'Light drizzle', 'icon' => '🌦️'],
        53 => ['label' => 'Drizzle', 'icon' => '🌦️'],
        55 => ['label' => 'Heavy drizzle', 'icon' => '🌧️'],
        61 => ['label' => 'Light rain', 'icon' => '🌧️'],
        63 => ['label' => 'Rain', 'icon' => '🌧️'],
        65 => ['label' => 'Heavy rain', 'icon' => '🌧️'],
        71 => ['label' => 'Light snow', 'icon' => '🌨️'],
        73 => ['label' => 'Snow', 'icon' => '❄️'],
        75 => ['label' => 'Heavy snow', 'icon' => '❄️'],
        77 => ['label' => 'Snow grains', 'icon' => '🌨️'],
        80 => ['label' => 'Light showers', 'icon' => '🌦️'],
        81 => ['label' => 'Showers', 'icon' => '🌧️'],
        82 => ['label' => 'Heavy showers', 'icon' => '🌧️'],
        85 => ['label' => 'Snow showers', 'icon' => '🌨️'],
        86 => ['label' => 'Heavy snow showers', 'icon' => '❄️'],
        95 => ['label' => 'Thunderstorm', 'icon' => '⛈️'],
        96 => ['label' => 'Thunderstorm w/ hail', 'icon' => '⛈️'],
        99 => ['label' => 'Thunderstorm w/ heavy hail', 'icon' => '⛈️'],
    ];

    return $codes[$code] ?? ['label' => 'Unknown', 'icon' => '🌡️'];
}

function weatherWindDirection(float $degrees): string
{
    $directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
    return $directions[((int)round($degrees / 45)) % 8];
}

function weatherDewPoint(float $tempC, float $humidity): float
{
    $a = 17.625;
    $b = 243.04;
    $safeHumidity = max(0.1, min(100, $humidity));
    $gamma = log($safeHumidity / 100) + ($a * $tempC) / ($b + $tempC);
    return weatherRound1(($b * $gamma) / ($a - $gamma));
}

function weatherWetBulb(float $tempC, float $humidity): float
{
    return $tempC * atan(0.151977 * sqrt($humidity + 8.313659))
        + atan($tempC + $humidity)
        - atan($humidity - 1.676331)
        + 0.00391838 * pow($humidity, 1.5) * atan(0.023101 * $humidity)
        - 4.686035;
}

function weatherDeltaT(float $tempC, float $humidity): float
{
    return weatherRound1($tempC - weatherWetBulb($tempC, $humidity));
}

function weatherSprayRating(
    float $deltaT,
    float $wind,
    float $tempC,
    float $precipMm,
    float $precipProbability
): array {
    if ($precipMm > 0) return ['rating' => 'Poor — raining', 'cls' => 'spray-poor'];
    if ($wind > 24) return ['rating' => 'Poor — wind too high', 'cls' => 'spray-poor'];
    if ($deltaT < 1) return ['rating' => 'Poor — inversion risk', 'cls' => 'spray-poor'];
    if ($deltaT > 12) return ['rating' => 'Poor — too hot/dry', 'cls' => 'spray-poor'];
    if ($tempC < 5) return ['rating' => 'Poor — too cold', 'cls' => 'spray-poor'];
    if ($deltaT < 2) return ['rating' => 'Marginal — low Delta T', 'cls' => 'spray-marginal'];
    if ($deltaT > 10) return ['rating' => 'Marginal — high Delta T', 'cls' => 'spray-marginal'];
    if ($wind > 15) return ['rating' => 'Marginal — windy', 'cls' => 'spray-marginal'];
    if ($wind < 3) return ['rating' => 'Marginal — calm/inversion risk', 'cls' => 'spray-marginal'];
    if ($tempC < 10) return ['rating' => 'Marginal — cool', 'cls' => 'spray-marginal'];
    if ($precipProbability >= 50) return ['rating' => 'Marginal — rain likely', 'cls' => 'spray-marginal'];
    if ($deltaT >= 2 && $deltaT <= 8 && $wind >= 3 && $wind <= 15 && $tempC >= 10 && $precipProbability < 30) {
        return ['rating' => 'Ideal', 'cls' => 'spray-ideal'];
    }
    return ['rating' => 'Good', 'cls' => 'spray-good'];
}

function weatherFetch(string $url, int $timeout, array $headers = []): ?string
{
    $headerLines = array_merge([
        'User-Agent: dispelled.ca weather dashboard (local farm conditions)',
        'Accept: application/json,text/html,application/xhtml+xml',
    ], $headers);
    $context = stream_context_create([
        'http' => [
            'method' => 'GET',
            'timeout' => $timeout,
            'ignore_errors' => true,
            'header' => implode("\r\n", $headerLines),
        ],
    ]);
    $body = @file_get_contents($url, false, $context);
    if ($body === false) return null;

    $status = 0;
    foreach ($http_response_header ?? [] as $line) {
        if (preg_match('#^HTTP/\S+\s+(\d{3})#', $line, $match)) {
            $status = (int)$match[1];
        }
    }
    return $status >= 200 && $status < 300 ? $body : null;
}

function weatherJsonObjectAt(string $text, int $start): ?array
{
    $depth = 0;
    $inString = false;
    $escaped = false;
    $length = strlen($text);

    for ($i = $start; $i < $length; $i++) {
        $char = $text[$i];
        if ($inString) {
            if ($escaped) {
                $escaped = false;
            } elseif ($char === '\\') {
                $escaped = true;
            } elseif ($char === '"') {
                $inString = false;
            }
            continue;
        }
        if ($char === '"') {
            $inString = true;
        } elseif ($char === '{') {
            $depth++;
        } elseif ($char === '}') {
            $depth--;
            if ($depth === 0) {
                $decoded = json_decode(substr($text, $start, $i - $start + 1), true);
                return is_array($decoded) ? $decoded : null;
            }
        }
    }
    return null;
}

function weatherArdillRecordToObservation(array $record): ?array
{
    if (!isset($record['obsTimeLocal'], $record['obsTimeUtc'], $record['imperial'])) return null;

    $observed = strtotime((string)$record['obsTimeUtc']);
    if ($observed === false || time() - $observed > 90 * 60 || !is_array($record['imperial'])) return null;
    $values = $record['imperial'];
    $number = static fn($value): ?float => is_numeric($value) ? (float)$value : null;
    $fToC = static fn(?float $value): ?float => $value === null ? null : ($value - 32) * 5 / 9;
    $miToKm = static fn(?float $value): ?float => $value === null ? null : $value * 1.609344;
    $inToMm = static fn(?float $value): ?float => $value === null ? null : $value * 25.4;
    $inHgToHpa = static fn(?float $value): ?float => $value === null ? null : $value * 33.8638866667;

    $pressureLow = $number($values['pressureMin'] ?? $values['pressure'] ?? null);
    $pressureHigh = $number($values['pressureMax'] ?? $values['pressure'] ?? null);
    $averagePressure = ($pressureLow === null && $pressureHigh === null)
        ? null
        : (($pressureLow ?? $pressureHigh) + ($pressureHigh ?? $pressureLow)) / 2;

    return [
        'observedAt' => (string)$record['obsTimeLocal'],
        'tempC' => $fToC($number($values['tempAvg'] ?? $values['temp'] ?? null)),
        'humidity' => $number($record['humidityAvg'] ?? $record['humidity'] ?? null),
        'dewPointC' => $fToC($number($values['dewptAvg'] ?? $values['dewpt'] ?? null)),
        'windKmh' => $miToKm($number($values['windspeedAvg'] ?? $values['windSpeed'] ?? null)),
        'windGustKmh' => $miToKm($number($values['windgustAvg'] ?? $values['windGust'] ?? null)),
        'windDirection' => $number($record['winddirAvg'] ?? $record['winddir'] ?? null),
        'pressureHpa' => $inHgToHpa($averagePressure),
        'precipRateMm' => $inToMm($number($values['precipRate'] ?? null)),
        'precipTodayMm' => $inToMm($number($values['precipTotal'] ?? null)),
        'uv' => $number($record['uvHigh'] ?? $record['uv'] ?? null),
    ];
}

function weatherArdillObservation(): ?array
{
    $stationId = 'IARDIL4';
    $body = weatherFetch("https://www.wunderground.com/dashboard/pws/{$stationId}", 10);
    if ($body === null) return null;

    $marker = '{"stationID":"' . $stationId . '"';
    $start = strrpos($body, $marker);
    if ($start !== false) {
        $record = weatherJsonObjectAt($body, $start);
        $observation = is_array($record) ? weatherArdillRecordToObservation($record) : null;
        if ($observation !== null) return $observation;
    }

    if (!preg_match('/apiKey=([A-Za-z0-9]+)/', $body, $match)) return null;
    $params = http_build_query([
        'apiKey' => $match[1],
        'stationId' => $stationId,
        'numericPrecision' => 'decimal',
        'format' => 'json',
        'units' => 'e',
    ]);
    $currentBody = weatherFetch("https://api.weather.com/v2/pws/observations/current?{$params}", 10);
    if ($currentBody === null) return null;

    $current = json_decode($currentBody, true);
    $record = $current['observations'][0] ?? null;
    return is_array($record) ? weatherArdillRecordToObservation($record) : null;
}

function getFullWeather(): ?array
{
    $cacheFile = CACHE_DIR . '/weather_full.json';
    $cached = null;
    if (is_file($cacheFile)) {
        $decoded = json_decode((string)file_get_contents($cacheFile), true);
        if (is_array($decoded) && empty($decoded['error'])) {
            $cached = $decoded;
            if (time() - filemtime($cacheFile) < 900) return $cached;
        }
    }

    $latitude = '49.9007';
    $longitude = '-105.79557';
    $timezone = new DateTimeZone('America/Regina');
    $today = new DateTimeImmutable('today', $timezone);
    $yesterday = $today->modify('-1 day');
    $seasonStart = $today->format('Y') . '-05-01';
    $fetchHistory = $yesterday->format('Y-m-d') >= $seasonStart;

    $weatherParams = http_build_query([
        'latitude' => $latitude,
        'longitude' => $longitude,
        'current' => 'temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m,precipitation,surface_pressure,cloud_cover,visibility,uv_index,is_day',
        'hourly' => 'temperature_2m,relative_humidity_2m,dew_point_2m,precipitation_probability,precipitation,snowfall,weather_code,wind_speed_10m,wind_direction_10m,visibility,uv_index,shortwave_radiation,cape,lifted_index,freezing_level_height,soil_temperature_0cm,soil_temperature_6cm,soil_temperature_18cm,soil_moisture_0_to_1cm,soil_moisture_3_to_9cm,soil_moisture_9_to_27cm',
        'daily' => 'weather_code,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,sunrise,sunset,uv_index_max,precipitation_sum,precipitation_hours,precipitation_probability_max,snowfall_sum,wind_speed_10m_max,wind_gusts_10m_max,wind_direction_10m_dominant,et0_fao_evapotranspiration,sunshine_duration,shortwave_radiation_sum',
        'temperature_unit' => 'celsius',
        'wind_speed_unit' => 'kmh',
        'timezone' => 'America/Regina',
        'forecast_days' => 7,
        'past_days' => 7,
    ]);
    $weatherBody = weatherFetch("https://api.open-meteo.com/v1/forecast?{$weatherParams}", 10);
    if ($weatherBody === null) return $cached;
    $wx = json_decode($weatherBody, true);
    if (!is_array($wx) || empty($wx['current']) || empty($wx['daily']) || empty($wx['hourly'])) return $cached;

    $history = null;
    if ($fetchHistory) {
        $historyParams = http_build_query([
            'latitude' => $latitude,
            'longitude' => $longitude,
            'start_date' => $seasonStart,
            'end_date' => $yesterday->format('Y-m-d'),
            'daily' => 'temperature_2m_max,temperature_2m_min',
            'timezone' => 'America/Regina',
        ]);
        $historyBody = weatherFetch("https://archive-api.open-meteo.com/v1/archive?{$historyParams}", 15);
        if ($historyBody !== null) $history = json_decode($historyBody, true);
    }
    $station = weatherArdillObservation();

    $seasonGdd = 0.0;
    $seasonGddDays = 0;
    if (is_array($history['daily']['time'] ?? null)) {
        foreach ($history['daily']['time'] as $index => $_date) {
            $high = $history['daily']['temperature_2m_max'][$index] ?? null;
            $low = $history['daily']['temperature_2m_min'][$index] ?? null;
            if ($high !== null && $low !== null) {
                $seasonGdd += max(0, ((float)$high + (float)$low) / 2 - 10);
                $seasonGddDays++;
            }
        }
    }

    $current = $wx['current'];
    $daily = $wx['daily'];
    $hourlyData = $wx['hourly'];
    $currentWmo = weatherWmo((int)$current['weather_code']);
    $currentTemp = (float)($station['tempC'] ?? $current['temperature_2m']);
    $currentHumidity = (float)($station['humidity'] ?? $current['relative_humidity_2m']);
    $currentWind = (float)($station['windKmh'] ?? $current['wind_speed_10m']);
    $currentWindDirection = (float)($station['windDirection'] ?? $current['wind_direction_10m']);
    $currentPrecip = (float)($station['precipRateMm'] ?? $current['precipitation'] ?? 0);
    $todayString = substr((string)$current['time'], 0, 10);

    $precipHistory = [];
    foreach ($daily['time'] as $index => $date) {
        if ($date < $todayString) {
            $precipHistory[] = ['date' => $date, 'precip' => weatherRound1((float)($daily['precipitation_sum'][$index] ?? 0))];
        }
    }
    $sevenDayPastTotal = weatherRound1(array_sum(array_column($precipHistory, 'precip')));
    $daysSinceRain = 0;
    for ($index = count($precipHistory) - 1; $index >= 0; $index--) {
        if ($precipHistory[$index]['precip'] >= 1) break;
        $daysSinceRain++;
    }

    $currentDeltaT = weatherDeltaT($currentTemp, $currentHumidity);
    $currentSpray = weatherSprayRating($currentDeltaT, round($currentWind), $currentTemp, $currentPrecip, 0);

    $startIndex = 0;
    foreach ($hourlyData['time'] as $index => $time) {
        if ($time >= $current['time']) {
            $startIndex = $index;
            break;
        }
    }
    $hourly = [];
    $end = min($startIndex + 24, count($hourlyData['time']));
    for ($index = $startIndex; $index < $end; $index++) {
        $wmo = weatherWmo((int)$hourlyData['weather_code'][$index]);
        $temp = (float)$hourlyData['temperature_2m'][$index];
        $humidity = (float)($hourlyData['relative_humidity_2m'][$index] ?? 50);
        $deltaT = weatherDeltaT($temp, $humidity);
        $spray = weatherSprayRating(
            $deltaT,
            round((float)($hourlyData['wind_speed_10m'][$index] ?? 0)),
            $temp,
            (float)($hourlyData['precipitation'][$index] ?? 0),
            (float)($hourlyData['precipitation_probability'][$index] ?? 0)
        );
        $hourly[] = [
            'time' => substr((string)$hourlyData['time'][$index], 11, 5),
            'temp' => (int)round($temp),
            'rh' => (int)round($humidity),
            'dew_point' => weatherRound1((float)($hourlyData['dew_point_2m'][$index] ?? 0)),
            'delta_t' => $deltaT,
            'spray_rating' => $spray['rating'],
            'spray_cls' => $spray['cls'],
            'precip_prob' => $hourlyData['precipitation_probability'][$index] ?? 0,
            'precip' => weatherRound1((float)($hourlyData['precipitation'][$index] ?? 0)),
            'snowfall' => weatherRound1((float)($hourlyData['snowfall'][$index] ?? 0)),
            'wind' => (int)round((float)($hourlyData['wind_speed_10m'][$index] ?? 0)),
            'wind_dir' => weatherWindDirection((float)($hourlyData['wind_direction_10m'][$index] ?? 0)),
            'icon' => $wmo['icon'],
            'uv' => weatherRound1((float)($hourlyData['uv_index'][$index] ?? 0)),
            'shortwave' => (int)round((float)($hourlyData['shortwave_radiation'][$index] ?? 0)),
            'cape' => (int)round((float)($hourlyData['cape'][$index] ?? 0)),
            'lifted_index' => weatherRound1((float)($hourlyData['lifted_index'][$index] ?? 0)),
            'freezing_level' => isset($hourlyData['freezing_level_height'][$index]) ? (int)round((float)$hourlyData['freezing_level_height'][$index]) : null,
            'soil_temp_0' => weatherRound1((float)($hourlyData['soil_temperature_0cm'][$index] ?? 0)),
            'soil_temp_6' => weatherRound1((float)($hourlyData['soil_temperature_6cm'][$index] ?? 0)),
            'soil_temp_18' => weatherRound1((float)($hourlyData['soil_temperature_18cm'][$index] ?? 0)),
            'soil_moist_0' => weatherRound1((float)($hourlyData['soil_moisture_0_to_1cm'][$index] ?? 0) * 100),
            'soil_moist_3' => weatherRound1((float)($hourlyData['soil_moisture_3_to_9cm'][$index] ?? 0) * 100),
            'soil_moist_9' => weatherRound1((float)($hourlyData['soil_moisture_9_to_27cm'][$index] ?? 0) * 100),
            'visibility' => round((float)($hourlyData['visibility'][$index] ?? 0) / 100) / 10,
        ];
    }

    $forecast = [];
    $gddCumulative = 0.0;
    foreach ($daily['time'] as $index => $date) {
        if ($date < $todayString) continue;
        $wmo = weatherWmo((int)$daily['weather_code'][$index]);
        $low = (int)round((float)$daily['temperature_2m_min'][$index]);
        $high = (int)round((float)$daily['temperature_2m_max'][$index]);
        $gdd = max(0, weatherRound1(($high + $low) / 2 - 10));
        $gddCumulative += $gdd;
        $forecast[] = [
            'date' => $date,
            'high' => $high,
            'low' => $low,
            'feels_high' => (int)round((float)$daily['apparent_temperature_max'][$index]),
            'feels_low' => (int)round((float)$daily['apparent_temperature_min'][$index]),
            'label' => $wmo['label'],
            'icon' => $wmo['icon'],
            'precip_prob' => $daily['precipitation_probability_max'][$index] ?? 0,
            'precip_sum' => weatherRound1((float)($daily['precipitation_sum'][$index] ?? 0)),
            'precip_hrs' => $daily['precipitation_hours'][$index] ?? 0,
            'snowfall_sum' => weatherRound1((float)($daily['snowfall_sum'][$index] ?? 0)),
            'wind_max' => (int)round((float)($daily['wind_speed_10m_max'][$index] ?? 0)),
            'gust_max' => (int)round((float)($daily['wind_gusts_10m_max'][$index] ?? 0)),
            'wind_dir' => weatherWindDirection((float)($daily['wind_direction_10m_dominant'][$index] ?? 0)),
            'uv_max' => weatherRound1((float)($daily['uv_index_max'][$index] ?? 0)),
            'sunrise' => !empty($daily['sunrise'][$index]) ? substr((string)$daily['sunrise'][$index], 11, 5) : '—',
            'sunset' => !empty($daily['sunset'][$index]) ? substr((string)$daily['sunset'][$index], 11, 5) : '—',
            'et0' => weatherRound1((float)($daily['et0_fao_evapotranspiration'][$index] ?? 0)),
            'sunshine_hrs' => weatherRound1((float)($daily['sunshine_duration'][$index] ?? 0) / 3600),
            'solar_sum' => weatherRound1((float)($daily['shortwave_radiation_sum'][$index] ?? 0)),
            'gdd' => $gdd,
            'gdd_cum' => weatherRound1($gddCumulative),
            'frost' => $low <= 2,
            'freeze' => $low <= 0,
        ];
    }
    if ($forecast) {
        $seasonGdd += $forecast[0]['gdd'];
        $seasonGddDays++;
    }

    $data = [
        'location' => 'Ardill, SK',
        'temp' => (int)round($currentTemp),
        'feelsLike' => (int)round((float)$current['apparent_temperature']),
        'dewPoint' => isset($station['dewPointC']) ? weatherRound1((float)$station['dewPointC']) : weatherDewPoint($currentTemp, $currentHumidity),
        'deltaT' => $currentDeltaT,
        'spray' => $currentSpray,
        'humidity' => (int)round($currentHumidity),
        'windSpeed' => (int)round($currentWind),
        'windDir' => weatherWindDirection($currentWindDirection),
        'windGusts' => (int)round((float)($station['windGustKmh'] ?? $current['wind_gusts_10m'] ?? 0)),
        'precip' => weatherRound1($currentPrecip),
        'precipLabel' => isset($station['precipRateMm']) ? 'Precip rate' : 'Precip now',
        'precipUnit' => isset($station['precipRateMm']) ? 'mm/h' : 'mm',
        'precipToday' => isset($station['precipTodayMm']) ? weatherRound1((float)$station['precipTodayMm']) : null,
        'pressure' => (int)round((float)($station['pressureHpa'] ?? $current['surface_pressure'] ?? 0)),
        'cloud' => $current['cloud_cover'] ?? 0,
        'visibility' => round((float)($current['visibility'] ?? 0) / 100) / 10,
        'uv' => weatherRound1((float)($station['uv'] ?? $current['uv_index'] ?? 0)),
        'label' => $currentWmo['label'],
        'icon' => $currentWmo['icon'],
        'time' => $station['observedAt'] ?? $current['time'],
        'sources' => [
            'current' => $station
                ? ['label' => 'Ardill weather station IARDIL4', 'observedAt' => $station['observedAt'], 'url' => 'https://www.wunderground.com/dashboard/pws/IARDIL4']
                : ['label' => 'Open-Meteo location model for Ardill', 'observedAt' => $current['time'], 'url' => null],
            'enrichment' => 'Open-Meteo forecast, soil, history, and unavailable local fields for Ardill',
        ],
        'precipHistory' => $precipHistory,
        'sevenDayPastTotal' => $sevenDayPastTotal,
        'daysSinceRain' => $daysSinceRain,
        'forecast' => $forecast,
        'hourly' => $hourly,
        'seasonGdd' => weatherRound1($seasonGdd),
        'seasonGddDays' => $seasonGddDays,
        'seasonGddStart' => $seasonStart,
    ];

    if (!is_dir(CACHE_DIR)) mkdir(CACHE_DIR, 0755, true);
    file_put_contents($cacheFile, json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), LOCK_EX);
    return $data;
}