pragma Singleton

import QtQuick
import Quickshell
import ZShell
import ZShell.Config

Singleton {
	id: root

	readonly property var cachedCities: new Map()
	property var cc
	property string city
	readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
	readonly property string feelsLike: formatTemp(cc?.feelsLikeC)
	property list<var> forecast
	property list<var> hourlyForecast
	readonly property int humidity: cc?.humidity ?? 0
	readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
	property string loc
	readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
	readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
	readonly property string temp: formatTemp(cc?.tempC)
	readonly property real windSpeed: cc?.windSpeed ?? 0

	function fetchCityFromCoords(coords: string): void {
		if (cachedCities.has(coords)) {
			city = cachedCities.get(coords);
			return;
		}

		const [lat, lon] = coords.split(",").map(s => s.trim());
		const lang = Qt.locale().name.split("_")[0] || "en";

		const fallbackToBigDataCloud = () => {
			const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=${lang}`;
			Requests.get(fallbackUrl, text => {
				const geo = JSON.parse(text);
				const geoCity = geo.city || geo.locality;
				if (geoCity) {
					city = fixCityName(geoCity);
					cachedCities.set(coords, city);
				} else {
					city = "Unknown City";
				}
			});
		};

		const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson&accept-language=${lang}`;
		Requests.get(nominatimUrl, text => {
			const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
			if (geo) {
				const geoCity = geo.type === "city" ? geo.name : geo.city;
				if (geoCity) {
					city = fixCityName(geoCity);
					cachedCities.set(coords, city);
					return;
				}
			}
			fallbackToBigDataCloud();
		}, fallbackToBigDataCloud);
	}

	function fetchCoordsFromCity(cityName: string): void {
		const lang = Qt.locale().name.split("_")[0] || "en";
		const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=${lang}&format=json`;

		Requests.get(url, text => {
			const json = JSON.parse(text);
			if (json.results && json.results.length > 0) {
				const result = json.results[0];
				loc = result.latitude + "," + result.longitude;
				city = fixCityName(result.name);
			} else {
				loc = "";
				reload();
			}
		});
	}

	function fetchWeatherData(): void {
		const url = getWeatherUrl();
		if (url === "")
			return;

		Requests.get(url, text => {
			const json = JSON.parse(text);
			if (!json.current || !json.daily)
				return;

			cc = {
				weatherCode: json.current.weather_code,
				weatherDesc: getWeatherCondition(json.current.weather_code),
				tempC: json.current.temperature_2m,
				feelsLikeC: json.current.apparent_temperature,
				humidity: json.current.relative_humidity_2m,
				windSpeed: json.current.wind_speed_10m,
				isDay: json.current.is_day,
				sunrise: json.daily.sunrise[0].replace("T", " "),
				sunset: json.daily.sunset[0].replace("T", " ")
			};

			const forecastList = [];
			for (let i = 0; i < json.daily.time.length; i++)
				forecastList.push({
					date: json.daily.time[i].replace(/-/g, "/"),
					maxTempC: json.daily.temperature_2m_max[i],
					minTempC: json.daily.temperature_2m_min[i],
					weatherCode: json.daily.weather_code[i],
					icon: Icons.getWeatherIcon(json.daily.weather_code[i])
				});
			forecast = forecastList;

			const hourlyList = [];
			const now = new Date();
			for (let i = 0; i < json.hourly.time.length; i++) {
				const time = new Date(json.hourly.time[i].replace("T", " "));

				if (time < now)
					continue;

				hourlyList.push({
					timestamp: json.hourly.time[i],
					hour: time.getHours(),
					tempC: Math.round(json.hourly.temperature_2m[i]),
					precipChance: json.hourly.precipitation_probability[i],
					weatherCode: json.hourly.weather_code[i],
					icon: Icons.getWeatherIcon(json.hourly.weather_code[i])
				});
			}
			hourlyForecast = hourlyList;
		});
	}

	function fixCityName(cityName: string): string {
		if (!cityName)
			return "";
		const mapping = {
			// Polish
			"Poznan": "Poznań",
			"Wroclaw": "Wrocław",
			"Krakow": "Kraków",
			"Gdansk": "Gdańsk",
			"Lodz": "Łódź",
			"Rzeszow": "Rzeszów",
			"Torun": "Toruń",
			"Bialystok": "Białystok",
			"Czestochowa": "Częstochowa",
			"Plock": "Płock",
			"Ruda Slaska": "Ruda Śląska",
			"Dabrowa Gornicza": "Dąbrowa Górnicza",
			"Elblag": "Elbląg",
			"Gorzow Wielkopolski": "Gorzów Wielkopolski",
			"Zielona Gora": "Zielona Góra",
			"Slupsk": "Słupsk",

			// German
			"Munchen": "München",
			"Koln": "Köln",
			"Dusseldorf": "Düsseldorf",
			"Nurnberg": "Nürnberg",

			// French & Spanish & Portuguese
			"Sao Paulo": "São Paulo",
			"Montreal": "Montréal",
			"Quebec": "Québec",
			"Bogota": "Bogotá",
			"Medellin": "Medellín",
			"Cordoba": "Córdoba",

			// Turkish
			"Istanbul": "İstanbul",
			"Izmir": "İzmir",

			// Scandinavian & others
			"Malmo": "Malmö",
			"Goteborg": "Göteborg",
			"Zurich": "Zürich",
			"Geneve": "Genève"
		};
		return mapping[cityName] || cityName;
	}

	function formatTemp(temp: var): string {
		return Config.services.useFahrenheit ? `${temp !== undefined ? Math.round(toFahrenheit(temp)) : "--"}°F` : `${temp !== undefined ? Math.round(temp) : "--"}°C`;
	}

	function getWeatherCondition(code: string): string {
		const conditions = {
			"0": "Clear",
			"1": "Clear",
			"2": "Partly cloudy",
			"3": "Overcast",
			"45": "Fog",
			"48": "Fog",
			"51": "Drizzle",
			"53": "Drizzle",
			"55": "Drizzle",
			"56": "Freezing drizzle",
			"57": "Freezing drizzle",
			"61": "Light rain",
			"63": "Rain",
			"65": "Heavy rain",
			"66": "Light rain",
			"67": "Heavy rain",
			"71": "Light snow",
			"73": "Snow",
			"75": "Heavy snow",
			"77": "Snow",
			"80": "Light rain",
			"81": "Rain",
			"82": "Heavy rain",
			"85": "Light snow showers",
			"86": "Heavy snow showers",
			"95": "Thunderstorm",
			"96": "Thunderstorm with hail",
			"99": "Thunderstorm with hail"
		};
		return conditions[code] || "Unknown";
	}

	function getWeatherUrl(): string {
		if (!loc || loc.indexOf(",") === -1)
			return "";

		const [lat, lon] = loc.split(",").map(s => s.trim());
		const baseUrl = "https://api.open-meteo.com/v1/forecast";
		const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m,precipitation_probability", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];

		return baseUrl + "?" + params.join("&");
	}

	function reload(): void {
		const configLocation = Config.services.weatherLocation;

		if (configLocation) {
			if (configLocation.indexOf(",") !== -1 && !isNaN(parseFloat(configLocation.split(",")[0]))) {
				loc = configLocation;
				fetchCityFromCoords(configLocation);
			} else {
				fetchCoordsFromCity(configLocation);
			}
		} else if (!loc || timer.elapsed() > 900) {
			Requests.get("https://ipinfo.io/json", text => {
				const response = JSON.parse(text);
				if (response.loc) {
					loc = response.loc;
					city = response.city ?? "";
					timer.restart();
				}
			});
		}
	}

	function toFahrenheit(celcius: real): real {
		return celcius * 9 / 5 + 32;
	}

	onLocChanged: fetchWeatherData()

	Connections {
		function onWeatherLocationChanged(): void {
			root.reload();
		}

		target: Config.services
	}

	Timer {
		interval: 3600000 // 1 hour
		repeat: true
		running: true

		onTriggered: fetchWeatherData()
	}

	ElapsedTimer {
		id: timer
	}
}
