const Desklet = imports.ui.desklet;
const Settings = imports.ui.settings;
const Mainloop = imports.mainloop;
const St = imports.gi.St;
const Soup = imports.gi.Soup;
const GLib = imports.gi.GLib;
const ByteArray = imports.byteArray;

const UUID = "weather-nws@cinnamon";
const USER_AGENT = "cinnamon-weather-nws-desklet/1.0";
const NOMINATIM_URL = "https://nominatim.openstreetmap.org/search";
const NWS_BASE = "https://api.weather.gov";

// Detect Soup version (2 vs 3)
const SOUP_VERSION = Soup.get_major_version ? Soup.get_major_version() : 2;

// Map NWS icon URL keywords to system icon names
const ICON_MAP = {
    "skc":          "weather-clear",
    "few":          "weather-few-clouds",
    "sct":          "weather-few-clouds",
    "bkn":          "weather-overcast",
    "ovc":          "weather-overcast",
    "wind_skc":     "weather-clear",
    "wind_few":     "weather-few-clouds",
    "wind_sct":     "weather-few-clouds",
    "wind_bkn":     "weather-overcast",
    "wind_ovc":     "weather-overcast",
    "snow":         "weather-snow",
    "rain_snow":    "weather-snow",
    "rain_sleet":   "weather-showers",
    "snow_sleet":   "weather-snow",
    "fzra":         "weather-freezing-rain",
    "rain_fzra":    "weather-freezing-rain",
    "snow_fzra":    "weather-snow",
    "sleet":        "weather-showers",
    "rain":         "weather-showers",
    "rain_showers": "weather-showers-scattered",
    "rain_showers_hi": "weather-showers-scattered",
    "tsra":         "weather-storm",
    "tsra_sct":     "weather-storm",
    "tsra_hi":      "weather-storm",
    "tornado":      "weather-severe-alert",
    "hurricane":    "weather-severe-alert",
    "tropical_storm": "weather-severe-alert",
    "dust":         "weather-fog",
    "smoke":        "weather-fog",
    "haze":         "weather-fog",
    "hot":          "weather-clear",
    "cold":         "weather-clear",
    "blizzard":     "weather-snow",
    "fog":          "weather-fog"
};

function _getIconName(nwsIconUrl, isDaytime) {
    // NWS icon URLs look like: https://api.weather.gov/icons/land/day/skc?size=medium
    // Extract the condition keyword from the path
    if (!nwsIconUrl) return "weather-clear-symbolic";

    let parts = nwsIconUrl.split("/");
    // Find the keyword after "day" or "night"
    for (let i = 0; i < parts.length; i++) {
        if (parts[i] === "day" || parts[i] === "night") {
            if (i + 1 < parts.length) {
                let keyword = parts[i + 1].split("?")[0].split(",")[0];
                let base = ICON_MAP[keyword] || "weather-clear";
                return base + "-symbolic";
            }
        }
    }
    return "weather-clear-symbolic";
}

class WeatherDesklet extends Desklet.Desklet {
    constructor(metadata, deskletId) {
        super(metadata, deskletId);

        this._httpSession = new Soup.Session();
        if (SOUP_VERSION >= 3) {
            this._httpSession.set_user_agent(USER_AGENT);
        } else {
            this._httpSession.user_agent = USER_AGENT;
        }

        // Cached grid info (only re-fetched when zip changes)
        this._gridOffice = null;
        this._gridX = null;
        this._gridY = null;
        this._locationName = null;
        this._cachedZip = null;

        this._timeoutId = null;

        // Bind settings
        this.settings = new Settings.DeskletSettings(this, UUID, deskletId);
        this.settings.bindProperty(
            Settings.BindingDirection.IN,
            "zip-code", "zipCode",
            this._onZipChanged, null
        );
        this.settings.bindProperty(
            Settings.BindingDirection.IN,
            "refresh-interval", "refreshInterval",
            this._onRefreshIntervalChanged, null
        );
        this.settings.bindProperty(
            Settings.BindingDirection.IN,
            "temperature-unit", "temperatureUnit",
            this._onUnitChanged, null
        );
        this.settings.bindProperty(
            Settings.BindingDirection.IN,
            "font-size", "fontSize",
            this._onFontSizeChanged, null
        );
        this.settings.bindProperty(
            Settings.BindingDirection.IN,
            "desklet-width", "deskletWidth",
            this._onDisplayChanged, null
        );

        this._setupLayout();
        this._applyDisplay();
        this._refresh();
    }

    _setupLayout() {
        this._root = new St.BoxLayout({
            vertical: true,
            style_class: "weather-root"
        });

        // Location label
        this._locationLabel = new St.Label({
            text: "Loading...",
            style_class: "weather-location"
        });
        this._root.add_child(this._locationLabel);

        // Current weather row: icon + temp
        let currentRow = new St.BoxLayout({
            vertical: false,
            style_class: "weather-current-row"
        });
        this._currentIcon = new St.Icon({
            icon_name: "weather-clear-symbolic",
            icon_size: 48,
            style_class: "weather-current-icon"
        });
        this._currentTemp = new St.Label({
            text: "--",
            style_class: "weather-current-temp"
        });
        currentRow.add_child(this._currentIcon);
        currentRow.add_child(this._currentTemp);
        this._root.add_child(currentRow);

        // Current description
        this._currentDesc = new St.Label({
            text: "",
            style_class: "weather-current-desc"
        });
        this._root.add_child(this._currentDesc);

        // Separator
        let sep = new St.BoxLayout({ style_class: "weather-separator" });
        this._root.add_child(sep);

        // Forecast rows (5 periods)
        this._forecastRows = [];
        for (let i = 0; i < 5; i++) {
            let row = new St.BoxLayout({
                vertical: false,
                style_class: "weather-forecast-row"
            });
            let nameLabel = new St.Label({
                text: "",
                style_class: "weather-forecast-name"
            });
            let tempLabel = new St.Label({
                text: "",
                style_class: "weather-forecast-temp"
            });
            let descLabel = new St.Label({
                text: "",
                style_class: "weather-forecast-desc"
            });
            row.add_child(nameLabel);
            row.add_child(tempLabel);
            row.add_child(descLabel);
            this._root.add_child(row);
            this._forecastRows.push({
                name: nameLabel,
                temp: tempLabel,
                desc: descLabel
            });
        }

        // Status label (for errors)
        this._statusLabel = new St.Label({
            text: "",
            style_class: "weather-status"
        });
        this._root.add_child(this._statusLabel);

        // Bottom bar: status spacer + reload button aligned right
        let bottomRow = new St.BoxLayout({
            vertical: false,
            style_class: "weather-bottom-row"
        });
        let spacer = new St.Bin({ x_expand: true });
        this._reloadBtn = new St.Button({ style_class: "weather-reload-btn" });
        let reloadIcon = new St.Icon({
            icon_name: "view-refresh-symbolic",
            style_class: "weather-reload-icon"
        });
        this._reloadBtn.set_child(reloadIcon);
        this._reloadBtn.connect("clicked", () => {
            this._cachedZip = null;
            this._gridOffice = null;
            this._refresh();
        });
        bottomRow.add_child(spacer);
        bottomRow.add_child(this._reloadBtn);
        this._root.add_child(bottomRow);

        this.setContent(this._root);
        this.setHeader("NWS Weather");
    }

    _setStatus(msg) {
        this._statusLabel.set_text(msg);
    }

    _clearStatus() {
        this._statusLabel.set_text("");
    }

    // --- HTTP helper ---
    _httpGet(url, callback) {
        let message = Soup.Message.new("GET", url);

        if (SOUP_VERSION >= 3) {
            this._httpSession.send_and_read_async(
                message, GLib.PRIORITY_DEFAULT, null,
                (session, result) => {
                    try {
                        let bytes = session.send_and_read_finish(result);
                        let status = message.get_status();
                        if (status === Soup.Status.OK || status === 200) {
                            let text = ByteArray.toString(bytes.get_data());
                            callback(JSON.parse(text), null);
                        } else {
                            callback(null, "HTTP " + status);
                        }
                    } catch (e) {
                        callback(null, e.message || String(e));
                    }
                }
            );
        } else {
            this._httpSession.queue_message(message, (_session, msg) => {
                try {
                    if (msg.status_code === 200) {
                        let text = message.response_body.data;
                        callback(JSON.parse(text), null);
                    } else {
                        callback(null, "HTTP " + msg.status_code);
                    }
                } catch (e) {
                    callback(null, e.message || String(e));
                }
            });
        }
    }

    // --- Refresh logic ---
    _refresh() {
        this._cancelTimer();

        let zip = (this.zipCode || "").trim();
        if (!/^\d{5}$/.test(zip)) {
            this._setStatus("Enter a valid 5-digit zip code");
            this._scheduleNext();
            return;
        }

        // If zip changed, re-geocode; otherwise go straight to forecast
        if (zip !== this._cachedZip) {
            this._setStatus("Looking up location...");
            this._geocodeZip(zip);
        } else {
            this._fetchForecast();
        }
    }

    _geocodeZip(zip) {
        let url = NOMINATIM_URL +
            "?postalcode=" + encodeURIComponent(zip) +
            "&country=US&format=json&limit=1";

        this._httpGet(url, (data, err) => {
            if (err || !data || data.length === 0) {
                this._setStatus("Could not find zip code: " + zip);
                this._scheduleNext();
                return;
            }

            let lat = parseFloat(data[0].lat).toFixed(4);
            let lon = parseFloat(data[0].lon).toFixed(4);
            this._lookupGrid(zip, lat, lon);
        });
    }

    _lookupGrid(zip, lat, lon) {
        let url = NWS_BASE + "/points/" + lat + "," + lon;

        this._httpGet(url, (data, err) => {
            if (err || !data || !data.properties) {
                this._setStatus("NWS lookup failed for coordinates");
                this._scheduleNext();
                return;
            }

            let props = data.properties;
            this._gridOffice = props.gridId;
            this._gridX = props.gridX;
            this._gridY = props.gridY;
            this._locationName = (props.relativeLocation && props.relativeLocation.properties)
                ? props.relativeLocation.properties.city + ", " +
                  props.relativeLocation.properties.state
                : "Zip " + zip;
            this._cachedZip = zip;

            this._fetchForecast();
        });
    }

    _fetchForecast() {
        if (!this._gridOffice) {
            this._setStatus("No grid data available");
            this._scheduleNext();
            return;
        }

        this._setStatus("Updating...");

        let baseUrl = NWS_BASE + "/gridpoints/" +
            this._gridOffice + "/" +
            this._gridX + "," + this._gridY;

        // Fetch both the hourly forecast (current actual temp) and the
        // regular forecast (named periods for the upcoming days) in parallel.
        let hourlyDone = false;
        let forecastDone = false;
        let hourlyPeriods = null;
        let forecastPeriods = null;

        let tryRender = () => {
            if (!hourlyDone || !forecastDone) return;
            if (!forecastPeriods) {
                this._setStatus("Forecast unavailable — retrying later");
                this._scheduleNext();
                return;
            }
            this._updateDisplay(hourlyPeriods, forecastPeriods);
            this._clearStatus();
            this._scheduleNext();
        };

        this._httpGet(baseUrl + "/forecast/hourly", (data, err) => {
            hourlyDone = true;
            if (!err && data && data.properties && data.properties.periods) {
                hourlyPeriods = data.properties.periods;
            }
            tryRender();
        });

        this._httpGet(baseUrl + "/forecast", (data, err) => {
            forecastDone = true;
            if (!err && data && data.properties && data.properties.periods) {
                forecastPeriods = data.properties.periods;
            }
            tryRender();
        });
    }

    // hourlyPeriods: from /forecast/hourly — used for actual current temp/conditions
    // forecastPeriods: from /forecast — used for the named upcoming periods list
    _updateDisplay(hourlyPeriods, forecastPeriods) {
        if (!forecastPeriods || forecastPeriods.length === 0) return;

        this._locationLabel.set_text(this._locationName || "");

        // Current conditions: use hourly[0] for the real right-now temp.
        // Fall back to forecastPeriods[0] if hourly unavailable.
        let current = (hourlyPeriods && hourlyPeriods.length > 0)
            ? hourlyPeriods[0]
            : forecastPeriods[0];

        this._currentTemp.set_text(this._formatTemp(current.temperature, current.temperatureUnit));
        this._currentDesc.set_text(current.shortForecast || "");
        this._currentIcon.set_icon_name(_getIconName(current.icon, current.isDaytime));

        // Upcoming periods from the regular (named) forecast
        for (let i = 0; i < 5; i++) {
            let period = forecastPeriods[i];
            if (period) {
                this._forecastRows[i].name.set_text(period.name || "");
                this._forecastRows[i].temp.set_text(
                    this._formatTemp(period.temperature, period.temperatureUnit)
                );
                this._forecastRows[i].desc.set_text(period.shortForecast || "");
            } else {
                this._forecastRows[i].name.set_text("");
                this._forecastRows[i].temp.set_text("");
                this._forecastRows[i].desc.set_text("");
            }
        }
    }

    _formatTemp(value, sourceUnit) {
        if (value === null || value === undefined) return "--";

        let unit = this.temperatureUnit || "F";

        if (unit === "C" && sourceUnit === "F") {
            value = Math.round((value - 32) * 5 / 9);
        } else if (unit === "F" && sourceUnit === "C") {
            value = Math.round(value * 9 / 5 + 32);
        } else {
            value = Math.round(value);
        }

        return value + "\u00B0" + unit;
    }

    // --- Timer management ---
    _scheduleNext() {
        this._cancelTimer();
        let intervalSeconds = (this.refreshInterval || 30) * 60;
        this._timeoutId = Mainloop.timeout_add_seconds(intervalSeconds, () => {
            this._refresh();
            return false; // one-shot; _refresh re-arms via _scheduleNext
        });
    }

    _cancelTimer() {
        if (this._timeoutId) {
            Mainloop.source_remove(this._timeoutId);
            this._timeoutId = null;
        }
    }

    // --- Settings callbacks ---
    _onZipChanged() {
        // Clear cache so next refresh re-geocodes
        this._cachedZip = null;
        this._gridOffice = null;
        this._refresh();
    }

    _onRefreshIntervalChanged() {
        // Restart timer with new interval
        this._refresh();
    }

    _onUnitChanged() {
        if (this._cachedZip) {
            this._fetchForecast();
        }
    }

    _onFontSizeChanged() {
        this._applyDisplay();
    }

    _onDisplayChanged() {
        this._applyDisplay();
    }

    _applyDisplay() {
        let base = this.fontSize || 14;
        let big = Math.round(base * 2.5);
        let med = Math.round(base * 0.93);
        let small = Math.round(base * 0.86);
        let width = this.deskletWidth || 280;
        let pad = Math.round(base * 0.86);

        this._root.set_style("padding: " + pad + "px; width: " + width + "px;");
        this._locationLabel.set_style("font-size: " + med + "px;");
        this._currentTemp.set_style("font-size: " + big + "px;");
        this._currentDesc.set_style("font-size: " + base + "px;");
        this._currentIcon.set_icon_size(Math.round(base * 3.4));
        this._statusLabel.set_style("font-size: " + base + "px;");
        let btnSize = Math.round(base * 1.6);
        this._reloadBtn.set_style("width: " + btnSize + "px; height: " + btnSize + "px; border-radius: 4px;");
        this._reloadBtn.get_child().set_icon_size(Math.round(base * 1.1));

        for (let i = 0; i < this._forecastRows.length; i++) {
            this._forecastRows[i].name.set_style("font-size: " + small + "px; min-width: " + Math.round(base * 7) + "px;");
            this._forecastRows[i].temp.set_style("font-size: " + small + "px; min-width: " + Math.round(base * 3.5) + "px;");
            this._forecastRows[i].desc.set_style("font-size: " + small + "px;");
        }
    }

    // --- Cleanup ---
    on_desklet_removed() {
        this._cancelTimer();
    }
}

function main(metadata, deskletId) {
    return new WeatherDesklet(metadata, deskletId);
}
