<template>
    <DialogWidget ref="dialog" :title="dialogTitle" :modal="false" :show-close="true"
        @close="onDialogClose" width="75vw" height="75vh" name="MultiChatDialog" icon="multichat24.png">
        <div class="dialog-widget-content">
            <div class="multi-chat-top">
                <div class="room-left-controls">
                    <select v-model="selectedRoom" class="room-select">
                    <option v-for="room in rooms" :key="room.id" :value="room.id">{{ room.title }}</option>
                    </select>
                    <button v-if="!adultOnlyMode" class="create-room-toggle-btn" type="button" @click="toggleRoomCreatePanel">
                        {{ showRoomCreatePanel ? $t('chat.multichat.createRoom.toggleShowChat') : $t('chat.multichat.createRoom.toggleCreateRoom') }}
                    </button>
                </div>
                <div class="right-controls">
                    <div class="status" :class="statusType">
                        <span class="dot"></span>
                        <span class="text">{{ statusText }}</span>
                        <button v-if="statusType === 'error' || statusType === 'disconnected'" class="reconnect-btn"
                            @click="connectChatSocket" type="button">↻</button>
                    </div>
                    <div class="options-dropdown-wrapper" ref="optionsWrap">
                        <button class="options-btn" @click="onOptionsToggle" type="button">
                            {{ $t('chat.multichat.options') }}
                        </button>
                        <div v-if="showOptions" class="options-dropdown">
                            <label>
                                <input type="checkbox" v-model="autoscroll" />
                                {{ $t('chat.multichat.autoscroll') }}
                            </label>
                            <div class="opts-divider" v-if="isAdmin"></div>
                            <div class="opts-row" v-if="isAdmin">
                                <button class="opts-btn" type="button" @click="reloadRoomsAdmin">{{ $t('chat.multichat.reloadRooms') }}</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <div v-if="!showColorPicker" class="multi-chat-body">
                <div v-if="!showRoomCreatePanel" class="multi-chat-output" ref="output" @mouseenter="mouseOverOutput = true"
                    @mouseleave="mouseOverOutput = false">
                    <div v-for="msg in messages" :key="msg.id" class="chat-message">
                        <template v-if="msg.type === 'scream'">
                            <span class="scream-line" :style="msg.color ? { color: msg.color } : null">
                                <span class="user">{{ msg.user }}</span>
                                <span class="scream-label">{{ $t('chat.multichat.screamColon') }}</span>
                                <span class="text"> {{ (msg.text || '').toUpperCase() }}</span>
                            </span>
                        </template>
                        <template v-else-if="msg.type === 'action'">
                            <span class="user" :style="msg.color ? { color: msg.color } : null">{{ msg.user }}</span>
                            <span class="action-text">{{ msg.action }}</span>
                            <span class="action-target" :style="msg.toColor ? { color: msg.toColor } : null">{{ msg.to
                                }}</span>
                        </template>
                        <template v-else>
                            <span class="user" :style="msg.color ? { color: msg.color } : null">{{ msg.user }}:</span>
                            <span class="text">{{ msg.text }}</span>
                        </template>
                    </div>
                </div>
                <div v-else class="room-create-panel">
                    <div class="room-create-title">{{ $t('chat.multichat.createRoom.title') }}</div>
                    <div class="room-create-grid">
                        <label>
                            {{ $t('chat.multichat.createRoom.labels.roomName') }} *
                            <input v-model.trim="roomCreateForm.roomName" type="text" :placeholder="$t('chat.multichat.createRoom.placeholders.roomName')"
                                :class="{ 'invalid-input': roomCreateValidation.roomName }" />
                            <span v-if="roomCreateValidation.roomName" class="room-create-error">{{ roomCreateValidation.roomName }}</span>
                        </label>
                        <label>
                            {{ $t('chat.multichat.createRoom.labels.visibility') }}
                            <select v-model="roomCreateForm.visibility">
                                <option value="">{{ $t('chat.multichat.createRoom.options.none') }}</option>
                                <option value="public">{{ $t('chat.multichat.createRoom.options.visibilityPublic') }}</option>
                                <option value="private">{{ $t('chat.multichat.createRoom.options.visibilityPrivate') }}</option>
                            </select>
                        </label>
                        <label>
                            {{ $t('chat.multichat.createRoom.labels.gender') }}
                            <select v-model="roomCreateForm.gender">
                                <option value="">{{ $t('chat.multichat.createRoom.options.none') }}</option>
                                <option value="m">{{ $t('chat.multichat.createRoom.options.genderMale') }}</option>
                                <option value="f">{{ $t('chat.multichat.createRoom.options.genderFemale') }}</option>
                                <option value="any">{{ $t('chat.multichat.createRoom.options.genderAny') }}</option>
                            </select>
                        </label>
                        <label>
                            {{ $t('chat.multichat.createRoom.labels.minAge') }}
                            <input v-model.number="roomCreateForm.minAge" type="number" min="0"
                                :class="{ 'invalid-input': roomCreateValidation.minAge }" />
                            <span v-if="roomCreateValidation.minAge" class="room-create-error">{{ roomCreateValidation.minAge }}</span>
                        </label>
                        <label>
                            {{ $t('chat.multichat.createRoom.labels.maxAge') }}
                            <input v-model.number="roomCreateForm.maxAge" type="number" min="0"
                                :class="{ 'invalid-input': roomCreateValidation.maxAge }" />
                            <span v-if="roomCreateValidation.maxAge" class="room-create-error">{{ roomCreateValidation.maxAge }}</span>
                        </label>
                        <label>
                            {{ $t('chat.multichat.createRoom.labels.password') }}
                            <input v-model.trim="roomCreateForm.password" type="text" :placeholder="$t('chat.multichat.createRoom.placeholders.password')"
                                :class="{ 'invalid-input': roomCreateValidation.password }" />
                            <span v-if="roomCreateValidation.password" class="room-create-error">{{ roomCreateValidation.password }}</span>
                        </label>
                        <label>
                            {{ $t('chat.multichat.createRoom.labels.rightId') }}
                            <select v-model="roomCreateForm.rightId" :class="{ 'invalid-input': roomCreateValidation.rightId }">
                                <option :value="null">{{ $t('chat.multichat.createRoom.options.none') }}</option>
                                <option v-for="right in roomCreateRights" :key="`right-${right.id}`" :value="right.id">
                                    {{ getUserRightLabel(right) }}
                                </option>
                            </select>
                            <span v-if="roomCreateValidation.rightId" class="room-create-error">{{ roomCreateValidation.rightId }}</span>
                        </label>
                        <label>
                            {{ $t('chat.multichat.createRoom.labels.typeId') }}
                            <select v-model="roomCreateForm.typeId" :class="{ 'invalid-input': roomCreateValidation.typeId }">
                                <option :value="null">{{ $t('chat.multichat.createRoom.options.none') }}</option>
                                <option v-for="roomType in roomCreateTypes" :key="`type-${roomType.id}`" :value="roomType.id">
                                    {{ getRoomTypeLabel(roomType) }}
                                </option>
                            </select>
                            <span v-if="roomCreateValidation.typeId" class="room-create-error">{{ roomCreateValidation.typeId }}</span>
                        </label>
                        <label class="checkbox-label">
                            <input type="checkbox" v-model="roomCreateForm.friendsOnly" />
                            {{ $t('chat.multichat.createRoom.labels.friendsOnly') }}
                        </label>
                    </div>
                    <div class="room-create-actions">
                        <button type="button" class="send-btn" @click="sendCreateRoomCommand" :disabled="!canSendRoomCreate">{{ $t('chat.multichat.createRoom.actions.create') }}</button>
                        <button type="button" class="create-room-reset-btn" @click="resetRoomCreateForm">{{ $t('chat.multichat.createRoom.actions.reset') }}</button>
                    </div>
                    <div v-if="roomCreateValidation.range" class="room-create-error room-create-error-block">{{ roomCreateValidation.range }}</div>
                    <div class="room-create-preview">
                        {{ $t('chat.multichat.createRoom.commandPrefix') }}: <code>{{ buildRoomCreateCommandPreview() || $t('chat.multichat.commandPreviewFallback') }}</code>
                    </div>
                    <div class="owned-rooms-section">
                        <div class="owned-rooms-title">{{ $t('chat.multichat.createRoom.ownedRooms.title') }}</div>
                        <div class="owned-rooms-hint">{{ $t('chat.multichat.createRoom.ownedRooms.hint') }}</div>
                        <div v-if="!ownRooms.length" class="owned-rooms-empty">
                            {{ $t('chat.multichat.createRoom.ownedRooms.empty') }}
                        </div>
                        <div v-else class="owned-rooms-list">
                            <div v-for="room in ownRooms" :key="`own-room-${room.id}`" class="owned-room-item">
                                <div class="owned-room-main">
                                    <span class="owned-room-name">{{ room.title }}</span>
                                    <span class="owned-room-badge" :class="room.isPublic ? 'public' : 'private'">
                                        {{ room.isPublic ? $t('chat.multichat.createRoom.ownedRooms.public') : $t('chat.multichat.createRoom.ownedRooms.private') }}
                                    </span>
                                </div>
                                <button type="button" class="owned-room-delete-btn" @click="deleteOwnedRoom(room)">
                                    {{ $t('common.delete') }}
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="user-list">
                    <div class="user-list-header">{{ $t('chat.multichat.participantsWithCount', { count: usersInRoom.length }) }}</div>
                    <div class="user-list-items">
                        <div class="user-list-item" v-for="u in usersInRoom" :key="u.name"
                            :class="{ selected: selectedTargetUser === u.name }" @click="selectTargetUser(u.name)"
                            :title="$t('chat.multichat.clickToSelectUser')">
                            <span class="user-dot"
                                :style="{ backgroundColor: (u.color || userColors[u.name] || '#ccc') }"></span>
                            <span class="user-name"
                                :style="(u.color || userColors[u.name]) ? { color: (u.color || userColors[u.name]) } : null">{{
                                u.name
                                }}</span>
                        </div>
                    </div>
                </div>
            </div>
            <div v-if="!showColorPicker && passwordPromptVisible" class="room-password-panel">
                <div class="room-password-title">{{ $t('chat.multichat.password.title') }}</div>
                <div class="room-password-message">
                    {{ passwordPromptInvalid ? $t('chat.multichat.password.invalidPrompt', { room: passwordPromptRoom }) : $t('chat.multichat.password.requiredPrompt', { room: passwordPromptRoom }) }}
                </div>
                <div class="room-password-controls">
                    <input
                        v-model="passwordPromptValue"
                        class="room-password-input"
                        type="password"
                        :placeholder="$t('chat.multichat.password.inputLabel')"
                        @keyup.enter="submitRoomPassword"
                    />
                    <button type="button" class="send-btn" @click="submitRoomPassword">{{ $t('chat.multichat.password.submit') }}</button>
                    <button type="button" class="create-room-reset-btn" @click="cancelRoomPassword">{{ $t('chat.multichat.password.cancel') }}</button>
                </div>
            </div>
            <div v-if="!showColorPicker" class="multi-chat-input">
                <input v-model="input" @keyup.enter="sendMessage" class="chat-input"
                    :placeholder="$t('chat.multichat.placeholder')" />
                <button @click="sendMessage" class="send-btn">{{ $t('chat.multichat.send') }}</button>
                <img @click="shout" src="/images/icons/scream.png" class="icon-btn" alt="Schreien" title="Schreien" />
                <img @click="action" src="/images/icons/activity.png" class="icon-btn"
                    :class="{ disabled: !selectedTargetUser }" :alt="$t('chat.multichat.action')"
                    :title="selectedTargetUser ? $t('chat.multichat.action_to', { to: selectedTargetUser }) : $t('chat.multichat.action_select_user')" />
                <button type="button" class="send-btn report-btn" :disabled="!selectedTargetUser" @click="reportSelectedUser">
                    {{ $t('chat.multichat.report') }}
                </button>
                <img @click="roll" src="/images/icons/dice24.png" class="icon-btn" alt="Würfeln" title="Würfeln" />
                <img @click="openColorPicker" src="/images/icons/colorpicker.png" class="icon-btn"
                    :alt="$t('chat.multichat.colorpicker')" :title="$t('chat.multichat.colorpicker')" />
            </div>

            <!-- Inline Color Picker Panel -->
            <div v-else class="color-picker-panel">
                <div class="picker-grid">
                    <div class="picker-column">
                        <div class="palette-wrap" @mousedown="onPaletteDown" @mousemove="onPaletteMove"
                            @mouseup="onPaletteUp" @mouseleave="onPaletteUp">
                            <canvas ref="paletteCanvas" :width="paletteWidth" :height="paletteHeight"></canvas>
                            <div class="palette-marker" v-if="pickX !== null"
                                :style="{ left: (pickX - 6) + 'px', top: (pickY - 6) + 'px' }"></div>
                        </div>
                    </div>
                    <div class="picker-column">
                        <div class="picker-row">
                            <label class="picker-label">{{ $t('chat.multichat.hex') }}</label>
                            <input class="hex-input" v-model="hexInput" @input="onHexInput" placeholder="#AABBCC" />
                        </div>
                        <div v-if="hexInvalid" class="picker-error">{{ $t('chat.multichat.invalid_hex') }}</div>
                        <div class="picker-preview" :style="{ color: selectedColor }">
                            {{ $t('chat.multichat.colorpicker_preview') }}
                        </div>
                        <div class="picker-sample" :style="{ background: selectedColor }"></div>
                    </div>
                </div>
                <div class="picker-actions">
                    <button class="btn" @click="onColorOk">{{ $t('chat.multichat.ok') }}</button>
                    <button class="btn secondary" @click="onColorCancel">{{ $t('chat.multichat.cancel') }}</button>
                </div>
            </div>
        </div>
    </DialogWidget>
</template>

<script>
import DialogWidget from '@/components/DialogWidget.vue';
import { fetchPublicRooms, fetchRoomCreateOptions, fetchOwnRooms, reportChatIncident } from '@/api/chatApi.js';
import { mapGetters } from 'vuex';
import { getChatWsUrl, getChatWsCandidates, getChatWsProtocols } from '@/services/chatWs.js';
import { confirmAction } from '@/utils/feedback.js';

export default {
    name: 'MultiChat',
    components: { DialogWidget },
    computed: {
        ...mapGetters(['user', 'menu']),
        dialogTitle() {
            return this.adultOnlyMode ? this.$t('chat.multichat.eroticTitle') : this.$t('chat.multichat.title');
        },
        isAdmin() {
            // Infer admin via presence of administration section in menu (server filters by rights)
            try {
                return !!(this.menu && this.menu.administration);
            } catch (_) { return false; }
        },
        roomCreateValidation() {
            const errors = {};
            const name = (this.roomCreateForm.roomName || '').trim();
            const minAge = this.parseOptionalInteger(this.roomCreateForm.minAge);
            const maxAge = this.parseOptionalInteger(this.roomCreateForm.maxAge);
            const rightId = this.parseOptionalInteger(this.roomCreateForm.rightId);
            const typeId = this.parseOptionalInteger(this.roomCreateForm.typeId);
            const password = this.roomCreateForm.password || '';

            if (!name) errors.roomName = this.$t('chat.multichat.createRoom.validation.roomNameRequired');
            if (minAge !== null && minAge < 0) errors.minAge = this.$t('chat.multichat.createRoom.validation.minAgeInvalid');
            if (maxAge !== null && maxAge < 0) errors.maxAge = this.$t('chat.multichat.createRoom.validation.maxAgeInvalid');
            if (minAge !== null && maxAge !== null && minAge > maxAge) {
                errors.range = this.$t('chat.multichat.createRoom.validation.ageRangeInvalid');
            }
            if (password.includes(' ')) errors.password = this.$t('chat.multichat.createRoom.validation.passwordSpaces');
            if (rightId !== null && rightId <= 0) errors.rightId = this.$t('chat.multichat.createRoom.validation.rightIdInvalid');
            if (typeId !== null && typeId <= 0) errors.typeId = this.$t('chat.multichat.createRoom.validation.typeIdInvalid');

            return errors;
        },
        canSendRoomCreate() {
            return Object.keys(this.roomCreateValidation).length === 0;
        }
    },
    mounted() {
        // Reconnect when network returns
    try { window.addEventListener('online', this.onOnline); } catch (_) { }
    try { document.addEventListener('click', this.onGlobalClick); } catch (_) { }
    },
    beforeUnmount() {
        // Safety: ensure connection is shut down on page/navigation leave
        this.opened = false;
        if (this.pendingRoomCreateTimer) {
            clearTimeout(this.pendingRoomCreateTimer);
            this.pendingRoomCreateTimer = null;
        }
        this.disconnectChatSocket();
    try { window.removeEventListener('online', this.onOnline); } catch (_) { }
    try { document.removeEventListener('click', this.onGlobalClick); } catch (_) { }
    },
    data() {
        return {
            rooms: [],
            selectedRoom: null,
            autoscroll: true,
            mouseOverOutput: false,
            messages: [],
            usersInRoom: [],
            selectedTargetUser: null,
            userColors: {},
            input: '',
            showOptions: false,
            debug: true,
            chatSocket: null,
            transportConnected: false,
            chatConnected: false,
            reconnectTimer: null,
            opened: false,
            statusType: 'idle',
            statusText: '',
            urlOverride: '',
            token: null,
            announcedRoomEnter: false,
            lastAnnouncedRoomName: '',
            showColorPicker: false,
            showRoomCreatePanel: false,
            adultOnlyMode: false,
            selectedColor: '#000000',
            lastColor: '#000000',
            hexInput: '#000000',
            hexInvalid: false,
            roomCreateForm: {
                roomName: '',
                visibility: '',
                gender: '',
                minAge: null,
                maxAge: null,
                password: '',
                friendsOnly: false,
                rightId: null,
                typeId: null
            },
            roomCreateRights: [],
            roomCreateTypes: [],
            ownRooms: [],
            pendingRoomCreateName: '',
            pendingRoomCreateAttempts: 0,
            pendingRoomCreateTimer: null,
            roomPasswords: {},
            passwordPromptVisible: false,
            passwordPromptInvalid: false,
            passwordPromptRoom: '',
            passwordPromptValue: '',
            // Palette state
            paletteWidth: 420,
            paletteHeight: 220,
            pickX: null,
            pickY: null,
            isPicking: false,
            maxLightness: 92, // exclude extremely light colors (>% L)
            // Reconnect control
            reconnectIntervalMs: 3000,
            reconnectAttempts: 0,
            maxReconnectAttempts: 20,
            heartbeatInterval: null,
            heartbeatIntervalMs: 30000, // 30 seconds
            connectAttemptTimeout: null,
            joinFallbackTimer: null,
            roomSyncInFlight: false,
            currentRoomName: '',
            // Faster handshake watchdog separate from reconnect interval
            handshakeWatchdogMs: 2500,
            // Rotate to next candidate if a connection never reaches 'open'
            connectOpenTimer: null,
            candidateOpenTimeoutMs: 900,
            // Happy eyeballs state
            connectRacing: false,
            connectWinnerChosen: false,
            pendingWs: [],
            raceFailures: 0,
            raceTotal: 0,
            happyDelayMs: 40,
            // Join fallback delay if token is slow to arrive
            joinFallbackDelayMs: 120,
            // Default: only one connection attempt in parallel (prevents duplicate daemon sockets)
            raceLimit: 1
        };
    },
    // Hinweis: Öffnen erfolgt über methods.open(), damit Parent per Ref aufrufen kann
    watch: {
        messages() {
            this.$nextTick(this.handleAutoscroll);
        },
        autoscroll(val) {
            if (val) this.handleAutoscroll();
        },
        selectedRoom(newVal, oldVal) {
            if (newVal && this.transportConnected) {
                const room = this.getSelectedRoomName();
                if (room) this.sendWithToken({ type: 'join', room, password: this.getRoomPassword(room) });
                this.messages = [];
                this.usersInRoom = [];
                this.selectedTargetUser = null;
                this.passwordPromptVisible = false;
                this.passwordPromptInvalid = false;
                this.passwordPromptRoom = '';
                this.passwordPromptValue = '';
            }
        }
    },
    methods: {
        getDefaultRoomCreateForm() {
            return {
                roomName: '',
                visibility: '',
                gender: '',
                minAge: null,
                maxAge: null,
                password: '',
                friendsOnly: false,
                rightId: null,
                typeId: null
            };
        },
        toggleRoomCreatePanel() {
            this.showRoomCreatePanel = !this.showRoomCreatePanel;
            if (!this.showRoomCreatePanel) return;
            this.showColorPicker = false;
        },
        resetRoomCreateForm() {
            this.roomCreateForm = this.getDefaultRoomCreateForm();
        },
        clearPendingRoomCreateTracking() {
            if (this.pendingRoomCreateTimer) {
                clearTimeout(this.pendingRoomCreateTimer);
                this.pendingRoomCreateTimer = null;
            }
            this.pendingRoomCreateName = '';
            this.pendingRoomCreateAttempts = 0;
        },
        roomNamesEqual(a, b) {
            return (a || '').trim().toLowerCase() === (b || '').trim().toLowerCase();
        },
        getRoomPassword(roomName) {
            if (!roomName) return '';
            return this.roomPasswords[roomName] || '';
        },
        handleRoomPasswordError(errorCode) {
            const room = this.getSelectedRoomName();
            if (!room || this.passwordPromptVisible) return;
            this.passwordPromptRoom = room;
            this.passwordPromptInvalid = errorCode === 'room_password_invalid';
            this.passwordPromptValue = '';
            this.passwordPromptVisible = true;
        },
        submitRoomPassword() {
            const room = this.passwordPromptRoom || this.getSelectedRoomName();
            if (!room) {
                this.passwordPromptVisible = false;
                return;
            }
            const password = (this.passwordPromptValue || '').trim();
            if (!password) {
                this.messages.push({
                    id: Date.now(),
                    user: 'System',
                    text: this.$t('chat.multichat.password.empty')
                });
                return;
            }
            this.roomPasswords[room] = password;
            this.passwordPromptVisible = false;
            this.passwordPromptInvalid = false;
            this.passwordPromptRoom = '';
            this.passwordPromptValue = '';
            this.sendWithToken({ type: 'join', room, password });
        },
        cancelRoomPassword() {
            const room = this.passwordPromptRoom || this.getSelectedRoomName();
            this.passwordPromptVisible = false;
            this.passwordPromptInvalid = false;
            this.passwordPromptRoom = '';
            this.passwordPromptValue = '';
            this.messages.push({
                id: Date.now(),
                user: 'System',
                text: this.$t('chat.multichat.password.cancelled', { room })
            });
        },
        tryConfirmRoomCreateSuccess() {
            if (!this.pendingRoomCreateName) return false;
            const created = this.ownRooms.find((r) => this.roomNamesEqual(r.title, this.pendingRoomCreateName));
            if (!created) return false;
            this.messages.push({
                id: Date.now(),
                user: 'System',
                text: this.$t('chat.multichat.createRoom.messages.created', { room: created.title || this.pendingRoomCreateName })
            });
            this.clearPendingRoomCreateTracking();
            return true;
        },
        scheduleRoomCreateConfirmationCheck() {
            if (!this.pendingRoomCreateName) return;
            if (this.pendingRoomCreateAttempts >= 6) {
                this.messages.push({
                    id: Date.now(),
                    user: 'System',
                    text: this.$t('chat.multichat.createRoom.messages.createNotConfirmed', { room: this.pendingRoomCreateName })
                });
                this.clearPendingRoomCreateTracking();
                return;
            }
            if (this.pendingRoomCreateTimer) {
                clearTimeout(this.pendingRoomCreateTimer);
                this.pendingRoomCreateTimer = null;
            }
            this.pendingRoomCreateTimer = setTimeout(async () => {
                this.pendingRoomCreateAttempts += 1;
                await this.loadOwnRooms();
                if (!this.tryConfirmRoomCreateSuccess()) {
                    this.scheduleRoomCreateConfirmationCheck();
                }
            }, 1200);
        },
        buildRoomCreateCommandPreview() {
            return this.buildRoomCreateCommand();
        },
        getUserRightLabel(right) {
            const raw = (right?.title || '').trim();
            if (!raw) return right?.id ?? '';
            const candidates = [
                `chat.multichat.createRoom.rights.${raw}`,
                `navigation.${raw}`,
                `navigation.m-administration.${raw}`
            ];
            for (const key of candidates) {
                if (this.$te(key)) return this.$t(key);
            }
            return raw
                .replace(/_/g, ' ')
                .replace(/([a-z])([A-Z])/g, '$1 $2')
                .replace(/\s+/g, ' ')
                .trim();
        },
        getRoomTypeLabel(roomType) {
            const raw = (roomType?.name || '').trim();
            if (!raw) return roomType?.id ?? '';
            const key = `chat.multichat.createRoom.types.${raw}`;
            return this.$te(key) ? this.$t(key) : raw;
        },
        async loadRoomCreateOptions() {
            try {
                const options = await fetchRoomCreateOptions();
                this.roomCreateRights = Array.isArray(options?.rights) ? options.rights : [];
                this.roomCreateTypes = Array.isArray(options?.roomTypes) ? options.roomTypes : [];
            } catch (e) {
                console.error('Failed loading room create options', e);
                this.roomCreateRights = [];
                this.roomCreateTypes = [];
            }
        },
        async loadOwnRooms() {
            try {
                const rooms = await fetchOwnRooms();
                this.ownRooms = Array.isArray(rooms) ? rooms : [];
                this.tryConfirmRoomCreateSuccess();
            } catch (e) {
                console.error('Failed loading own rooms', e);
                this.ownRooms = [];
            }
        },
        async deleteOwnedRoom(room) {
            const title = room?.title || '';
            const confirmed = await confirmAction(this, {
                title: this.$t('chat.multichat.createRoom.ownedRooms.title'),
                message: this.$t('chat.multichat.createRoom.ownedRooms.confirmDelete', { room: title })
            });
            if (!confirmed) return;
            if (!this.transportConnected) {
                this.messages.push({
                    id: Date.now(),
                    user: 'System',
                    text: this.$t('chat.multichat.createRoom.messages.noConnection')
                });
                return;
            }
            try {
                const payload = { type: 'message', message: `/dr ${title}` };
                if (this.debug) console.log('[Chat WS >>]', payload);
                this.sendWithToken(payload);
                this.messages.push({
                    id: Date.now(),
                    user: 'System',
                    text: this.$t('chat.multichat.createRoom.ownedRooms.deleteSent', { room: title })
                });
                this.requestRoomRefreshAfterCreate();
                setTimeout(() => this.loadOwnRooms(), 700);
                setTimeout(() => this.loadOwnRooms(), 2000);
            } catch (e) {
                console.error('Failed deleting own room', e);
                this.messages.push({
                    id: Date.now(),
                    user: 'System',
                    text: this.$t('chat.multichat.createRoom.ownedRooms.deleteError')
                });
            }
        },
        parseOptionalInteger(value) {
            if (value === null || value === undefined || value === '') return null;
            const num = Number(value);
            if (!Number.isFinite(num)) return null;
            return Math.trunc(num);
        },
        buildRoomCreateCommand() {
            const name = (this.roomCreateForm.roomName || '').trim();
            if (!name) return '';
            const parts = ['/cr', name];
            if (this.roomCreateForm.visibility === 'public') {
                parts.push('public', 'public=true');
            } else if (this.roomCreateForm.visibility === 'private') {
                parts.push('private', 'public=false');
            }
            if (this.roomCreateForm.gender) parts.push(`gender=${this.roomCreateForm.gender}`);
            const minAge = this.parseOptionalInteger(this.roomCreateForm.minAge);
            if (minAge !== null && minAge >= 0) {
                parts.push(`min_age=${minAge}`);
            }
            const maxAge = this.parseOptionalInteger(this.roomCreateForm.maxAge);
            if (maxAge !== null && maxAge >= 0) {
                parts.push(`max_age=${maxAge}`);
            }
            const password = (this.roomCreateForm.password || '').trim();
            if (password) parts.push(`password=${password}`);
            if (this.roomCreateForm.friendsOnly) parts.push('friends_only=true');
            const rightId = this.parseOptionalInteger(this.roomCreateForm.rightId);
            if (rightId !== null && rightId > 0) {
                parts.push(`right_id=${rightId}`);
            }
            const typeId = this.parseOptionalInteger(this.roomCreateForm.typeId);
            if (typeId !== null && typeId > 0) {
                parts.push(`type_id=${typeId}`);
            }
            return parts.join(' ');
        },
        requestRoomRefreshAfterCreate() {
            const requestRooms = () => {
                if (this.isAdmin) this.sendWithToken({ type: 'reload_rooms' });
                this.sendWithToken({ type: 'rooms' });
            };
            setTimeout(requestRooms, 500);
            setTimeout(requestRooms, 1800);
        },
        sendCreateRoomCommand() {
            if (!this.transportConnected) {
                this.messages.push({ id: Date.now(), user: 'System', text: this.$t('chat.multichat.createRoom.messages.noConnection') });
                return;
            }
            if (!this.canSendRoomCreate) {
                this.messages.push({ id: Date.now(), user: 'System', text: this.$t('chat.multichat.createRoom.messages.invalidForm') });
                return;
            }
            const command = this.buildRoomCreateCommand();
            if (!command) {
                this.messages.push({ id: Date.now(), user: 'System', text: this.$t('chat.multichat.createRoom.messages.roomNameMissing') });
                return;
            }
            const payload = { type: 'message', message: command };
            if (this.debug) console.log('[Chat WS >>]', payload);
            this.sendWithToken(payload);
            this.messages.push({ id: Date.now(), user: 'System', text: this.$t('chat.multichat.createRoom.messages.sent', { command }) });
            this.clearPendingRoomCreateTracking();
            this.pendingRoomCreateName = (this.roomCreateForm.roomName || '').trim();
            this.pendingRoomCreateAttempts = 0;
            this.requestRoomRefreshAfterCreate();
            this.scheduleRoomCreateConfirmationCheck();
        },
        selectTargetUser(name) {
            if (this.selectedTargetUser === name) {
                this.selectedTargetUser = null; // toggle off
            } else {
                this.selectedTargetUser = name;
            }
        },
        explainCloseCode(code) {
            const map = {
                1000: 'normal closure',
                1001: 'going away (server down or browser nav)',
                1002: 'protocol error (handshake or frames)',
                1003: 'unsupported data',
                1005: 'no status received (browser internal)',
                1006: 'abnormal closure (network/TCP reset/firewall/CORS/TLS)',
                1007: 'invalid payload data',
                1008: 'policy violation (auth/permissions)',
                1009: 'message too big',
                1010: 'mandatory extension missing',
                1011: 'internal server error',
                1015: 'TLS handshake failure'
            };
            return map[code] || 'unknown reason';
        },
        // Wird extern aufgerufen um den Dialog zu öffnen
        open(rooms, options = {}) {
            this.adultOnlyMode = Boolean(options?.adultOnly);
            // Falls externe Räume übergeben wurden, nutzen; sonst vom Server laden
            if (Array.isArray(rooms) && rooms.length) {
                this.initializeRooms(rooms);
            } else {
                this.loadRooms();
            }
        },
        onDialogClose() {
            // Mark as closed first so any async close events won't schedule reconnect
            this.opened = false;
            this.clearPendingRoomCreateTracking();
            this.disconnectChatSocket();
            // Remove network event listeners
            window.removeEventListener('online', this.onOnline);
            window.removeEventListener('offline', this.onOffline);
        },
        onOptionsToggle(e) {
            e?.stopPropagation?.();
            this.showOptions = !this.showOptions;
        },
        onGlobalClick(e) {
            if (!this.showOptions) return;
            const wrap = this.$refs.optionsWrap;
            if (wrap && wrap.contains(e.target)) return;
            this.showOptions = false;
        },
        onOnline() {
            if (this.opened && !this.chatConnected && !this.connectRacing && (!this.chatWs || this.chatWs.readyState !== WebSocket.OPEN)) {
                this.reconnectAttempts = 0; // Reset attempts on network recovery
                this.reconnectIntervalMs = 3000; // Reset to base interval
                this.connectChatSocket();
            }
        },
        onOffline() {
            this.setStatus('disconnected');
        },
        async loadRooms() {
            try {
                const data = await fetchPublicRooms(this.adultOnlyMode ? { adultOnly: true } : {});
                this.initializeRooms(Array.isArray(data) ? data : []);
            } catch (e) {
                console.error('Fehler beim Laden der Räume', e);
                this.initializeRooms([]);
            }
        },
        async reloadRoomsAdmin() {
            if (!this.isAdmin) return;
            try {
                this.sendWithToken({ type: 'reload_rooms' });
                this.sendWithToken({ type: 'rooms' });
                this.messages.push({ id: Date.now(), user: 'System', text: 'Raum-Reload angefordert.' });
            } catch (e) {
                console.error('Fehler beim Neuladen der Räume', e);
                this.messages.push({ id: Date.now(), user: 'System', text: 'Fehler beim Neuladen der Raumliste.' });
            }
        },
        initializeRooms(rooms) {
            this.rooms = rooms;
            this.selectedRoom = rooms.length ? rooms[0].id : null;
            this.currentRoomName = '';
            this.autoscroll = true;
            try { this.urlOverride = localStorage.getItem('chatWsOverride') || ''; } catch (_) { this.urlOverride = ''; }
            this.messages = [];
            this.usersInRoom = [];
            this.selectedTargetUser = null;
            this.input = '';
            this.showOptions = false;
            this.showRoomCreatePanel = false;
            this.loadRoomCreateOptions();
            this.loadOwnRooms();
            this.announcedRoomEnter = false;
            this.lastAnnouncedRoomName = '';
            this.$refs.dialog.open();
            this.opened = true;
            // Stelle die WS-Verbindung her, wenn der Dialog geöffnet wird
            this.connectChatSocket();
            // Add network event listeners
            window.addEventListener('online', this.onOnline);
            window.addEventListener('offline', this.onOffline);
        },
        connectChatSocket() {
            if (this.connectRacing) return; // avoid overlapping races
            try { this.chatWs?.close(1000, 'reconnect'); } catch (_) { }
            this.chatWs = null;
            this.wsUrl = getChatWsUrl();
            // bump attempt counter for cache-busting
            this._wsAttempt = (this._wsAttempt || 0) + 1;
            const pageProto = (typeof window !== 'undefined' ? window.location.protocol : '');
            const candidates = getChatWsCandidates();
            console.log('[Chat WS] connect attempts', candidates, '| page protocol:', pageProto);
            if (pageProto === 'https:' && this.wsUrl.startsWith('ws://')) {
                console.warn('[Chat WS] insecure WS on HTTPS page — use wss:// or set VITE_CHAT_WS_URL');
            }
            this.setStatus('connecting');
            // Race candidates in parallel (happy eyeballs) so the first to open wins
            this.startHappyEyeballs(candidates);
            this.token = null;
            this.pending = [];
            this.transportConnected = false;
            this.chatConnected = false;
            this.announcedRoomEnter = false;
            this.wsStartAt = Date.now();
        },
        // Append a cache-busting query parameter per attempt to avoid stale routing/caches
        bustUrl(url) {
            try {
                const u = new URL(url, (typeof window !== 'undefined') ? window.location.href : 'http://localhost');
                u.searchParams.set('cb', `${Date.now()}-${this._wsAttempt || 0}`);
                return u.toString();
            } catch (_) {
                const sep = url.includes('?') ? '&' : '?';
                return `${url}${sep}cb=${Date.now()}-${this._wsAttempt || 0}`;
            }
        },
        // Start parallel connection attempts; the first to open wins
        startHappyEyeballs(candidates) {
            if (!this.opened) return;
            this.cleanupPendingSockets();
            this.connectRacing = true;
            this.connectWinnerChosen = false;
            this.raceFailures = 0;
            const limited = Array.isArray(candidates) ? candidates.slice(0, this.getRaceLimit()) : [];
            this.raceTotal = limited.length;
            if (!Array.isArray(candidates) || !candidates.length) {
                console.warn('[Chat WS] no candidates to try');
                this.scheduleReconnect();
                return;
            }
            const protocols = getChatWsProtocols();
            this.pendingWs = [];
            const delay = this.happyDelayMs || 120;
            limited.forEach((url, i) => {
                const t = setTimeout(() => {
                    // spawn a candidate
                    this.spawnCandidate(this.bustUrl(url), protocols);
                }, i * delay);
                // track starter timers to be safe (optional)
                this.pendingWs.push({ starterTimer: t, url, ws: null, openTimer: null });
            });
        },
        getRaceLimit() {
            return 1;
        },
        spawnCandidate(url, protocols) {
            if (!this.opened) return;
            let rec = this.pendingWs.find(r => r.url === url && r.ws === null);
            try {
                const ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url);
                if (!rec) { rec = { url, starterTimer: null, ws: null, openTimer: null }; this.pendingWs.push(rec); }
                rec.ws = ws;
                const startAt = Date.now();
                console.log('[Chat WS] racing', url, '| protocols:', protocols || '(none)');
                // If this socket never opens fast, let another win
        rec.openTimer = setTimeout(() => {
                    if (!this.connectWinnerChosen && ws.readyState === WebSocket.CONNECTING) {
                        console.warn('[Chat WS] candidate handshake timeout — closing to let others win:', url);
            try { ws.close(1000, 'race timeout'); } catch (_) { }
                    }
                }, this.candidateOpenTimeoutMs);
                ws.addEventListener('open', () => {
                    const dt = Date.now() - startAt;
                    if (this.connectWinnerChosen) {
                        // Someone already won; close this one quietly
                        try { ws.close(); } catch (_) { }
                        return;
                    }
                    // Winner selection
                    this.connectWinnerChosen = true;
                    this.connectRacing = false;
                    this.wsUrl = url;
                    this.chatWs = ws;
                    this.transportConnected = true;
                    this.reconnectAttempts = 0; // Reset reconnect counter on successful connection
                    this.reconnectIntervalMs = 3000; // Reset to base interval
                    this.startHeartbeat(); // Start heartbeat to keep connection alive
                    console.log('[Chat WS] open in', dt, 'ms', '| protocol:', ws.protocol || '(none)', '| url:', url);
                    // Close all other candidates
            this.pendingWs.forEach(r => {
                        if (r.ws && r.ws !== ws) {
                try { r.ws.close(1000, 'race loser'); } catch (_) { }
                r.ws = null;
                        }
                        if (r.openTimer) { clearTimeout(r.openTimer); r.openTimer = null; }
                        if (r.starterTimer) { clearTimeout(r.starterTimer); r.starterTimer = null; }
                    });
            // Drop references to losers so GC can collect
            this.pendingWs = [];
                    // Prepare handshake like before
                    const initRoom = this.resolveInitialRoomName();
                    const init = { type: 'init', name: this.user?.username || '', room: initRoom, password: this.getRoomPassword(initRoom) };
                    if (this.debug) console.log('[Chat WS >>]', init);
                    this.wsSend(init);
                    if (this.connectAttemptTimeout) clearTimeout(this.connectAttemptTimeout);
                    this.connectAttemptTimeout = setTimeout(() => {
                        this.connectAttemptTimeout = null;
                        if (this.opened && !this.chatConnected) {
                            console.warn('[Chat WS] handshake watchdog fired — no room_entered within', this.handshakeWatchdogMs, 'ms');
                            this.scheduleReconnect();
                        }
                    }, this.handshakeWatchdogMs);
                    if (this.joinFallbackTimer) { clearTimeout(this.joinFallbackTimer); this.joinFallbackTimer = null; }
                    // Attach message/error/close for the winner
                    ws.addEventListener('message', (ev) => {
                        const data = ev.data;
                        if (this.debug) console.log('[Chat WS << RAW]', data);
                        this.wsProcessChunk(typeof data === 'string' ? data : '');
                    });
                    ws.addEventListener('error', (e) => {
                        console.warn('[Chat WS] error on winner | readyState:', ws.readyState, '| url:', url, e);
                        this.setStatus('error');
                    });
                    ws.addEventListener('close', (e) => {
                        const hint = this.explainCloseCode(e.code);
                        console.warn('[Chat WS] close | code:', e.code, hint, '| reason:', e.reason, '| wasClean:', e.wasClean, '| url:', url);
                        if (this.connectAttemptTimeout) { clearTimeout(this.connectAttemptTimeout); this.connectAttemptTimeout = null; }
                        if (this.joinFallbackTimer) { clearTimeout(this.joinFallbackTimer); this.joinFallbackTimer = null; }
                        this.transportConnected = false;
                        this.chatConnected = false;
                        this.setStatus('disconnected');
                        if (this.opened) this.scheduleReconnect();
                    });
                });
                // For non-winners: count failures so we can retry if none succeed
                const onFail = (label) => {
                    if (!this.connectRacing || this.connectWinnerChosen) return;
                    this.raceFailures = (this.raceFailures || 0) + 1;
                    const dt = Date.now() - startAt;
                    console.warn('[Chat WS] candidate', label, 'after', dt, 'ms | url:', url);
                    if (rec && rec.openTimer) { clearTimeout(rec.openTimer); rec.openTimer = null; }
                    if (rec && rec.ws) { try { rec.ws.close(1000, 'race fail'); } catch (_) {} rec.ws = null; }
                    if (this.raceFailures >= this.raceTotal) {
                        // All failed
                        this.connectRacing = false;
                        if (this.opened && !this.chatConnected) this.scheduleReconnect();
                    }
                };
                ws.addEventListener('error', () => onFail('error'));
                ws.addEventListener('close', () => onFail('close'));
            } catch (e) {
                console.warn('[Chat WS] spawn failed for', url, e);
            }
        },
        cleanupPendingSockets() {
            if (!this.pendingWs) this.pendingWs = [];
            this.connectRacing = false;
            this.connectWinnerChosen = false;
            try {
                this.pendingWs.forEach(r => {
                    if (r.starterTimer) { clearTimeout(r.starterTimer); r.starterTimer = null; }
                    if (r.openTimer) { clearTimeout(r.openTimer); r.openTimer = null; }
                    if (r.ws) {
                        try { r.ws.close(1000, 'cleanup'); } catch (_) { }
                        r.ws = null;
                    }
                });
            } catch (_) { }
            this.pendingWs = [];
        },
        tryNextCandidate(candidates, index) {
            if (!this.opened) return;
            if (index >= candidates.length) {
                console.warn('[Chat WS] all candidates failed');
                if (this.opened && !this.chatConnected) this.scheduleReconnect();
                return;
            }
            const url = this.bustUrl(candidates[index]);
            const protocols = getChatWsProtocols();
            this.wsUrl = url;
            console.log('[Chat WS] trying', url, `(candidate ${index + 1}/${candidates.length})`, '| protocols:', protocols || '(none)');
            try { this.chatWs?.close(1000, 'switch candidate'); } catch (_) { }
            if (this.connectOpenTimer) { clearTimeout(this.connectOpenTimer); this.connectOpenTimer = null; }
            const ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url);
            this.chatWs = ws;
            this.wsStartAt = Date.now();
            // If the socket never opens within a short time, assume this candidate is bad and try next
            this.connectOpenTimer = setTimeout(() => {
                this.connectOpenTimer = null;
                if (ws.readyState === WebSocket.CONNECTING && !this.transportConnected) {
                    console.warn('[Chat WS] candidate handshake timeout — closing to try next:', url);
                    try { ws.close(1000, 'candidate timeout'); } catch (_) { }
                }
            }, this.candidateOpenTimeoutMs);
            ws.addEventListener('open', () => {
                this.transportConnected = true;
                const dt = Date.now() - (this.wsStartAt || Date.now());
                console.log('[Chat WS] open in', dt, 'ms', '| protocol:', ws.protocol || '(none)', '| url:', url);
                const initRoom = this.resolveInitialRoomName();
                const init = { type: 'init', name: this.user?.username || '', room: initRoom, password: this.getRoomPassword(initRoom) };
                if (this.debug) console.log('[Chat WS >>]', init);
                this.wsSend(init);
                if (this.connectOpenTimer) { clearTimeout(this.connectOpenTimer); this.connectOpenTimer = null; }
                if (this.connectAttemptTimeout) clearTimeout(this.connectAttemptTimeout);
                this.connectAttemptTimeout = setTimeout(() => {
                    this.connectAttemptTimeout = null;
                    if (this.opened && !this.chatConnected) {
                        console.warn('[Chat WS] handshake watchdog fired — no room_entered within', this.handshakeWatchdogMs, 'ms');
                        this.scheduleReconnect();
                    }
                }, this.handshakeWatchdogMs);
                // No extra join here; init already included the room
                if (this.joinFallbackTimer) { clearTimeout(this.joinFallbackTimer); this.joinFallbackTimer = null; }
            });
            ws.addEventListener('message', (ev) => {
                const data = ev.data;
                if (this.debug) console.log('[Chat WS << RAW]', data);
                this.wsProcessChunk(typeof data === 'string' ? data : '');
            });
            ws.addEventListener('error', (e) => {
                const dt = Date.now() - (this.wsStartAt || Date.now());
                console.warn('[Chat WS] error after', dt, 'ms', '| readyState:', ws.readyState, '| url:', url, e);
                this.setStatus('error');
            });
            ws.addEventListener('close', (e) => {
                const dt = Date.now() - (this.wsStartAt || Date.now());
                const hint = this.explainCloseCode(e.code);
                console.warn('[Chat WS] close after', dt, 'ms', '| code:', e.code, hint, '| reason:', e.reason, '| wasClean:', e.wasClean, '| url:', url);
                if (this.connectOpenTimer) { clearTimeout(this.connectOpenTimer); this.connectOpenTimer = null; }
                if (this.connectAttemptTimeout) { clearTimeout(this.connectAttemptTimeout); this.connectAttemptTimeout = null; }
                if (this.joinFallbackTimer) { clearTimeout(this.joinFallbackTimer); this.joinFallbackTimer = null; }
                if (!this.transportConnected) {
                    // This candidate never opened; try the next one immediately
                    this.tryNextCandidate(candidates, index + 1);
                    return;
                }
                this.transportConnected = false;
                this.chatConnected = false;
                this.setStatus('disconnected');
                if (this.opened) this.scheduleReconnect();
            });
            // Handshake watchdog is started upon successful open (after init sent)
        },
        scheduleReconnect() {
            if (this.reconnectTimer) return;
            
            // Check if we've exceeded max attempts
            if (this.reconnectAttempts >= this.maxReconnectAttempts) {
                console.error('[Chat WS] Max reconnect attempts reached, waiting longer before retry');
                this.reconnectAttempts = 0; // Reset counter
                this.reconnectIntervalMs = 30000; // Wait 30 seconds
            } else {
                this.reconnectAttempts++;
                // Use exponential backoff with jitter
                const baseDelay = Math.min(3000 * Math.pow(1.5, this.reconnectAttempts - 1), 15000);
                const jitter = Math.random() * 1000; // Add up to 1 second jitter
                this.reconnectIntervalMs = baseDelay + jitter;
            }
            
            console.log('[Chat WS] scheduleReconnect in', Math.round(this.reconnectIntervalMs), 'ms', '| opened:', this.opened, '| attempt:', this.reconnectAttempts);
            this.reconnectTimer = setTimeout(() => {
                this.reconnectTimer = null;
                if (this.opened && !this.chatConnected) {
                    this.connectChatSocket();
                }
            }, this.reconnectIntervalMs);
        },
        applyUrlOverride() {
            try {
                const val = (this.urlOverride || '').trim();
                if (val) localStorage.setItem('chatWsOverride', val); else localStorage.removeItem('chatWsOverride');
            } catch (_) { }
            console.log('[Chat WS] url override applied', this.urlOverride || '(cleared)');
            if (this.opened) this.connectChatSocket();
        },
        clearUrlOverride() {
            this.urlOverride = '';
            this.applyUrlOverride();
        },
        disconnectChatSocket() {
            if (this.reconnectTimer) {
                clearTimeout(this.reconnectTimer);
                this.reconnectTimer = null;
            }
            this.stopHeartbeat(); // Stop heartbeat when disconnecting
            this.cleanupPendingSockets();
            if (this.connectOpenTimer) { clearTimeout(this.connectOpenTimer); this.connectOpenTimer = null; }
            if (this.connectAttemptTimeout) { clearTimeout(this.connectAttemptTimeout); this.connectAttemptTimeout = null; }
            if (this.joinFallbackTimer) { clearTimeout(this.joinFallbackTimer); this.joinFallbackTimer = null; }
            try {
                if (this.chatWs) {
                    if (this.chatWs.readyState === WebSocket.OPEN) {
                        this.chatWs.close(1000, 'dialog closed');
                    } else if (this.chatWs.readyState === WebSocket.CONNECTING) {
                        this.chatWs.close(1000, 'dialog closed');
                    }
                }
            } catch (_) { }
            this.chatWs = null;
            this.chatConnected = false;
            this.announcedRoomEnter = false;
            this.lastAnnouncedRoomName = '';
            this.currentRoomName = '';
            this.roomSyncInFlight = false;
            this.usersInRoom = [];
            this.selectedTargetUser = null;
        },
        startHeartbeat() {
            this.stopHeartbeat(); // Clear any existing heartbeat
            if (!this.opened || !this.chatConnected) return;
            
            this.heartbeatInterval = setInterval(() => {
                if (!this.chatWs || this.chatWs.readyState !== WebSocket.OPEN) {
                    console.warn('[Chat WS] Heartbeat failed - connection not open, attempting reconnect');
                    this.stopHeartbeat();
                    this.scheduleReconnect();
                    return;
                }
                
                try {
                    // Keepalive via supported protocol command
                    if (this.token) this.wsSend({ type: 'userlist', token: this.token });
                } catch (error) {
                    console.warn('[Chat WS] Heartbeat failed:', error);
                    this.stopHeartbeat();
                    this.scheduleReconnect();
                }
            }, this.heartbeatIntervalMs);
        },
        stopHeartbeat() {
            if (this.heartbeatInterval) {
                clearInterval(this.heartbeatInterval);
                this.heartbeatInterval = null;
            }
        },
        getSelectedRoomName() {
            const r = this.rooms.find(x => x.id === this.selectedRoom);
            return r?.title || r?.name || '';
        },
        resolveInitialRoomName() {
            const selectedRoomName = this.getSelectedRoomName();
            if (selectedRoomName) {
                return selectedRoomName;
            }

            const fallbackRoom = this.rooms[0];
            if (!fallbackRoom) {
                return '';
            }

            if (!this.selectedRoom) {
                this.selectedRoom = fallbackRoom.id;
            }
            return fallbackRoom.title || fallbackRoom.name || '';
        },
        handleIncoming(msg) {
            if (!msg) return;
            if (msg.type === 'message' || msg.type === 4) {
                // Some servers echo sender messages without userName but include token
                const isSelfByToken = !!(msg.token && this.token && msg.token === this.token);
                const user = msg.userName || msg.from || (isSelfByToken ? (this.user?.username || 'User') : 'User');
                let color = null;
                if (msg.color) {
                    color = ('' + msg.color).trim();
                    if (color && !color.startsWith('#')) color = '#' + color;
                    this.userColors[user] = color;
                } else if (this.userColors[user]) {
                    color = this.userColors[user];
                }
                const displayUser = (this.user?.username && user === this.user.username) ? 'ich' : user;
                this.messages.push({ id: msg.id || Date.now(), user: displayUser, text: msg.message || msg.text || '', color });
            } else if (msg.type === 6 || msg.type === 'scream') {
                // Schreien: <user> SCHREIT: <text>
                const user = msg.userName || msg.from || 'User';
                let color = null;
                if (msg.color) {
                    color = ('' + msg.color).trim();
                    if (color && !color.startsWith('#')) color = '#' + color;
                    this.userColors[user] = color;
                } else if (this.userColors[user]) {
                    color = this.userColors[user];
                }
                this.messages.push({
                    id: msg.id || Date.now(),
                    user,
                    text: (msg.message || msg.text || ''),
                    color,
                    type: 'scream'
                });
            } else if (msg.type === 'system') {
                // Nur gezielte Systemmeldungen anzeigen
                if (msg.code === 'room_entered' || msg.tr === 'room_entered') {
                    // Verbunden-Status setzen
                    this.chatConnected = true;
                    this.setStatus('connected');
                    if (this.connectAttemptTimeout) { clearTimeout(this.connectAttemptTimeout); this.connectAttemptTimeout = null; }
                    if (this.joinFallbackTimer) { clearTimeout(this.joinFallbackTimer); this.joinFallbackTimer = null; }
                    const actualRoom = msg.to || msg.room || msg.roomName || '';
                    const desiredRoom = this.resolveInitialRoomName();
                    this.currentRoomName = actualRoom || '';
                    if (
                        this.adultOnlyMode
                        && desiredRoom
                        && actualRoom
                        && desiredRoom !== actualRoom
                        && !this.roomSyncInFlight
                    ) {
                        this.roomSyncInFlight = true;
                        this.sendWithToken({ type: 'join', room: desiredRoom, password: this.getRoomPassword(desiredRoom) });
                        window.setTimeout(() => {
                            this.roomSyncInFlight = false;
                        }, 1000);
                        return;
                    }
                    // Eigene Beitrittsmeldung einmalig mit Raumname anzeigen
                    const room = actualRoom || this.getSelectedRoomName();
                    if (room && this.lastAnnouncedRoomName !== room) {
                        const text = this.$t('chat.multichat.system.room_entered', { room });
                        this.messages.push({ id: msg.id || Date.now(), user: 'System', text });
                        this.lastAnnouncedRoomName = room;
                        this.announcedRoomEnter = true;
                    }
                    return;
                }
                // Color changes
                if (msg.code === 'color_changed') {
                    // Server schickt die gültige Farbe zurück – diese verwenden
                    let color = (msg.color || '').trim();
                    if (color && !color.startsWith('#')) color = '#' + color;
                    if (!color) color = '#000000';
                    // UI-Infozeile
                    const text = this.$t('chat.multichat.system.color_changed_self', { color });
                    this.messages.push({ id: msg.id || Date.now(), user: 'System', text });
                    // Lokalen Zustand aktualisieren, damit künftige Aktionen dieselbe Farbe anzeigen
                    this.lastColor = color;
                    if (this.user?.username) {
                        this.userColors[this.user.username] = color;
                    }
                    if (this.showColorPicker) {
                        this.selectedColor = color;
                        this.hexInput = color;
                        // Marker position grob anpassen
                        try {
                            const hsl = this.hexToHsl(color);
                            this.pickX = Math.round((hsl.h / 360) * (this.paletteWidth - 1));
                            this.pickY = Math.round(((100 - hsl.l) / 100) * (this.paletteHeight - 1));
                        } catch (_) { }
                    }
                    return;
                }
                if (msg.code === 'user_color_changed') {
                    let color = (msg.color || '').trim();
                    if (color && !color.startsWith('#')) color = '#' + color;
                    if (!color) color = '#000000';
                    const user = msg.userName || 'User';
                    const text = this.$t('chat.multichat.system.color_changed_user', { user, color });
                    this.messages.push({ id: msg.id || Date.now(), user: 'System', text });
                    // Cache für künftige Messages
                    this.userColors[user] = color;
                    // Auch Liste rechts aktualisieren
                    const idx = this.usersInRoom.findIndex(u => u.name === user);
                    if (idx >= 0) {
                        const updated = { ...this.usersInRoom[idx], color };
                        // Vue 3 reactivity: direct assignment is fine
                        this.usersInRoom.splice(idx, 1, updated);
                    }
                    return;
                }
            }
        },
        setStatus(type) {
            this.statusType = type;
            const t = this.$t;
            switch (type) {
                case 'connecting':
                    this.statusText = t('chat.multichat.status.connecting');
                    break;
                case 'connected':
                    this.statusText = t('chat.multichat.status.connected');
                    break;
                case 'disconnected':
                    this.statusText = t('chat.multichat.status.disconnected');
                    break;
                case 'error':
                    this.statusText = t('chat.multichat.status.error');
                    break;
                default:
                    this.statusText = '';
            }
        },
        handleAutoscroll() {
            if (this.autoscroll && !this.mouseOverOutput) {
                const out = this.$refs.output;
                if (out) out.scrollTop = out.scrollHeight;
            }
        },
        sendMessage() {
            if (!this.input.trim()) return;
            const payload = { type: 'message', message: this.input };
            if (this.debug) console.log('[Chat WS >>]', payload);
            this.sendWithToken(payload);
            this.input = '';
        },
        shout() {
            if (!this.input.trim()) return;
            const payload = { type: 'scream', message: this.input };
            if (this.debug) console.log('[Chat WS >>]', payload);
            this.sendWithToken(payload);
            this.input = '';
        },
        action() {
            if (!this.input.trim()) return;
            if (!this.selectedTargetUser) return; // Nur mit Auswahl
            const payload = { type: 'do', message: `${this.input} ${this.selectedTargetUser}`.trim() };
            if (this.debug) console.log('[Chat WS >>]', payload);
            this.sendWithToken(payload);
            this.input = '';
        },
        async reportSelectedUser() {
            if (!this.selectedTargetUser || !this.messages.length) return;
            const confirmed = await confirmAction(this, {
                title: this.$t('chat.multichat.reportConfirmTitle'),
                message: this.$t('chat.multichat.reportConfirmMessage', { user: this.selectedTargetUser })
            });
            if (!confirmed) return;

            const payload = {
                context: 'multi_chat',
                reporterHashedId: this.user?.hashedId || null,
                reporterUsername: this.user?.username || null,
                offenderUsername: this.selectedTargetUser,
                incidentAt: new Date().toISOString(),
                chatHistory: this.messages.map((entry) => ({
                    type: entry?.type || 'message',
                    user: entry?.user || '',
                    text: entry?.action
                        ? `${entry.action} ${entry.to || ''}`.trim()
                        : (entry?.text || ''),
                    timestamp: Date.now(),
                })),
                metadata: {
                    roomId: this.selectedRoom || null,
                    roomName: this.getSelectedRoomName() || null,
                    usersInRoom: this.usersInRoom.map((u) => u.name),
                }
            };
            try {
                await reportChatIncident(payload);
                this.messages.push({ id: Date.now(), user: 'System', text: this.$t('chat.multichat.reportSent') });
            } catch (_) {
                this.messages.push({ id: Date.now(), user: 'System', text: this.$t('chat.multichat.reportError') });
            }
        },
        roll() {
            const payload = { type: 'dice', message: '' };
            if (this.debug) console.log('[Chat WS >>]', payload);
            this.sendWithToken(payload);
        },
        openColorPicker() {
            this.selectedColor = this.lastColor || '#000000';
            this.hexInput = this.selectedColor;
            this.showColorPicker = true;
            this.$nextTick(() => {
                this.drawPalette();
                // Initialize marker roughly by hue (x) and lightness (y)
                let hsl = this.hexToHsl(this.selectedColor);
                if (hsl.l > this.maxLightness) {
                    hsl = { ...hsl, l: this.maxLightness };
                    const clampedHex = this.hslToHex(hsl.h, 100, hsl.l);
                    this.selectedColor = clampedHex;
                    this.hexInput = clampedHex;
                }
                this.pickX = Math.round((hsl.h / 360) * (this.paletteWidth - 1));
                this.pickY = Math.round(((100 - hsl.l) / 100) * (this.paletteHeight - 1));
            });
        },
        onColorOk() {
            const color = this.selectedColor;
            if (color) {
                const payload = { type: 'color', value: color };
                if (this.debug) console.log('[Chat WS >>]', payload);
                this.sendWithToken(payload);
                this.lastColor = color;
            }
            this.showColorPicker = false;
        },
        onColorCancel() {
            this.showColorPicker = false;
        },
        onHexInput() {
            const val = (this.hexInput || '').trim();
            const hex = this.normalizeHex(val);
            if (!hex) {
                this.hexInvalid = true;
                return;
            }
            this.hexInvalid = false;
            let hsl = this.hexToHsl(hex);
            if (hsl.l > this.maxLightness) {
                hsl = { ...hsl, l: this.maxLightness };
                const clamped = this.hslToHex(hsl.h, 100, hsl.l);
                this.selectedColor = clamped;
                this.hexInput = clamped;
            } else {
                this.selectedColor = hex;
            }
            this.pickX = Math.round((hsl.h / 360) * (this.paletteWidth - 1));
            this.pickY = Math.round(((100 - hsl.l) / 100) * (this.paletteHeight - 1));
        },
        // Palette drawing: x -> hue(0..360), y -> lightness(100..0), saturation fixed 100%
        drawPalette() {
            const canvas = this.$refs.paletteCanvas;
            if (!canvas) return;
            const ctx = canvas.getContext('2d');
            const w = this.paletteWidth, h = this.paletteHeight;
            const img = ctx.createImageData(w, h);
            let i = 0;
            for (let y = 0; y < h; y++) {
                const l = 100 - (y / (h - 1)) * 100; // 100..0
                for (let x = 0; x < w; x++) {
                    const hue = (x / (w - 1)) * 360;
                    const rgb = this.hslToRgb(hue, 100, l);
                    img.data[i++] = rgb.r;
                    img.data[i++] = rgb.g;
                    img.data[i++] = rgb.b;
                    img.data[i++] = 255;
                }
            }
            ctx.putImageData(img, 0, 0);
            // Overlay the excluded too-light region at the top
            const yLimit = Math.round(((100 - this.maxLightness) / 100) * (h - 1));
            if (yLimit > 0) {
                ctx.fillStyle = 'rgba(255,255,255,0.5)';
                ctx.fillRect(0, 0, w, yLimit);
                // Optional: boundary line
                ctx.strokeStyle = 'rgba(0,0,0,0.2)';
                ctx.beginPath();
                ctx.moveTo(0, yLimit + 0.5);
                ctx.lineTo(w, yLimit + 0.5);
                ctx.stroke();
            }
        },
        onPaletteDown(e) {
            this.isPicking = true;
            this.pickFromEvent(e);
        },
        onPaletteMove(e) {
            if (!this.isPicking) return;
            this.pickFromEvent(e);
        },
        onPaletteUp() {
            this.isPicking = false;
        },
        pickFromEvent(e) {
            const rect = this.$refs.paletteCanvas.getBoundingClientRect();
            let x = Math.round(e.clientX - rect.left);
            let y = Math.round(e.clientY - rect.top);
            x = Math.max(0, Math.min(this.paletteWidth - 1, x));
            y = Math.max(0, Math.min(this.paletteHeight - 1, y));
            let hue = (x / (this.paletteWidth - 1)) * 360;
            let l = 100 - (y / (this.paletteHeight - 1)) * 100;
            if (l > this.maxLightness) {
                l = this.maxLightness;
                // recompute y aligned with clamped l
                y = Math.round(((100 - l) / 100) * (this.paletteHeight - 1));
            }
            this.pickX = x; this.pickY = y;
            const hex = this.hslToHex(hue, 100, l);
            this.selectedColor = hex;
            this.hexInput = hex;
            this.hexInvalid = false;
        },
        normalizeHex(val) {
            let v = val.startsWith('#') ? val.slice(1) : val;
            if (v.length === 3 && /^[0-9a-fA-F]{3}$/.test(v)) {
                v = v.split('').map(ch => ch + ch).join('');
            }
            if (/^[0-9a-fA-F]{6}$/.test(v)) {
                return '#' + v.toUpperCase();
            }
            return null;
        },
        hexToHsl(hex) {
            const rgb = this.hexToRgb(hex);
            return this.rgbToHsl(rgb.r, rgb.g, rgb.b);
        },
        hslToHex(h, s, l) {
            const rgb = this.hslToRgb(h, s, l);
            return this.rgbToHex(rgb.r, rgb.g, rgb.b);
        },
        hexToRgb(hex) {
            const m = /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec(hex);
            const r = parseInt(m[1], 16);
            const g = parseInt(m[2], 16);
            const b = parseInt(m[3], 16);
            return { r, g, b };
        },
        rgbToHex(r, g, b) {
            const toHex = (n) => n.toString(16).padStart(2, '0');
            return '#' + toHex(r) + toHex(g) + toHex(b);
        },
        rgbToHsl(r, g, b) {
            r /= 255; g /= 255; b /= 255;
            const max = Math.max(r, g, b), min = Math.min(r, g, b);
            let h, s, l = (max + min) / 2;
            if (max === min) {
                h = s = 0;
            } else {
                const d = max - min;
                s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
                switch (max) {
                    case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                    case g: h = (b - r) / d + 2; break;
                    case b: h = (r - g) / d + 4; break;
                }
                h *= 60;
            }
            return { h: Math.round(h || 0), s: Math.round(s * 100), l: Math.round(l * 100) };
        },
        hslToRgb(h, s, l) {
            s /= 100; l /= 100;
            const c = (1 - Math.abs(2 * l - 1)) * s;
            const x = c * (1 - Math.abs((h / 60) % 2 - 1));
            const m = l - c / 2;
            let r1 = 0, g1 = 0, b1 = 0;
            if (0 <= h && h < 60) { r1 = c; g1 = x; b1 = 0; }
            else if (60 <= h && h < 120) { r1 = x; g1 = c; b1 = 0; }
            else if (120 <= h && h < 180) { r1 = 0; g1 = c; b1 = x; }
            else if (180 <= h && h < 240) { r1 = 0; g1 = x; b1 = c; }
            else if (240 <= h && h < 300) { r1 = x; g1 = 0; b1 = c; }
            else { r1 = c; g1 = 0; b1 = x; }
            const r = Math.round((r1 + m) * 255);
            const g = Math.round((g1 + m) * 255);
            const b = Math.round((b1 + m) * 255);
            return { r, g, b };
        },

        // utility methods for WebSocket protocol
        wsSend(obj) {
            try {
                const s = JSON.stringify(obj);
                if (this.debug) console.log('[Chat WS >> RAW]', s);
                this.chatWs?.send(s);
            } catch (_) { }
        },
        sendWithToken(obj) {
            if (this.token) {
                obj.token = this.token;
                this.wsSend(obj);
            } else {
                this.pending.push(() => this.sendWithToken(obj));
            }
        },
        flushPending() {
            const arr = this.pending; this.pending = [];
            arr.forEach(fn => { try { fn(); } catch (_) { } });
        },
        wsProcessChunk(text) {
            const parts = this.safeSplitJson(text);
            for (const p of parts) {
                try {
                    const obj = JSON.parse(p);
                    this.onWsObject(obj);
                } catch (_) {
                    this.handleIncoming({ type: 'system', text: p });
                }
            }
        },
        safeSplitJson(str) {
            if (!str) return [];
            const trimmed = str.trim();
            if (trimmed.startsWith('{') && trimmed.endsWith('}')) return [trimmed];
            const arr = str.split('}{');
            if (arr.length === 1) return [str];
            return arr.map((seg, i) => (i === 0 ? seg + '}' : (i === arr.length - 1 ? '{' + seg : '{' + seg + '}')));
        },
        onWsObject(obj) {
            if (!obj) return;
            if (obj.type === 'error') {
                const text = obj.message || 'chat_error';
                if (text === 'room_password_required' || text === 'room_password_invalid') {
                    this.handleRoomPasswordError(text);
                    return;
                }
                this.setStatus('error');
                this.messages.push({ id: Date.now(), user: 'System', text: `Fehler: ${text}` });
                return;
            }
            // Token handshake: only when explicitly marked as token-type
            if (obj.type === 'token' || obj.type === 1) {
                let tok = obj.token;
                if (!tok) {
                    if (typeof obj.message === 'string') tok = obj.message;
                    else if (obj.message && typeof obj.message === 'object') tok = obj.message.token;
                }
                if (tok) {
                    this.token = tok;
                    this.sendWithToken({ type: 'rooms' });
                    this.sendWithToken({ type: 'userlist' });
                    // No extra join here; we already sent init with room
                    if (this.joinFallbackTimer) { clearTimeout(this.joinFallbackTimer); this.joinFallbackTimer = null; }
                    this.flushPending();
                }
                return;
            }
            // Type 7: user action (message is a JSON string with { action, to, tr })
            if (obj.type === 7) {
                const from = (obj.userName || obj.user || obj.name || '').toString();
                let fromColor = obj.color ? ('' + obj.color).trim() : null;
                if (fromColor && !fromColor.startsWith('#')) fromColor = '#' + fromColor;
                let act = '', to = '';
                try {
                    const payload = typeof obj.message === 'string' ? JSON.parse(obj.message) : obj.message;
                    if (payload && typeof payload === 'object') {
                        act = (payload.action || '').toString().trim();
                        to = (payload.to || '').toString().trim();
                    } else if (typeof payload === 'string') {
                        // if server ever sends plain string, best-effort: first word action, rest to
                        const parts = payload.trim().split(/\s+/);
                        act = parts.shift() || '';
                        to = parts.join(' ');
                    }
                } catch (_) {
                    // ignore parse errors; will fallback below if needed
                }
                const toColor = this.userColors[to] || null;
                const senderColor = fromColor || this.userColors[from] || null;
                if (from && act) {
                    if (from && fromColor) this.userColors[from] = fromColor; // cache sender color
                    this.messages.push({
                        id: obj.id || Date.now(),
                        type: 'action',
                        user: from,
                        action: act,
                        to,
                        color: senderColor,
                        toColor
                    });
                    return;
                }
                // fallback: show as plain message
                const fallbackText = (typeof obj.message === 'string') ? obj.message : JSON.stringify(obj.message || {});
                this.handleIncoming({ type: 'message', message: fallbackText, userName: from, color: senderColor });
                return;
            }
            // Type 2: user list in room
            if (obj.type === 2) {
                console.log('🔍 Type 2 message received:', obj);
                const list = Array.isArray(obj.message) ? obj.message : 
                           (Array.isArray(obj.users) ? obj.users : 
                           (Array.isArray(obj.message?.userlist) ? obj.message.userlist : []));
                console.log('📊 Extracted user list:', list);
                const byName = new Map();
                for (const it of list) {
                    if (it && typeof it === 'object') {
                        const name = it.userName || it.name || it.username || '';
                        if (!name) continue;
                        let color = (it.color || '').toString().trim();
                        if (color && !color.startsWith('#')) color = '#' + color;
                        byName.set(name, { name, color: color || null });
                        // Cache known colors for messages
                        if (color) this.userColors[name] = color;
                    }
                }
                this.usersInRoom = Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name, 'de'));
                console.log('📊 Final usersInRoom:', this.usersInRoom);
                // Auswahl behalten, falls User noch existiert; sonst löschen
                if (this.selectedTargetUser && !byName.has(this.selectedTargetUser)) {
                    this.selectedTargetUser = null;
                }
                return;
            }
            if (obj.type === 3 && Array.isArray(obj.message)) {
                const mapped = obj.message
                    .filter(r => r && r.name)
                    .map((r, index) => ({
                        id: r.name || index + 1,
                        title: r.name,
                        name: r.name,
                        users: Number(r.users || 0)
                    }));
                if (mapped.length > 0) {
                    const currentName = this.getSelectedRoomName();
                    this.rooms = mapped;
                    if (currentName) {
                        const existing = mapped.find(r => r.name === currentName);
                        this.selectedRoom = existing ? existing.id : mapped[0].id;
                    } else if (!this.selectedRoom) {
                        this.selectedRoom = mapped[0].id;
                    }
                }
                return;
            }
            if (obj.type === 5) {
                if (obj.message === 'room_entered') {
                    const to = obj.to || obj.room || this.getSelectedRoomName();
                    this.handleIncoming({ type: 'system', code: 'room_entered', tr: 'room_entered', to });
                    this.sendWithToken({ type: 'userlist' });
                    return;
                }
                const msg = obj.message;
                if (typeof msg === 'string') {
                    // Some servers send a JSON-encoded string here; parse if it looks like JSON
                    const s = msg.trim();
                    if (s.startsWith('{') && s.endsWith('}')) {
                        try {
                            const jm = JSON.parse(s);
                            const tr = jm.tr || jm.code || '';
                            if (tr === 'user_entered_room' || tr === 'user_entered_chat') {
                                const who = obj.userName || obj.user || obj.name || '';
                                // cache provided color for future messages
                                let color = (obj.color || jm.color || '').toString().trim();
                                if (color && !color.startsWith('#')) color = '#' + color;
                                if (who && color) this.userColors[who] = color;
                                const to = jm.to || jm.name || jm.room || this.getSelectedRoomName();
                                const isSelf = !!(this.user?.username && who === this.user.username);
                                if (isSelf) {
                                    this.chatConnected = true;
                                    this.setStatus('connected');
                                    const text = this.$t?.('chat.multichat.system.room_entered', { room: to }) || `Du betrittst ${to}`;
                                    this.messages.push({ id: Date.now(), user: 'System', text });
                                } else {
                                    const text = `${who} betritt ${to}`;
                                    this.messages.push({ id: Date.now(), user: 'System', text });
                                }
                                return;
                            }
                            if (tr === 'user_left_room') {
                                const who = jm.userName || obj.userName || obj.user || obj.name || 'User';
                                const dest = jm.destination || jm.to || jm.name || jm.room || '';
                                let color = (jm.userColor || obj.color || this.userColors[who] || jm.color || '').toString().trim();
                                if (color && !color.startsWith('#')) color = '#' + color;
                                // Action-style line with localized action mid-part and colored user name
                                this.messages.push({
                                    id: Date.now(),
                                    type: 'action',
                                    user: who,
                                    action: this.$t?.('chat.multichat.action_phrases.left_room') || 'wechselt zu Raum',
                                    to: dest,
                                    color,
                                    toColor: null
                                });
                                // Update right-side user list (remove from current room)
                                const idx = this.usersInRoom.findIndex(u => u.name === who);
                                if (idx >= 0) this.usersInRoom.splice(idx, 1);
                                if (this.selectedTargetUser === who) this.selectedTargetUser = null;
                                return;
                            }
                            if (tr === 'user_disconnected') {
                                const who = obj.userName || obj.user || obj.name || 'User';
                                let color = (obj.color || this.userColors[who] || jm.color || '').toString().trim();
                                if (color && !color.startsWith('#')) color = '#' + color;
                                this.messages.push({
                                    id: Date.now(),
                                    type: 'action',
                                    user: who,
                                    action: this.$t?.('chat.multichat.action_phrases.left_chat') || 'hat den Chat verlassen.',
                                    to: '',
                                    color,
                                    toColor: null
                                });
                                const idx = this.usersInRoom.findIndex(u => u.name === who);
                                if (idx >= 0) this.usersInRoom.splice(idx, 1);
                                if (this.selectedTargetUser === who) this.selectedTargetUser = null;
                                return;
                            }
                            if (tr === 'room_change_user') {
                                const who = obj.userName || obj.user || obj.name || '';
                                const to = jm.to || jm.name || jm.room || this.getSelectedRoomName();
                                const isSelf = !!(this.user?.username && who === this.user.username);
                                if (isSelf) {
                                    this.chatConnected = true;
                                    this.setStatus('connected');
                                    const text = this.$t?.('chat.multichat.system.room_entered', { room: to }) || `Du betrittst ${to}`;
                                    this.messages.push({ id: Date.now(), user: 'System', text });
                                } else {
                                    const text = `${who} betritt ${to}`;
                                    this.messages.push({ id: Date.now(), user: 'System', text });
                                }
                                return;
                            }
                            if (tr === 'room_entered') {
                                const to = jm.to || jm.name || jm.room || '';
                                this.handleIncoming({ type: 'system', code: 'room_entered', tr: 'room_entered', to });
                                return;
                            }
                            if (tr === 'color_changed' || tr === 'user_color_changed') {
                                this.handleIncoming({ type: 'system', code: tr, color: obj.color || jm.color, userName: obj.userName || jm.userName || '' });
                                return;
                            }
                            // Fallback: forward as generic system event
                            {
                                const to = jm.to || jm.name || jm.room || '';
                                const code = tr || 'room_entered';
                                this.handleIncoming({ type: 'system', code, tr: code, to });
                                return;
                            }
                        } catch (_) { /* fall through to plain string handlers */ }
                    }
                    if (msg === 'user_entered_chat') {
                        const who = obj.userName || obj.user || obj.name || '';
                        // cache provided color for future messages
                        let color = (obj.color || '').toString().trim();
                        if (color && !color.startsWith('#')) color = '#' + color;
                        if (who && color) this.userColors[who] = color;
                        const isSelf = !!(this.user?.username && who === this.user.username);
                        if (isSelf) {
                            this.chatConnected = true;
                            this.setStatus('connected');
                        }
                        const room = this.getSelectedRoomName();
                        const text = isSelf
                            ? (this.$t?.('chat.multichat.system.room_entered', { room }) || `Du betrittst ${room}`)
                            : `${who} betritt ${room}`;
                        this.messages.push({ id: Date.now(), user: 'System', text });
                        return;
                    }
                    if (msg === 'user_entered_room') {
                        const who = obj.userName || obj.user || obj.name || '';
                        const room = this.getSelectedRoomName();
                        const isSelf = !!(this.user?.username && who === this.user.username);
                        if (isSelf) {
                            this.chatConnected = true;
                            this.setStatus('connected');
                            const text = this.$t?.('chat.multichat.system.room_entered', { room }) || `Du betrittst ${room}`;
                            this.messages.push({ id: Date.now(), user: 'System', text });
                        } else if (who) {
                            const text = `${who} betritt ${room}`;
                            this.messages.push({ id: Date.now(), user: 'System', text });
                        }
                        return;
                    }
                    if (msg === 'user_left_room') {
                        const who = obj.userName || obj.user || obj.name || 'User';
                        const room = this.getSelectedRoomName();
                        let color = (obj.color || this.userColors[who] || '').toString().trim();
                        if (color && !color.startsWith('#')) color = '#' + color;
                        this.messages.push({
                            id: Date.now(),
                            type: 'action',
                            user: who,
                            action: this.$t?.('chat.multichat.action_phrases.leaves_room') || 'verlässt',
                            to: room,
                            color,
                            toColor: null
                        });
                        const idx = this.usersInRoom.findIndex(u => u.name === who);
                        if (idx >= 0) this.usersInRoom.splice(idx, 1);
                        if (this.selectedTargetUser === who) this.selectedTargetUser = null;
                        return;
                    }
                    if (msg === 'user_disconnected') {
                        const who = obj.userName || obj.user || obj.name || 'User';
                        let color = (obj.color || this.userColors[who] || '').toString().trim();
                        if (color && !color.startsWith('#')) color = '#' + color;
                        // Render as an action-style system line, localized
                        this.messages.push({
                            id: Date.now(),
                            type: 'action',
                            user: who,
                            action: this.$t?.('chat.multichat.action_phrases.left_chat') || 'hat den Chat verlassen.',
                            to: '',
                            color,
                            toColor: null
                        });
                        // Remove from right-side user list
                        const idx = this.usersInRoom.findIndex(u => u.name === who);
                        if (idx >= 0) this.usersInRoom.splice(idx, 1);
                        if (this.selectedTargetUser === who) this.selectedTargetUser = null;
                        return;
                    }
                    if (msg === 'room_change_user') {
                        const who = obj.userName || obj.user || obj.name || '';
                        const isSelf = !!(this.user?.username && who === this.user.username);
                        // Without structured payload we can't know target room reliably; show generic info
                        if (isSelf) {
                            this.chatConnected = true;
                            this.setStatus('connected');
                        }
                        const text = isSelf
                            ? this.$t?.('chat.multichat.system.room_entered', { room: this.getSelectedRoomName() }) || `Du betrittst ${this.getSelectedRoomName()}`
                            : `${who} betritt ${this.getSelectedRoomName()}`;
                        this.messages.push({ id: Date.now(), user: 'System', text });
                        return;
                    }
                    if (msg === 'room_entered') {
                        const to = obj.to || obj.name || obj.room || '';
                        this.handleIncoming({ type: 'system', code: 'room_entered', tr: 'room_entered', to });
                        return;
                    }
                    if (msg === 'color_changed' || msg === 'user_color_changed') {
                        this.handleIncoming({ type: 'system', code: msg, color: obj.color, userName: obj.userName || '' });
                        return;
                    }
                    this.handleIncoming({ type: 'system', text: msg });
                    return;
                }
                if (typeof msg === 'object' && msg) {
                    const tr = msg.tr || '';
                    if (tr === 'user_entered_chat') {
                        const who = obj.userName || obj.user || obj.name || '';
                        let color = (obj.color || '').toString().trim();
                        if (color && !color.startsWith('#')) color = '#' + color;
                        if (who && color) this.userColors[who] = color;
                        const to = msg.to || msg.name || msg.room || this.getSelectedRoomName();
                        const isSelf = !!(this.user?.username && who === this.user.username);
                        if (isSelf) {
                            this.chatConnected = true;
                            this.setStatus('connected');
                            const text = this.$t?.('chat.multichat.system.room_entered', { room: to }) || `Du betrittst ${to}`;
                            this.messages.push({ id: Date.now(), user: 'System', text });
                        } else {
                            const text = `${who} betritt ${to}`;
                            this.messages.push({ id: Date.now(), user: 'System', text });
                        }
                        return;
                    }
                    if (tr === 'user_entered_room') {
                        const who = obj.userName || obj.user || obj.name || '';
                        const to = msg.to || msg.name || msg.room || this.getSelectedRoomName();
                        const isSelf = !!(this.user?.username && who === this.user.username);
                        if (isSelf) {
                            this.chatConnected = true;
                            this.setStatus('connected');
                            const text = this.$t?.('chat.multichat.system.room_entered', { room: to }) || `Du betrittst ${to}`;
                            this.messages.push({ id: Date.now(), user: 'System', text });
                        } else {
                            const text = `${who} betritt ${to}`;
                            this.messages.push({ id: Date.now(), user: 'System', text });
                        }
                        return;
                    }
                    if (tr === 'user_left_room') {
                        const who = msg.userName || obj.userName || obj.user || obj.name || 'User';
                        const dest = msg.destination || msg.to || msg.name || msg.room || '';
                        let color = (msg.userColor || obj.color || this.userColors[who] || msg.color || '').toString().trim();
                        if (color && !color.startsWith('#')) color = '#' + color;
                        this.messages.push({
                            id: Date.now(),
                            type: 'action',
                            user: who,
                            action: this.$t?.('chat.multichat.action_phrases.left_room') || 'wechselt zu Raum',
                            to: dest,
                            color,
                            toColor: null
                        });
                        const idx = this.usersInRoom.findIndex(u => u.name === who);
                        if (idx >= 0) this.usersInRoom.splice(idx, 1);
                        if (this.selectedTargetUser === who) this.selectedTargetUser = null;
                        return;
                    }
                    if (tr === 'user_disconnected') {
                        const who = obj.userName || obj.user || obj.name || 'User';
                        let color = (obj.color || this.userColors[who] || '').toString().trim();
                        if (color && !color.startsWith('#')) color = '#' + color;
                        this.messages.push({
                            id: Date.now(),
                            type: 'action',
                            user: who,
                            action: this.$t?.('chat.multichat.action_phrases.left_chat') || 'hat den Chat verlassen.',
                            to: '',
                            color,
                            toColor: null
                        });
                        const idx = this.usersInRoom.findIndex(u => u.name === who);
                        if (idx >= 0) this.usersInRoom.splice(idx, 1);
                        if (this.selectedTargetUser === who) this.selectedTargetUser = null;
                        return;
                    }
                    // Explicit handling for user room changes
                    if (tr === 'room_change_user') {
                        const who = obj.userName || obj.user || obj.name || '';
                        const to = msg.to || msg.name || msg.room || '';
                        const isSelf = !!(this.user?.username && who === this.user.username);
                        if (isSelf) {
                            this.chatConnected = true;
                            this.setStatus('connected');
                            // Show the same line we use for room_entered for consistency
                            const text = this.$t?.('chat.multichat.system.room_entered', { room: to }) || `Du betrittst ${to}`;
                            this.messages.push({ id: Date.now(), user: 'System', text });
                        } else {
                            const text = `${who} betritt ${to}`;
                            this.messages.push({ id: Date.now(), user: 'System', text });
                        }
                        return;
                    }
                    const to = msg.to || msg.name || msg.room || '';
                    const code = tr || 'room_entered';
                    this.handleIncoming({ type: 'system', code, tr: code, to });
                    return;
                }
            }
            if (obj.type === 6) {
                this.handleIncoming({ type: 'scream', userName: obj.userName || obj.user || obj.name || '', message: obj.message || '', color: obj.color || null });
                return;
            }
            if (typeof obj.message === 'string' && (obj.userName || obj.user || obj.name || obj.token)) {
                // Pass through token if present so we can detect self-message when userName is missing
                this.handleIncoming({ type: 'message', message: obj.message, userName: obj.userName || obj.user || obj.name, color: obj.color || null, token: obj.token });
                return;
            }
            this.handleIncoming({ type: 'system', text: JSON.stringify(obj) });
        }
    }
};
</script>

<style scoped>
.multi-chat-top {
    display: flex;
    align-items: center;
    gap: 1em;
    margin-bottom: 0.5em;
    justify-content: space-between;
}

.room-left-controls {
    display: flex;
    align-items: center;
    gap: 0.5em;
}

.room-select {
    min-width: 10em;
}

.create-room-toggle-btn {
    border: 1px solid #bbb;
    background: #f5f5f5;
    border-radius: 4px;
    padding: 0.3em 0.8em;
    cursor: pointer;
}

.right-controls {
    display: flex;
    align-items: center;
    gap: 0.6em;
}

.status {
    display: inline-flex;
    align-items: center;
    gap: 0.4em;
    padding: 0.2em 0.5em;
    border-radius: 4px;
    border: 1px solid #bbb;
    background: #f9f9f9;
}

.status .dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #999;
}

.status.connected .dot {
    background: #2e7d32;
}

.status.connecting .dot {
    background: #f9a825;
}

.status.disconnected .dot {
    background: #6d4c41;
}

.status.error .dot {
    background: #c62828;
}

.reconnect-btn {
    margin-left: 0.4em;
    border: 1px solid #bbb;
    background: #fff;
    border-radius: 4px;
    padding: 0.1em 0.4em;
    cursor: pointer;
}

.options-dropdown-wrapper {
    position: relative;
}

.options-btn {
    background: #f5f5f5;
    border-radius: 4px;
    padding: 0.3em 0.8em;
    font-size: 0.95em;
    border: 1px solid #bbb;
    cursor: pointer;
}

.options-dropdown {
    position: absolute;
    top: 2.2em;
    right: 0;
    background: #fff;
    border: 1px solid #bbb;
    border-radius: 4px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    padding: 0.7em 1.2em;
    z-index: 10;
    min-width: 12em;
}

.opts-row {
    display: flex;
    gap: 0.4em;
    align-items: center;
    margin-top: 0.4em;
}

.opts-divider {
    height: 1px;
    background: #eee;
    margin: 0.5em 0;
}

.url-override {
    min-width: 16em;
}

.multi-chat-output {
    background: #fff;
    color: #000;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    margin-bottom: 0.5em;
    padding: 0.7em;
    border-radius: 4px;
    font-size: 1em;
    display: flex;
    flex-direction: column;
    border: 1px solid #222;
}

.room-create-panel {
    border: 1px solid #222;
    border-radius: 4px;
    background: #fff;
    padding: 0.7em;
    overflow-y: auto;
}

.room-create-title {
    font-weight: bold;
    margin-bottom: 0.6em;
}

.room-create-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.6em;
}

.room-create-grid label {
    display: flex;
    flex-direction: column;
    gap: 0.25em;
}

.room-create-grid input,
.room-create-grid select {
    border: 1px solid #bbb;
    border-radius: 3px;
    padding: 0.35em 0.5em;
}

.invalid-input {
    border-color: #c62828 !important;
    background: #fff6f6;
}

.checkbox-label {
    flex-direction: row !important;
    align-items: center;
    gap: 0.4em !important;
}

.room-create-actions {
    display: flex;
    gap: 0.5em;
    margin-top: 0.8em;
}

.create-room-reset-btn {
    border: 1px solid #bbb;
    background: #eee;
    border-radius: 3px;
    padding: 0.3em 0.8em;
    cursor: pointer;
}

.room-create-preview {
    margin-top: 0.7em;
    font-size: 0.9em;
    color: #444;
}

.owned-rooms-section {
    margin-top: 0.9em;
    border-top: 1px solid #eee;
    padding-top: 0.7em;
}

.owned-rooms-title {
    font-weight: bold;
    margin-bottom: 0.4em;
}

.owned-rooms-hint {
    font-size: 0.85em;
    color: #666;
    margin-bottom: 0.45em;
}

.owned-rooms-empty {
    font-size: 0.9em;
    color: #666;
}

.owned-rooms-list {
    display: flex;
    flex-direction: column;
    gap: 0.35em;
}

.owned-room-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.6em;
    border: 1px solid #eee;
    border-radius: 4px;
    padding: 0.35em 0.5em;
}

.owned-room-main {
    display: flex;
    align-items: center;
    gap: 0.5em;
    min-width: 0;
}

.owned-room-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.owned-room-badge {
    font-size: 0.75em;
    border-radius: 999px;
    padding: 0.1em 0.5em;
    border: 1px solid #bbb;
}

.owned-room-badge.public {
    background: #e8f5e9;
    border-color: #81c784;
}

.owned-room-badge.private {
    background: #fff3e0;
    border-color: #ffb74d;
}

.owned-room-delete-btn {
    border: 1px solid #bbb;
    border-radius: 3px;
    background: #fff;
    cursor: pointer;
    padding: 0.2em 0.55em;
}

.room-create-error {
    color: #b00020;
    font-size: 0.85em;
}

.room-create-error-block {
    margin-top: 0.5em;
}

.room-password-panel {
    margin-top: 0.6em;
    margin-bottom: 0.6em;
    padding: 0.6em;
    border: 1px solid #d7d7d7;
    border-radius: 6px;
    background: #f8f9fb;
}

.room-password-title {
    font-weight: bold;
    margin-bottom: 0.25em;
}

.room-password-message {
    font-size: 0.9em;
    color: #444;
    margin-bottom: 0.45em;
}

.room-password-controls {
    display: flex;
    align-items: center;
    gap: 0.45em;
}

.room-password-input {
    flex: 1;
    min-width: 0;
    border: 1px solid #bbb;
    border-radius: 3px;
    padding: 0.35em 0.5em;
}

.chat-message {
    margin-bottom: 0.3em;
}

.user {
    font-weight: bold;
    color: #90caf9;
}

.scream-label {
    font-weight: bold;
    text-transform: none;
}

.action-text,
.action-target {
    display: inline;
}

.action-text::before,
.action-target::before {
    content: " ";
}

.color-picker-panel {
    background: #f5f5f5;
    border-radius: 6px;
    padding: 1em;
    display: flex;
    flex-direction: column;
    gap: 0.8em;
    height: 16em;
}

.picker-row label {
    display: flex;
    align-items: center;
    gap: 0.6em;
}

.picker-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1em;
}

.palette-wrap {
    position: relative;
    width: max-content;
}

.palette-marker {
    position: absolute;
    width: 12px;
    height: 12px;
    border: 2px solid #fff;
    border-radius: 50%;
    box-shadow: 0 0 2px rgba(0, 0, 0, 0.8);
    pointer-events: none;
}

.picker-column {
    display: flex;
    flex-direction: column;
    gap: 0.6em;
}

.picker-label {
    min-width: 7em;
}

.hex-input {
    width: 10em;
}

.picker-val {
    min-width: 3em;
    text-align: right;
}

.picker-sample {
    border: 1px solid #ddd;
    border-radius: 4px;
    height: 6em;
}

.picker-error {
    color: #b00020;
}

.picker-preview {
    background: #fff;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 0.6em 0.8em;
    font-weight: bold;
}

.picker-actions {
    display: flex;
    gap: 0.6em;
}

.picker-actions .btn {
    padding: 0.4em 0.8em;
}

.picker-actions .btn.secondary {
    background: #eee;
}

.multi-chat-input {
    display: flex;
    align-items: center;
    gap: 0.5em;
}

.chat-input {
    flex: 1;
    padding: 0.4em 0.7em;
    border-radius: 3px;
    border: 1px solid #bbb;
}

.send-btn {
    padding: 0.3em 1.1em;
    border-radius: 3px;
    background: #1976d2;
    color: #fff;
    border: none;
    cursor: pointer;
}

.icon-btn {
    width: 24px;
    height: 24px;
    cursor: pointer;
    margin-left: 0.2em;
    vertical-align: middle;
}

.icon-btn.disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.dialog-form,
.multi-chat-input {
    flex-shrink: 0;
}

.dialog-widget-content {
    display: flex;
    flex-direction: column;
    height: 100%;
}

/* Two-column chat + user list */
.multi-chat-body {
    display: grid;
    grid-template-columns: 1fr 220px;
    gap: 0.6em;
    flex: 1 1 auto;
    min-height: 0;
}

.user-list {
    display: flex;
    flex-direction: column;
    min-height: 0;
}

.user-list-header {
    font-weight: bold;
    margin-bottom: 0.4em;
}

.user-list-items {
    border: 1px solid #222;
    background: #fff;
    border-radius: 4px;
    padding: 0.4em 0.5em;
    overflow-y: auto;
    min-height: 0;
}

.user-list-item {
    display: flex;
    align-items: center;
    gap: 0.4em;
    padding: 0.15em 0.1em;
}

.user-list-item.selected {
    background: rgba(25, 118, 210, 0.12);
    border-radius: 4px;
}

.user-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #ccc;
}

.user-name {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
</style>
