import {queryHourly, getCoords, queryCurrent} from './sensors.mjs';
import {getCelestialCurrent} from './celestial.mjs';
import {localDateStr} from './config.mjs';
import {frostRisk} from './openweather.mjs';

export let lastForecast = { ts: 0, slots: [], summary: null }
export const forecastTTL = 5 * 60 * 60_000

function estimatePrecipitation(precipChance, clouds) {
	if (precipChance < 30) return 0;
	const intensity = (precipChance / 100) * clouds;
	if (intensity > 0.7) return Math.round(intensity * 15 * 10) / 10;
	if (intensity > 0.4) return Math.round(intensity * 6  * 10) / 10;
	return Math.round(intensity * 2 * 10) / 10;
}

function estimateUV(sunElevation, cloudFraction) {
	if (sunElevation <= 0) return 0;
	const clearSkyUV = Math.sin((sunElevation * Math.PI) / 180) * 12;
	return Math.round(clearSkyUV * (1 - cloudFraction * 0.75) * 10) / 10;
}

function linearTrend(values) {
	const n = values.length;
	if (n < 2) return 0;
	const xMean = (n - 1) / 2;
	const yMean = values.reduce((a, b) => a + b, 0) / n;
	const num = values.reduce((s, v, i) => s + (i - xMean) * (v - yMean), 0);
	const den = values.reduce((s, _, i) => s + (i - xMean) ** 2, 0);
	return den === 0 ? 0 : num / den;
}

function dewPoint(tempC, humidity) {
	const a = 17.27, b = 237.7;
	const gamma = (a * tempC) / (b + tempC) + Math.log(humidity / 100);
	return (b * gamma) / (a - gamma);
}

function heatIndex(tempC, humidity) {
	const T = tempC * 9 / 5 + 32;
	if (T < 80) return tempC;
	const HI =
		-42.379 + 2.04901523 * T + 10.14333127 * humidity
		- 0.22475541 * T * humidity - 0.00683783 * T * T
		- 0.05481717 * humidity * humidity + 0.00122874 * T * T * humidity
		+ 0.00085282 * T * humidity * humidity - 0.00000199 * T * T * humidity * humidity;
	return (HI - 32) * 5 / 9;
}

function vaporPressureDeficit(tempC, humidity) {
	const svp = 0.6108 * Math.exp((17.27 * tempC) / (tempC + 237.3));
	return Math.round(svp * (1 - humidity / 100) * 1000) / 1000;
}

function absoluteHumidity(tempC, humidity) {
	const svp = 0.6108 * Math.exp((17.27 * tempC) / (tempC + 237.3));
	return Math.round((humidity / 100 * svp * 2165) / (tempC + 273.15) * 1000) / 1000;
}

function pressureRule(trend3h, currentHpa) {
	if (trend3h < -2.0) return { label: 'Storm likely',     precipitation_chance: 0.85 };
	if (trend3h < -0.8) return { label: 'Rain likely',      precipitation_chance: 0.65 };
	if (trend3h < -0.3) return { label: 'Cloudy',           precipitation_chance: 0.35 };
	if (trend3h >  1.5) return { label: 'Clearing',         precipitation_chance: 0.05 };
	if (trend3h >  0.3) return { label: 'Improving',        precipitation_chance: 0.10 };
	if (currentHpa < 1000) return { label: 'Unsettled',     precipitation_chance: 0.30 };
	if (currentHpa < 1013) return { label: 'Partly cloudy', precipitation_chance: 0.15 };
	return { label: 'Fair', precipitation_chance: 0.05 };
}

function solarTempDelta(sunElevation, cloudFraction) {
	if (sunElevation <= 0) return 0;
	return Math.sin((sunElevation * Math.PI) / 180) * (1 - cloudFraction) * 6;
}

function nocturnalCooling(cloudFraction) {
	return (1 - cloudFraction) * 1.5;
}

function forecastHumidity(absHumidity, forecastTempC) {
	const svp = 0.6108 * Math.exp((17.27 * forecastTempC) / (forecastTempC + 237.3));
	const rh = (absHumidity * (forecastTempC + 273.15)) / (svp * 2165) * 100;
	return Math.min(100, Math.max(0, Math.round(rh * 10) / 10));
}

function estimateClouds(humidity, pressureTrend) {
	let base = humidity / 100 * 0.8;
	if (pressureTrend < -0.3) base = Math.min(1, base + 0.2);
	if (pressureTrend >  0.3) base = Math.max(0, base - 0.15);
	return Math.round(base * 100) / 100;
}

function forecastWind(currentWind, pressureTrend) {
	const boost = pressureTrend < -1 ? 1.3 : pressureTrend < -0.5 ? 1.1 : 1.0;
	return Math.round(currentWind * boost * 10) / 10;
}

export async function get24HourForecast(currentSensors) {
	const now    = new Date();
	const sixAgo = new Date(now - 6 * 3600 * 1000);

	const coords  = await getCoords();
	const history = await queryHourly(sixAgo, now);

	const pressures  = history.map(r => r.pressure_hpa).filter(Number.isFinite);
	const temps      = history.map(r => r.temperature).filter(Number.isFinite);
	const humidities = history.map(r => r.humidity).filter(Number.isFinite);
	const winds      = history.map(r => r.wind_speed).filter(Number.isFinite);

	const pressureTrend = linearTrend(pressures);
	const tempTrend     = linearTrend(temps);
	const humidityTrend = linearTrend(humidities);
	const pressure3h    = pressureTrend * 3;

	const seedTemp     = currentSensors.temperature  ?? temps.at(-1)      ?? 20;
	const seedHumidity = currentSensors.humidity     ?? humidities.at(-1) ?? 60;
	const seedPressure = currentSensors.pressure_hpa ?? pressures.at(-1)  ?? 1013;
	const seedWind     = currentSensors.wind_speed   ?? winds.at(-1)      ?? 0;
	const seedAbsHum   = currentSensors.humidity_abs ?? absoluteHumidity(seedTemp, seedHumidity);

	const weather = pressureRule(pressure3h, seedPressure);

	let runningAccumulation = 0;
	const slots = Array.from({ length: 24 }, (_, i) => {
		const time = new Date(now.getTime() + (i + 1) * 3600 * 1000);
		time.setMinutes(0, 0, 0);

		const pressureDamping      = Math.exp(-i * 0.08);
		const pressure             = seedPressure + pressureTrend * (i + 1) * pressureDamping;
		const pressure_slp         = Math.round((pressure + (coords.altitude ?? 0) / 8.5) * 100) / 100;
		const clouds               = estimateClouds(seedHumidity + humidityTrend * i, pressureTrend);
		const bgTemp               = seedTemp + tempTrend * (i + 1);
		const temperature          = Math.round(bgTemp * 10) / 10;
		const humidity             = forecastHumidity(seedAbsHum, temperature);
		const wind_speed           = forecastWind(seedWind, pressureTrend);
		const precipitation_chance = Math.round(weather.precipitation_chance * (1 - i * 0.02) * 100);
		const raining              = precipitation_chance > 60 ? 'True' : 'False';
		const precipitation        = estimatePrecipitation(precipitation_chance, clouds);
		runningAccumulation        = Math.round((runningAccumulation + precipitation) * 100) / 100;

		const celestial = getCelestialCurrent(coords.latitude, coords.longitude, time);
		const uv_index  = estimateUV(celestial.sun_elevation ?? 0, clouds);

		return {
			time,
			temperature,
			humidity,
			humidity_abs:           absoluteHumidity(temperature, humidity),
			dew_point:              Math.round(dewPoint(temperature, humidity) * 100) / 100,
			heat_index:             Math.round(heatIndex(temperature, humidity) * 100) / 100,
			vapor_pressure_deficit: vaporPressureDeficit(temperature, humidity),
			pressure_hpa:           Math.round(pressure * 100) / 100,
			pressure_slp,
			pressure_rate:          Math.round(pressureTrend * 100) / 100,
			storm_trend:            pressure3h < -2 ? 1 : pressure3h > 1.5 ? -1 : 0,
			wind_speed,
			wind_gusts:             Math.round(wind_speed * 1.4 * 10) / 10,
			wind_direction:         currentSensors.wind_direction ?? 0,
			clouds,
			raining,
			precipitation,
			precipitation_chance,
			accumulation:           runningAccumulation,
			uv_index,
			uv_dose:                0,
			solar_wm2:              0,
			daily_light_integral:   0,
			lux:                    0,
			visibility:             50,
			...celestial,
		};
	});

	for (const slot of slots) {
		if (slot.sunrise && slot.sunset) {
			const t    = slot.time instanceof Date    ? slot.time.getTime()    : new Date(slot.time).getTime();
			const rise = slot.sunrise instanceof Date ? slot.sunrise.getTime() : new Date(slot.sunrise).getTime();
			const set  = slot.sunset  instanceof Date ? slot.sunset.getTime()  : new Date(slot.sunset).getTime();
			slot.daytime = t >= rise && t < set;
		} else {
			slot.daytime = (slot.sun_elevation ?? 0) > 0;
		}

		const solar = solarTempDelta(slot.sun_elevation ?? 0, slot.clouds);
		const cool  = slot.daytime ? 0 : nocturnalCooling(slot.clouds);
		slot.temperature            = Math.round((slot.temperature + solar - cool) * 10) / 10;
		slot.humidity               = forecastHumidity(seedAbsHum, slot.temperature);
		slot.dew_point              = Math.round(dewPoint(slot.temperature, slot.humidity) * 100) / 100;
		slot.heat_index             = Math.round(heatIndex(slot.temperature, slot.humidity) * 100) / 100;
		slot.vapor_pressure_deficit = vaporPressureDeficit(slot.temperature, slot.humidity);
		slot.humidity_abs           = absoluteHumidity(slot.temperature, slot.humidity);
		slot.frost_risk             = frostRisk(slot.temperature, slot.dew_point, slot.humidity)

		slot.solar_wm2            = slot.sun_elevation > 0
			? Math.round(Math.sin((slot.sun_elevation * Math.PI) / 180) * 1000 * (1 - slot.clouds * 0.75) * 100) / 100
			: 0;
		slot.daily_light_integral = Math.round(slot.solar_wm2 * 3600 / 1_000_000 * 4.57 * 100) / 100;
		slot.lux                  = slot.solar_wm2 > 0 ? Math.round(slot.solar_wm2 * 120) : 0;
		slot.visibility           = slot.clouds > 0.85 ? 10 : 50;

		Object.assign(slot, getWeatherCondition(slot));
	}

	return slots;
}

function groupByLocalDay(slots) {
	const days = {}
	for (const slot of slots) {
		const key = localDateStr(new Date(slot.time))
		if (!days[key]) days[key] = []
		days[key].push(slot)
	}
	return days
}

function summarise(slots) {
	if (!slots.length) return null

	const daytime = slots.filter(s => s.daytime)
	const source  = daytime.length ? daytime : slots

	const avg    = (arr) => arr.length ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length * 100) / 100 : null
	const peak   = (arr) => arr.length ? Math.max(...arr) : null
	const fin    = (key) => source.map(s => s[key]).filter(Number.isFinite)
	const finAll = (key) => slots.map(s => s[key]).filter(Number.isFinite)

	const labelCount = {}
	for (const s of source) labelCount[s.label] = (labelCount[s.label] || 0) + 1
	const label    = Object.entries(labelCount).sort((a, b) => b[1] - a[1])[0][0]
	const dominant = source.find(s => s.label === label)

	return {
		time:                   slots[0].time,
		label:                  dominant.label,
		icon:                   dominant.icon,
		temperature:            peak(fin('temperature')),
		temperature_max:        peak(finAll('temperature')),
		temperature_min:        Math.min(...finAll('temperature')),
		humidity:               avg(fin('humidity')),
		humidity_abs:           avg(fin('humidity_abs')),
		dew_point:              avg(fin('dew_point')),
		heat_index:             peak(fin('heat_index')),
		vapor_pressure_deficit: avg(fin('vapor_pressure_deficit')),
		pressure_hpa:           avg(fin('pressure_hpa')),
		pressure_slp:           avg(fin('pressure_slp')),
		pressure_rate:          avg(fin('pressure_rate')),
		storm_trend:            slots.at(-1)?.storm_trend ?? 0,
		wind_speed:             peak(fin('wind_speed')),
		wind_gusts:             peak(fin('wind_gusts')),
		wind_direction:         avg(fin('wind_direction')),
		clouds:                 avg(fin('clouds')),
		raining:                peak(fin('precipitation_chance')) > 60 ? 'True' : 'False',
		precipitation:          avg(fin('precipitation')),
		precipitation_chance:   Math.round(peak(fin('precipitation_chance'))),
		accumulation:           finAll('precipitation').reduce((a, b) => a + b, 0),
		frost_risk:             slots.find(s => s.frost_risk !== 'None')?.frost_risk ?? slots.at(-1)?.frost_risk ?? 'None',
		uv_index:               peak(fin('uv_index')),
		uv_dose:                finAll('uv_dose').reduce((a, b) => a + b, 0),
		solar_wm2:              avg(fin('solar_wm2')),
		daily_light_integral:   Math.round(finAll('daily_light_integral').reduce((a, b) => a + b, 0) * 100) / 100,
		lux:                    peak(fin('lux')),
		visibility:             avg(fin('visibility')),
		sunrise:                slots.find(s => s.sunrise)?.sunrise   ?? null,
		sunset:                 slots.find(s => s.sunset)?.sunset     ?? null,
		daylight:               slots.find(s => s.daylight)?.daylight ?? null,
		moon_phase:             dominant.moon_phase                   ?? null,
		moon_illumination:      dominant.moon_illumination            ?? null,
		moon_elevation:         dominant.moon_elevation               ?? null,
		moon_azimuth:           dominant.moon_azimuth                 ?? null,
		moon_new:               dominant.moon_new                     ?? null,
		moon_full:              dominant.moon_full                    ?? null,
		moonrise:               slots.find(s => s.moonrise)?.moonrise ?? null,
		moonset:                slots.find(s => s.moonset)?.moonset   ?? null,
	}
}

export async function getForecast() {
	if (Date.now() - lastForecast.ts < forecastTTL) return lastForecast;
	const sensors = await queryCurrent().catch(() => null)
	if (!sensors) return lastForecast
	const slots = await get24HourForecast(sensors).catch(() => null)
	if (!slots) return lastForecast

	const days       = groupByLocalDay(slots)
	const todayKey   = localDateStr(new Date())
	const todaySlots = days[todayKey] ?? slots
	lastForecast = { ts: Date.now(), slots, summary: summarise(todaySlots) }
	return lastForecast
}

export function getWeatherCondition(data) {
	if(data.lightning_rate > 0 && data.raining === 'True') {
		return {label: 'Thunderstorm', icon: '11d'};
	}
	if(data.raining === 'True') {
		if(data.precipitation > 7.6) return {label: 'Heavy Rain',    icon: '10d'};
		if(data.precipitation > 2.5) return {label: 'Moderate Rain', icon: '10d'};
		return {label: 'Light Rain', icon: '09d'};
	}
	if(data.visibility < 10) return {label: 'Mist', icon: '50d'};

	const dayNight = data.daytime ? 'd' : 'n';
	const c = data.clouds > 1 ? data.clouds / 100 : data.clouds;
	if(c > 0.85) return {label: 'Overcast Clouds',  icon: `04${dayNight}`};
	if(c > 0.50) return {label: 'Broken Clouds',    icon: `04${dayNight}`};
	if(c > 0.25) return {label: 'Scattered Clouds', icon: `03${dayNight}`};
	if(c > 0.10) return {label: 'Few Clouds',       icon: `02${dayNight}`};
	return {label: 'Clear Sky', icon: `01${dayNight}`};
}
