import { FCSClient } from "../components/fcs-client-lib.js";

class WebSocketManager {
    constructor(config = {}) {
        this.config = {
            apiKey: config.apiKey || "none",
            animationDuration: config.animationDuration || 1e3,
            animateFullValue: config.animateFullValue || false,
            animateBgFlash: config.animateBgFlash || false,
            animationClasses: {
                positive: config.positiveClass || "fx-positive",
                negative: config.negativeClass || "fx-negative",
                neutral: config.neutralClass || "fx-neutral",
                positiveBg: config.positiveBgClass || "fx-positive-bg",
                negativeBg: config.negativeBgClass || "fx-negative-bg",
                neutralBg: config.neutralBgClass || "fx-neutral-bg"
            },
            subscriptionDelay: config.subscriptionDelay || 1,
            defaultTimeframe: config.defaultTimeframe || "1D",
            observerConfig: config.observerConfig || {
                root: null,
                rootMargin: "50px",
                threshold: .01
            },
            subscriptionDebounceDelay: config.subscriptionDebounceDelay || 1e3,
            tickerSelector: config.tickerSelector || "[data-ticker]",
            tickerAttribute: config.tickerAttribute || "data-ticker",
            timeperiodAttribute: config.timeperiodAttribute || "data-timeperiod",
            persistentAttribute: config.persistentAttribute || "data-ticker-persistent",
            rootNode: config.rootNode || null,
            scopeContainer: config.scopeContainer || null,
            decimalPlaces: config.decimalPlaces || 2,
            priceFields: config.priceFields || {
                animated: [ "o", "h", "l", "c", "a", "b" ],
                normal: [ "v", "t", "update" ]
            },
            heatmapMode: config.heatmapMode || false,
            heatmapThresholds: config.heatmapThresholds || [ .5, 1, 2, 3 ],
            heatmapClasses: config.heatmapClasses || {
                positive: [ "heatmap-positive-1", "heatmap-positive-2", "heatmap-positive-3", "heatmap-positive-4", "heatmap-positive-5" ],
                negative: [ "heatmap-negative-1", "heatmap-negative-2", "heatmap-negative-3", "heatmap-negative-4", "heatmap-negative-5" ],
                neutral: "heatmap-neutral"
            },
            onProfileChange: config.onProfileChange || null
        };
        this.#connection = null;
        this.#subscriptionTimeout = null;
        this.#animationTimeouts = new Map;
        this._elementCache = new Map;
        this._prices = new Map;
        this._decimalPlacesCache = new Map;
        this.#observer = null;
        this.#visibleSymbols = new Set;
        this.#subscriptionDebounceTimer = null;
        this.#pendingSubscriptions = new Map;
        this.#processedSubscriptions = new Set;
        this._symbolSessions = new Map;
        this.#setupIntersectionObserver();
    }
    #connection;
    #subscriptionTimeout;
    #animationTimeouts;
    #observer;
    #visibleSymbols;
    #subscriptionDebounceTimer;
    #pendingSubscriptions;
    #processedSubscriptions;
    connect() {
        if (this.isConnected()) {
            return;
        }
        try {
            this.#connection = new FCSClient(this.config.apiKey);
            this.#connection.connect().then(obj => {
                this.#processExistingElements();
            }).catch(err => {});
            this.#connection.onmessage = data => {
                try {
                    this.#handleIncomingMessage(data);
                } catch (error) {}
            };
        } catch (error) {}
    }
    #handleIncomingMessage(messageData) {
        const messageType = messageData.type;
        switch (messageType) {
          case "price":
            if (messageData.prices?.mode === "profile") {
                this.#handleProfileMessage(messageData);
                break;
            }
            const isInitialMode = messageData.prices.mode === "initial";
            this.#updatePrice(messageData, isInitialMode);
            break;

          case "error":
            break;

          default:
            break;
        }
    }
    isConnected() {
        return this.#connection && this.#connection.isConnected;
    }
    disconnect() {
        if (this.#connection) {
            this.#connection.close();
            this.#connection = null;
        }
        for (const [key, timeoutId] of this.#animationTimeouts) {
            clearTimeout(timeoutId);
        }
        this.#animationTimeouts.clear();
        if (this.#subscriptionTimeout) {
            clearTimeout(this.#subscriptionTimeout);
            this.#subscriptionTimeout = null;
        }
        if (this.#observer) {
            this.#observer.disconnect();
            this.#observer = null;
        }
        if (this.#subscriptionDebounceTimer) {
            clearTimeout(this.#subscriptionDebounceTimer);
            this.#subscriptionDebounceTimer = null;
        }
        this.#visibleSymbols.clear();
        this.#pendingSubscriptions.clear();
        this.#processedSubscriptions.clear();
        this._elementCache.clear();
        this._prices.clear();
        this._decimalPlacesCache.clear();
    }
    subscribe(symbol, timeframe = null) {
        if (!this.isConnected()) {
            return false;
        }
        const tf = timeframe || this.config.defaultTimeframe;
        return this.#connection.join(symbol, tf);
    }
    unsubscribe(symbol, timeframe = null) {
        if (!this.isConnected()) {
            return false;
        }
        if (!symbol) {
            return false;
        }
        const tf = timeframe || this.config.defaultTimeframe;
        return this.#connection.leave(symbol, tf);
    }
    #isPersistentElement(element) {
        if (!element.hasAttribute(this.config.persistentAttribute)) {
            return false;
        }
        const value = element.getAttribute(this.config.persistentAttribute);
        return value !== null && value !== "";
    }
    subscribeAllSymbols() {
        if (!this.isConnected()) {
            let retryCount = 0;
            const maxRetries = 5;
            const checkConnection = setInterval(() => {
                retryCount++;
                if (this.isConnected()) {
                    clearInterval(checkConnection);
                    this.subscribeAllSymbols();
                } else if (retryCount >= maxRetries) {
                    clearInterval(checkConnection);
                }
            }, 500);
            return;
        }
        if (this.#subscriptionTimeout) {
            clearTimeout(this.#subscriptionTimeout);
            this.#subscriptionTimeout = null;
        }
        this.#subscriptionTimeout = setTimeout(() => {
            const root = this.config.rootNode || document;
            let tickerElements;
            if (this.config.scopeContainer) {
                const containerElement = root.querySelector(this.config.scopeContainer);
                if (!containerElement) {
                    this.#subscriptionTimeout = null;
                    return;
                }
                tickerElements = containerElement.querySelectorAll(this.config.tickerSelector);
            } else {
                tickerElements = root.querySelectorAll(this.config.tickerSelector);
            }
            if (tickerElements.length === 0) {
                this.#subscriptionTimeout = null;
                return;
            }
            this._elementCache.clear();
            for (let i = 0; i < tickerElements.length; i++) {
                const element = tickerElements[i];
                const isPersistent = this.#isPersistentElement(element);
                const ticker = element.getAttribute(this.config.tickerAttribute);
                const timeperiod = element.getAttribute(this.config.timeperiodAttribute) || this.config.defaultTimeframe;
                const subscriptionKey = ticker ? `${ticker}_${isPersistent ? "1" : "0"}_${timeperiod}` : null;
                const symbolKey = ticker ? `${ticker}_${timeperiod}` : null;
                if (subscriptionKey && !this.#processedSubscriptions.has(subscriptionKey)) {
                    this.#processedSubscriptions.add(subscriptionKey);
                    if (isPersistent && ticker && symbolKey && !this.#visibleSymbols.has(symbolKey) && this.isConnected()) {
                        this.#visibleSymbols.add(symbolKey);
                        if (!this.subscribe(ticker, timeperiod)) {
                            this.#visibleSymbols.delete(symbolKey);
                        }
                    }
                }
                if (!isPersistent && !element.hasAttribute("data-ws-observed")) {
                    element.setAttribute("data-ws-observed", "true");
                    if (this.#observer) {
                        this.#observer.observe(element);
                    }
                }
            }
            this.#subscriptionTimeout = null;
        }, this.config.subscriptionDelay);
    }
    #setupIntersectionObserver() {
        this.#observer = new IntersectionObserver(entries => {
            entries.forEach(entry => {
                const element = entry.target;
                const ticker = element.getAttribute(this.config.tickerAttribute);
                const timeperiod = element.getAttribute(this.config.timeperiodAttribute) || this.config.defaultTimeframe;
                const isPersistent = this.#isPersistentElement(element);
                if (!ticker) return;
                const symbolKey = `${ticker}_${timeperiod}`;
                if (entry.isIntersecting) {
                    this.#pendingSubscriptions.set(symbolKey, {
                        ticker: ticker,
                        timeperiod: timeperiod,
                        action: "subscribe",
                        isPersistent: isPersistent
                    });
                } else {
                    if (!isPersistent) {
                        this.#pendingSubscriptions.set(symbolKey, {
                            ticker: ticker,
                            timeperiod: timeperiod,
                            action: "unsubscribe",
                            isPersistent: isPersistent
                        });
                    } else {
                        this.#pendingSubscriptions.delete(symbolKey);
                    }
                }
            });
            this.#resetSubscriptionDebounce();
        }, this.config.observerConfig);
    }
    #processExistingElements() {
        const root = this.config.rootNode || document;
        let allTickerElements;
        if (this.config.scopeContainer) {
            const containerElement = root.querySelector(this.config.scopeContainer);
            if (!containerElement) {
                return;
            }
            allTickerElements = containerElement.querySelectorAll(this.config.tickerSelector);
        } else {
            allTickerElements = root.querySelectorAll(this.config.tickerSelector);
        }
        let subscribedCount = 0;
        let persistentCount = 0;
        for (let i = 0; i < allTickerElements.length; i++) {
            const element = allTickerElements[i];
            const ticker = element.getAttribute(this.config.tickerAttribute);
            const isPersistent = this.#isPersistentElement(element);
            if (isPersistent) {
                persistentCount++;
            }
            if (isPersistent && this.isConnected()) {
                const timeperiod = element.getAttribute(this.config.timeperiodAttribute) || this.config.defaultTimeframe;
                if (ticker) {
                    const subscriptionKey = `${ticker}_${isPersistent ? "1" : "0"}_${timeperiod}`;
                    const symbolKey = `${ticker}_${timeperiod}`;
                    if (!this.#processedSubscriptions.has(subscriptionKey)) {
                        this.#processedSubscriptions.add(subscriptionKey);
                        if (!this.#visibleSymbols.has(symbolKey)) {
                            this.#visibleSymbols.add(symbolKey);
                            if (this.subscribe(ticker, timeperiod)) {
                                subscribedCount++;
                            }
                        }
                    }
                }
            }
            if (!isPersistent && !element.hasAttribute("data-ws-observed")) {
                element.setAttribute("data-ws-observed", "true");
                if (this.#observer) {
                    this.#observer.observe(element);
                }
            }
        }
    }
    #resetSubscriptionDebounce() {
        if (this.#subscriptionDebounceTimer) {
            clearTimeout(this.#subscriptionDebounceTimer);
        }
        this.#subscriptionDebounceTimer = setTimeout(() => {
            this.#processPendingSubscriptions();
            this.#subscriptionDebounceTimer = null;
        }, this.config.subscriptionDebounceDelay);
    }
    #processPendingSubscriptions() {
        if (this.#pendingSubscriptions.size === 0) {
            return;
        }
        const toSubscribe = [];
        const toUnsubscribe = [];
        for (const [symbolKey, change] of this.#pendingSubscriptions) {
            if (change.action === "subscribe") {
                toSubscribe.push({
                    symbolKey: symbolKey,
                    change: change
                });
            } else if (change.action === "unsubscribe") {
                toUnsubscribe.push({
                    symbolKey: symbolKey,
                    change: change
                });
            }
        }
        for (let i = 0; i < toUnsubscribe.length; i++) {
            const {symbolKey: symbolKey, change: change} = toUnsubscribe[i];
            if (this.#visibleSymbols.has(symbolKey)) {
                this.#visibleSymbols.delete(symbolKey);
                this.unsubscribe(change.ticker, change.timeperiod);
            }
        }
        for (let i = 0; i < toSubscribe.length; i++) {
            const {symbolKey: symbolKey, change: change} = toSubscribe[i];
            if (!this.#visibleSymbols.has(symbolKey)) {
                this.#visibleSymbols.add(symbolKey);
                this.subscribe(change.ticker, change.timeperiod);
            }
        }
        this.#pendingSubscriptions.clear();
    }
    unsubscribeAll() {
        if (!this.isConnected()) {
            return false;
        }
        const success = this.#connection.removeAll();
        if (this.#subscriptionDebounceTimer) {
            clearTimeout(this.#subscriptionDebounceTimer);
            this.#subscriptionDebounceTimer = null;
        }
        this.#pendingSubscriptions.clear();
        this.#processedSubscriptions.clear();
        this.#visibleSymbols.clear();
        this._elementCache.clear();
        this._prices.clear();
        this._decimalPlacesCache.clear();
        for (const [key, timeoutId] of this.#animationTimeouts) {
            clearTimeout(timeoutId);
        }
        this.#animationTimeouts.clear();
        return success;
    }
    #updatePrice(messageData, skipAnimation = false) {
        if (!messageData || !messageData.symbol || !messageData.prices) return;
        if (document.hidden) return;
        const symbol = messageData.symbol.replace(/[^a-zA-Z0-9]/g, "_");
        let prices = messageData.prices;
        let cachedData = this._decimalPlacesCache.get(symbol);
        let changeDigits;
        if (cachedData !== undefined && cachedData.stable) {
            changeDigits = cachedData.digits;
        } else if (prices.c !== undefined && prices.c !== null && prices.c !== 0) {
            const closeStr = String(prices.c);
            if (/[eE]/.test(closeStr)) {
                changeDigits = cachedData?.digits || this.config.decimalPlaces;
            } else {
                const closeParts = closeStr.split(".");
                const newDigits = closeParts.length > 1 ? closeParts[1].length : cachedData?.digits ?? 0;
                if (cachedData === undefined) {
                    changeDigits = newDigits;
                    this._decimalPlacesCache.set(symbol, {
                        digits: newDigits,
                        updateCount: 1,
                        stable: false
                    });
                } else {
                    const newUpdateCount = cachedData.updateCount + 1;
                    const finalDigits = newDigits > cachedData.digits ? newDigits : cachedData.digits;
                    changeDigits = finalDigits;
                    this._decimalPlacesCache.set(symbol, {
                        digits: finalDigits,
                        updateCount: newUpdateCount,
                        stable: newUpdateCount >= 5
                    });
                }
            }
        } else {
            changeDigits = cachedData?.digits || this.config.decimalPlaces;
        }
        let sessionPrefix = "";
        const priceMode = prices?.mode;
        if (priceMode === "pre_market") {
            sessionPrefix = "PRE_";
        } else if (priceMode === "post_market") {
            sessionPrefix = "POST_";
        } else {
            const cachedSession = this._symbolSessions.get(symbol);
            if (cachedSession === "pre_market") {
                sessionPrefix = "PRE_";
            } else if (cachedSession === "post_market") {
                sessionPrefix = "POST_";
            }
        }
        if (priceMode === "askbid") {
            if (sessionPrefix) {
                prices = {
                    a: prices.a,
                    b: prices.b,
                    c: (prices.a + prices.b) / 2,
                    update: Math.floor(Date.now() / 1e3),
                    v: prices.v
                };
            } else {
                prices = {
                    a: prices.a,
                    b: prices.b,
                    t: prices.t,
                    update: prices.update,
                    v: prices.v
                };
            }
        }
        const timeperiod = messageData.timeframe || this.config.defaultTimeframe;
        const {animated: animated, normal: normal} = this.config.priceFields;
        const domUpdates = [];
        const priceKeys = Object.keys(prices);
        for (let i = 0; i < priceKeys.length; i++) {
            const field = priceKeys[i];
            let newValue = prices[field];
            if (newValue === undefined || newValue === null) {
                if (!skipAnimation && field === "update") {
                    newValue = Math.floor(Date.now() / 1e3);
                } else {
                    continue;
                }
            }
            const isAnimatedField = animated.includes(field);
            const isNormalField = normal.includes(field);
            if (!isAnimatedField && !isNormalField) {
                continue;
            }
            const className = this.#generateClassName(symbol, field, timeperiod, sessionPrefix);
            const oldPrice = this._prices.get(className);
            if (oldPrice === newValue) {
                continue;
            }
            this._prices.set(className, newValue);
            let elements = this._elementCache.get(className);
            if (!elements) {
                const searchRoot = this.config.rootNode || document;
                const elementsCollection = searchRoot.querySelectorAll(`.${className}`);
                if (elementsCollection.length === 0) continue;
                if (elementsCollection.length === 1) {
                    elements = [ elementsCollection[0] ];
                } else {
                    elements = Array.from(elementsCollection);
                }
                this._elementCache.set(className, elements);
            }
            if (elements.length === 0) continue;
            const firstElement = elements[0];
            const oldValueText = firstElement.textContent;
            if (isNormalField) {
                const formattedValue = this.#formatValue(newValue, field, changeDigits);
                if (firstElement.textContent !== formattedValue) {
                    domUpdates.push({
                        elements: elements,
                        type: "text",
                        value: formattedValue
                    });
                }
                continue;
            }
            const oldValue = parseFloat(oldValueText.replace(/,/g, ""));
            const matchResult = this.#matchDecimalPlaces(oldValueText, newValue, changeDigits);
            const adjustedNewValueStr = String(matchResult.value);
            const isScientific = matchResult.isScientific;
            let animationClass = null;
            if (!skipAnimation && !isNaN(oldValue)) {
                if (isScientific) {
                    if (oldValueText.replace(/,/g, "") !== adjustedNewValueStr) {
                        animationClass = newValue > oldValue ? this.config.animationClasses.positive : this.config.animationClasses.negative;
                    }
                } else {
                    if (oldValue !== 0) {
                        const difference = Math.abs(newValue - oldValue);
                        if (difference > 1e-7) {
                            animationClass = newValue > oldValue ? this.config.animationClasses.positive : this.config.animationClasses.negative;
                        }
                    }
                }
            }
            if (animationClass) {
                const timeoutKey = `${symbol}_${field}`;
                if (this.config.animateFullValue || this.config.animateBgFlash) {
                    const formattedValue = addThousandSeparator(adjustedNewValueStr);
                    domUpdates.push({
                        elements: elements,
                        type: "animate-direct",
                        value: formattedValue,
                        animationClass: animationClass,
                        timeoutKey: timeoutKey
                    });
                } else {
                    const wrappedHtml = this.#wrapChangedPart(oldValueText, adjustedNewValueStr, animationClass);
                    domUpdates.push({
                        elements: elements,
                        type: "html",
                        value: wrappedHtml,
                        animationClass: animationClass,
                        timeoutKey: timeoutKey
                    });
                }
            } else {
                const formattedValue = addThousandSeparator(adjustedNewValueStr);
                if (firstElement.textContent !== formattedValue) {
                    domUpdates.push({
                        elements: elements,
                        type: "text",
                        value: formattedValue
                    });
                }
            }
        }
        if (domUpdates.length > 0) {
            const textUpdates = [];
            const htmlUpdates = [];
            const animateDirectUpdates = [];
            for (let i = 0; i < domUpdates.length; i++) {
                const update = domUpdates[i];
                if (update.type === "text") {
                    textUpdates.push(update);
                } else if (update.type === "animate-direct") {
                    animateDirectUpdates.push(update);
                } else {
                    htmlUpdates.push(update);
                }
            }
            requestAnimationFrame(() => {
                for (let i = 0; i < textUpdates.length; i++) {
                    const update = textUpdates[i];
                    const elements = update.elements;
                    const value = update.value;
                    for (let j = 0; j < elements.length; j++) {
                        elements[j].textContent = value;
                    }
                }
                for (let i = 0; i < animateDirectUpdates.length; i++) {
                    const update = animateDirectUpdates[i];
                    const elements = update.elements;
                    const value = update.value;
                    const existingTimeout = this.#animationTimeouts.get(update.timeoutKey);
                    if (existingTimeout) {
                        clearTimeout(existingTimeout);
                    }
                    const bgClass = this.config.animateBgFlash ? update.animationClass === this.config.animationClasses.positive ? this.config.animationClasses.positiveBg : this.config.animationClasses.negativeBg : null;
                    const {positive: positive, negative: negative, neutral: neutral, positiveBg: positiveBg, negativeBg: negativeBg, neutralBg: neutralBg} = this.config.animationClasses;
                    for (let j = 0; j < elements.length; j++) {
                        const el = elements[j];
                        el.classList.remove(positive, negative, neutral);
                        if (this.config.animateBgFlash) {
                            el.classList.remove(positiveBg, negativeBg, neutralBg);
                        }
                        el.textContent = value;
                        el.classList.add(update.animationClass);
                        if (bgClass) {
                            el.classList.add(bgClass);
                        }
                    }
                    const timeoutId = setTimeout(() => {
                        for (let k = 0; k < elements.length; k++) {
                            elements[k].classList.remove(update.animationClass);
                            if (bgClass) {
                                elements[k].classList.remove(bgClass);
                            }
                        }
                        this.#animationTimeouts.delete(update.timeoutKey);
                    }, this.config.animationDuration);
                    this.#animationTimeouts.set(update.timeoutKey, timeoutId);
                }
                for (let i = 0; i < htmlUpdates.length; i++) {
                    const update = htmlUpdates[i];
                    const elements = update.elements;
                    const value = update.value;
                    const existingTimeout = this.#animationTimeouts.get(update.timeoutKey);
                    if (existingTimeout) {
                        clearTimeout(existingTimeout);
                    }
                    for (let j = 0; j < elements.length; j++) {
                        elements[j].innerHTML = value;
                    }
                    const timeoutId = setTimeout(() => {
                        for (let k = 0; k < elements.length; k++) {
                            const animSpan = elements[k].querySelector(`.${update.animationClass}`);
                            if (animSpan) {
                                animSpan.classList.remove(update.animationClass);
                            }
                        }
                        this.#animationTimeouts.delete(update.timeoutKey);
                    }, this.config.animationDuration);
                    this.#animationTimeouts.set(update.timeoutKey, timeoutId);
                }
            });
        }
        this.#updateChangeValues(symbol, prices, timeperiod, changeDigits, sessionPrefix);
    }
    #generateClassName(symbol, field, timeperiod, sessionPrefix = "") {
        let className = "";
        if (field === "updateTime") {
            className = `${symbol}_${sessionPrefix}updateTime_${timeperiod}`;
        } else if (field === "tm") {
            className = `${symbol}_${sessionPrefix}tm_${timeperiod}`;
        } else {
            className = `${symbol}_${sessionPrefix}${field}_${timeperiod}`;
        }
        return className.toUpperCase();
    }
    #formatValue(value, field, decimalPlaces) {
        if (field === "v") {
            return convertNumberToShortForm(value, true, decimalPlaces);
        } else if (field === "ch") {
            return convertNumberToShortForm(value);
        } else if (field === "chp") {
            return convertNumberToShortForm(value, true, 2) + "%";
        } else if (field === "update" || field === "tm" || field === "t") {
            return formatDateTime(value);
        } else {
            if (Number.isInteger(value)) return addThousandSeparator(String(value));
            return addThousandSeparator(String(value.toFixed(decimalPlaces)));
        }
    }
    #matchDecimalPlaces(oldValueStr, newValue, changeDigits) {
        const newValueStr = String(newValue);
        const isScientific = /[eE]/.test(newValueStr) || /[eE]/.test(oldValueStr);
        if (isScientific) {
            return {
                value: newValueStr,
                isScientific: true
            };
        }
        return {
            value: newValue.toFixed(changeDigits),
            isScientific: false
        };
    }
    #wrapChangedPart(oldValueStr, newValueStr, animationClass) {
        const isScientific = /[eE]/.test(oldValueStr) || /[eE]/.test(newValueStr);
        if (isScientific) {
            return this.#wrapScientificNotation(oldValueStr, newValueStr, animationClass);
        }
        const oldClean = oldValueStr.replace(/,/g, "");
        const newClean = newValueStr.replace(/,/g, "");
        if (oldClean === newClean) {
            return addThousandSeparator(newClean);
        }
        let diffIndex = -1;
        const maxLen = Math.max(oldClean.length, newClean.length);
        for (let i = 0; i < maxLen; i++) {
            if (oldClean[i] !== newClean[i]) {
                diffIndex = i;
                break;
            }
        }
        if (diffIndex === -1) {
            return addThousandSeparator(newClean);
        }
        const unchangedCleanPart = newClean.substring(0, diffIndex);
        const fullFormattedValue = addThousandSeparator(newClean);
        let formattedUnchanged = "";
        let formattedChanged = "";
        if (unchangedCleanPart.length > 0) {
            let digitCount = 0;
            let splitIndex = 0;
            for (let i = 0; i < fullFormattedValue.length; i++) {
                if (fullFormattedValue[i] !== "," && fullFormattedValue[i] !== ".") {
                    digitCount++;
                }
                if (digitCount === unchangedCleanPart.replace(".", "").length) {
                    splitIndex = i + 1;
                    break;
                }
            }
            formattedUnchanged = fullFormattedValue.substring(0, splitIndex);
            formattedChanged = fullFormattedValue.substring(splitIndex);
        } else {
            formattedChanged = fullFormattedValue;
        }
        return formattedUnchanged + `<span class="${animationClass}">${formattedChanged}</span>`;
    }
    #wrapScientificNotation(oldValueStr, newValueStr, animationClass) {
        const oldClean = String(oldValueStr).replace(/,/g, "");
        const newClean = String(newValueStr);
        if (oldClean === newClean) {
            return newClean;
        }
        const oldParts = oldClean.split(/([eE][+-]?\d+)/);
        const newParts = newClean.split(/([eE][+-]?\d+)/);
        const oldMantissa = oldParts[0] || "";
        const oldExponent = oldParts[1] || "";
        const newMantissa = newParts[0] || "";
        const newExponent = newParts[1] || "";
        let result = "";
        let mantissaDiffIndex = -1;
        for (let i = 0; i < Math.max(oldMantissa.length, newMantissa.length); i++) {
            if (oldMantissa[i] !== newMantissa[i]) {
                mantissaDiffIndex = i;
                break;
            }
        }
        if (mantissaDiffIndex !== -1) {
            const unchangedPart = newMantissa.substring(0, mantissaDiffIndex);
            const changedPart = newMantissa.substring(mantissaDiffIndex);
            result = unchangedPart + `<span class="${animationClass}">${changedPart}${newExponent}</span>`;
        } else if (oldExponent !== newExponent) {
            result = newMantissa + `<span class="${animationClass}">${newExponent}</span>`;
        } else {
            result = newClean;
        }
        return result;
    }
    #getHeatmapClass(changePercent) {
        const absChange = Math.abs(changePercent);
        const thresholds = this.config.heatmapThresholds;
        const {positive: positive, negative: negative, neutral: neutral} = this.config.heatmapClasses;
        if (absChange < .01) {
            return neutral;
        }
        let level = 0;
        for (let i = 0; i < thresholds.length; i++) {
            if (absChange >= thresholds[i]) {
                level = i + 1;
            }
        }
        level = Math.min(level, 4);
        if (changePercent > 0) {
            return positive[level];
        } else {
            return negative[level];
        }
    }
    #updateChangeValues(symbol, prices, timeperiod, changeDigits, sessionPrefix = "") {
        if (prices.ch === undefined || prices.chp === undefined) {
            return;
        }
        const changeClassName = `${symbol}_${sessionPrefix}CH_${timeperiod}`;
        const changePercentClassName = `${symbol}_${sessionPrefix}CHP_${timeperiod}`;
        const searchRoot = this.config.rootNode || document;
        let changeElements = this._elementCache.get(changeClassName);
        if (!changeElements) {
            const elements = searchRoot.querySelectorAll(`.${changeClassName}`);
            if (elements.length > 0) {
                changeElements = Array.from(elements);
                this._elementCache.set(changeClassName, changeElements);
            }
        }
        let changePercentElements = this._elementCache.get(changePercentClassName);
        if (!changePercentElements) {
            const elements = searchRoot.querySelectorAll(`.${changePercentClassName}`);
            if (elements.length > 0) {
                changePercentElements = Array.from(elements);
                this._elementCache.set(changePercentClassName, changePercentElements);
            }
        }
        if ((!changeElements || changeElements.length === 0) && (!changePercentElements || changePercentElements.length === 0)) {
            return;
        }
        const change = prices.ch;
        const changePercent = prices.chp;
        let statusClass;
        if (this.config.heatmapMode) {
            statusClass = this.#getHeatmapClass(changePercent);
        } else {
            statusClass = this.config.animationClasses.neutral;
            if (change > 0) statusClass = this.config.animationClasses.positive; else if (change < 0) statusClass = this.config.animationClasses.negative;
        }
        const updates = [];
        if (changeElements && changeElements.length > 0) {
            const isScientificNotation = /[eE]/.test(String(change));
            let formattedChange;
            if (change === 0) {
                formattedChange = '<span class="sign"></span>' + (0).toFixed(changeDigits);
            } else if (isScientificNotation || Math.abs(change) < 1e-6) {
                const sign = change > 0 ? "+" : "−";
                formattedChange = `<span class="sign">${sign}</span>` + Math.abs(change).toExponential(3);
            } else {
                const sign = change > 0 ? "+" : "−";
                formattedChange = `<span class="sign">${sign}</span>` + convertNumberToShortForm(Math.abs(change), true, changeDigits);
            }
            const firstEl = changeElements[0];
            if (firstEl.innerHTML !== formattedChange) {
                updates.push({
                    elements: changeElements,
                    html: formattedChange,
                    statusClass: statusClass
                });
            }
        }
        if (changePercentElements && changePercentElements.length > 0) {
            let formattedPercent;
            if (changePercent === 0) {
                formattedPercent = '<span class="sign"></span>0.00%';
            } else {
                const sign = changePercent > 0 ? "+" : "−";
                formattedPercent = `<span class="sign">${sign}</span>` + convertNumberToShortForm(Math.abs(changePercent), true, 2) + "%";
            }
            const firstEl = changePercentElements[0];
            if (firstEl.innerHTML !== formattedPercent) {
                updates.push({
                    elements: changePercentElements,
                    html: formattedPercent,
                    statusClass: statusClass
                });
            }
        }
        if (updates.length > 0) {
            let classesToRemove;
            if (this.config.heatmapMode) {
                const {positive: positive, negative: negative, neutral: neutral} = this.config.heatmapClasses;
                const allHeatmapClasses = [ ...positive, ...negative, neutral ];
                classesToRemove = allHeatmapClasses.map(c => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
            } else {
                const {positive: positive, negative: negative, neutral: neutral} = this.config.animationClasses;
                classesToRemove = [ positive, negative, neutral ].filter(Boolean).map(c => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
            }
            const removeClassRegex = classesToRemove ? new RegExp(`(${classesToRemove})`, "g") : null;
            requestAnimationFrame(() => {
                for (let i = 0; i < updates.length; i++) {
                    const update = updates[i];
                    const elements = update.elements;
                    for (let j = 0; j < elements.length; j++) {
                        const el = elements[j];
                        if (update.html !== undefined) {
                            el.innerHTML = update.html;
                        } else {
                            el.textContent = update.text;
                        }
                        if (removeClassRegex) {
                            el.className = el.className.replace(removeClassRegex, "").replace(/\s+/g, " ").trim() + " " + update.statusClass;
                        } else {
                            el.classList.add(update.statusClass);
                        }
                    }
                }
            });
        }
    }
    #handleProfileMessage(messageData) {
        if (!messageData?.prices?.mode || messageData.prices.mode !== "profile") {
            return;
        }
        const profile = messageData.prices.profile;
        if (!profile) return;
        const currentSession = profile.current_session;
        const symbol = messageData.symbol;
        const timeframe = messageData.timeframe || this.config.defaultTimeframe;
        if (!currentSession || !symbol) return;
        const symbolKey = symbol.replace(/[^a-zA-Z0-9]/g, "_");
        const previousSession = this._symbolSessions.get(symbolKey);
        if (previousSession === currentSession) {
            return;
        }
        this._symbolSessions.set(symbolKey, currentSession);
        if (typeof this.config.onProfileChange === "function") {
            this.config.onProfileChange({
                symbol: symbol,
                symbolKey: symbolKey,
                timeframe: timeframe,
                currentSession: currentSession,
                previousSession: previousSession || null,
                profile: profile
            });
        }
    }
    getSymbolSession(symbol) {
        const symbolKey = symbol.replace(/[^a-zA-Z0-9]/g, "_");
        return this._symbolSessions.get(symbolKey) || null;
    }
    #updateHolidayStatus(symbolKey) {
        const className = `${symbolKey}_market_session`;
        const elements = document.querySelectorAll(`.${className}`);
        if (elements.length === 0) return;
        for (const el of elements) {
            if (el.classList.contains("label-tag-gray") && el.querySelector('use[href="#market-holiday"]')) {
                continue;
            }
            el.classList.remove("skeleton", "skeleton-badge-icon");
            el.className = el.className.replace(/label-tag-\w+/g, "").trim();
            if (!el.classList.contains("market-status")) el.classList.add("market-status");
            if (!el.classList.contains("asset-badge")) el.classList.add("asset-badge");
            el.classList.add("label-tag-gray");
            el.setAttribute("data-tooltip-title", "Holiday");
            el.innerHTML = `<svg width="22" height="22" fill="none" stroke="currentColor"><use href="#market-holiday"></use></svg>`;
        }
    }
}

export { WebSocketManager };

if (typeof window !== "undefined") {
    window.WebSocketManager = WebSocketManager;
}