import GObject from "gi://GObject";
import GLib from "gi://GLib";
import Soup from "gi://Soup";

import * as Main from "resource:///org/gnome/shell/ui/main.js";
import * as QuickSettings from "resource:///org/gnome/shell/ui/quickSettings.js";
import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";

function clamp01(x) {
    return Math.max(0, Math.min(1, x));
}

function jsonRpc(method, params = {}) {
    return JSON.stringify({ jsonrpc: "2.0", id: 1, method, params });
}

// Soup 3: callback-based async -> Promise wrapper
async function postJson(session, url, bodyStr) {
    const msg = Soup.Message.new("POST", url);
    msg.request_headers.append("Content-Type", "application/json");
    msg.set_request_body_from_bytes(
        "application/json",
        new GLib.Bytes(bodyStr),
    );

    const bytes = await new Promise((resolve, reject) => {
        session.send_and_read_async(
            msg,
            GLib.PRIORITY_DEFAULT,
            null,
            (sess, res) => {
                try {
                    resolve(sess.send_and_read_finish(res));
                } catch (e) {
                    reject(e);
                }
            },
        );
    });

    const text = new TextDecoder().decode(bytes.get_data());
    return JSON.parse(text);
}

function connectedClients(group) {
    return (group?.clients ?? []).filter((c) => c?.connected);
}

function connectedClientIds(group) {
    return connectedClients(group)
        .map((c) => c.id)
        .filter(Boolean);
}

function avgConnectedVolumePercent(group) {
    const percents = connectedClients(group)
        .map((c) => c?.config?.volume?.percent)
        .filter((p) => typeof p === "number");

    if (!percents.length) return null;

    return percents.reduce((a, b) => a + b, 0) / percents.length;
}

function groupLabel(group) {
    const base =
        group?.name && group.name.trim()
            ? group.name.trim()
            : (group?.stream_id ?? "Group");

    const connected = connectedClients(group).length;
    const total = group?.clients?.length ?? 0;

    return `${base} (${connected}/${total})`;
}

const SnapcastSlider = GObject.registerClass(
    class SnapcastSlider extends QuickSettings.QuickSlider {
        constructor(extension) {
            super({
                title: "Snapcast",
                iconName: "audio-volume-high-symbolic",
            });

            this._ext = extension;
            this._settings = extension.getSettings();

            // --- Soup session with timeouts (seconds) ---
            this._session = new Soup.Session({
                timeout: 5, // total request timeout
                idle_timeout: 5, // idle timeout
                user_agent: "Snapshell/1",
            });

            this._pollId = 0;
            this._debounceId = 0;
            this._sliderChangedId = 0;
            this._settingsChangedId = 0;

            this._isUpdatingFromServer = false;
            this._groupClientIds = [];

            this._reloadSettings();

            this._settingsChangedId = this._settings.connect("changed", () => {
                this._reloadSettings();
                this._restartPolling();
                this._pollOnce().catch(logError);
            });

            this._sliderChangedId = this.slider.connect("notify::value", () => {
                if (this._isUpdatingFromServer) return;
                this._debouncedSetVolume();
            });

            this._restartPolling();
            this._pollOnce().catch(logError);
        }

        _reloadSettings() {
            this._url = this._settings.get_string("server-url").trim();
            this._groupId = this._settings.get_string("group-id").trim();
            this._pollSeconds = this._settings.get_uint("poll-seconds");

            if (!this._pollSeconds || this._pollSeconds < 2)
                this._pollSeconds = 5;
        }

        _restartPolling() {
            if (this._pollId) {
                GLib.source_remove(this._pollId);
                this._pollId = 0;
            }

            this._pollId = GLib.timeout_add_seconds(
                GLib.PRIORITY_DEFAULT,
                this._pollSeconds,
                () => {
                    this._pollOnce().catch(logError);
                    return GLib.SOURCE_CONTINUE;
                },
            );
        }

        _debouncedSetVolume() {
            if (this._debounceId) {
                GLib.source_remove(this._debounceId);
                this._debounceId = 0;
            }

            this._debounceId = GLib.timeout_add(
                GLib.PRIORITY_DEFAULT,
                150,
                () => {
                    this._debounceId = 0;
                    this._setGroupVolumeFromSlider().catch(logError);
                    return GLib.SOURCE_REMOVE;
                },
            );
        }

        async _pollOnce() {
            if (!this._url) {
                this.title = "Snapcast — set server URL";
                this._groupClientIds = [];
                return;
            }
            if (!this._groupId) {
                this.title = "Snapcast — select a group";
                this._groupClientIds = [];
                return;
            }

            const resp = await postJson(
                this._session,
                this._url,
                jsonRpc("Server.GetStatus", {}),
            );
            const groups = resp?.result?.server?.groups ?? [];
            const group = groups.find((g) => g.id === this._groupId);

            if (!group) {
                this.title = "Snapcast — group not found";
                this._groupClientIds = [];
                return;
            }

            this._groupClientIds = connectedClientIds(group);
            this.title = `Snapcast — ${groupLabel(group)}`;

            const avg = avgConnectedVolumePercent(group);
            if (typeof avg === "number") {
                const v = clamp01(avg / 100);

                this._isUpdatingFromServer = true;
                try {
                    if (Math.abs(this.slider.value - v) > 0.01)
                        this.slider.value = v;
                } finally {
                    this._isUpdatingFromServer = false;
                }
            }
        }

        async _setGroupVolumeFromSlider() {
            if (!this._groupClientIds.length) return;

            const percent = Math.round(clamp01(this.slider.value) * 100);

            await Promise.allSettled(
                this._groupClientIds.map((id) => {
                    const body = jsonRpc("Client.SetVolume", {
                        id,
                        volume: { percent },
                    });
                    return postJson(this._session, this._url, body);
                }),
            );
        }

        destroy() {
            if (this._pollId) GLib.source_remove(this._pollId);
            if (this._debounceId) GLib.source_remove(this._debounceId);
            if (this._sliderChangedId)
                this.slider.disconnect(this._sliderChangedId);
            if (this._settingsChangedId)
                this._settings.disconnect(this._settingsChangedId);
            super.destroy();
        }
    },
);

const SnapcastIndicator = GObject.registerClass(
    class SnapcastIndicator extends QuickSettings.SystemIndicator {
        constructor(extension) {
            super();
            this.quickSettingsItems.push(new SnapcastSlider(extension));
        }

        destroy() {
            this.quickSettingsItems.forEach((item) => item.destroy());
            super.destroy();
        }
    },
);

export default class SnapshellExtension extends Extension {
    enable() {
        this._indicator = new SnapcastIndicator(this);
        Main.panel.statusArea.quickSettings.addExternalIndicator(
            this._indicator,
        );
    }

    disable() {
        this._indicator?.destroy();
        this._indicator = null;
    }
}
