pragma Singleton

import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons

// Location and weather service with decoupled geocoding and weather fetching.
Singleton {
  id: root

  property string locationFile: Quickshell.env("NOCTALIA_WEATHER_FILE") || (Settings.cacheDir + "location.json")
  property int weatherUpdateFrequency: 30 * 60
  property bool isFetchingWeather: false

  // Talia weather
  readonly property int taliaMascotWeatherMonth: 3
  readonly property int taliaMascotWeatherDay: 1

  readonly property bool taliaWeatherMascotDayActive: {
    const d = Time.now;
    return d.getMonth() === root.taliaMascotWeatherMonth && d.getDate() === root.taliaMascotWeatherDay;
  }

  readonly property bool taliaWeatherMascotActive: taliaWeatherMascotDayActive || Settings.data.location.weatherTaliaMascotAlways

  readonly property alias data: adapter

  // True when the user has set a location name or enabled auto-locate
  readonly property bool locationConfigured: Settings.data.location.name !== "" || Settings.data.location.autoLocate

  // Stable UI properties - only updated when location is successfully geocoded
  property bool coordinatesReady: false
  property string stableLatitude: ""
  property string stableLongitude: ""
  property string stableName: ""

  FileView {
    id: locationFileView
    path: locationFile
    printErrors: false
    onAdapterUpdated: saveTimer.start()
    onLoaded: {
      Logger.d("Location", "Loaded cached data");
      if (adapter.latitude !== "" && adapter.longitude !== "" && adapter.weatherLastFetch > 0) {
        root.stableLatitude = adapter.latitude;
        root.stableLongitude = adapter.longitude;
        root.stableName = adapter.name;
        root.coordinatesReady = true;
        Logger.i("Location", "Coordinates ready");
      }
      update();
    }
    onLoadFailed: function (error) {
      update();
    }

    JsonAdapter {
      id: adapter
      property string latitude: ""
      property string longitude: ""
      property string name: ""
      property int weatherLastFetch: 0
      property var weather: null
    }
  }

  // Formatted coordinates for UI display
  readonly property string displayCoordinates: {
    if (!root.coordinatesReady || root.stableLatitude === "" || root.stableLongitude === "") {
      return "";
    }
    const lat = parseFloat(root.stableLatitude).toFixed(4);
    const lon = parseFloat(root.stableLongitude).toFixed(4);
    return `${lat}, ${lon}`;
  }

  // Auto-geolocate timer - periodically updates location via IP geolocation
  Timer {
    id: autoLocateTimer
    interval: 30 * 60 * 1000
    running: Settings.data.location.autoLocate
    repeat: true
    triggeredOnStart: true
    onTriggered: root.geolocateAndApply()
  }

  // Update timer runs when weather is enabled or location-based scheduling is active
  Timer {
    id: updateTimer
    interval: 20 * 1000
    running: Settings.data.location.weatherEnabled || Settings.data.colorSchemes.schedulingMode == "location"
    repeat: true
    onTriggered: {
      update();
    }
  }

  Timer {
    id: saveTimer
    running: false
    interval: 1000
    onTriggered: locationFileView.writeAdapter()
  }

  function init() {
    Logger.i("Location", "Service started");
  }

  function resetWeather() {
    Logger.i("Location", "Resetting location and weather data");

    root.coordinatesReady = false;
    root.stableLatitude = "";
    root.stableLongitude = "";
    root.stableName = "";

    adapter.latitude = "";
    adapter.longitude = "";
    adapter.name = "";
    adapter.weatherLastFetch = 0;
    adapter.weather = null;
    isFetchingWeather = false;
    update();
  }

  // Main update function - geocodes location if needed, then fetches weather if enabled
  function update() {
    updateLocation();

    if (Settings.data.location.weatherEnabled) {
      updateWeatherData();
    }
  }

  // Runs independently of weather toggle
  function updateLocation() {
    const locationChanged = adapter.name !== Settings.data.location.name;
    const needsGeocoding = (adapter.latitude === "") || (adapter.longitude === "") || locationChanged;

    if (!needsGeocoding) {
      return;
    }

    if (isFetchingWeather) {
      return;
    }

    isFetchingWeather = true;

    if (locationChanged) {
      root.coordinatesReady = false;
      Logger.d("Location", "Location changed from", adapter.name, "to", Settings.data.location.name);
    }

    geocodeLocation(Settings.data.location.name, function (latitude, longitude, name, country) {
      adapter.name = Settings.data.location.name;
      adapter.latitude = latitude.toString();
      adapter.longitude = longitude.toString();
      root.stableLatitude = adapter.latitude;
      root.stableLongitude = adapter.longitude;
      root.stableName = `${name}, ${country}`;
      root.coordinatesReady = true;

      isFetchingWeather = false;
      Logger.i("Location", `Geocoded ${Settings.data.location.name}: ${root.stableLatitude}, ${root.stableLongitude}`);

      if (locationChanged) {
        adapter.weatherLastFetch = 0;
        updateWeatherData();
      }
    }, errorCallback);
  }

  // Fetch weather data if enabled and coordinates are available
  function updateWeatherData() {
    if (!Settings.data.location.weatherEnabled) {
      return;
    }

    if (isFetchingWeather) {
      return;
    }

    if (adapter.latitude === "" || adapter.longitude === "") {
      Logger.w("Location", "Cannot fetch weather without coordinates");
      return;
    }
    const needsWeatherUpdate = (adapter.weatherLastFetch === "") || (adapter.weather === null) || (Time.timestamp >= adapter.weatherLastFetch + weatherUpdateFrequency);

    if (needsWeatherUpdate) {
      isFetchingWeather = true;
      fetchWeatherData(adapter.latitude, adapter.longitude, errorCallback);
    }
  }

  // Query geocoding API to convert location name to coordinates
  function geocodeLocation(locationName, callback, errorCallback) {
    if (locationName === "") {
      isFetchingWeather = false;
      return;
    }

    Logger.d("Location", "Geocoding location name");
    var geoUrl = "https://api.noctalia.dev/geocode?city=" + encodeURIComponent(locationName);
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
      if (xhr.readyState === XMLHttpRequest.DONE) {
        if (xhr.status === 200) {
          try {
            var geoData = JSON.parse(xhr.responseText);
            if (geoData.lat != null) {
              callback(geoData.lat, geoData.lng, geoData.name, geoData.country);
            } else {
              errorCallback("Location", "could not resolve location name");
            }
          } catch (e) {
            errorCallback("Location", "Failed to parse geocoding data: " + e);
          }
        } else {
          errorCallback("Location", `Geocoding error: ${xhr.status} ${xhr.responseText}`);
        }
      }
    };
    xhr.open("GET", geoUrl);
    xhr.send();
  }

  // Fetch weather data from Open-Meteo API
  function fetchWeatherData(latitude, longitude, errorCallback) {
    Logger.d("Location", "Fetching weather from api.open-meteo.com");
    var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "&current_weather=true&current=relativehumidity_2m,surface_pressure,is_day&daily=temperature_2m_max,temperature_2m_min,weathercode,sunset,sunrise&timezone=auto";
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
      if (xhr.readyState === XMLHttpRequest.DONE) {
        if (xhr.status === 200) {
          try {
            var weatherData = JSON.parse(xhr.responseText);
            //console.log(JSON.stringify(weatherData))

            // Save core data
            data.weather = weatherData;
            data.weatherLastFetch = Time.timestamp;

            // Update stable display values only when complete and successful
            root.stableLatitude = data.latitude = weatherData.latitude.toString();
            root.stableLongitude = data.longitude = weatherData.longitude.toString();
            root.coordinatesReady = true;

            isFetchingWeather = false;
            Logger.d("Location", "Cached weather to disk - stable coordinates updated");
          } catch (e) {
            errorCallback("Location", "Failed to parse weather data");
          }
        } else {
          errorCallback("Location", `Weather error: ${xhr.status} ${xhr.responseText}`);
        }
      }
    };
    xhr.open("GET", url);
    xhr.send();
  }

  // Geolocate via IP address using the Noctalia API
  function geolocate(callback, errorCallback) {
    Logger.d("Location", "Geolocating via IP");
    var url = "https://api.noctalia.dev/geolocate";
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
      if (xhr.readyState === XMLHttpRequest.DONE) {
        if (xhr.status === 200) {
          try {
            var data = JSON.parse(xhr.responseText);
            if (data.lat != null) {
              callback(data.lat, data.lng, data.city, data.country);
            } else {
              errorCallback("Location", "Geolocate: no coordinates returned");
            }
          } catch (e) {
            errorCallback("Location", "Failed to parse geolocate data: " + e);
          }
        } else {
          errorCallback("Location", `Geolocate error: ${xhr.status} ${xhr.responseText}`);
        }
      }
    };
    xhr.open("GET", url);
    xhr.send();
  }

  // Geolocate via IP and apply the result as the current location
  function geolocateAndApply() {
    if (isFetchingWeather) {
      Logger.w("Location", "Geolocate skipped, fetch already in progress");
      return;
    }
    geolocate(function (lat, lng, city, country) {
      Logger.i("Location", "Geolocated to", city + ",", country + ":", lat + "," + lng);

      const locationChanged = adapter.name !== city;
      Settings.data.location.name = city;
      adapter.name = city;
      adapter.latitude = lat.toString();
      adapter.longitude = lng.toString();
      root.stableLatitude = adapter.latitude;
      root.stableLongitude = adapter.longitude;
      root.stableName = `${city}, ${country}`;
      root.coordinatesReady = true;

      if (locationChanged) {
        adapter.weatherLastFetch = 0;
        adapter.weather = null;
      }

      if (Settings.data.location.weatherEnabled) {
        updateWeatherData();
      }
    }, errorCallback);
  }

  // --------------------------------
  function errorCallback(module, message) {
    Logger.w(module, message);
    isFetchingWeather = false;
  }

  // --------------------------------
  function weatherSymbolFromCode(code) {
    var isDay = data.weather ? data.weather.current_weather.is_day : true;
    if (code === 0)
      return isDay ? "weather-sun" : "weather-moon";
    if (code === 1 || code === 2)
      return isDay ? "weather-cloud-sun" : "weather-moon-stars";
    if (code === 3)
      return "weather-cloud";
    if (code >= 45 && code <= 48)
      return "weather-cloud-haze";
    if (code >= 51 && code <= 67)
      return "weather-cloud-rain";
    if (code >= 80 && code <= 82)
      return "weather-cloud-rain";
    if (code >= 71 && code <= 77)
      return "weather-cloud-snow";
    if (code >= 71 && code <= 77)
      return "weather-cloud-snow";
    if (code >= 85 && code <= 86)
      return "weather-cloud-snow";
    if (code >= 95 && code <= 99)
      return "weather-cloud-lightning";
    return "weather-cloud";
  }

  // --------------------------------
  function taliaWeatherImageFromCode(code) {
    var isDay = data.weather ? data.weather.current_weather.is_day : true;
    if (code >= 40 && code <= 49)
      return Quickshell.shellDir + "/Assets/Talia/TaliaDazed.png";
    if (code >= 95 && code <= 99)
      return Quickshell.shellDir + "/Assets/Talia/TaliaFear.png";
    var wet = (code >= 51 && code <= 67) || (code >= 80 && code <= 82) || (code >= 71 && code <= 77) || (code >= 85 && code <= 86);
    if (wet)
      return Quickshell.shellDir + "/Assets/Talia/TaliaSob.png";
    if ((code === 0 || code === 1 || code === 2) && isDay === false)
      return Quickshell.shellDir + "/Assets/Talia/TaliaVampire.png";
    if ((code === 0 && isDay === true) || code === 1 || code === 2)
      return Quickshell.shellDir + "/Assets/Talia/TaliaParty.png";
    return Quickshell.shellDir + "/Assets/Talia/TaliaBlank.png";
  }

  // --------------------------------
  function weatherDescriptionFromCode(code) {
    if (code === 0)
      return "Clear sky";
    if (code === 1)
      return "Mainly clear";
    if (code === 2)
      return "Partly cloudy";
    if (code === 3)
      return "Overcast";
    if (code === 45 || code === 48)
      return "Fog";
    if (code >= 51 && code <= 67)
      return "Drizzle";
    if (code >= 71 && code <= 77)
      return "Snow";
    if (code >= 80 && code <= 82)
      return "Rain showers";
    if (code >= 95 && code <= 99)
      return "Thunderstorm";
    return "Unknown";
  }

  // --------------------------------
  function celsiusToFahrenheit(celsius) {
    return 32 + celsius * 1.8;
  }
}
