core/iframe-bridge.js

/**
 * @fileoverview IframeBridge — Einheitliches Kommunikationssystem für iframe-basierte Module.
 *
 * Ersetzt das bisherige Mischsystem aus URL-Parametern, manuellen postMessage-Aufrufen
 * und globalem Keyboard-Forwarding durch ein strukturiertes Host/Client-Protokoll.
 *
 * Architektur:
 *   - IframeBridgeHost  (läuft in der Parent-Seite, steuert ein iframe)
 *   - IframeBridgeClient (läuft im iframe, kommuniziert mit Parent)
 *   - Beide nutzen ein gemeinsames Message-Schema mit Namespace:Action-Typen
 *
 * Lifecycle:
 *   1. Client sendet SYSTEM:READY
 *   2. Host empfängt → sendet CONFIG:INIT (+ flusht Queue)
 *   3. Client empfängt → wendet Config an → sendet SYSTEM:READY_ACK
 *   4. Host markiert ready=true, regulärer Betrieb beginnt
 *
 * Debug-Integration:
 *   Nutzt DebugConfig.log() mit den Domains CORE_IFRAME_BRIDGE, CORE_IFRAME_BRIDGE_HOST,
 *   CORE_IFRAME_BRIDGE_CLIENT. Aktivierung: DEBUG_CONFIG.DEBUG_CORE_IFRAME_BRIDGE = true
 *
 * @author Alexander Wolf
 * @version 1.0
 * @see docs/conventions/IFRAME_BRIDGE_PROTOCOL.md
 */

// ============================================================================
// SHARED PROTOCOL
// ============================================================================

/**
 * Gemeinsames Protokoll für Message-Validierung und -Erzeugung.
 * Wird intern von Host und Client genutzt.
 *
 * @namespace IframeBridgeProtocol
 */
const IframeBridgeProtocol = {

    /** @type {string} Marker-Feld zur Erkennung von Bridge-Messages */
    MARKER: '_bridge',

    /** @type {string} Bridge-Protokollversion */
    VERSION: '1.0',

    /**
     * Erzeugt ein standardisiertes Message-Objekt.
     *
     * @param {string} type - Hierarchischer Typ im Format NAMESPACE:ACTION
     * @param {Object} [payload={}] - Beliebiges Datenobjekt
     * @param {string} [sourceId=''] - Eindeutige Absender-Kennung
     * @returns {{ _bridge: true, version: string, type: string, payload: Object, sourceId: string, timestamp: number, messageId: string }}
     */
    createMessage(type, payload = {}, sourceId = '') {
        return {
            _bridge: true,
            version: this.VERSION,
            type,
            payload,
            sourceId,
            timestamp: Date.now(),
            messageId: this._generateId()
        };
    },

    /**
     * Prüft ob ein eingehendes Datenobjekt eine gültige Bridge-Message ist.
     *
     * @param {*} data - Das rohe event.data-Objekt
     * @returns {boolean}
     */
    isValidMessage(data) {
        return data && data._bridge === true && typeof data.type === 'string';
    },

    /**
     * Mapping von alten Message-Typen auf neue Namespace:Action-Typen.
     * Ermöglicht Abwärtskompatibilität während der Migration.
     *
     * @type {Object<string, string>}
     */
    LEGACY_TYPE_MAP: {
        'keydown':                  'INPUT:KEYBOARD',
        'gameState':                'GAME:STATE',
        'gameWon':                  'GAME:WON',
        'UPDATE_PARAMS':            'CONFIG:UPDATE',
        'TREE_COMMAND':             'TREE:COMMAND',
        'TREE_READY':               'SYSTEM:READY',
        'NODE_CLICKED':             'TREE:NODE_CLICKED',
        'NODE_FOCUSED':             'TREE:NODE_FOCUSED',
        'NODE_EXPANSION_REQUEST':   'TREE:EXPANSION_REQUEST',
        'VIZ_READY':                'SYSTEM:READY',
        'VIZ_COMMAND':              'VIZ:COMMAND'
    },

    /**
     * Konvertiert eine Legacy-Message in das neue Format.
     * Gibt null zurück wenn keine Konvertierung möglich ist.
     *
     * @param {Object} data - Das rohe event.data-Objekt (ohne _bridge)
     * @returns {Object|null} Konvertierte Message oder null
     */
    convertLegacyMessage(data) {
        if (!data || !data.type) return null;

        const newType = this.LEGACY_TYPE_MAP[data.type];
        if (!newType) return null;

        // Payload extrahieren (alles außer 'type')
        const payload = {};
        for (const key of Object.keys(data)) {
            if (key !== 'type') {
                payload[key] = data[key];
            }
        }

        // Spezielle Konvertierungen
        if (data.type === 'keydown') {
            payload.eventType = 'keydown';
        }
        if (data.type === 'TREE_COMMAND' && data.command) {
            // TREE_COMMAND hat Payload in data.command
            return this.createMessage(newType, data.command, 'legacy');
        }

        return this.createMessage(newType, payload, 'legacy');
    },

    /**
     * Erzeugt eine kurze, eindeutige ID für Message-Korrelation.
     *
     * @private
     * @returns {string} z.B. 'msg_a1b2c3d4'
     */
    _generateId() {
        return 'msg_' + Math.random().toString(36).substring(2, 10);
    }
};


// ============================================================================
// IFRAME BRIDGE HOST (Parent-Seite)
// ============================================================================

/**
 * Host-seitige Bridge zur Steuerung eines iframes.
 * Läuft in der Parent-Seite und sendet Messages an das iframe.
 *
 * @class IframeBridgeHost
 *
 * @example
 * const bridge = new IframeBridgeHost(document.getElementById('gameFrame'), {
 *     sourceId: 'learning-path-01',
 *     forwardKeyboard: ['ArrowLeft', 'ArrowRight'],
 *     initConfig: {
 *         ui: { hideAi: true, hideBackBtn: true },
 *         runtime: { level: 2 }
 *     }
 * });
 *
 * bridge.on('GAME:WON', (payload) => {
 *     console.log('Gewonnen mit', payload.moves, 'Zügen');
 * });
 */
class IframeBridgeHost {

    /**
     * @param {HTMLIFrameElement} iframeElement - Das zu steuernde iframe-Element
     * @param {Object} [options={}] - Konfigurationsoptionen
     * @param {string} [options.sourceId='host'] - Eindeutige Kennung dieses Hosts
     * @param {boolean|string[]} [options.forwardKeyboard=false] - Keyboard-Forwarding (true=alle, Array=Filter)
     * @param {Object} [options.initConfig=null] - Config, die nach SYSTEM:READY gesendet wird
     * @param {Object} [options.initConfig.ui] - UI-Konfiguration (hideControls, etc.)
     * @param {Object} [options.initConfig.runtime] - Runtime-Konfiguration (level, maxDepth, etc.)
     * @param {number} [options.handshakeTimeout=5000] - Timeout für den Handshake in ms
     * @param {string} [options.targetOrigin=BRIDGE_TARGET_ORIGIN] - postMessage targetOrigin
     * @param {boolean} [options.acceptLegacy=true] - Legacy-Messages akzeptieren und konvertieren
     */
    constructor(iframeElement, options = {}) {
        if (!iframeElement || iframeElement.tagName !== 'IFRAME') {
            this._log('Constructor called without valid iframe element', { element: iframeElement }, 'error');
            throw new Error('IframeBridgeHost: Valid HTMLIFrameElement required');
        }

        /** @type {HTMLIFrameElement} */
        this.iframe = iframeElement;

        /** @type {string} */
        this.sourceId = options.sourceId || 'host';

        /** @type {string} Für lokale Entwicklung: '*'. Production: window.location.origin. */
        this.targetOrigin = options.targetOrigin || (typeof BRIDGE_TARGET_ORIGIN !== 'undefined' ? BRIDGE_TARGET_ORIGIN : '*');

        /** @type {boolean} */
        this.acceptLegacy = options.acceptLegacy !== undefined ? options.acceptLegacy : true;

        /** @type {boolean} Ist die Bridge betriebsbereit? */
        this.ready = false;

        /** @type {Object|null} Initiale Konfiguration */
        this._initConfig = options.initConfig || null;

        /** @type {number} Handshake-Timeout in ms */
        this._handshakeTimeout = options.handshakeTimeout || 5000;

        /** @type {Array<Object>} Gepufferte Messages (vor Handshake) */
        this._queue = [];

        /** @type {Map<string, Set<Function>>} Event-Listener */
        this._listeners = new Map();

        /** @type {Function|null} Gebundener Message-Listener für Cleanup */
        this._boundMessageHandler = null;

        /** @type {Function|null} Gebundener Keyboard-Listener für Cleanup */
        this._boundKeyboardHandler = null;

        /** @type {number|null} Handshake-Timeout-Timer */
        this._handshakeTimer = null;

        this._log('Initializing', { sourceId: this.sourceId, hasConfig: !!this._initConfig });

        // Message-Listener registrieren
        this._boundMessageHandler = (event) => this._handleMessage(event);
        window.addEventListener('message', this._boundMessageHandler);

        // Keyboard-Forwarding einrichten
        if (options.forwardKeyboard) {
            this._setupKeyboardForwarding(options.forwardKeyboard);
        }

        // Handshake-Timeout starten
        this._handshakeTimer = setTimeout(() => {
            if (!this.ready) {
                this._log('Handshake timeout — forcing ready state', {
                    timeout: this._handshakeTimeout,
                    queueSize: this._queue.length
                }, 'warn');
                this._setReady();
            }
        }, this._handshakeTimeout);
    }

    // ==================== PUBLIC API ====================

    /**
     * Sendet eine Message an das iframe.
     * Wird gepuffert falls die Bridge noch nicht bereit ist.
     *
     * @param {string} type - Hierarchischer Typ (z.B. 'CONFIG:UPDATE')
     * @param {Object} [payload={}] - Datenobjekt
     */
    send(type, payload = {}) {
        const message = IframeBridgeProtocol.createMessage(type, payload, this.sourceId);

        if (!this.ready) {
            this._log('Queueing message (not ready)', { type, queueSize: this._queue.length + 1 });
            this._queue.push(message);
            return;
        }

        this._postMessage(message);
    }

    /**
     * Registriert einen Event-Listener für einen Message-Typ.
     * Unterstützt Wildcard-Matching via Namespace-Prefix (z.B. 'GAME:' für alle GAME-Events).
     *
     * @param {string} type - Message-Typ oder Namespace-Prefix
     * @param {Function} callback - Callback(payload, fullMessage)
     * @returns {IframeBridgeHost} this (für Chaining)
     */
    on(type, callback) {
        if (!this._listeners.has(type)) {
            this._listeners.set(type, new Set());
        }
        this._listeners.get(type).add(callback);
        return this;
    }

    /**
     * Registriert einen einmaligen Event-Listener.
     *
     * @param {string} type - Message-Typ
     * @param {Function} callback - Callback(payload, fullMessage)
     * @returns {IframeBridgeHost} this (für Chaining)
     */
    once(type, callback) {
        const wrapper = (payload, message) => {
            this.off(type, wrapper);
            callback(payload, message);
        };
        wrapper._original = callback;
        return this.on(type, wrapper);
    }

    /**
     * Entfernt einen Event-Listener.
     *
     * @param {string} type - Message-Typ
     * @param {Function} [callback] - Spezifischer Callback (ohne: alle für diesen Typ)
     * @returns {IframeBridgeHost} this (für Chaining)
     */
    off(type, callback) {
        if (!callback) {
            this._listeners.delete(type);
        } else if (this._listeners.has(type)) {
            const set = this._listeners.get(type);
            set.delete(callback);
            // Auch wrapped once-Callbacks finden
            for (const fn of set) {
                if (fn._original === callback) {
                    set.delete(fn);
                    break;
                }
            }
        }
        return this;
    }

    /**
     * Gibt ein Promise zurück, das resolved wenn die Bridge bereit ist.
     *
     * @returns {Promise<void>}
     */
    waitForReady() {
        if (this.ready) return Promise.resolve();
        return new Promise((resolve) => {
            this.once('SYSTEM:READY_ACK', () => resolve());
            // Auch bei Timeout resolven
            const timer = setTimeout(() => resolve(), this._handshakeTimeout + 100);
            this.once('SYSTEM:READY_ACK', () => clearTimeout(timer));
        });
    }

    /**
     * Räumt alle Listener und Timer auf.
     * Muss aufgerufen werden wenn der Host nicht mehr benötigt wird.
     */
    destroy() {
        this._log('Destroying bridge');

        if (this._boundMessageHandler) {
            window.removeEventListener('message', this._boundMessageHandler);
            this._boundMessageHandler = null;
        }
        if (this._boundKeyboardHandler) {
            document.removeEventListener('keydown', this._boundKeyboardHandler);
            this._boundKeyboardHandler = null;
        }
        if (this._handshakeTimer) {
            clearTimeout(this._handshakeTimer);
            this._handshakeTimer = null;
        }

        this._listeners.clear();
        this._queue = [];
        this.ready = false;
    }

    // ==================== INTERNAL ====================

    /**
     * Verarbeitet eingehende postMessage-Events.
     *
     * @private
     * @param {MessageEvent} event
     */
    _handleMessage(event) {
        let message = null;

        if (IframeBridgeProtocol.isValidMessage(event.data)) {
            message = event.data;
        } else if (this.acceptLegacy) {
            message = IframeBridgeProtocol.convertLegacyMessage(event.data);
        }

        if (!message) return;

        this._log('Received', { type: message.type, sourceId: message.sourceId });

        // Handshake: SYSTEM:READY vom Client
        if (message.type === 'SYSTEM:READY' && !this.ready) {
            this._log('Client READY signal received — sending CONFIG:INIT');

            if (this._initConfig) {
                this._postMessage(
                    IframeBridgeProtocol.createMessage('CONFIG:INIT', this._initConfig, this.sourceId)
                );
            }
            // Queue sofort flushen
            this._setReady();
        }

        // Handshake: SYSTEM:READY_ACK vom Client
        if (message.type === 'SYSTEM:READY_ACK') {
            this._log('Client READY_ACK received — handshake complete');
        }

        // Event an Listener dispatchen
        this._dispatch(message);
    }

    /**
     * Dispatcht eine Message an registrierte Listener.
     * Prüft exakte Matches und Namespace-Prefix-Matches.
     *
     * @private
     * @param {Object} message
     */
    _dispatch(message) {
        const { type, payload } = message;

        // Exakter Match
        if (this._listeners.has(type)) {
            for (const cb of this._listeners.get(type)) {
                try {
                    cb(payload, message);
                } catch (err) {
                    this._log('Listener error', { type, error: err.message }, 'error');
                }
            }
        }

        // Namespace-Prefix-Match (z.B. 'GAME:' matcht 'GAME:WON')
        const namespace = type.split(':')[0] + ':';
        if (namespace !== type && this._listeners.has(namespace)) {
            for (const cb of this._listeners.get(namespace)) {
                try {
                    cb(payload, message);
                } catch (err) {
                    this._log('Listener error (namespace)', { type, error: err.message }, 'error');
                }
            }
        }

        // Wildcard
        if (this._listeners.has('*')) {
            for (const cb of this._listeners.get('*')) {
                try {
                    cb(payload, message);
                } catch (err) {
                    this._log('Listener error (wildcard)', { type, error: err.message }, 'error');
                }
            }
        }
    }

    /**
     * Sendet eine Message direkt via postMessage an das iframe.
     *
     * @private
     * @param {Object} message - Fertige Bridge-Message
     */
    _postMessage(message) {
        if (!this.iframe || !this.iframe.contentWindow) {
            this._log('Cannot post — iframe contentWindow not available', { type: message.type }, 'error');
            return;
        }
        this.iframe.contentWindow.postMessage(message, this.targetOrigin);
        this._log('Sent', { type: message.type, payloadKeys: Object.keys(message.payload) });
    }

    /**
     * Markiert die Bridge als bereit und flusht die Queue.
     *
     * @private
     */
    _setReady() {
        if (this.ready) return;
        this.ready = true;

        if (this._handshakeTimer) {
            clearTimeout(this._handshakeTimer);
            this._handshakeTimer = null;
        }

        // Queue flushen
        if (this._queue.length > 0) {
            this._log('Flushing queue', { count: this._queue.length });
            for (const msg of this._queue) {
                this._postMessage(msg);
            }
            this._queue = [];
        }
    }

    /**
     * Richtet automatisches Keyboard-Forwarding ein.
     *
     * @private
     * @param {boolean|string[]} config - true für alle Keys, Array für Filter
     */
    _setupKeyboardForwarding(config) {
        const keyFilter = Array.isArray(config) ? config : null;

        this._boundKeyboardHandler = (e) => {
            if (keyFilter && !keyFilter.includes(e.key)) return;

            this.send('INPUT:KEYBOARD', {
                key: e.key,
                code: e.code,
                shift: e.shiftKey,
                ctrl: e.ctrlKey,
                alt: e.altKey,
                eventType: 'keydown'
            });
        };

        document.addEventListener('keydown', this._boundKeyboardHandler);
        this._log('Keyboard forwarding enabled', { filter: keyFilter || 'all keys' });
    }

    /**
     * Internes Logging via DebugConfig.
     *
     * @private
     * @param {string} message - Log-Nachricht
     * @param {Object} [payload] - Strukturiertes Datenobjekt
     * @param {string} [level='debug'] - Log-Level
     */
    _log(message, payload, level = 'debug') {
        if (typeof DebugConfig !== 'undefined' && typeof DEBUG_DOMAINS !== 'undefined') {
            const domain = DEBUG_DOMAINS.CORE_IFRAME_BRIDGE_HOST || DEBUG_DOMAINS.CORE_IFRAME_BRIDGE || 'CORE_IFRAME_BRIDGE_HOST';
            if (payload) {
                DebugConfig.log(domain, level, `[IframeBridgeHost:${this.sourceId}] ${message}`, payload);
            } else {
                DebugConfig.log(domain, level, `[IframeBridgeHost:${this.sourceId}] ${message}`);
            }
        }
    }
}


// ============================================================================
// IFRAME BRIDGE CLIENT (iframe-Seite)
// ============================================================================

/**
 * Client-seitige Bridge, die im iframe läuft.
 * Kommuniziert mit dem Parent via window.parent.postMessage.
 *
 * @class IframeBridgeClient
 *
 * @example
 * const bridge = new IframeBridgeClient({ sourceId: 'rotatebox-game' });
 *
 * bridge.on('CONFIG:INIT', (config) => {
 *     if (config.ui?.hideControls) hideToolbar();
 *     if (config.runtime?.level) loadLevel(config.runtime.level);
 * });
 *
 * bridge.on('INPUT:KEYBOARD', (payload) => {
 *     if (payload.key === 'ArrowLeft') rotateLeft();
 * });
 *
 * // Spielstatus nach oben melden
 * bridge.send('GAME:WON', { moves: 6 });
 */
class IframeBridgeClient {

    /**
     * @param {Object} [options={}] - Konfigurationsoptionen
     * @param {string} [options.sourceId='client'] - Eindeutige Kennung dieses Clients
     * @param {string} [options.clientType='generic'] - Typ des Clients ('game', 'playground', 'viz')
     * @param {string} [options.targetOrigin=BRIDGE_TARGET_ORIGIN] - postMessage targetOrigin
     * @param {boolean} [options.acceptLegacy=true] - Legacy-Messages akzeptieren und konvertieren
     * @param {boolean} [options.autoReady=true] - Automatisch SYSTEM:READY senden
     * @param {string[]} [options.autoRelayToParent=[]] - Namespace-Prefixe, die automatisch an window.parent weitergeleitet werden (z.B. ['TREE:', 'NODE:'])
     */
    constructor(options = {}) {
        /** @type {string} */
        this.sourceId = options.sourceId || 'client';

        /** @type {string} */
        this.clientType = options.clientType || 'generic';

        /** @type {string} Für lokale Entwicklung: '*'. Production: window.location.origin. */
        this.targetOrigin = options.targetOrigin || (typeof BRIDGE_TARGET_ORIGIN !== 'undefined' ? BRIDGE_TARGET_ORIGIN : '*');

        /** @type {boolean} */
        this.acceptLegacy = options.acceptLegacy !== undefined ? options.acceptLegacy : true;

        /** @type {boolean} Ist die Bridge betriebsbereit? */
        this.ready = false;

        /** @type {Object} Empfangene CONFIG:INIT-Daten */
        this.config = {};

        /** @type {string[]} Namespace-Prefixe für Auto-Relay an window.parent (3-Ebenen-Modell) */
        this._autoRelayPrefixes = options.autoRelayToParent || [];

        /** @type {Map<string, Set<Function>>} Event-Listener */
        this._listeners = new Map();

        /** @type {Function|null} Gebundener Message-Listener */
        this._boundMessageHandler = null;

        /** @type {boolean} Läuft in einem iframe? */
        this._isInIframe = (window.parent !== window);

        this._log('Initializing', {
            sourceId: this.sourceId,
            clientType: this.clientType,
            isInIframe: this._isInIframe,
            autoRelayPrefixes: this._autoRelayPrefixes
        });

        // Message-Listener registrieren
        this._boundMessageHandler = (event) => this._handleMessage(event);
        window.addEventListener('message', this._boundMessageHandler);

        // Automatisch SYSTEM:READY senden
        if (options.autoReady !== false) {
            // Kurz warten bis DOM stabil ist
            if (document.readyState === 'complete') {
                this._sendReady();
            } else {
                window.addEventListener('load', () => this._sendReady());
            }
        }
    }

    // ==================== PUBLIC API ====================

    /**
     * Sendet eine Message an den Parent.
     * Im Standalone-Betrieb (kein iframe) wird die Message ignoriert.
     *
     * @param {string} type - Hierarchischer Typ (z.B. 'GAME:WON')
     * @param {Object} [payload={}] - Datenobjekt
     */
    send(type, payload = {}) {
        if (!this._isInIframe) {
            this._log('Not in iframe — message discarded', { type });
            return;
        }

        const message = IframeBridgeProtocol.createMessage(type, payload, this.sourceId);
        window.parent.postMessage(message, this.targetOrigin);
        this._log('Sent to parent', { type, payloadKeys: Object.keys(payload) });
    }

    /**
     * Registriert einen Event-Listener für einen Message-Typ.
     *
     * @param {string} type - Message-Typ oder Namespace-Prefix
     * @param {Function} callback - Callback(payload, fullMessage)
     * @returns {IframeBridgeClient} this (für Chaining)
     */
    on(type, callback) {
        if (!this._listeners.has(type)) {
            this._listeners.set(type, new Set());
        }
        this._listeners.get(type).add(callback);
        return this;
    }

    /**
     * Registriert einen einmaligen Event-Listener.
     *
     * @param {string} type - Message-Typ
     * @param {Function} callback - Callback(payload, fullMessage)
     * @returns {IframeBridgeClient} this (für Chaining)
     */
    once(type, callback) {
        const wrapper = (payload, message) => {
            this.off(type, wrapper);
            callback(payload, message);
        };
        wrapper._original = callback;
        return this.on(type, wrapper);
    }

    /**
     * Entfernt einen Event-Listener.
     *
     * @param {string} type - Message-Typ
     * @param {Function} [callback] - Spezifischer Callback
     * @returns {IframeBridgeClient} this (für Chaining)
     */
    off(type, callback) {
        if (!callback) {
            this._listeners.delete(type);
        } else if (this._listeners.has(type)) {
            const set = this._listeners.get(type);
            set.delete(callback);
            for (const fn of set) {
                if (fn._original === callback) {
                    set.delete(fn);
                    break;
                }
            }
        }
        return this;
    }

    /**
     * Sendet manuell das SYSTEM:READY-Signal.
     * Normalerweise automatisch (autoReady=true).
     */
    sendReady() {
        this._sendReady();
    }

    /**
     * Räumt alle Listener auf.
     */
    destroy() {
        this._log('Destroying bridge');

        if (this._boundMessageHandler) {
            window.removeEventListener('message', this._boundMessageHandler);
            this._boundMessageHandler = null;
        }

        this._listeners.clear();
        this.ready = false;
    }

    // ==================== INTERNAL ====================

    /**
     * Sendet das SYSTEM:READY-Signal an den Parent.
     *
     * @private
     */
    _sendReady() {
        this._log('Sending SYSTEM:READY');
        this.send('SYSTEM:READY', { clientType: this.clientType });
    }

    /**
     * Verarbeitet eingehende postMessage-Events.
     *
     * @private
     * @param {MessageEvent} event
     */
    _handleMessage(event) {
        let message = null;

        if (IframeBridgeProtocol.isValidMessage(event.data)) {
            message = event.data;
        } else if (this.acceptLegacy) {
            message = IframeBridgeProtocol.convertLegacyMessage(event.data);
        }

        if (!message) return;

        this._log('Received', { type: message.type, sourceId: message.sourceId });

        // CONFIG:INIT verarbeiten
        if (message.type === 'CONFIG:INIT') {
            this.config = message.payload || {};
            this.ready = true;
            this._log('Config applied', { config: this.config });

            // READY_ACK senden
            this.send('SYSTEM:READY_ACK', {});
        }

        // Auto-Relay: Bestimmte Namespaces automatisch an window.parent weiterleiten
        // Ermöglicht 3-Ebenen-Kommunikation (TreeViz → Playground → Learning Path)
        if (this._autoRelayPrefixes.length > 0 && this._isInIframe) {
            const shouldRelay = this._autoRelayPrefixes.some(prefix => message.type.startsWith(prefix));
            if (shouldRelay) {
                const relayPayload = {
                    ...message.payload,
                    relayedFrom: message.sourceId || 'unknown'
                };
                const relayMessage = IframeBridgeProtocol.createMessage(
                    message.type, relayPayload, this.sourceId
                );
                window.parent.postMessage(relayMessage, this.targetOrigin);
                this._log('Auto-relayed to parent', {
                    type: message.type,
                    relayedFrom: message.sourceId,
                    via: this.sourceId
                });
            }
        }

        // An Listener dispatchen
        this._dispatch(message);
    }

    /**
     * Dispatcht eine Message an registrierte Listener.
     *
     * @private
     * @param {Object} message
     */
    _dispatch(message) {
        const { type, payload } = message;

        // Exakter Match
        if (this._listeners.has(type)) {
            for (const cb of this._listeners.get(type)) {
                try {
                    cb(payload, message);
                } catch (err) {
                    this._log('Listener error', { type, error: err.message }, 'error');
                }
            }
        }

        // Namespace-Prefix-Match
        const namespace = type.split(':')[0] + ':';
        if (namespace !== type && this._listeners.has(namespace)) {
            for (const cb of this._listeners.get(namespace)) {
                try {
                    cb(payload, message);
                } catch (err) {
                    this._log('Listener error (namespace)', { type, error: err.message }, 'error');
                }
            }
        }

        // Wildcard
        if (this._listeners.has('*')) {
            for (const cb of this._listeners.get('*')) {
                try {
                    cb(payload, message);
                } catch (err) {
                    this._log('Listener error (wildcard)', { type, error: err.message }, 'error');
                }
            }
        }
    }

    /**
     * Internes Logging via DebugConfig.
     *
     * @private
     * @param {string} message - Log-Nachricht
     * @param {Object} [payload] - Strukturiertes Datenobjekt
     * @param {string} [level='debug'] - Log-Level
     */
    _log(message, payload, level = 'debug') {
        if (typeof DebugConfig !== 'undefined' && typeof DEBUG_DOMAINS !== 'undefined') {
            const domain = DEBUG_DOMAINS.CORE_IFRAME_BRIDGE_CLIENT || DEBUG_DOMAINS.CORE_IFRAME_BRIDGE || 'CORE_IFRAME_BRIDGE_CLIENT';
            if (payload) {
                DebugConfig.log(domain, level, `[IframeBridgeClient:${this.sourceId}] ${message}`, payload);
            } else {
                DebugConfig.log(domain, level, `[IframeBridgeClient:${this.sourceId}] ${message}`);
            }
        }
    }
}


// ============================================================================
// GLOBAL EXPORTS
// ============================================================================

if (typeof window !== 'undefined') {
    window.IframeBridgeProtocol = IframeBridgeProtocol;
    window.IframeBridgeHost = IframeBridgeHost;
    window.IframeBridgeClient = IframeBridgeClient;
}