window.rpgRollerApi = (() => {
    const sessionPrefix = "rpgroller.";
    const stateStream = {
        source: null,
        dotNetRef: null,
        campaignId: null,
        reconnectDelayMs: 1000,
        reconnectTimer: null,
        stopped: true
    };

    function toAppUrl(url) {
        if (!url || typeof url !== "string") {
            return url;
        }

        if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) {
            return url;
        }

        const relativeUrl = url.startsWith("/") ? url.slice(1) : url;
        return new URL(relativeUrl, document.baseURI).toString();
    }

    function clearReconnectTimer() {
        if (stateStream.reconnectTimer) {
            clearTimeout(stateStream.reconnectTimer);
            stateStream.reconnectTimer = null;
        }
    }

    function invokeDotNet(method, ...args) {
        if (!stateStream.dotNetRef) {
            return;
        }

        stateStream.dotNetRef.invokeMethodAsync(method, ...args).catch(() => {
        });
    }

    function scheduleReconnect() {
        if (stateStream.stopped || stateStream.reconnectTimer) {
            return;
        }

        const delay = stateStream.reconnectDelayMs;
        stateStream.reconnectTimer = setTimeout(() => {
            stateStream.reconnectTimer = null;
            if (stateStream.stopped) {
                return;
            }

            stateStream.reconnectDelayMs = Math.min(stateStream.reconnectDelayMs * 2, 30000);
            connectStateStream();
        }, delay);
    }

    function connectStateStream() {
        if (stateStream.stopped || !stateStream.campaignId) {
            return;
        }

        clearReconnectTimer();
        invokeDotNet("OnConnectionStateChanged", "reconnecting");

        const source = new EventSource(toAppUrl(`api/events/state?campaignId=${encodeURIComponent(stateStream.campaignId)}`));
        stateStream.source = source;

        source.onopen = () => {
            stateStream.reconnectDelayMs = 1000;
            invokeDotNet("OnConnectionStateChanged", "connected");
        };

        source.addEventListener("state", (event) => {
            try {
                const payload = JSON.parse(event.data);
                invokeDotNet("OnStateEventReceived", payload);
            } catch {
                invokeDotNet("OnStateEventReceived", {
                    campaignId: stateStream.campaignId,
                    totalVersion: 0,
                    rosterVersion: 0,
                    logVersion: 0,
                    characterVersions: []
                });
            }
        });

        source.onerror = () => {
            if (stateStream.source === source) {
                source.close();
                stateStream.source = null;
            }

            if (stateStream.stopped) {
                return;
            }

            invokeDotNet("OnConnectionStateChanged", "reconnecting");
            scheduleReconnect();
        };
    }

    function stopStateEvents() {
        stateStream.stopped = true;
        clearReconnectTimer();

        if (stateStream.source) {
            stateStream.source.close();
            stateStream.source = null;
        }

        stateStream.campaignId = null;
        stateStream.dotNetRef = null;
    }

    window.addEventListener("offline", () => {
        invokeDotNet("OnConnectionStateChanged", "offline");
    });

    window.addEventListener("online", () => {
        if (!stateStream.stopped) {
            connectStateStream();
        }
    });

    async function request(method, url, body) {
        const options = {
            method,
            credentials: "same-origin",
            headers: {
                Accept: "application/json"
            }
        };

        if (body !== null && body !== undefined) {
            options.headers["Content-Type"] = "application/json";
            options.body = JSON.stringify(body);
        }

        let response;
        try {
            response = await fetch(toAppUrl(url), options);
        } catch {
            return {
                ok: false,
                status: 0,
                error: "Network error. Check your connection and retry."
            };
        }

        let parsed = null;
        const text = await response.text();
        if (text) {
            try {
                parsed = JSON.parse(text);
            } catch {
                parsed = null;
            }
        }

        if (!response.ok) {
            return {
                ok: false,
                status: response.status,
                error: parsed && typeof parsed.error === "string" ? parsed.error : "Request failed.",
                code: parsed && typeof parsed.code === "string" ? parsed.code : null
            };
        }

        return {
            ok: true,
            status: response.status,
            data: parsed
        };
    }

    function getSessionValue(key) {
        return sessionStorage.getItem(`${sessionPrefix}${key}`);
    }

    function setSessionValue(key, value) {
        const qualifiedKey = `${sessionPrefix}${key}`;
        if (value === null || value === undefined || value === "") {
            sessionStorage.removeItem(qualifiedKey);
            return;
        }

        sessionStorage.setItem(qualifiedKey, value);
    }

    function startStateEvents(campaignId, dotNetRef) {
        stopStateEvents();
        stateStream.stopped = false;
        stateStream.dotNetRef = dotNetRef;
        stateStream.campaignId = campaignId;
        stateStream.reconnectDelayMs = 1000;
        connectStateStream();
    }

    function scrollElementToBottom(element) {
        if (!element) {
            return;
        }

        element.scrollTop = element.scrollHeight;
    }

    function clearInputValue(element) {
        if (!element) {
            return;
        }

        element.value = "";
    }

    function initializeAuthPage() {
        const root = document.querySelector("[data-auth-page]");
        if (!root) {
            return;
        }

        const statusElement = root.querySelector("[data-auth-status]");
        const forms = root.querySelectorAll("[data-auth-form]");
        forms.forEach((form) => {
            form.addEventListener("submit", async (event) => {
                event.preventDefault();
                await submitAuthForm(root, form, statusElement);
            });
        });
    }

    function clearAuthErrors(root) {
        const statusElement = root.querySelector("[data-auth-status]");
        if (statusElement) {
            statusElement.hidden = true;
            statusElement.textContent = "";
            statusElement.classList.remove("error", "success");
        }

        root.querySelectorAll("[data-form-error], [data-field-error]").forEach((element) => {
            element.hidden = true;
            element.textContent = "";
        });
    }

    function setAuthStatus(statusElement, message, isError) {
        if (!statusElement) {
            return;
        }

        statusElement.hidden = !message;
        statusElement.textContent = message || "";
        statusElement.classList.toggle("error", !!message && isError);
        statusElement.classList.toggle("success", !!message && !isError);
    }

    function setFormError(form, message) {
        const errorElement = form.querySelector("[data-form-error]");
        if (!errorElement) {
            return;
        }

        errorElement.hidden = !message;
        errorElement.textContent = message || "";
    }

    function setFieldError(form, fieldName, message) {
        const errorElement = form.querySelector(`[data-field-error="${fieldName}"]`);
        if (!errorElement) {
            return;
        }

        errorElement.hidden = !message;
        errorElement.textContent = message || "";
    }

    function setInvalidCredentialsErrors(form) {
        setFieldError(form, "username", "Invalid username or password.");
        setFieldError(form, "password", "Invalid username or password.");
    }

    function readFormData(form) {
        return Object.fromEntries(new FormData(form).entries());
    }

    function validateAuthForm(formType, payload) {
        const errors = {};

        if (!payload.username || !payload.username.trim()) {
            errors.username = "Username is required.";
        }

        if (formType === "register") {
            if (!payload.displayName || !payload.displayName.trim()) {
                errors.displayName = "Display name is required.";
            }

            if (!payload.password || payload.password.length < 8) {
                errors.password = "Password must be at least 8 characters.";
            }
        } else if (!payload.password) {
            errors.password = "Password is required.";
        }

        return errors;
    }

    function setSubmitting(form, isSubmitting) {
        const submitButton = form.querySelector("button[type=\"submit\"]");
        if (!submitButton) {
            return;
        }

        submitButton.disabled = isSubmitting;
        submitButton.textContent = isSubmitting
            ? submitButton.dataset.submittingLabel || submitButton.textContent
            : submitButton.dataset.submitLabel || submitButton.textContent;
    }

    async function submitAuthForm(root, form, statusElement) {
        clearAuthErrors(root);

        const formType = form.dataset.authForm;
        const payload = readFormData(form);
        const errors = validateAuthForm(formType, payload);

        Object.entries(errors).forEach(([fieldName, message]) => {
            setFieldError(form, fieldName, message);
        });

        if (Object.keys(errors).length > 0) {
            setAuthStatus(statusElement, "Resolve validation issues before submitting.", true);
            setFormError(form, "Resolve validation issues before submitting.");
            return;
        }

        const endpoint = formType === "register" ? "/api/auth/register" : "/api/auth/login";
        const requestBody = formType === "register"
            ? {
                username: payload.username.trim(),
                displayName: payload.displayName.trim(),
                password: payload.password
            }
            : {
                username: payload.username.trim(),
                password: payload.password
            };

        setSubmitting(form, true);
        try {
            const response = await request("POST", endpoint, requestBody);
            if (!response.ok) {
                setAuthStatus(statusElement, response.error || "Request failed.", true);

                if (formType === "register" && response.code === "duplicate_username") {
                    setFieldError(form, "username", "Username is already taken. Choose another one.");
                } else if (formType === "login" && response.code === "invalid_credentials") {
                    setInvalidCredentialsErrors(form);
                } else {
                    setFormError(form, response.error || "Request failed.");
                }

                setFormError(form, response.error || "Request failed.");
                return;
            }

            if (formType === "login") {
                window.location.assign(toAppUrl("/play"));
                return;
            }

            form.reset();
            setAuthStatus(statusElement, "Registration successful. You can log in now.", false);
        } finally {
            setSubmitting(form, false);
        }
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", initializeAuthPage, { once: true });
    } else {
        initializeAuthPage();
    }

    return {
        request,
        getSessionValue,
        setSessionValue,
        startStateEvents,
        stopStateEvents,
        scrollElementToBottom,
        clearInputValue
    };
})();
