(() => {
    const self = reg_ns("me");

    self.LOGIN_ACCOUNT_TOKENS = JSON.parse(
        window.localStorage.getItem("atto:login_account_tokens") || "{}",
    );

    self.define("logout", async () => {
        if (
            !(await trigger("atto::confirm", [
                "Are you sure you would like to do this?",
            ]))
        ) {
            return;
        }

        fetch("/api/v1/auth/logout", {
            method: "POST",
        })
            .then((res) => res.json())
            .then((res) => {
                trigger("atto::toast", [
                    res.ok ? "success" : "error",
                    res.message,
                ]);

                if (res.ok) {
                    setTimeout(() => {
                        window.location.href = "/";
                    }, 150);
                }
            });
    });

    self.define("remove_post", async (_, id) => {
        if (
            !(await trigger("atto::confirm", [
                "Are you sure you want to do this?",
            ]))
        ) {
            return;
        }

        fetch(`/api/v1/posts/${id}`, {
            method: "DELETE",
        })
            .then((res) => res.json())
            .then((res) => {
                trigger("atto::toast", [
                    res.ok ? "success" : "error",
                    res.message,
                ]);
            });
    });

    self.define("react", async (_, element, asset, asset_type, is_like) => {
        fetch("/api/v1/reactions", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                asset,
                asset_type,
                is_like,
            }),
        })
            .then((res) => res.json())
            .then((res) => {
                trigger("atto::toast", [
                    res.ok ? "success" : "error",
                    res.message,
                ]);

                if (res.ok) {
                    const like = element.parentElement.querySelector(
                        '[hook_element="reaction.like"]',
                    );

                    const dislike = element.parentElement.querySelector(
                        '[hook_element="reaction.dislike"]',
                    );

                    if (is_like) {
                        like.classList.add("green");
                        like.querySelector("svg").classList.add("filled");

                        dislike.classList.remove("red");
                    } else {
                        dislike.classList.add("red");

                        like.classList.remove("green");
                        like.querySelector("svg").classList.remove("filled");
                    }
                }
            });
    });

    self.define("remove_notification", (_, id) => {
        fetch(`/api/v1/notifications/${id}`, {
            method: "DELETE",
        })
            .then((res) => res.json())
            .then((res) => {
                trigger("atto::toast", [
                    res.ok ? "success" : "error",
                    res.message,
                ]);
            });
    });

    self.define("update_notification_read_status", (_, id, read) => {
        fetch(`/api/v1/notifications/${id}/read_status`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                read,
            }),
        })
            .then((res) => res.json())
            .then((res) => {
                trigger("atto::toast", [
                    res.ok ? "success" : "error",
                    res.message,
                ]);
            });
    });

    self.define("clear_notifs", async () => {
        if (
            !(await trigger("atto::confirm", [
                "Are you sure you want to do this?",
            ]))
        ) {
            return;
        }

        fetch("/api/v1/notifications/my", {
            method: "DELETE",
        })
            .then((res) => res.json())
            .then((res) => {
                trigger("atto::toast", [
                    res.ok ? "success" : "error",
                    res.message,
                ]);
            });
    });

    self.define("repost", (_, id, content, community) => {
        fetch(`/api/v1/posts/${id}/repost`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                content,
                community,
            }),
        })
            .then((res) => res.json())
            .then((res) => {
                trigger("atto::toast", [
                    res.ok ? "success" : "error",
                    res.message,
                ]);

                if (res.ok) {
                    setTimeout(() => {
                        window.location.href = `/post/${res.payload}`;
                    }, 100);
                }
            });
    });

    self.define("report", (_, asset, asset_type) => {
        window.open(
            `/mod_panel/file_report?asset=${asset}&asset_type=${asset_type}`,
        );
    });

    self.define("seen", () => {
        fetch("/api/v1/auth/user/me/seen", {
            method: "POST",
        })
            .then((res) => res.json())
            .then((res) => {
                if (!res.ok) {
                    trigger("atto::toast", ["error", res.message]);
                }
            });
    });

    self.define("remove_question", async (_, id) => {
        if (
            !(await trigger("atto::confirm", [
                "Are you sure you want to do this?",
            ]))
        ) {
            return;
        }

        fetch(`/api/v1/questions/${id}`, {
            method: "DELETE",
        })
            .then((res) => res.json())
            .then((res) => {
                trigger("atto::toast", [
                    res.ok ? "success" : "error",
                    res.message,
                ]);
            });
    });

    self.define("ip_block_question", async (_, id) => {
        if (
            !(await trigger("atto::confirm", [
                "Are you sure you want to do this?",
            ]))
        ) {
            return;
        }

        fetch(`/api/v1/questions/${id}/block_ip`, {
            method: "POST",
        })
            .then((res) => res.json())
            .then((res) => {
                trigger("atto::toast", [
                    res.ok ? "success" : "error",
                    res.message,
                ]);
            });
    });

    // token switcher
    self.define(
        "set_login_account_tokens",
        ({ $ }, value) => {
            $.LOGIN_ACCOUNT_TOKENS = value;
            window.localStorage.setItem(
                "atto:login_account_tokens",
                JSON.stringify(value),
            );
        },
        ["object"],
    );

    self.define("login", ({ $ }, username) => {
        const token = self.LOGIN_ACCOUNT_TOKENS[username];

        if (!token) {
            return;
        }

        window.location.href = `/api/v1/auth/token?token=${token}`;
    });

    self.define("render_token_picker", ({ $ }, element) => {
        element.innerHTML = "";
        for (const token of Object.entries($.LOGIN_ACCOUNT_TOKENS)) {
            element.innerHTML += `<button class="quaternary w-full justify-start" onclick="trigger('me::login', ['${token[0]}'])">
                <img
                    title="${token[0]}'s avatar"
                    src="/api/v1/auth/user/${token[0]}/avatar?selector_type=username"
                    alt="Avatar image"
                    class="avatar"
                    style="--size: 24px"
                />

                <span>${token[0]}</span>
            </button>`;
        }
    });

    self.define("switch_account", () => {
        document.getElementById("tokens_dialog").showModal();
    });
})();

(() => {
    const self = reg_ns("connections");

    self.define("pkce_verifier", async (_, length) => {
        let text = "";
        const possible =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

        for (let i = 0; i < length; i++) {
            text += possible.charAt(
                Math.floor(Math.random() * possible.length),
            );
        }

        return text;
    });

    self.define("pkce_challenge", async (_, verifier) => {
        const data = new TextEncoder().encode(verifier);
        const digest = await window.crypto.subtle.digest("SHA-256", data);
        return btoa(
            String.fromCharCode.apply(null, [...new Uint8Array(digest)]),
        )
            .replace(/\+/g, "-")
            .replace(/\//g, "_")
            .replace(/=+$/, "");
    });

    self.define("delete", async (_, connection) => {
        if (
            !(await trigger("atto::confirm", [
                "Are you sure you want to do this?",
            ]))
        ) {
            return;
        }

        fetch(`/api/v1/auth/user/connections/${connection}`, {
            method: "DELETE",
        })
            .then((res) => res.json())
            .then((res) => {
                trigger("atto::toast", [
                    res.ok ? "success" : "error",
                    res.message,
                ]);

                if (res.ok) {
                    window.location.reload();
                }
            });
    });

    self.define("push_con_data", async (_, connection, data) => {
        return await (
            await fetch("/api/v1/auth/user/connections/_data", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({
                    connection,
                    data,
                }),
            })
        ).json();
    });

    self.define("push_con_state", async (_, connection, data) => {
        return await (
            await fetch("/api/v1/auth/user/connections/_state", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({
                    connection,
                    data,
                }),
            })
        ).json();
    });

    self.define("push_con_shown", async (_, connection, shown) => {
        return await (
            await fetch("/api/v1/auth/user/connections/_shown", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({
                    connection,
                    shown,
                }),
            })
        ).json();
    });
})();

(() => {
    const self = reg_ns("spotify");

    self.define("create_connection", (_, client_id) => {
        fetch("/api/v1/auth/user/connections/spotify", {
            method: "POST",
        })
            .then((res) => res.json())
            .then(async (res) => {
                // create challenge and store
                const verifier = await trigger("connections::pkce_verifier", [
                    128,
                ]);

                const challenge = await trigger("connections::pkce_challenge", [
                    verifier,
                ]);

                await trigger("connections::push_con_data", [
                    "Spotify",
                    {
                        verifier,
                        challenge,
                    },
                ]);

                // ...
                const params = new URLSearchParams();
                params.append("client_id", client_id);
                params.append("response_type", "code");
                params.append(
                    "redirect_uri",
                    `${window.location.origin}/auth/connections_link/Spotify`,
                );
                params.append(
                    "scope",
                    "user-read-recently-played user-read-playback-state user-read-currently-playing",
                );
                params.append("code_challenge_method", "S256");
                params.append("code_challenge", challenge);

                window.open(
                    `https://accounts.spotify.com/authorize?${params.toString()}`,
                );

                window.location.reload();
            });
    });

    self.define("get_token", async (_, client_id, verifier, code) => {
        const params = new URLSearchParams();
        params.append("client_id", client_id);
        params.append("grant_type", "authorization_code");
        params.append("code", code);
        params.append(
            "redirect_uri",
            `${window.location.origin}/auth/connections_link/Spotify`,
        );
        params.append("code_verifier", verifier);

        const result = await fetch("https://accounts.spotify.com/api/token", {
            method: "POST",
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            body: params,
        });

        const { access_token, refresh_token, expires_in } = await result.json();
        return [access_token, refresh_token, expires_in];
    });

    self.define("refresh_token", async (_, client_id, token) => {
        const params = new URLSearchParams();
        params.append("client_id", client_id);
        params.append("grant_type", "refresh_token");
        params.append("refresh_token", token);

        const result = await fetch("https://accounts.spotify.com/api/token", {
            method: "POST",
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            body: params,
        });

        const { access_token, refresh_token, expires_in } = await result.json();
        return [access_token, refresh_token, expires_in];
    });

    self.define("profile", async (_, token) => {
        return await (
            await fetch("https://api.spotify.com/v1/me", {
                method: "GET",
                headers: { Authorization: `Bearer ${token}` },
            })
        ).json();
    });

    self.define("get_playing", async (_, token) => {
        // <https://developer.spotify.com/documentation/web-api/reference/get-information-about-the-users-current-playback>
        return await (
            await fetch("https://api.spotify.com/v1/me/player", {
                method: "GET",
                headers: { Authorization: `Bearer ${token}` },
            })
        ).json();
    });

    self.define("publish_playing", async (_, playing) => {
        if (!playing.is_playing) {
            return await trigger("connections::push_con_state", [
                "Spotify",
                {
                    external_urls: {},
                    data: {},
                },
            ]);
        }

        if (playing.item.is_local) {
            return;
        }

        if (
            window.localStorage.getItem("atto:connections.spotify/id") ===
            playing.item.id
        ) {
            // item already pushed to connection, no need right now
            return;
        }

        window.localStorage.setItem(
            "atto:connections.spotify/id",
            playing.item.id,
        );

        return await trigger("connections::push_con_state", [
            "Spotify",
            {
                external_urls: {
                    track: playing.item.external_urls.spotify,
                    artist: playing.item.artists[0].external_urls.spotify,
                    album: playing.item.album.external_urls.spotify,
                    album_img: playing.item.album.images[0].url,
                },
                data: {
                    id: playing.item.id,
                    // track
                    track: playing.item.name,
                    artist: playing.item.artists[0].name,
                    album: playing.item.album.name,
                    // image
                    // img_w: playing.item.album.images[0].width.toString(),
                    // img_h: playing.item.album.images[0].width.toString(),
                    // times
                    timestamp: playing.timestamp.toString(),
                    progress_ms: playing.progress_ms.toString(),
                    duration_ms: playing.item.duration_ms.toString(),
                },
            },
        ]);
    });

    self.define("ms_time_text", (_, ms) => {
        const minutes = Math.floor(ms / 60000);
        const seconds = ((ms % 60000) / 1000).toFixed(0);
        return `${minutes}:${(seconds < 10 ? "0" : "") + seconds}`;
    });

    self.define(
        "timestamp",
        ({ $ }, updated_, progress_ms_, duration_ms_, display = "full") => {
            if (duration_ms_ === "0") {
                return;
            }

            const now = new Date().getTime();
            const updated = Number.parseInt(updated_) + 8000;

            let elapsed_since_update = now - updated;
            if (elapsed_since_update < 0) {
                elapsed_since_update = 0;
            }

            const progress_ms =
                Number.parseInt(progress_ms_) + elapsed_since_update;

            const duration_ms = Number.parseInt(duration_ms_);

            if (progress_ms > duration_ms) {
                // song is over
                return "";
            }

            if (display === "full") {
                return `${$.ms_time_text(progress_ms)}/${$.ms_time_text(duration_ms)} <span class="fade">(${Math.floor((progress_ms / duration_ms) * 100)}%)</span>`;
            }

            if (display === "left") {
                return $.ms_time_text(progress_ms);
            }

            return $.ms_time_text(duration_ms);
        },
    );
})();

(() => {
    const self = reg_ns("last_fm");

    self.define("api", async (_, method, data) => {
        return JSON.parse(
            (
                await (
                    await fetch(
                        "/api/v1/auth/user/connections/last_fm/api_proxy",
                        {
                            method: "POST",
                            headers: {
                                "Content-Type": "application/json",
                            },
                            body: JSON.stringify({
                                method,
                                data,
                            }),
                        },
                    )
                ).json()
            ).payload,
        );
    });

    self.define("create_connection", (_, client_id) => {
        fetch("/api/v1/auth/user/connections/last_fm", {
            method: "POST",
        })
            .then((res) => res.json())
            .then(async (_) => {
                const params = new URLSearchParams();
                params.append("api_key", client_id);
                params.append(
                    "cb",
                    `${window.location.origin}/auth/connections_link/LastFm`,
                );

                window.open(`https://last.fm/api/auth?${params.toString()}`);

                window.location.reload();
            });
    });

    self.define("pull_track_info", async (_, artist, name) => {
        const params = new URLSearchParams();
        params.append("query", `query=artist:"${artist}" and track:"${name}"`);
        params.append("limit", "1");
        params.append("fmt", "json");

        return (
            await (
                await fetch(
                    `https://musicbrainz.org/ws/2/recording?${params.toString()}`,
                )
            ).json()
        ).recordings[0];
    });

    self.define("get_session", async ({ $ }, token) => {
        return await $.api("auth.getSession", {
            token,
        });
    });

    self.define("get_playing", async ({ $ }, user, session_token) => {
        // <https://lastfm-docs.github.io/api-docs/user/getRecentTracks/>
        return (
            await $.api("user.getRecentTracks", {
                user,
                sk: session_token,
                limit: "1",
                extended: "1",
            })
        ).recenttracks.track[0];
    });

    self.define("publish_playing", async ({ $ }, playing) => {
        if (!playing || !playing["@attr"] || !playing["@attr"].nowplaying) {
            return await trigger("connections::push_con_state", [
                "LastFm",
                {
                    external_urls: {},
                    data: {},
                },
            ]);
        }

        const mb_info = await $.pull_track_info(
            playing.artist.name,
            playing.name,
        );

        if (
            window.localStorage.getItem("atto:connections.last_fm/name") ===
            playing.name + mb_info.id
        ) {
            // item already pushed to connection, no need right now
            return;
        }

        window.localStorage.setItem(
            "atto:connections.last_fm/name",
            playing.name + mb_info.id,
        );

        return await trigger("connections::push_con_state", [
            "LastFm",
            {
                external_urls: {
                    track: playing.url,
                    artist: playing.artist.url,
                    track_img: playing.image[2]["#text"],
                },
                data: {
                    id: mb_info.id,
                    // track
                    track: playing.name,
                    artist: playing.artist.name,
                    album: playing.album["#text"],
                    // times
                    timestamp: new Date().getTime().toString(),
                    duration_ms: (mb_info.length || 0).toString(),
                },
            },
        ]);
    });
})();