// @ts-check
import path from "path";

let isStringifying = false;

/**
 * Patches console methods to capture log messages and send them to the server.
 * This is useful for debugging and logging in the client.
 * @param {"log" | "warn" | "info" | "debug" | "error" | "internal"} level
 * @param {...unknown} message - The log message to capture.
 */
function sendLogToServer(level, ...message) {
    if (isStringifying) return;
    if ("hot" in import.meta) {
        try {
            isStringifying = true;
            // console.time("sendLogToServer");
            let msg = /** @type {string} */ (stringifyLog(message));
            // console.timeEnd("sendLogToServer");
            // keep messages below payload limit
            if (msg.length > 100_000) {
                msg = msg.slice(0, 100_000) + "... <truncated>";
            }
            // @ts-ignore
            import.meta.hot.send("needle:client-log", { level, message: msg });
        } catch (/** @type {any} */ e) {
            // silently fail but send a message
            try {
                import.meta.hot?.send("needle:client-log", { level: "error", message: `Error during logging: ${e?.message ?? String(e)}` });
            }
            catch (/** @type {unknown} */ e2) {
                // fallback failed as well
            }
        }
        finally {
            isStringifying = false;
        }
    }
}

/** @param {(...args: unknown[]) => void} fn @param {unknown[]} args */
function logHelper(fn, args) {
    const error = new Error();
    const stack = error.stack;
    const caller = stack?.split('\n')[3]; // Get the actual caller
    const path = caller?.trim();
    if (!path) {
        return fn(...args);
    }
    const pathWithoutBrackets = path.replaceAll("(", "").replaceAll(")", "");
    fn(...args, `\n» ${pathWithoutBrackets}`);
}

if (import.meta && "hot" in import.meta) {

    function patchLogs() {
        const originalLog = /** @type {(...args: unknown[]) => void} */ (console.log.bind(console));
        const originalWarn = /** @type {(...args: unknown[]) => void} */ (console.warn.bind(console));
        const originalInfo = /** @type {(...args: unknown[]) => void} */ (console.info.bind(console));
        const originalDebug = /** @type {(...args: unknown[]) => void} */ (console.debug.bind(console));
        const originalError = /** @type {(...args: unknown[]) => void} */ (console.error.bind(console));

        console.log = /** @type {(...args: unknown[]) => void} */ (function (...args) {
            logHelper(originalLog, args);
            sendLogToServer("log", ...args);
        })
        console.warn = /** @type {(...args: unknown[]) => void} */ ((...args) => {
            logHelper(originalWarn, args);
            sendLogToServer("warn", ...args);
        })
        console.info = /** @type {(...args: unknown[]) => void} */ ((...args) => {
            logHelper(originalInfo, args);
            sendLogToServer("info", ...args);
        })
        console.debug = /** @type {(...args: unknown[]) => void} */ ((...args) => {
            logHelper(originalDebug, args);
            sendLogToServer("debug", ...args);
        })
        console.error = /** @type {(...args: unknown[]) => void} */ ((...args) => {
            logHelper(originalError, args);
            sendLogToServer("error", ...args);
        })
        return () => {
            console.log = originalLog;
            console.warn = originalWarn;
            console.info = originalInfo;
            console.debug = originalDebug;
            console.error = originalError;
        }
    }

    const query = new URLSearchParams(window.location.search);
    if (query.has("needle-debug")) {
        patchLogs();
    }
    else {
        // const unpatch = patchLogs();
        // setTimeout(() => {
        //     sendLogToServer("internal", "Stop listening to console.log.");
        //     unpatch();
        // }, 10_000);

        const threshold = 100;
        const devToolsArePotentiallyOpen = window.outerHeight - window.innerHeight > threshold || window.outerWidth - window.innerWidth > threshold;
        if (devToolsArePotentiallyOpen) {
            sendLogToServer("internal", "Console logging is disabled (devtools are open)");
        }
        else {
            sendLogToServer("internal", "Console logging is enabled");
            patchLogs();
        }
    }



    try {
        sendLogToServer("internal", `Page loaded
URL: ${window.location.href}
UserAgent: ${navigator.userAgent}
Screen: ${window.innerWidth} x ${window.innerHeight}px
Device Pixel Ratio: ${window.devicePixelRatio}
Device Memory: ${"deviceMemory" in navigator ? navigator.deviceMemory : "Not available"} GB
Online: ${navigator.onLine}
Language: ${navigator.language}
Timezone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}
Connection: ${"connection" in navigator ? JSON.stringify(navigator.connection) : "Not available"}
User Activation: ${"userActivation" in navigator ? JSON.stringify(navigator.userActivation) : "Not available"}
`);

        /** @typedef {{ vendor?: string, architecture?: string, device?: string, description?: string, features?: unknown, limits?: unknown }} GPUAdapterInfoLike */
        if ("gpu" in navigator) {
            // @ts-ignore
            navigator.gpu.requestAdapter()
                .then(/** @type {(adapter: {requestDevice(): Promise<{adapterInfo: GPUAdapterInfoLike}>}|null) => Promise<{adapterInfo: GPUAdapterInfoLike}>|null} */ (adapter) => adapter ? adapter.requestDevice() : null)
                .then(/** @type {(device: {adapterInfo: GPUAdapterInfoLike}|null) => void} */ (device) => {
                    if (device) {
                        const adapterInfo = device.adapterInfo;
                        if (adapterInfo) {
                            sendLogToServer("internal", [`WebGPU adapter info`, {
                                vendor: adapterInfo.vendor,
                                architecture: adapterInfo.architecture,
                                device: adapterInfo.device,
                                description: adapterInfo.description,
                                features: adapterInfo.features,
                                limits: adapterInfo.limits
                            }]);
                        }
                    }
                });
        }
    }
    catch (/** @type {any} */ e) {
        // silently fail
        sendLogToServer("error", `Error during initial log: ${e?.message ?? String(e)}`);
    }

    window.addEventListener('error', (event) => {
        const typedErrorEvent = /** @type {{ error: unknown, message: string }} */ (/** @type {unknown} */ (event));
        const error = /** @type {{ stack?: string, message?: string } | null | undefined} */ (typedErrorEvent.error);
        const errorMessage = error ? error.stack || error.message : typedErrorEvent.message;
        sendLogToServer("error", errorMessage);
    });
    window.addEventListener('unhandledrejection', (event) => {
        const typedRejEvent = /** @type {{ reason: unknown }} */ (/** @type {unknown} */ (event));
        const rejectionReason = /** @type {{ stack?: string, message?: string } | null | undefined} */ (typedRejEvent.reason);
        const reason = rejectionReason ? rejectionReason.stack || rejectionReason.message : "Unhandled rejection without reason";
        sendLogToServer("error", `Unhandled promise rejection: ${reason}`);
    });
    window.addEventListener('beforeunload', () => {
        sendLogToServer("internal", "Page is unloading\n\n");
    });
    document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'hidden') {
            sendLogToServer("internal", "Page is hidden");
        }
        else if (document.visibilityState === 'visible') {
            sendLogToServer("internal", "Page is visible again");
        }
        else {
            sendLogToServer("internal", `Page visibility changed to ${document.visibilityState}`);
        }
    });
    window.addEventListener("focus", () => {
        sendLogToServer("internal", "Page gained focus");
    });
    window.addEventListener("blur", () => {
        sendLogToServer("internal", "Page lost focus");
    });
    window.addEventListener('load', () => {
        sendLogToServer("internal", "Page fully loaded");
    });
    window.addEventListener('DOMContentLoaded', () => {
        sendLogToServer("internal", "DOM fully loaded and parsed");
    });
    window.addEventListener('online', () => {
        sendLogToServer("internal", "Browser is online");
    });
    window.addEventListener('offline', () => {
        sendLogToServer("warn", "Browser is offline");
    });
    window.addEventListener('resize', () => {
        sendLogToServer("internal", `Window resized to ${window.innerWidth}x${window.innerHeight}px`);
    });
    window.addEventListener('orientationchange', () => {
        sendLogToServer("internal", `Orientation changed to ${screen.orientation.type}`);
    });
    window.addEventListener('fullscreenchange', () => {
        if (document.fullscreenElement) {
            sendLogToServer("internal", "Entered fullscreen mode");
        } else {
            sendLogToServer("internal", "Exited fullscreen mode");
        }
    });


    // url change event
    window.addEventListener('hashchange', () => {
        sendLogToServer("internal", `URL hash changed to ${location.hash}`);
    });
    window.addEventListener('popstate', () => {
        sendLogToServer("internal", `History state changed: ${JSON.stringify(/** @type {{ state: unknown }} */ (history).state)}`);
    });



}





// #region copied from common/logger.js


/**
 * Stringifies a log message, handling circular references and formatting.
 * @param {unknown} log
 * @param {Set<unknown>} [seen]
 */
function stringifyLog(log, seen = /** @type {Set<unknown>} */ (new Set()), depth = 0) {
    const isServer = typeof window === "undefined";
    const stringify_limits = {
        string: isServer ? 100_000 : 1_000,
        object_keys: isServer ? 300 : 200,
        object_depth: isServer ? 10 : 3,
        array_items: isServer ? 2_000 : 100,
    }

    if (typeof log === "string") {
        if (log.length > stringify_limits.string) log = `${log.slice(0, stringify_limits.string)}... <truncated ${log.length - stringify_limits.string} characters>`;
        return log;
    }
    if (typeof log === "number" || typeof log === "boolean") {
        return String(log);
    }
    if (log === null) {
        return "null";
    }
    if (log === undefined) {
        return "undefined";
    }
    if (typeof log === "function") {
        return "<function>";
    }

    if (seen.has(log)) return "<circular>";

    if (Array.isArray(log)
        || log instanceof ArrayBuffer
        || log instanceof Uint8Array
        || log instanceof Float32Array
        || log instanceof Int32Array
        || log instanceof Uint32Array
        || log instanceof Uint16Array
        || log instanceof Uint8ClampedArray
        || log instanceof Int16Array
        || log instanceof Int8Array
        || log instanceof BigInt64Array
        || log instanceof BigUint64Array
        || log instanceof Float64Array
    ) {
        const logArr = /** @type {ArrayLike<unknown>} */ (/** @type {unknown} */ (log));
        seen.add(logArr);
        return stringifyArray(logArr);
    }
    if (typeof log === "object") {

        if (depth > stringify_limits.object_depth) {
            return "<object too deep>";
        }

        seen.add(log);

        if (log instanceof Error) {
            return `<Error: ${log.message}\nStack: ${log.stack}>`;
        }

        const logObj = /** @type {Record<string, unknown>} */ (log);
        const keys = Object.keys(logObj);
        let res = "{";
        for (let i = 0; i < keys.length; i++) {
            const key = keys[i];
            let value = logObj[key];
            if (i >= stringify_limits.object_keys) {
                res += `, ... <truncated ${keys.length - i} keys>`;
                break;
            }

            if (typeof value === "number") {
                // clamp precision for numbers if it has decimal places
                if (value % 1 !== 0) {
                    value = Number(value.toFixed(4));
                }
            }
            let str = stringifyLog(value, seen, depth + 1);
            if (typeof value === "object") {
                if (Array.isArray(value)) {
                    str = `[${str}]`;
                }
            }
            else if (typeof value === "string") {
                str = `"${str}"`;
            }
            if (i > 0) res += ", ";
            res += `"${key}":${str}`;
        }
        res += "}";
        return res;
        // let entries = Object.entries(log).map(([key, value], index) => {
        //     if (index > stringify_limits.object_keys) return `"${key}": <truncated>`;
        //     return `"${key}": ${stringifyLog(value, seen, depth + 1)}`;
        // });
        // return `{ ${entries.join(", ")} }`;
    }

    return String(log);

    /** @param {ArrayLike<unknown>} arr @returns {string} */
    function stringifyArray(arr) {
        let res = "";
        for (let i = 0; i < arr.length; i++) {
            let entry = arr[i];
            if (res && i > 0) res += ", ";
            if (i > stringify_limits.array_items) {
                res += "<truncated " + (arr.length - i) + ">";
                break;
            }
            res += stringifyLog(entry, seen, depth + 1);
        }
        return res;
    }
}

