/**
 *  Copyright © 2025, Luis Andrés Lange <https://javacomm.net>
 *
 *  Previously released under Apache License 2.0; now licensed under MPL 2.0.
 *
 *  This Source Code Form is subject to the terms of the Mozilla Public
 *  License, v. 2.0. If a copy of the MPL was not distributed with this
 *  file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 *  ----------------------------------------------------------------------------
 *
 *  Exhibit B - "Incompatible With Secondary Licenses" Notice
 *
 *  This Source Code Form is "Incompatible With Secondary Licenses",
 *  as defined by the Mozilla Public License, v. 2.0.
 *
 *  In short:
 *  - This file may be used, modified, and distributed under MPL 2.0 only.
 *  - It may NOT be relicensed under GPL, LGPL, AGPL, or any other Secondary License.
 *
 *  Rationale:
 *  - Ensures that the code remains MPL-2.0.
 *  - Avoids legal conflicts with GPL-licensed libraries (e.g., VideoLAN).
 *  - Maximizes usability for commercial and security-critical applications.
 *
 */
package net.javacomm.server;

import static net.javacomm.protocol.HEADER.CONFIRM;
import static net.javacomm.protocol.HEADER.ERROR;
import static net.javacomm.protocol.HEADER.REQUEST;
import static net.javacomm.protocol.HEADER.RESPONSE;
import jakarta.websocket.CloseReason;
import jakarta.websocket.CloseReason.CloseCodes;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.MessageHandler;
import jakarta.websocket.PongMessage;
import jakarta.websocket.SendHandler;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.PrivateKey;
import java.sql.SQLException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import javax.crypto.SecretKey;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import net.javacomm.database.DatabaseException;
import net.javacomm.database.WebdatabaseImpl;
import net.javacomm.database.entity.EntityUser;
import net.javacomm.database.entity.Konferenzraum;
import net.javacomm.database.entity.Online;
import net.javacomm.database.entity.PrivateChatfile;
import net.javacomm.database.entity.Record;
import net.javacomm.database.entity.Screencast;
import net.javacomm.database.entity.ViewSenderSession;
import net.javacomm.facade.DatabaseService;
import net.javacomm.multilingual.MultilingualString;
import net.javacomm.multilingual.schema.ISO639;
import net.javacomm.multilingual.schema.KEY;
import net.javacomm.protocol.Agent;
import net.javacomm.protocol.BEAMEROFF;
import net.javacomm.protocol.BREAKROOMS;
import net.javacomm.protocol.BlacklistTypes;
import net.javacomm.protocol.CALLPRIVATECHAT;
import net.javacomm.protocol.CALLREMOTEUSER;
import net.javacomm.protocol.CANDIDATETOPICMEMBER;
import net.javacomm.protocol.CHANGETOPICMEMBER;
import net.javacomm.protocol.CHATMESSAGE;
import net.javacomm.protocol.CHATONLINELIST;
import net.javacomm.protocol.CHATUSERLIST;
import net.javacomm.protocol.CONFERENCE;
import net.javacomm.protocol.CONFERENCEAUDIO;
import net.javacomm.protocol.CONFERENCEMUTE;
import net.javacomm.protocol.CONFERENCEVIDEO;
import net.javacomm.protocol.CREATETEMPROOM;
import net.javacomm.protocol.ChatUser;
import net.javacomm.protocol.Command;
import net.javacomm.protocol.DELETEROOM;
import net.javacomm.protocol.DELETEUPLOADFILES;
import net.javacomm.protocol.DIAL;
import net.javacomm.protocol.DOWNLOAD;
import net.javacomm.protocol.ENTERROOM;
import net.javacomm.protocol.FILETYPES;
import net.javacomm.protocol.HISTORYMESSAGE;
import net.javacomm.protocol.IMAGE;
import net.javacomm.protocol.INCOMINGCALL;
import net.javacomm.protocol.LEAVEPRIVATECHAT;
import net.javacomm.protocol.LEAVEROOM;
import net.javacomm.protocol.MESSAGE;
import net.javacomm.protocol.MICROERROR;
import net.javacomm.protocol.MessageDecoder;
import net.javacomm.protocol.MessageEncoder;
import net.javacomm.protocol.ONCALL;
import net.javacomm.protocol.ONHOOK;
import net.javacomm.protocol.PRIVATECHATFILE;
import net.javacomm.protocol.PRIVATEMESSAGE;
import net.javacomm.protocol.PROYECTORCLOSING;
import net.javacomm.protocol.Protocol;
import net.javacomm.protocol.READROOMS;
import net.javacomm.protocol.READTOPICROOMOWNER;
import net.javacomm.protocol.ROOMLIST;
import net.javacomm.protocol.Roomtype;
import net.javacomm.protocol.SCREENCAST;
import net.javacomm.protocol.SEARCHFILES;
import net.javacomm.protocol.STOPSENDINGVIDEO;
import net.javacomm.protocol.Searchfile;
import net.javacomm.protocol.TOPICMEMBER;
import net.javacomm.protocol.Token;
import net.javacomm.protocol.UPDATEFILETYPES;
import net.javacomm.protocol.UPDATEPHONE;
import net.javacomm.protocol.UPDATEPORT;
import net.javacomm.protocol.UPDATEUSER;
import net.javacomm.protocol.UPLOADFILES;
import net.javacomm.protocol.USERONLINELIST;
import net.javacomm.protocol.USRLOGIN;
import net.javacomm.protocol.crypto.Crypto;
import net.javacomm.restserver.Screenshot;

//Die zentrale Warteschlange für ALLES (Text & Audio)
//Wir nutzen Object, damit MESSAGE und byte[] nebeneinander existieren können.
//private static final Map<String, ConcurrentLinkedQueue<Object>> messageQueues = new ConcurrentHashMap<>();
//
////Die Zündschlösser (Besen-Logik)
//private static final Map<String, AtomicBoolean> running = new ConcurrentHashMap<>();
//
////Fehlerzähler für das automatische Schließen bei Verbindungsabbruch
//private static final Map<String, AtomicInteger> errorCounter = new ConcurrentHashMap<>();
//
////Der Threadpool, der die Worker bereitstellt
//private static final ExecutorService senderFIFO = Executors.newCachedThreadPool();



@ServerEndpoint(value = "/portal", encoders = {MessageEncoder.class}, decoders = {MessageDecoder.class})
public class Webserver extends Endpoint {

  private final static Logger log = LogManager.getLogger(Webserver.class);

  /**
   * key/value ---> (sessionId/Session) sessionId ist ein Datenbankwert
   */
  private static final ConcurrentHashMap<String, Session> sessionmap = new ConcurrentHashMap<>();

  /**
   * Der Key ist ein UUIDv4 Conference-Schlüssel und die CopyOnWriteArrayList eine
   * Websocket-Session-Liste von allen Teilnehmern. Der Absender einer Nachricht
   * ist in der ArrayList enthalten. An ihn darf nicht gesendet werden, weil es
   * bei Audio/Video/Daten eine Rückkopplung gibt.
   */
  public static final ConcurrentHashMap<String, CopyOnWriteArrayList<String>> conferenceMap = new ConcurrentHashMap<>();
  /**
   * (pseudoid/session)
   */
  public static final ConcurrentHashMap<String, String> onlineMap = new ConcurrentHashMap<>();

  /**
   * Nachrichtenwarteschlangen pro Session
   */
  private static final ConcurrentHashMap<String, ConcurrentLinkedQueue<Object>> messageQueues = new ConcurrentHashMap<>();
  /**
   * Ein Executor-Threadpool für die Sender
   */
  private static final ConcurrentHashMap<String, AtomicBoolean> running = new ConcurrentHashMap<>();
  /**
   * Maximale Anzahl Fehler, bevor eine Session geschlossen wird
   */
  private static final int MAX_ERRORS_BEFORE_CLOSE = 50;

  /**
   * Jeder audioChunk ist an eine streamID(String) gekoppelt. Die streamID ist
   * eine UUIDv4. Jede streamID gehört zu einem Stream mit einem Anwender.
   */
  private static final ConcurrentHashMap<String, CONFERENCEAUDIO> audioChunks = new ConcurrentHashMap<>();

  /**
   * Fehlerzähler pro Session
   */
  private static final ConcurrentHashMap<String, AtomicInteger> errorCounter = new ConcurrentHashMap<>();

  // Nachrichten FIFO pro Client garantiert.
  private static ExecutorService senderFIFO;

  private static ScheduledExecutorService executorKeepAlive;

  static Runnable keepalvieRunnable = () -> {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();

    int minutos = databaseservice.forumCycle();
    int result = databaseservice.changeToForum(minutos); // in ein Forum
                                                         // wandeln 150
                                                         // Minuten

    if (result > 0) {
      // wenn gewandelt, dann Raumliste aktualisieren
      // kann auch wieder einen Zähler einbauen
      // Für Testzwecke auf 5 Minuten eingestellt
      // Roomliste übergeben
      try {
        ROOMLIST roomlist = databaseservice.fetchRoomlist(); // Alle vorhandenen Chaträume werden ermittelt
        // sende eine vollständige Raumliste an alle Anwender
        // ROMMLIST beschreibt die Räume(Pausenraum, Forum etc.) und nnicht die Anzahl
        // der User und deren Attribute JLauncher Alle Räume anzeigen
        broadcastAll(roomlist);
      }
      catch (DatabaseException e2) {
        log.warn(e2.getMessage(), e2.getCause());
        return;
      }
    }

  };

  static Runnable activeStreamsCleaner = () -> {
    try {
      if (audioChunks.isEmpty()) return;

      int removedCount = audioChunks.size();
      audioChunks.clear();

      log.info("Cleaner (Sicherheitsnetz): {} verwaiste Meta-Einträge bereinigt.", removedCount);
    }
    catch (Exception e) {
      log.warn("Fehler im activeStreamsCleaner: {}", e.getMessage());
    }
  };

  /**
   * Sende eine Nachricht an alle Anwender.
   * 
   * @param message
   *                diese Nachricht wird gesendet
   */
  public static void broadcastAll(MESSAGE message) {
    ArrayList<Session> snapshot = new ArrayList<>(sessionmap.values());
    for (Session client : snapshot) {
      sendMessage(message, client.getId());
    }
  }



  /**
   * Lösche den User aus einer Reihe von Tabellen.
   * 
   * @param userid
   *                  dieser Anwender wird bereinigt
   * @param sessionid
   *                  unter dieser Sessionid war der User angemeldet
   */
  private static void cleanUser(String userid, String sessionid) {

    // muss geprüft werden
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();

    String pseudoid = databaseservice.fetchPseudoidByUserid(userid);

    databaseservice.deleteUserChatrooms(userid);
    databaseservice.deleteTransferLib(userid);

    // War der der der letzte in einem Temproom
    // Durchsuche alle Temprooms, ob sie leer sind
    // Wenn ja, lösche den Temproom und alle Messages

    // select all TEMPROOMS
    List<String> temproom = databaseservice.fetchTemproomlist();
    for (String room : temproom) {
      int countUser = databaseservice.countTemproomUser(room);
      // deleteChatroom
      if (countUser == 0) {
        databaseservice.deleteTemproom(room);
      }
      // User wird aus einem Pausenraum/Group herausgenommen
      databaseservice.remove(userid, room);
    }

    // die privatechats müssen auch gelöscht werden!
    // die Gegenseite muss benachrichtigt werden
    // input sessionID dieser User ist gegangen
//    String pseudoid = databaseservice.fetchPseudoidBySessionid(sessionid);
    List<String> counterIds = databaseservice.deleteAllPrivateChats(pseudoid); // pseudoid statt sessionid
    LEAVEPRIVATECHAT leavePrivatechat = new LEAVEPRIVATECHAT();
    leavePrivatechat.setCommand(Command.LEAVEPRIVATECHAT);
    leavePrivatechat.setHeader(REQUEST);
    leavePrivatechat.setDataset(Protocol.DATASET);
    leavePrivatechat.setRemotePseudoid(pseudoid);

    try {
      List<String> sessionIds = counterIds.stream()
          .peek(pseudo -> log.info("Verarbeite Pseudo-ID: {}", pseudo))
          .map(pseudo -> databaseservice.fetchSessionByPseudoid(pseudo)).collect(Collectors.toList());
      log.info("Alle Private Chats von {} löschen", userid);
      log.info("-----> {}", leavePrivatechat.toString());

      sendMessage(leavePrivatechat, sessionIds);
    }
    catch (Exception e) {
      log.info(e);
    }

//  Telefonate
    // Hole den Partner von uid
//    log.info("Hole den Partner von uid");
    String partneruid = databaseservice.findPartnerUserid(userid);
//    log.info("partneruid {}", partneruid);
    String partnerPseudoid = databaseservice.fetchPseudoidByUserid(partneruid);
//    log.info("partnerPseudoid {}", partnerPseudoid);
    if (partneruid != null) {
      databaseservice.setOnBusy(partneruid, false);
      log.debug("partneruid benachrichtigen, dass die Verbindung weg ist");

      // uid hat aufgelegt
      // uid!=partneruid
      boolean isCaller = databaseservice.isPartnerCaller(partneruid); // true partneruid ist calleruid
      ONHOOK onhook = new ONHOOK();
      onhook.setCommand(Command.ONHOOK);
      onhook.setHeader(REQUEST);
      onhook.setDataset(Protocol.DATASET);
      onhook.setCallerPseudoid(isCaller ? partnerPseudoid : pseudoid); // Wer ist Anrufer?
      // ReceiverUserid ersetzen druch ReceiverPseudoid
      onhook.setReceiverPseudoid(isCaller ? pseudoid : partnerPseudoid); // Wer ist Empfänger
      onhook.setTo(isCaller ? false : true); // Wer hat aufgelegt? // true der Caller hat aufgelegt
      onhook.setSignal(true);
      log.info("-----> " + onhook.toString());
      String key = databaseservice.getSessionidByUserid(partneruid);
      sendMessage(onhook, key);
    }

    // Der Gegenüber müsste benachrichtigt werden, dass ein plötzlicher Ausfall
    // vorliegt. Die Gegenseite muss benachrichtig werden, dass der Gesprächspartner
    // nicht mehr online ist

    // konferenzraum ist immer null, weil der Status von OFFHOOK schon 0 ist.
    // OFFHOOK wird über CONFERENCE auf 0 gesetzt, wenn der Anwender geht
    Optional<Konferenzraum> optional = databaseservice.fetchConferenceRoom(userid);
    optional.ifPresent(konferenzraum -> {
      databaseservice.updateKonferenzraumOnlinestatus(
          konferenzraum.getKonferenzraum(), konferenzraum.getOrganisator(), userid, false
      );
      // ich habe einen Anwender gefunden, der bis jetzt online war.

      try {

        // CONFERENCEVIDEO Finde alle Empfänger von USERID; falls gefunden
        // CONFERENECEVIDEO SHARING=FALSE

        List<String> receivers = databaseservice.fetchReceiversBySender(userid);

        log.info("receivers {}", receivers.size());

        if (!receivers.isEmpty()) {
          String nickname = databaseservice.fetchNickname(userid);
          CONFERENCEVIDEO conferencevideo = new CONFERENCEVIDEO();
          conferencevideo.setCommand(Command.CONFERENCEVIDEO);
          conferencevideo.setHeader(REQUEST);
          conferencevideo.setDataset(Protocol.DATASET);
          conferencevideo.setKonferenzraum(konferenzraum.getKonferenzraum());
          conferencevideo.setNickname(nickname);
          conferencevideo.setShared(false);
          log.info("-----> {}", conferencevideo.toString());
          sendMessage(conferencevideo, receivers);
        }

        String senderFromReceiver = databaseservice.senderFromReceiver(userid);
        // senderFromReceiver kann null sein
        boolean genau1x = databaseservice.countVideosender(senderFromReceiver);
        log.info("genau1x=" + genau1x);
        if (genau1x) {
          // Lösche aus TB_VIDEO_SENDER wird von STOPSENDINGVIDEO über den Client
          // ausgelöst
          // databaseservice.deleteVideoSenderUser(userid);

          // Sende STOPSENDINGVIDEO
          STOPSENDINGVIDEO stopSendingVideo = new STOPSENDINGVIDEO();
          stopSendingVideo.setCommand(Command.STOPSENDINGVIDEO);
          stopSendingVideo.setHeader(REQUEST);
          stopSendingVideo.setDataset(Protocol.DATASET);
          stopSendingVideo.setUserid(senderFromReceiver);

          log.info("-----> " + stopSendingVideo.toString());

          // Hole Sessionid vom Bildersender
          String bildersender = databaseservice.getSessionidByUserid(senderFromReceiver);
          sendMessage(stopSendingVideo, bildersender);
        }
      }
      catch (DatabaseException e) {
        log.info(e.getMessage());
      }
      databaseservice.deleteVideoReceiver(userid);
      // hier kann der Projektor gelöscht werden, wenn offhook=false ist. Nur
      // ein Receiver kann einen eingeschalteten Projektor haben

      // Lösche alle verwaisten Sender aus TB_VIDEO_SENDER
      // Wenn der Sender verwaist zurückbleibt, dann lösche ihn und alle anderen, die
      // du findest
      databaseservice.deleteOrphainedSender();

      databaseservice.switchProjektor(false, userid);

      // CONFERENCE
      CONFERENCE response = databaseservice
          .readKonferenzuser(konferenzraum.getKonferenzraum(), konferenzraum.getOrganisator());
      response.setOffhook(false);
      response.setPseudoid(pseudoid);

      // in der RESPONSE muss die Session für die uerserid fehlen, weil der User
      // gegangen ist. Die Session wird später aus der TB_ONLINE gelöscht.

      // TODO muss geprüft werden, weil userid fehlt, deshalb ist der Filter
      // wirkungslos

      Arrays.stream(response.getKonferenzraumUser())
          .filter(konferenzuser -> pseudoid.equals(konferenzuser.getPseudoid()))
          .forEach(found -> found.setWebsocketSession(null));

      // an alle Sessions eine Antwort senden
      // response ---> CONFERENCE sendet an alle Teilnehmer in einem Konferenzraum
      // in offhook steht derjenige, der aufgelegt oder abgelegt hat
      List<String> sessions = databaseservice
          .fetchKonferenzraumSessions(konferenzraum.getKonferenzraum(), konferenzraum.getOrganisator());
      log.info("Ausgang <----- " + response.toString());
      sendMessage(response, sessions);

    });

    databaseservice.deleteScreencastUser(sessionid);
    databaseservice.switchProjektor(false, userid);
    databaseservice.deleteVideoUser(userid); // Lösche
    // aus
    // TB_VIDEO
    databaseservice.deleteVideoSenderUser(userid); // Lösche aus TB_VIDEO_SENDER
    databaseservice.deletePartner(userid); // TB_TOPHONE
    // muss zuletzt gelöscht werden, wegen xxxSessionidByUserid()

    databaseservice.deleteOnline(userid); // Lösche user aus der TB_ONLINE
    // Alle Einträge in transfer_lib vom User löschen
    databaseservice.deleteUserChatfiles(userid);
    Screenshot.removeScreenshot(pseudoid); // pseudoid
    onlineMap.remove(pseudoid); // pseudoid

  }



  /**
   * 
   * @param sessionId
   *                  diese sessionId
   */
  private static void handleError(String sessionId) {
    log.error("[Dispatcher] Session {} entfernt (geschlossen oder tot)", sessionId);
    sessionmap.remove(sessionId);
    messageQueues.remove(sessionId);
    running.remove(sessionId);
  }



  /**
   * Der Empfänger wird in einen FIFO gestellt. Sendewarteschlange pro Session.
   *
   * @param client
   *               an diesen Client wird gesendet
   */
  /**
   * Der Empfänger wird in einen FIFO gestellt. Sendewarteschlange pro Session.
   * Verarbeitet nun sowohl MESSAGE-Objekte (Text) als auch byte[] (Audio).
   *
   * @param client
   *               an diesen Client wird gesendet
   */
  /**
   * Der Empfänger wird in einen FIFO gestellt. Sendewarteschlange pro Session.
   * Verarbeitet nun sowohl MESSAGE-Objekte (Text) als auch byte[] (Audio).
   *
   * @param client
   *               an diesen Client wird gesendet
   */
  private static void processQueue(Session client) {
    String sessionId = client.getId();

    // Aktuelle Session validieren
    Session current = sessionmap.get(sessionId);
    if (current == null || !current.isOpen()) {
      log.warn("[Dispatcher] Stale or closed session {} – Queue wird verworfen", sessionId);
      messageQueues.remove(sessionId);
      AtomicBoolean runningFlag = running.get(sessionId);
      if (runningFlag != null) runningFlag.set(false);
      return;
    }

    ConcurrentLinkedQueue<Object> queue = messageQueues.get(sessionId);
    if (queue == null) return;

    AtomicBoolean runningFlag = running.get(sessionId);

    try {

//      int currentQueueSize = queue.size();
//      if (currentQueueSize > 50) {
//          log.warn("[Performance] Session {} hat hohen Backpressure! Queue-Größe: {}", sessionId, currentQueueSize);
//          // Optional: Wenn die Queue extrem voll ist (z.B. > 100), könnte man hier alte Audio-Pakete 
//          // am Kopf der Queue entfernen, um Platz für Kommandos zu machen.
//      }

      Object data = queue.poll();
      if (data == null) {
        if (runningFlag != null) runningFlag.set(false);
        return;
      }

      if (!current.isOpen()) {
        if (runningFlag != null) runningFlag.set(false);
        return;
      }

      // Gemeinsamer SendHandler für Text und Audio
      SendHandler handler = result -> {
        try {
          if (!result.isOK()) {
            int errors = errorCounter.computeIfAbsent(sessionId, id -> new AtomicInteger(0))
                .incrementAndGet();

            if (errors >= 45) {
              log.warn(
                  "[Dispatcher] Fehler beim Senden an Session {} (Fehler #{}/{})", sessionId, errors,
                  MAX_ERRORS_BEFORE_CLOSE
              );
            }

            if (errors >= MAX_ERRORS_BEFORE_CLOSE) {
              log.error("[Dispatcher] Session {} entfernt nach {} Fehlern", sessionId, errors);
              handleError(sessionId);
              errorCounter.remove(sessionId);
              if (current.isOpen()) {
                try {
                  current.close();
                }
                catch (IOException e) {
                  log.info(e.getMessage());
                }
              }
            }
          }
          else {
            errorCounter.remove(sessionId);
          }
        }
        finally {
          // Den nächsten Sendevorgang erst anstoßen, wenn dieser komplett fertig ist
          if (current.isOpen() && !queue.isEmpty()) {
            senderFIFO.execute(() -> processQueue(current));
          }
          else {
            if (runningFlag != null) runningFlag.set(false);
          }
        }
      };

      // Typ-Unterscheidung für den Versand
      if (data instanceof byte[]) {
        current.getAsyncRemote().sendBinary(ByteBuffer.wrap((byte[]) data), handler);
      }
      else if (data instanceof MESSAGE) {
        current.getAsyncRemote().sendObject(data, handler);
      }
      else {
        log.warn("[Dispatcher] Unbekannter Typ {} in Queue", data.getClass().getSimpleName());
        senderFIFO.execute(() -> processQueue(current));
      }

    }
    catch (Exception e) {
      log.error("[Dispatcher] Exception beim Senden an Session {}: {}", sessionId, e.getMessage());
      if (current.isOpen()) {
        handleError(sessionId);
        errorCounter.remove(sessionId);
        try {
          current.close();
        }
        catch (IOException e1) {
          log.info(e1.getMessage());
        }
      }
      if (runningFlag != null) runningFlag.set(false);
    }
  }



  /**
   * 
   * @param session
   *                die Session vom gegangegen User
   * @param userid
   *                dieser User ist gegangen
   */
  public static void purge(String session, String userid) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();

    CloseReason reason = new CloseReason(CloseCodes.GOING_AWAY, "KeepAlive exprired");
    log.info("keepalive Zeitüberschreitung vom user mit der Session = " + session + " / " + userid);
    Screencast screencast = databaseservice.getScreencastByReceiverSession(session);
    if (screencast.getSenderSession() != null) {
      // Empfänger verliert Verbindung
      String senderuid = databaseservice.getUserBySession(screencast.getSenderSession());
      String senderPseudoid = databaseservice.fetchPseudoidByUserid(senderuid);

      if (senderuid != null) {
        log.info("BEAMER senderuid=" + userid);
        // BEAMEROFF senden, wenn eine Bildschirmübertragung läuft, an wen senden?
        // Wenn der user in der Tabelle TB_SCREENCAST als RECEIVER eingetragen ist, dann
        // BEMEROFF an den SENDER schicken
        BEAMEROFF beamer = new BEAMEROFF();
        beamer.setCommand(Command.BEAMEROFF);
        beamer.setHeader(REQUEST);
        beamer.setDataset(Protocol.DATASET);
        beamer.setPseudoid(senderPseudoid);
        sendMessage(beamer, screencast.getSenderSession());
      }
    }
    else {
      String callerSession = databaseservice.getSessionidByUserid(userid);
      Screencast screencast2 = databaseservice.getScreencastByCallerSession(callerSession);
      String receiverSession = screencast2.getReceiverSession();
      log.info(
          "PROYECTORCLOSING senderuid {}, callerSession {}, receiverSession {}", userid, callerSession,
          receiverSession
      );
      if (receiverSession != null) {

        String pseudoid = databaseservice.fetchPseudoidByUserid(userid);

        // Sender verliert Verbindung
        // muss PROYECTORCLOSING sein
        // PROYECTORCLOSING senden
        PROYECTORCLOSING proyector = new PROYECTORCLOSING();
        proyector.setCommand(Command.PROYECTORCLOSING);
        proyector.setHeader(REQUEST);
        proyector.setDataset(Protocol.DATASET);
        proyector.setPseudoid(pseudoid);
        log.info(proyector.toString());
        sendMessage(proyector, receiverSession);
        databaseservice.deleteScreencastUser(receiverSession); // Lösche aus TB_SCREENCAST
      }
    }
    // darf nicht vor getScreencastByReceiverSession stehen, weil die
    // RECEIVERSESSION schon gelöscht wurde
    // muss zuletzt gelöscht werden, wegen xxxSessionidByUserid()

    log.info("löschen userid {}", userid);
    cleanUser(userid, session);

    Session closeSession = sessionmap.get(session);
    try {
      closeSession.close(reason);
    }
    catch (IOException e) {
      log.info(e.getMessage(), e);
    }

//  an alle User USERONLINELIST senden 
    USERONLINELIST userOnlinelist = databaseservice.fetchUseronlinelist();
    List<String> ids = sessionmap.values().stream().map(Session::getId).collect(Collectors.toList());
    List<String> sessionAsString = List.copyOf(ids);
    log.info("-----> {}", userOnlinelist.toString());
    sendMessage(userOnlinelist, sessionAsString);
  }



  /**
   * Alle 90 Sekunden durch Scheduler für alle Sessions in der sessionmap
   */
  public static void runHealthCheck() {
    long now = System.currentTimeMillis();
    long maxIdleTime = 120_000; // 2 Minuten

//    log.info("runHealthCheck");
    sessionmap.values().forEach(session -> {
      if (!session.isOpen()) {
        purge(session.getId(), (String) session.getUserProperties().get("userid"));
        return;
      }

      // 1. Hole den letzten Lebenszeichen-Zeitstempel aus den UserProperties
      Long lastSeen = (Long) session.getUserProperties().get("LAST_ACTIVE");

      // Initialisierung, falls noch kein Pong kam (direkt nach onOpen)
      if (lastSeen == null) {
        lastSeen = now;
        session.getUserProperties().put("LAST_ACTIVE", now);
      }

      // 2. PRÜFUNG: Ist der Client überfällig?
      if (now - lastSeen > maxIdleTime) {
        log.warn("Session {} ist überfällig ({}ms). Schließe Verbindung.", session.getId(), now - lastSeen);
        purge(session.getId(), (String) session.getUserProperties().get("userid"));

//        try {
//          // Das ist der "Gnadenschuss"
//          session.close(new CloseReason(CloseReason.CloseCodes.GOING_AWAY, "Timeout"));
//        }
//        catch (IOException e) {
//          purge(session.getId(), (String) session.getUserProperties().get("userid"));
//        }
      }
      else {
        // 3. SCHUBSER: Wenn er noch im Zeitrahmen ist, schicke den nächsten Ping

//        if ("louisxiv".equals(((String) session.getUserProperties().get("userid")))) {
//          return;
//        }

//        log.info("SCHUBSER: Wenn er noch im Zeitrahmen ist, schicke den nächsten Ping");
        try {
          session.getBasicRemote().sendPing(ByteBuffer.wrap("PING".getBytes()));
        }
        catch (Exception e) {
          log.error("Ping-Versand gescheitert für {}", session.getId());
          purge(session.getId(), (String) session.getUserProperties().get("userid"));
        }
      }
    });
  }



  /**
   * Sende eine Botschaft an viele Anwender aber nicht an alle Anwender.
   * 
   * @param message
   *                         diese Botschaft
   * @param receiverSessions
   *                         an diese Empfänger wird gesendet
   */
  public static void sendMessage(MESSAGE message, List<String> receiverSessions) {
    ArrayList<String> snapshot = new ArrayList<>(receiverSessions);
    synchronized (message) {
      for (String receiver : snapshot) {
        sendMessage(message, receiver);
      }
    }
  }



  /**
   * Der Server sendet eine einzelne Botschaft an den Client. Diese Methode ist
   * threadsafe.
   * 
   * 
   * @param message
   *                diese Botschaft
   * @param client
   *                dieser Client ist der Empfänger
   */
  public static void sendMessage(MESSAGE message, Session client) {
//    alte Session-Objekte können noch im Speicher existieren, auch wenn die Online-Tabelle bereits nur die aktuelle Session zeigt
//
//    Dein sendMessage() benutzt das Session-Objekt, nicht die Session-ID
//
//    messageQueues und processQueue laufen über das Session-Objekt
//
//    Das ist die einzige Stelle, wo ein „stale reference“-Problem auftreten kann, trotz Einmal-Anmeldung und funktionierender Online-Tabelle.

    if (client == null || !client.isOpen()) {
      log.warn("[Dispatcher] Session ist null oder geschlossen, Nachricht verworfen");
      return;
    }

    // Queue für die Session anlegen

    synchronized (message) {
      synchronized (client) {
        messageQueues.computeIfAbsent(client.getId(), id -> new ConcurrentLinkedQueue<>()).add(message);
      }

      // Läuft schon ein Worker? Falls nein: starten
      running.computeIfAbsent(client.getId(), id -> new AtomicBoolean(false));
      if (running.get(client.getId()).compareAndSet(false, true)) {
        senderFIFO.execute(() -> processQueue(client));
      }
    }
  }



  /**
   * Der Server sendet eine einzelne Botschaft an den Client. Diese Methode ist
   * threadsafe.
   * 
   * @param message
   *                  diese Botschaft
   * @param sessionid
   *                  an diesen Client wird gesendet
   */
  private static void sendMessage(MESSAGE message, String sessionid) {
    Session client = sessionmap.get(sessionid);
    if (client == null) {
      sessionmap.remove(sessionid);
      return;
    }
    synchronized (message) {
      sendMessage(message, client);
    }
  }



  /**
   * Der Server wird heruntergefahren und alle offenen Threads beendet.
   */
  public static void shutdown() {
    executorKeepAlive.shutdown();
    try {
      boolean done = executorKeepAlive.awaitTermination(5, TimeUnit.SECONDS);
      if (!done) {
        executorKeepAlive.shutdownNow();
      }
    }
    catch (InterruptedException e) {
      log.error(e.getMessage(), e.getCause());
    }
    log.info("KEEPALIVE is down");

    senderFIFO.shutdown();
    try {
      boolean done = senderFIFO.awaitTermination(2, TimeUnit.SECONDS);
      if (!done) {
        senderFIFO.shutdownNow();
      }
    }
    catch (InterruptedException e) {
      log.error(e.getMessage(), e.getCause());
    }
    log.info("senderFIFO is down");

  }



  /**
   * Die Application wird gerade gestartet und als erstes werden die
   * Hintergrundthreads gestartet.
   */
  public static void startup() {
    executorKeepAlive = Executors.newScheduledThreadPool(
        3, runnable -> new Thread(Thread.currentThread().getThreadGroup(), runnable, "keepalive")
    );
    executorKeepAlive.scheduleWithFixedDelay(Webserver::runHealthCheck, 60, 90, TimeUnit.SECONDS);
    executorKeepAlive.scheduleWithFixedDelay(keepalvieRunnable, 20, 60, TimeUnit.SECONDS);
    executorKeepAlive.scheduleWithFixedDelay(activeStreamsCleaner, 60, 60, TimeUnit.SECONDS);
    senderFIFO = Executors.newCachedThreadPool();
  }



  /**
   * Jeder Login startet eine neue Instance.
   */
  public Webserver() {
    log.info("new client instance has started...");
  }



  /**
   * Chatuserliste wird gesendet;CHATUSERLIST fetchRoomlist wird gesende;ROOMLIST
   * Onlineliste wird gesendet;ONLINELIST
   * 
   * @param chatRaume
   *                  diese Räume werden aktualisiert
   */
  private void broadcast(List<String> chatRaume) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    final Logger log = LogManager.getLogger(Webserver.class);

    // an alle Anwender wird gesendet und nicht nur die Anwender im Raum
    // die Benutzerlsiten(Nicknames) werden aktualisert
    // die Aktualisierung bezieht sich nur auf bestimmte Räume

    chatRaume.forEach(room -> {
      CHATUSERLIST chatuserlist = databaseservice.fetchChatuserlist(room);
      // muss noch optimiert werden, weil er an alle Anwender sendet
      // er soll nur an die Anwender in einem Raum senden
      // TODO wenn die user/Sessions ermittelt wurden, ist alles vorhanden
      broadcastAll(chatuserlist);
    });

    // Roomliste übergeben
    ROOMLIST roomlist;
    try {
      roomlist = databaseservice.fetchRoomlist(); // Alle vorhandenen Chaträume werden ermittelt
    }
    catch (DatabaseException e2) {
      log.warn(e2.getMessage(), e2.getCause());
      return;
    }
    // sende eine vollständige Raumliste an alle Anwender
    // ROMMLIST beschreibt die Räume(Pausenraum, Forum etc.) und nicht die Anzahl
    // der User und deren Attribute JLauncher Alle Räume anzeigen
    broadcastAll(roomlist);
    // Onlinelist übergeben Onlinelist hat einen Request
    // Ermittle die Anzehl aller User in einem Raum
    // Zurückgegeben werden der Raum und die Anzahl der Anwender in dem Raum
    CHATONLINELIST onlinelist;
    try {
      onlinelist = databaseservice.fetchOnlinelist();
    }
    catch (DatabaseException e1) {
      log.warn(e1.getMessage(), e1.getCause());
      return;
    }
    // Sende an alle Anwender
    broadcastAll(onlinelist);
  }



  /**
   * 
   * Bekommt eine positiv response, wenn die Gegenstelle nicht bereit ist.
   * Rufnummerunterdrückung Bekommt eine error response, wenn die gegenstelle
   * offline ist
   * 
   * @param request
   * @return
   */
  private CALLPRIVATECHAT callPrivatechat(CALLREMOTEUSER remoteuser) {
    if (CONFIRM == remoteuser.getHeader()) {
      CALLPRIVATECHAT call = new CALLPRIVATECHAT();
      call.setCommand(Command.CALLPRIVATECHAT);
      call.setHeader(CONFIRM);
      call.setDataset(Protocol.DATASET);
      call.setRemoteNickname(remoteuser.getRemoteNickname());
      call.setRemotePseudoid(remoteuser.getRemotePseudoid());
      return call;
    }
    else if (RESPONSE == remoteuser.getHeader()) {
      CALLPRIVATECHAT call = new CALLPRIVATECHAT();
      call.setCommand(Command.CALLPRIVATECHAT);
      call.setHeader(RESPONSE);
      call.setDataset(Protocol.DATASET);
      call.setLocalNickname(remoteuser.getLocalNickname());
      call.setPseudoid(remoteuser.getPseudoid());
      call.setRemoteNickname(remoteuser.getRemoteNickname());
      call.setRemotePseudoid(remoteuser.getRemotePseudoid());
      call.setMultilingualkey(KEY.SERVER_LEHNT_CHATANFRAGEN_AB);
      return call;
    }
    CALLPRIVATECHAT call = new CALLPRIVATECHAT();
    return call;

  }



  private CANDIDATETOPICMEMBER candidateMember(String pseudoid, String room) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    CANDIDATETOPICMEMBER response = databaseservice.fetchCandidatemember(pseudoid, room);
    return response;
  }



  /**
   * 
   * @param pseudoid
   *                 dieser User
   * @param room
   *                 dieser Raum
   * @param roomtype
   *                 dieser Raumtyp
   * @param chatuser
   * 
   * @return modifizierte Anwenderliste in diesem Themenraum
   */
  private CHANGETOPICMEMBER changeTopicmember(String pseudoid, String room, Roomtype roomtype,
      ChatUser[] chatuser) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    CHANGETOPICMEMBER response = databaseservice.changeMitglieder(pseudoid, room, roomtype, chatuser);
    return response;
  }



  private CHATMESSAGE chatmessage(CHATMESSAGE request) {
    CHATMESSAGE response = new CHATMESSAGE();
    if (REQUEST == request.getHeader()) {
      DatabaseService databaseservice = WebdatabaseImpl.getInstance();
      log.info("<----- {}", request.toString());
      try {
        Record entity = databaseservice.insertChatmessage(
            request.getRoom(), request.getChatUser().getPseudoid(), request.getAttachment(),
            request.getMessage()
        );

        response.setCommand(Command.CHATMESSAGE);
        response.setHeader(RESPONSE);
        response.setDataset(Protocol.DATASET);
        response.setFilename(request.getFilename()); // Protocollaänderunge nicht übertragen
        response.setFilesize(request.getFilesize()); // Protocollaänderunge nicht übertragen

        ChatUser user = new ChatUser();
        user.setBackgroundColor(request.getChatUser().getBackgroundColor());
        user.setForegroundColor(request.getChatUser().getForegroundColor());
        user.setNickname(request.getChatUser().getNickname());

        response.setChatUser(user);
        response.setRoom(entity.getChatid());
        response.setAttachment(entity.getAnlage());
        response.setMessage(entity.getMessage());

        // Zeit ist nicht aus der Datenbank
        ZonedDateTime tz = ZonedDateTime.now(ZoneId.of(DatabaseService.TIMEZONE));
        long value = tz.toInstant().toEpochMilli();
        response.setDatetime(value);

        if (response.getAttachment() != null) databaseservice.updateDeletable(true, response.getAttachment());
        return response;
      }
      catch (DatabaseException e) {
        log.info(e.getMessage());
        response = new CHATMESSAGE();
        response.setCommand(Command.CHATMESSAGE);
        response.setHeader(ERROR);
        response.setDataset(Protocol.DATASET);
        response.setErrorMessage(e.getMessage());
      }
    }
    else {
      response.setCommand(Command.CHATMESSAGE);
      response.setHeader(ERROR);
      response.setDataset(Protocol.DATASET);
      response.setErrorMessage("CHATMESSAGE ist kein REQUEST");
    }
    return response;
  }



  private void cleanupConferenceRAM(String sessionid) {
    // Wir iterieren über alle aktiven Konferenzen auf dem Server
    conferenceMap.forEach((confKey, participants) -> {
      // Entferne NUR diesen einen User aus der Liste dieser Konferenz
      if (participants.remove(sessionid)) {
        log.info("Conferencekey {}, Session gelöscht {}", confKey, sessionid);

        // NUR WENN die Liste jetzt leer ist, wird der Key für diese Konferenz gelöscht
        if (participants.isEmpty()) {
          conferenceMap.remove(confKey);
          log.info("Conferencekey {} aus RAM gelöscht.", confKey);
        }
      }
    });
  }



  CREATETEMPROOM createTemproom(CREATETEMPROOM request) {
    if (REQUEST == request.getHeader()) {
      DatabaseService databaseservice = WebdatabaseImpl.getInstance();
      CREATETEMPROOM response = databaseservice.createRoom(request);
      return response;
    }
    return new CREATETEMPROOM();
  }



  private DELETEROOM deleteRoom(String pseudoid, String room) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    Optional<String> userid = databaseservice.fetchUserByPseudoid(pseudoid);
    boolean done = databaseservice.deleteRoom(room, userid.get());
    DELETEROOM response = new DELETEROOM();
    if (done) {
      response.setCommand(Command.DELETEROOM);
      response.setHeader(RESPONSE);
      response.setDataset(Protocol.DATASET);
      response.setRoom(room);
      response.setPseudoid(pseudoid);
      StringBuilder buffer = new StringBuilder();
      buffer.append("Der Besprechungsraum <b><span style=\"color:red\">");
      buffer.append(room);
      buffer.append("</b></span> wurde gelöscht.");
      response.setText(buffer.toString());
    }
    else {
      response.setCommand(Command.DELETEROOM);
      response.setHeader(ERROR);
      response.setDataset(Protocol.DATASET);
      response.setRoom(room);
      response.setPseudoid(pseudoid);
      StringBuilder buffer = new StringBuilder();
      buffer.append("Diesen Besprechungsraum <b><span style=\"color:red\">");
      buffer.append(room);
      buffer.append("</b></span> gibt es nicht.");
      response.setText(buffer.toString());
    }
    return response;
  }



  /**
   * Lösche alle Dateien eines Anwenders, die er für den Upload freigegeben hatte.
   * 
   * @param pseudoid
   *                 dieser Anwender
   * @param ip
   *                 diese IPv4-Adresse
   * @return
   */
  private DELETEUPLOADFILES deleteuploadfiles(String pseudoid, String ip) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    try {
      Optional<String> userid = databaseservice.fetchUserByPseudoid(pseudoid);
      databaseservice.deleteUploadfiles(userid.get(), ip);
      DELETEUPLOADFILES deletefiles = new DELETEUPLOADFILES();
      deletefiles.setCommand(Command.DELETEUPLOADFILES);
      deletefiles.setHeader(CONFIRM);
      return deletefiles;
    }
    catch (SQLException e) {
      log.warn(e.getMessage(), e.getCause());
      DELETEUPLOADFILES deletefiles = new DELETEUPLOADFILES();
      deletefiles.setCommand(Command.DELETEUPLOADFILES);
      deletefiles.setHeader(ERROR);
      deletefiles.setDataset(Protocol.DATASET);
      deletefiles.setErrorCode(e.getErrorCode());
      deletefiles.setErrorMessage(e.getMessage());
      deletefiles.setText("A database error has occured.");
      return deletefiles;
    }
  }



  /**
   * Der Anwender mit der {@code sessionid} ist gegangen.
   * 
   * @param sessionid
   *                  ein eindeutiger Onlineschlüssel
   */
  private void disconnect(String sessionid) {
    // Session markieren
    Session session = sessionmap.remove(sessionid);
    if (session != null) {
      session.getUserProperties().put("closed", true);
    }

    // Nachrichtenwarteschlange wegräumen
    messageQueues.remove(sessionid);

    // Räume/DB aktualisieren
    DatabaseService db = WebdatabaseImpl.getInstance();
    List<String> roomlist = db.fetchRoomlist(sessionid);
    removeUser(sessionid);
    broadcast(roomlist);

  }



  private HISTORYMESSAGE historymessage(HISTORYMESSAGE request) {
    if (REQUEST == request.getHeader()) {
      DatabaseService databaseservice = WebdatabaseImpl.getInstance();
      HISTORYMESSAGE response = databaseservice.fetchHistorymessage(request.getRoom());
      return response;
    }
    HISTORYMESSAGE history = new HISTORYMESSAGE();
    return history;
  }



  private ONCALL onCall(ONCALL request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();

    Optional<String> userid = databaseservice.fetchUserByPseudoid(request.getPseudoid());

    databaseservice.userOnCall(userid.get(), request.isOnCall());

    ONCALL response = new ONCALL();
    response.setCommand(request.getCommand());
    response.setDataset(request.getDataset());
    response.setHeader(RESPONSE);
    response.setOnCall(request.isOnCall());
    response.setPseudoid(request.getPseudoid());
    return response;
  }



  /**
   * Die Methode wird aufgerufen, wenn die Server-Client-Verbindung gerade
   * geschlossen wird.
   * 
   * @param session
   *                diese Session wird geschlossen.
   * @param reason
   *                der Grund für den Verbindungsabbruch
   */
  @Override
  public void onClose(Session session, CloseReason reason) {
    // Markiere Session als geschlossen, bevor Properties entfernt werden
    session.getUserProperties().put("closed", true);

    log.warn(
        "onClose: sessionId={} code={} reason={}", session == null ? "<null>" : session.getId(),
        reason == null ? -1 : reason.getCloseCode().getCode(),
        reason == null ? "<null>" : reason.getReasonPhrase()
    );

    String sessionid = session.getId();
    disconnect(sessionid);

    // Cleanup verzögert – nach dem Disconnect ist kein Handler mehr aktiv
    session.getUserProperties().remove("streamId");
    session.getUserProperties().remove("userid");
    session.getUserProperties().remove("userid|flac");

    // Konferenzen aus dem RAM löschen
    cleanupConferenceRAM(session.getId());

  }



  @Override
  public void onError(Session session, Throwable throwable) {
    // Markiere Session als geschlossen
    session.getUserProperties().put("closed", true);

    log.error(
        "onError: sessionId={} err={}", session == null ? "<null>" : session.getId(),
        throwable == null ? "<null>" : throwable.getMessage(), throwable
    );

    if (throwable != null) {
      log.fatal("Fatal error in WebSocket: {}", throwable.getMessage());
    }

    disconnect(session.getId());

    // Danach Properties wegräumen
    session.getUserProperties().remove("streamId");
    session.getUserProperties().remove("userid");
    session.getUserProperties().remove("userid|flac");
  }



  /**
   * Führt die notwendigen Bereinigungsarbeiten durch, wenn ein
   * Keepalive-Intervall überschritten wurde.
   * <p>
   * Diese Methode entfernt den Anwender aus den aktiven Datenbanktabellen und
   * informiert alle verbleibenden Teilnehmer über das Verlassen des Anwenders.
   * Sie sollte aufgerufen werden, sobald ein Verbindungsverlust oder ein Timeout
   * detektiert wurde.
   * </p>
   */
  public void onKeepaliveTimeout() {

    try {
      DatabaseService databaseservice = WebdatabaseImpl.getInstance();
      DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG)
          .withLocale(Locale.getDefault());

      CloseReason reason = new CloseReason(CloseCodes.GOING_AWAY, "KeepAlive exprired");

      // Ist die Zeitscheibe abgelaufen?
      List<Online> online = Collections.synchronizedList(databaseservice.deleteOnlineKeepalive());
      online.forEach(user -> {

        ZonedDateTime zdtKeepalive = user.getKeepalive().withZoneSameInstant(ZoneId.systemDefault());
        String datumKeepAlive = zdtKeepalive.format(formatter);

        log.info(datumKeepAlive);
        log.info(
            "keepalive Zeitüberschreitung vom user mit der Session = " + user.getSession()
                + " / "
                + user.getUid()
        );

        Screencast screencast = databaseservice.getScreencastByReceiverSession(user.getSession());
        if (screencast.getSenderSession() != null) {
          // Empfänger verliert Verbindung
          String senderuid = databaseservice.getUserBySession(screencast.getSenderSession());
          if (senderuid != null) {
            log.info("BEAMER senderuid=" + user.getUid());
            String senderPseudoid = databaseservice.fetchPseudoidByUserid(senderuid);
            // BEAMEROFF senden, wenn eine Bildschirmübertragung läuft, an wen senden?
            // Wenn der user in der Tabelle TB_SCREENCAST als RECEIVER eingetragen ist, dann
            // BEMEROFF an den SENDER schicken
            BEAMEROFF beamer = new BEAMEROFF();
            beamer.setCommand(Command.BEAMEROFF);
            beamer.setHeader(REQUEST);
            beamer.setDataset(Protocol.DATASET);
            beamer.setPseudoid(senderPseudoid);
            sendMessage(beamer, screencast.getSenderSession());
          }
        }
        else {
//        log.info("PROYECTORCLOSING senderuid=" + user.getUid());
          String callerSession = databaseservice.getSessionidByUserid(user.getUid());
          Screencast screencast2 = databaseservice.getScreencastByCallerSession(callerSession);
//        log.info("callerSession=" + callerSession);
          String receiverSession = screencast2.getReceiverSession();
//        log.info("receiverSession=" + receiverSession);
          if (receiverSession != null) {
            // Sender verliert Verbindung
            // muss PROYECTORCLOSING sein
            // PROYECTORCLOSING senden

            PROYECTORCLOSING proyector = new PROYECTORCLOSING();
            proyector.setCommand(Command.PROYECTORCLOSING);
            proyector.setHeader(REQUEST);
            proyector.setDataset(Protocol.DATASET);
            proyector.setPseudoid(user.getPseudoid()); // pseudoid aus Online
            log.info(proyector.toString());
            sendMessage(proyector, receiverSession);
            databaseservice.deleteScreencastUser(receiverSession); // Lösche aus TB_SCREENCAST
          }
        }
        // darf nicht vor getScreencastByReceiverSession stehen, weil die
        // RECEIVERSESSION schon gelöscht wurde
        // muss zuletzt gelöscht werden, wegen xxxSessionidByUserid()

        cleanUser(user.getUid(), user.getSession());

        Session closeSession = sessionmap.get(user.getSession());
        try {
          closeSession.close(reason);
        }
        catch (IOException e) {
          log.info(e.getMessage(), e);
        }
      });
    }
    catch (SQLException e) {
      log.error(e);
    }
  }



  /**
   * Eine Server-Client-Verbindung wird in diesem Moment hergestellt.
   * 
   * @param session
   *                die gerade geöffnete Session
   * @param config
   *                Websocket Endpoint
   */
  @Override
  public void onOpen(Session session, EndpointConfig config) {
    session.getAsyncRemote().setSendTimeout(2000);
    session.getUserProperties().put("streamId", new ByteArrayOutputStream());
    session.getUserProperties().put("userid|flac", new ByteArrayOutputStream());
    session.getUserProperties().put("authorized", false);

    log.info("onOpen: sessionId={}", session.getId());
    sessionmap.put(session.getId(), session);
    messageQueues.computeIfAbsent(session.getId(), id -> new ConcurrentLinkedQueue<>());

    // Registrierung für den Pong-Handler
    session.addMessageHandler(new MessageHandler.Whole<PongMessage>() {
      @Override
      public void onMessage(PongMessage pongMessage) {
        // Ein Pong ist angekommen!
        // pongMessage.getApplicationData() enthält die Daten, die im Ping gesendet
        // wurden
        session.getUserProperties().put("LAST_ACTIVE", System.currentTimeMillis());
//        log.info("Pong erhalten von Session {}", session.getId());
      }
    });

    /**
     * MessageHandler für empfangene binary Nachrichten (Screenshots / Audio)
     */
    // --- PATCH START ---
    // Datei: BinarySender / Dispatcher

    // In sendBinary (session.addMessageHandler)
    session.addMessageHandler(byte[].class, data -> {

      if (!(Boolean) session.getUserProperties().get("authorized")) {
        try {
          sessionmap.remove(session.getId());
          // Nachrichtenwarteschlange wegräumen
          messageQueues.remove(session.getId());
          log.info("Verbindung schließen für " + session.getId());
          session.close();
          log.info("Verbindung wurde erfolgreich geschlossen für " + session.getId());
        }
        catch (IOException e) {
          log.warn(e.getMessage(), e);
        }
        return;
      }

      Boolean closed = (Boolean) session.getUserProperties().get("closed");
      if (Boolean.TRUE.equals(closed)) return;

      ByteArrayOutputStream streamId = (ByteArrayOutputStream) session.getUserProperties().get("streamId");
      ByteArrayOutputStream userid_flac = (ByteArrayOutputStream) session.getUserProperties()
          .get("userid|flac");

      if (streamId == null) return;

      try {
        streamId.reset();
        streamId.write(data, 0, 36);
      }
      catch (Exception e) {
        return;
      }

      CONFERENCEAUDIO meta = audioChunks.remove(streamId.toString());
      if (meta == null) return;

      // der Nachrichtensender
      String sourceWebsocketSession = meta.getWebsocketSession();
      byte[] audioPacket;
      synchronized (userid_flac) {
        userid_flac.reset();
        userid_flac.writeBytes(sourceWebsocketSession.getBytes());
        userid_flac.write(data, 36, data.length - 36);
        audioPacket = userid_flac.toByteArray();
      }

      // Ab dieser Stelle wird der eingehende Audiostream an die Clients verteilt
      // meta.getReceiverSessions() ist nach der Protokolländerung von TARGET
      // optional und möglicherweise leer
      // In diesem Fall kommt der Audiostream bei den Clients nie an.

      // ConferenceKey übernimmt jetzt die Verteilung an die Clients
      // ConferenceKey muss immer in CONFERENCEAUDIO gesetzt sein

// Hier beginnt neu
      String conferencekey = meta.getConferencekey();
      List<String> targets = conferenceMap.get(conferencekey);
      if (targets != null) {

        for (String targetId : targets) {

          // BOCK-SCHUTZ: Sende niemals an dich selbst zurück
          if (targetId.equals(session.getId())) {
            continue;
          }
          Session targetSession = sessionmap.get(targetId);
          if (targetSession == null || !targetSession.isOpen()) continue;

          // Hier nutzen wir deine Logik: In Queue packen und Worker starten
          ConcurrentLinkedQueue<Object> queue = messageQueues.get(targetId);
          if (queue != null) {
            // Optional: Audio-Backpressure. Wenn schon mehr als 10 Pakete warten,
            // verwerfen wir das neue, um Kommandos nicht zu verzögern.
            if (queue.size() < 20) {
              queue.add(audioPacket);
            }

            // Deine bewährte Logik zum Starten des Workers
            running.computeIfAbsent(targetId, id -> new AtomicBoolean(false));
            if (running.get(targetId).compareAndSet(false, true)) {
              senderFIFO.execute(() -> processQueue(targetSession));
            }
          }

        }

      }

    });

    // --- processQueue bleibt unverändert, nur sicherstellen:
    // running Flag + pendingSends verhindern Command-Loss
    // Keine Änderung nötig, da Commands bereits seriell durch processQueue laufen

    // --- PATCH END ---

    session.addMessageHandler(new MessageHandler.Whole<MESSAGE>() {
      @Override
      public void onMessage(MESSAGE message) {

        if (!(message instanceof USRLOGIN)) {
          // TODO session prüfen
          // ENTERROOM etc.

          if (!(Boolean) session.getUserProperties().get("authorized")) {

            try {
              sessionmap.remove(session.getId());
              // Nachrichtenwarteschlange wegräumen
              messageQueues.remove(session.getId());
              log.info("Verbindung schließen für " + session.getId());
              session.close();
              log.info("Verbindung wurde erfolgreich geschlossen für " + session.getId());
            }
            catch (IOException e) {
              log.warn(e.getMessage(), e);
            }
            return;
          }
        }

        if (message instanceof CHATMESSAGE) {
          CHATMESSAGE chatmessage = (CHATMESSAGE) message;
          // Die Nachricht wird noch nicht dauerhaft in der Datenbank abgelegt */

          CHATMESSAGE response = chatmessage(chatmessage);
          if (ERROR == response.getHeader()) {
            log.info("Antwort wird nicht gesendet.");
            return;
          }
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          List<String> sessionlist = databaseservice.fetchSessionlist(chatmessage.getRoom());
          log.info("-----> {}", response.toString());
          sendMessage(response, sessionlist);
          return;
        }
        else if (message instanceof CONFERENCEAUDIO) {
          CONFERENCEAUDIO conferenceaudio = (CONFERENCEAUDIO) message;
          // der Anwender sendet seinen Stream an alle

//          log.info(
//              "Check: Stream {} hat ReceiverSessions: {}", conferenceaudio.getStreamId(),
//              (conferenceaudio.getReceiverSessions() == null ? "NULL"
//                  : conferenceaudio.getReceiverSessions().length)
//          );
          audioChunks.put(conferenceaudio.getStreamId(), conferenceaudio);
          return;
        }
        else if (message instanceof CONFERENCEVIDEO) {
          CONFERENCEVIDEO conferenceVideo = (CONFERENCEVIDEO) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          // Bevor die Übertragung beginnen kann, muss geprüft werden, ob die anderen
          // Teilnehmer 1:1 Übertragungen am Laufen haben

          log.info("<----- " + conferenceVideo.toString());

          String userid = databaseservice.getUserBySession(session.getId());

          // filter alle receiverSessions heraus, die keine eigenen Übertragungen haben

          String conferencekey = conferenceVideo.getConferencekey();

          CopyOnWriteArrayList<String> originalList = Webserver.conferenceMap.get(conferencekey);
          originalList.forEach(id -> log.info("In original enthaltene Session: {}", id));
          log.info("---");

          CopyOnWriteArrayList<String> receiverSessions = new CopyOnWriteArrayList<>(originalList);
          receiverSessions.removeIf(receiver -> receiver.equals(session.getId()));

          receiverSessions.forEach(id -> log.info("In receiverSessions enthaltene Session: {}", id));
          log.info("--");

          if (conferenceVideo.isShared()) {
            // Übertragungsaufbau

            try {
              if (databaseservice.isVideoRunning(userid) > 0) {
                // ERROR
                CONFERENCEVIDEO error = new CONFERENCEVIDEO();
                error.setCommand(Command.CONFERENCEVIDEO);
                error.setHeader(ERROR);
                error.setDataset(Protocol.DATASET);
                error.setMultilingualkey(KEY.SERVER_DRITTE_PERSON);
                error.setShared(false);
                log.info("-----> " + error.toString());
                sendMessage(error, session);
                return;
              }
            }
            catch (DatabaseException e) {
              return;
            }

            databaseservice.insertVideoSender(userid);
            // die Empfänger müssen noch eingetragen werden!!!!!!!!!!
            // finde zu allen receiverSessions die userid

            // über conferencekey alle userids finden
            for (String receiverSession : receiverSessions) {
              String receiverUid = databaseservice.getUserBySession(receiverSession);
              if (receiverUid != null) databaseservice.insertVideo(userid, receiverUid);
            }
          }
          else {
            // SHARING = false
            // Übertragungsabbau ist immer erlaubt
            // Ist dieser userssion in ein sender in der TB_SCREENCAST?

            ViewSenderSession viewSenderSession = databaseservice.fetchVideosenderByReceiver(userid);

            if (viewSenderSession == null) {
              databaseservice.deleteVideoSenderUser(userid); // Lösche TB_VIDEO_SENDER
              databaseservice.deleteVideoUser(userid);
            }
            else {
              Screencast screencast = databaseservice
                  .getScreencastByCallerSession(viewSenderSession.getSession());
              // kommt die Session in der TB_SCREENCAST vor?
              if (screencast.getSenderSession() == null) {
                databaseservice.deleteVideoSenderUser(userid);
                databaseservice.deleteVideoUser(userid);
              }
              else {
                databaseservice.deleteVideoSenderUser(userid); // Lösche TB_VIDEO_SENDER
                databaseservice.deleteVideoScreencaster(userid); // Lösche TB_VIDEO
              }
            }

          }
          log.info("-----> " + conferenceVideo.toString());
          // übertrage an alle Empfänger
          sendMessage(conferenceVideo, receiverSessions);

          return;
        }
        else if (message instanceof CONFERENCE) {
          CONFERENCE conference = (CONFERENCE) message;
          log.info("<----- " + conference.toString());
          if (conference.getHeader() == REQUEST) {

            // OFFHOOK=false, dann ONPROJEKTOR = aus FENSTER Schließen senden,
            // IMAGE_RECEIVER stoppen

            // Lies den Status der Konferenzteilnehmer aus.
            // Wer ist online und wer ist offline

            String konferenzraum = conference.getKonferenzraum();

//            String userid = conference.getUserid(); // er hat die Anfrage gestellt
            boolean offhook = conference.isOffhook();
            DatabaseService databaseservice = WebdatabaseImpl.getInstance();

            String userid = databaseservice.getUserBySession(session.getId());

            String organisatorUid = databaseservice
                .fetchOrganisatorByConferencekey(conference.getConferenceKey());

//            log.info("userid {}, offhook {}", userid, offhook);

            databaseservice.updateKonferenzraumOnlinestatus(konferenzraum, organisatorUid, userid, offhook);
            // in CONFERENCE sind aller Telkomitglieder mit ihrem Status anwesend/abwesend
            // enthalten.
            if (!offhook) {
              // Hörer aufgelegt
              // Hole den Sender vom Receiver
              // Wenn der Sender nicht

              // User legt auf: Raus aus der RAM-Map
              CopyOnWriteArrayList<String> sessions = conferenceMap.get(conference.getConferenceKey());
              if (sessions != null) {
                sessions.remove(session.getId());
                // SCHRUMPFEN: Wenn Raum leer, Key löschen
                if (sessions.isEmpty()) {
                  conferenceMap.remove(conference.getConferenceKey());
                }

                log.info("verbleibende Sessions {}", sessions.size());

              }

              try {
                String senderFromReceiver = databaseservice.senderFromReceiver(userid);
                // senderFromReceiver kann null sein
                boolean genau1x = databaseservice.countVideosender(senderFromReceiver);
                log.info("genau1x=" + genau1x);
                if (genau1x) {

                  // Sende STOPSENDINGVIDEO
                  STOPSENDINGVIDEO stopSendingVideo = new STOPSENDINGVIDEO();
                  stopSendingVideo.setCommand(Command.STOPSENDINGVIDEO);
                  stopSendingVideo.setHeader(REQUEST);
                  stopSendingVideo.setDataset(Protocol.DATASET);
                  stopSendingVideo.setUserid(senderFromReceiver);

                  log.info("-----> " + stopSendingVideo.toString());

                  // Hole Sessionid vom Bildersender
                  String bildersender = databaseservice.getSessionidByUserid(senderFromReceiver);
                  sendMessage(stopSendingVideo, bildersender);

                }
              }
              catch (DatabaseException e) {
                log.info("!!!!!!!!!!!!!!!!!!Kein Sender für den Empfänger '" + userid + "' gefunden, ok");
              }

              databaseservice.deleteVideoReceiver(userid);
              // Lösche alle verwaisten Sender aus TB_VIDEO_SENDER
              // Wenn der Sender verwaist zurückbleibt, dann lösche ihn und alle anderen, die
              // du findest
              databaseservice.deleteOrphainedSender();

              // hier kann der Projektor gelöscht werden, wenn offhook=false ist. Nur
              // ein Receiver kann einen eingeschalteten Projektor haben
              databaseservice.switchProjektor(false, userid);
            }
            else {

              conferenceMap.computeIfAbsent(conference.getConferenceKey(), k -> new CopyOnWriteArrayList<>())
                  .addIfAbsent(session.getId()); // keine doppelten Einträge

              log.info("verbleibende Sessions {}", conferenceMap.get(conference.getConferenceKey()).size());

              // Der Anwender hat den Telefonhörer in einem Konfernzraum abgenommen.
              // Läuft in diesem gerade eine Videokonferenz?
              // Wenn ja, dann muss der Projektor beim Anwender eingeschaltet werden.
              // Der Anwender muss in die TB_VIDEO eingetragen werden.
              // Der Client muss den Projektor einschalten

              Optional<String> optional = databaseservice
                  .fetchKonferenzraumActiveUser(konferenzraum, organisatorUid);

              optional.ifPresent(videosender -> {
                databaseservice.insertVideo(videosender, userid);
                CONFERENCEVIDEO conferencevideo = new CONFERENCEVIDEO();
                conferencevideo.setCommand(Command.CONFERENCEVIDEO);
                conferencevideo.setHeader(REQUEST);
                conferencevideo.setDataset(Protocol.DATASET);
                conferencevideo.setKonferenzraum(konferenzraum);

                String conferencekey = conference.getConferenceKey();

//                CopyOnWriteArrayList<String> originalList = Webserver.conferenceMap.get(conferencekey);
//                originalList.forEach(id -> log.info("In original enthaltene Session: {}", id));
//                CopyOnWriteArrayList<String> receiverSessions = new CopyOnWriteArrayList<>(originalList);
//                receiverSessions.removeIf(receiver -> receiver.equals(session.getId()));

                conferencevideo.setConferencekey(conferencekey);
                conferencevideo.setShared(true);
                conferencevideo.setNickname(databaseservice.fetchNickname(videosender));
                log.info("-----> " + conferencevideo.toString());
                sendMessage(conferencevideo, conference.getSession());
              });

              // Konferenzraum Active_USER
            }
            CONFERENCE conferencE = databaseservice.readKonferenzuser(konferenzraum, organisatorUid);
            conferencE.setOffhook(offhook);
            conferencE.setPseudoid(conference.getPseudoid());
            // an alle Sessions eine Antwort senden
            // response ---> CONFERENCE sendet an alle Teilnehmer in einem Konferenzraum
            // in offhook steht derjenige, der aufgelgt oder abgelegt hat
            List<String> sessions = databaseservice.fetchKonferenzraumSessions(konferenzraum, organisatorUid);
            log.info("-----> " + conferencE.toString());
            sendMessage(conferencE, sessions);
          }
          return;
        }
        else if (message instanceof CONFERENCEMUTE) {
          CONFERENCEMUTE mute = (CONFERENCEMUTE) message;
          log.info("<----- {}", mute.toString());
          switch(mute.getHeader()) {
            case CONFIRM:
              break;
            case ERROR:
              break;
            case REQUEST:
              try {
                DatabaseService databaseservice = WebdatabaseImpl.getInstance();
                Optional<String> userid = databaseservice.fetchUserByPseudoid(mute.getPseudoid());
                databaseservice.updateMute(mute.isMute(), userid.get());
                // kann null sein, weil der Empfänger nicht auswertet
                mute.setPseudoid(null);

                CopyOnWriteArrayList<String> originalList = Webserver.conferenceMap
                    .get(mute.getConferencekey());
                originalList.forEach(id -> log.info("In MUTE original enthaltene Session: {}", id));
                log.info("---");

                CopyOnWriteArrayList<String> receiverSessions = new CopyOnWriteArrayList<>(originalList);
                receiverSessions.removeIf(receiver -> receiver.equals(session.getId()));
                receiverSessions.forEach(id -> log.info("In receiverSessions enthaltene Session: {}", id));

                log.info("------> {}", mute.toString());
                sendMessage(mute, receiverSessions);
              }
              catch (SQLException e) {
                log.error(e.getMessage(), e);
              }
              break;
            case RESPONSE:
              break;
            default:
              break;
          }
          return;
        }
        else if (message instanceof PROYECTORCLOSING) {
          PROYECTORCLOSING projector = (PROYECTORCLOSING) message;
          log.info("<----- " + projector.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();

          Optional<String> userid = databaseservice.fetchUserByPseudoid(projector.getPseudoid());

          switch(projector.getHeader()) {
            case RESPONSE:
              databaseservice.switchProjektor(false, userid.get());
              break;
            case REQUEST:
              // Teilenframe geschlossen
              String callerSession = databaseservice.getSessionidByUserid(userid.get());

              // Conferencekey finden und löschen

              Set<Map.Entry<String, CopyOnWriteArrayList<String>>> set = conferenceMap.entrySet();
              set.removeIf(entry -> entry.getValue().contains(callerSession));

              log.info("conferenceMap {}", conferenceMap.size());

              Screencast screencast = databaseservice.getScreencastByCallerSession(callerSession);
              log.info("Die Projektorverbindung wurde gekappt.");
              log.info("Der Anwender(Sender) ist gerade offline gegangen= " + userid);
              String receiverSession = screencast.getReceiverSession();
              if (receiverSession == null) {
                log.info("-----> Der Empfänger ist nicht mehr vorhanden");
                return;
              }
              databaseservice.deleteScreencastUser(receiverSession);
              databaseservice.deleteVideoSenderUser(userid.get());
              databaseservice.deleteVideoUser(userid.get());
              log.info("-----> " + projector.toString());
              sendMessage(projector, receiverSession);
              return;
            default:
              return;
          }
          return;
        }
        else if (message instanceof BEAMEROFF) {
          BEAMEROFF beamer = (BEAMEROFF) message;
          log.info("<----- " + beamer.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          // beamer.getPseudoid() ist der Empfänger
          Optional<String> userid = databaseservice.fetchUserByPseudoid(beamer.getPseudoid());
          switch(beamer.getHeader()) {
            case REQUEST:

              String sender = databaseservice.senderFromReceiver(userid.get());
              // Achtung die Löschreihenfolge spielt eine Rolle
              // muss vor deleteVideoUser stehen
              if (sender != null) databaseservice.deleteVideoSenderUser(sender);
              databaseservice.deleteVideoUser(userid.get());

              // aus der TB_SCREENCAST löschen
              String sessionid = session.getId();
              Screencast screencast = databaseservice.getScreencastByReceiverSession(sessionid);
              // die screencast.getSenderSession kann null sein, weil zuvor ein RESPONSE
              // eingegenagen ist.
              // der Sender hatte den Abbrechen-Knopf betätigt.
              // Später hat dann der Empfänger den Projektorframe geschlossen.
              // die sessionid ist in der Tabelle nicht mehr enthalten.
              if (screencast.getSenderSession() == null) return;
              databaseservice.deleteScreencastUser(screencast.getReceiverSession());

              Set<Map.Entry<String, CopyOnWriteArrayList<String>>> set = conferenceMap.entrySet();
              set.removeIf(entry -> entry.getValue().contains(sessionid));

              log.info("conferenceMap {}", conferenceMap.size());

              // weiterleiten an den Sender
              log.info("-----> " + beamer.toString());
              sendMessage(beamer, screencast.getSenderSession());
              break;
            case RESPONSE:
              log.info("conferenceMap {}", conferenceMap.size());
              databaseservice.deleteVideoSenderUser(userid.get()); // Lösche aus TB_VIDEO_SENDER
              databaseservice.switchProjektor(false, userid.get()); // Projektor ist ausgeschaltet
              break;
            case CONFIRM:
              break;
            case ERROR:
              break;
            default:
              break;
          }
          return;
        }
        else if (message instanceof IMAGE) {
          IMAGE image = (IMAGE) message;
          log.info("<----- " + image.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          Session item;
          Optional<String> imageSenderUid;
          String imageReceiverUid;
          String conferencekey;
          switch(image.getHeader()) {
            case REQUEST:
              // an den Empfänger weiterleiten

              String receiverSessionId = databaseservice.getSessionidByNickname(image.getReceiverNickname());

              if (receiverSessionId == null) {
                // Der Empfänger 'Nickname' ist verschwunden.
                IMAGE error = new IMAGE();
                error.setCommand(Command.IMAGE);
                error.setHeader(ERROR);
                error.setDataset(Protocol.DATASET);
                error.setSenderPseudoid(image.getSenderPseudoid());
                error.setSenderNickname(image.getSenderNickname());
                error.setReceiverNickname(image.getReceiverNickname());
                error.setMultilingualkey(KEY.SERVER_DER_EMPFAENGER_IST_VERSCHWUNDEN);
                log.info("-----> " + error.toString());

                sendMessage(error, session);
                return;
              }

              String receiverUserid = databaseservice.getUserBySession(receiverSessionId);

              if (databaseservice.blockConferenceCall(receiverUserid)) {
                IMAGE error = new IMAGE();
                error.setCommand(Command.IMAGE);
                error.setHeader(ERROR);
                error.setDataset(Protocol.DATASET);
                error.setSenderPseudoid(image.getSenderPseudoid());
                error.setSenderNickname(image.getSenderNickname());
                error.setReceiverNickname(image.getReceiverNickname());
                error.setMultilingualkey(KEY.STRING_IST_IN_EINER_KONFERENZ);

                log.info("-----> " + error.toString());

                sendMessage(error, session);
                return;
              }

              try {
                if (databaseservice.isReceiverSender(receiverUserid)) {
                  // Er ist bereits Sender
                  IMAGE error = new IMAGE();
                  error.setCommand(Command.IMAGE);
                  error.setHeader(ERROR);
                  error.setDataset(Protocol.DATASET);
                  error.setSenderPseudoid(image.getSenderPseudoid());
                  error.setSenderNickname(image.getSenderNickname());
                  error.setReceiverNickname(image.getReceiverNickname());
                  error.setMultilingualkey(KEY.SERVER_VIDEO_RUNNING);
                  sendMessage(error, session);
                  log.info("-----> " + error.toString());

                  return;
                }
              }
              catch (DatabaseException e) {
                log.fatal(e.getMessage(), e);
                return;
              }

              item = sessionmap.get(receiverSessionId);
              if (item == null) {
                IMAGE error = new IMAGE();
                error.setCommand(Command.IMAGE);
                error.setHeader(ERROR);
                error.setDataset(Protocol.DATASET);
                error.setSenderPseudoid(image.getSenderPseudoid());
                error.setSenderNickname(image.getSenderNickname());
                error.setReceiverNickname(image.getReceiverNickname());
                error.setMultilingualkey(KEY.SERVER_DER_EMPFAENGER_IST_VERSCHWUNDEN);
                log.info("-----> " + error.toString());

                sendMessage(error, session);
                return;
              }
              // 2. Nimmt der Empfänger Übertragungen an?
              if (!databaseservice.isOnrekord(receiverSessionId)) {
                IMAGE error = new IMAGE();
                error.setCommand(Command.IMAGE);
                error.setHeader(ERROR);
                error.setDataset(Protocol.DATASET);
                error.setSenderPseudoid(image.getSenderPseudoid());
                error.setSenderNickname(image.getSenderNickname());
                error.setReceiverNickname(image.getReceiverNickname());
                error.setMultilingualkey(KEY.SERVER_LEHNT_UEBERTRAGUNGEN_AB);
                log.info("-----> " + error.toString());

                sendMessage(error, session);
                return;
              }
              // 3. Empfänger muss noch seinen Projektor aussschalten, weil er in einer
              // Sitzung ist.
              if (databaseservice.isOnProjektor(receiverSessionId)) {
                IMAGE error = new IMAGE();
                error.setCommand(Command.IMAGE);
                error.setHeader(ERROR);
                error.setDataset(Protocol.DATASET);
                error.setSenderPseudoid(image.getSenderPseudoid());
                error.setSenderNickname(image.getSenderNickname());
                error.setReceiverNickname(image.getReceiverNickname());
                error.setMultilingualkey(KEY.SERVER_MUSS_NOCH_SEINEN_PROJEKTOR_AUSSCHALTEN);
                log.info("-----> " + error.toString());

                sendMessage(error, session);
                return;
              }
              // 4. Der Sender darf nicht Empfänger sein
              imageSenderUid = databaseservice.fetchUserByPseudoid(image.getSenderPseudoid());
              try {
                if (databaseservice.isSenderReceiver(imageSenderUid.get())) {
                  // Er ist bereits Empfänger
                  IMAGE error = new IMAGE();
                  error.setCommand(Command.IMAGE);
                  error.setHeader(ERROR);
                  error.setDataset(Protocol.DATASET);
                  error.setSenderPseudoid(image.getSenderPseudoid());
                  error.setSenderNickname(image.getSenderNickname());
                  error.setReceiverNickname(image.getReceiverNickname());
                  error.setMultilingualkey(KEY.SERVER_FORWARDING_IMAGE);
                  log.info("-----> " + error.toString());

                  sendMessage(error, session);
                  return;
                }
              }
              catch (DatabaseException e) {
                log.fatal(e.getMessage(), e);
                return;
              }
              // 5. Anfrage wird an den Empfänger weitergeleitet
              // Bevor der Sender eingetragen wird, prüfen ob er schon in der Tabelle steht
              databaseservice.insertVideoSender(imageSenderUid.get());

              log.info("-----> " + image.toString());

              sendMessage(image, receiverSessionId);
              break;
            case RESPONSE:
              imageSenderUid = databaseservice.fetchUserByPseudoid(image.getSenderPseudoid());
              imageReceiverUid = databaseservice.fetchUserByNickname(image.getReceiverNickname());
              String senderSessionId = databaseservice.getSessionidByUserid(imageSenderUid.get());
              databaseservice.setProjektorSession(session.getId(), senderSessionId);
              databaseservice.insertVideo(imageSenderUid.get(), imageReceiverUid);
              conferencekey = UUID.randomUUID().toString();
              conferenceMap.put(conferencekey, new CopyOnWriteArrayList<>());

//              log.info(
//                  "conferencekey {}, session.getId() {}, senderSessionId {}", conferencekey, session.getId(),
//                  senderSessionId
//              );
              conferenceMap.get(conferencekey).add(session.getId());
              conferenceMap.get(conferencekey).add(senderSessionId);
//              log.info(conferenceMap.get(conferencekey).size());

              image.setConferencekey(conferencekey);
              log.info("-----> " + image.toString());
              sendMessage(image, senderSessionId);
              break;
            case ERROR:
              break;
            case CONFIRM:
              break;
          }
          return;
        }
        else if (message instanceof SCREENCAST) {
          SCREENCAST screencast = (SCREENCAST) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          switch(screencast.getHeader()) {
            case CONFIRM:
              break;
            case ERROR:
              break;
            case REQUEST:
              databaseservice.updateRekorderport(screencast.getCallerUserid(), screencast.getCallerPort());
              String receiverSessionid = databaseservice
                  .getSessionidByNickname(screencast.getReceiverNickname());
              // 1. Ist der Empfänger noch online?
              if (receiverSessionid == null) {
                // Der Empfänger ist nicht mehr online
                // Antwort mit Error senden
                SCREENCAST response = new SCREENCAST();
                response.setCommand(Command.SCREENCAST);
                response.setHeader(ERROR);
                response.setDataset(Protocol.DATASET);
                response.setReceiverNickname(screencast.getReceiverNickname());
                response.setErrorMessage(
                    "<html><b>" + screencast.getReceiverNickname() + "</b> ist nicht mehr online.</html>"
                );
                sendMessage(response, session);
                return;
              }

              // ich muss die
              // ic habe nur den ReceiverNickname
              if (databaseservice.isOnProjektor(receiverSessionid)) {
                log.info("Projektor ist eingeschaltet, Error zurücksenden");
                SCREENCAST response = new SCREENCAST();
                response.setCommand(Command.SCREENCAST);
                response.setHeader(ERROR);
                response.setDataset(Protocol.DATASET);
                response.setReceiverNickname(screencast.getReceiverNickname());
                response.setErrorMessage("<html>Der Projektor ist in Betrieb.</html>");
                sendMessage(response, session);
                return;
              }

              // 2. Nimmt der Empfänger Übertragungen an?

              if (!databaseservice.isOnrekord(receiverSessionid)) {
                SCREENCAST response = new SCREENCAST();
                response.setCommand(Command.SCREENCAST);
                response.setHeader(ERROR);
                response.setDataset(Protocol.DATASET);
                response.setReceiverNickname(screencast.getReceiverNickname());
                response.setErrorMessage(
                    "<html><b>" + screencast.getReceiverNickname()
                        + "</b> nimmt keine Bildschirmübertragungen entgegen.</html>"
                );
                sendMessage(response, session);
                return;
              }
              // weiterleiten an den Empfänger, damit er ein Fenster öffnet
              sendMessage(screencast, receiverSessionid);
              break;
            case RESPONSE:
              // Antwort weiterleiten an den Sender
              String callerSession = databaseservice.getSessionidByUserid(screencast.getCallerUserid());
              // eine Projektorverbindung wird eingetragen
              databaseservice.setProjektorSession(session.getId(), callerSession);
              sendMessage(screencast, callerSession);
              break;
            default:
              break;
          }
          return;
        }
        else if (message instanceof BREAKROOMS) {
          BREAKROOMS breakrooms = (BREAKROOMS) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          List<String> rooms = databaseservice.fetchBreakrooms(breakrooms.getUserid());
          BREAKROOMS response = new BREAKROOMS();
          response.setCommand(Command.BREAKROOMS);
          response.setHeader(RESPONSE);
          response.setDataset(Protocol.DATASET);
          response.setUserid(breakrooms.getUserid());
          response.setRooms(rooms.toArray(new String[rooms.size()]));
          sendMessage(response, session);
          return;
        }
        else if (message instanceof READROOMS) {
          READROOMS readrooms = (READROOMS) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          Optional<String> userid = databaseservice.fetchUserByPseudoid(readrooms.getPseudoid());
          List<String> rooms = databaseservice.fetchRooms(userid.get(), readrooms.getRoomtype());
          READROOMS response = new READROOMS();
          response.setCommand(Command.READROOMS);
          response.setHeader(RESPONSE);
          response.setDataset(Protocol.DATASET);
          response.setPseudoid(readrooms.getPseudoid());
          response.setRoomtype(readrooms.getRoomtype());
          response.setRooms(rooms.toArray(new String[rooms.size()]));
          sendMessage(response, session);
          return;
        }
        else if (message instanceof UPDATEFILETYPES) {
          UPDATEFILETYPES types = (UPDATEFILETYPES) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          Optional<String> userid = databaseservice.fetchUserByPseudoid(types.getPseudoid());
          databaseservice.updateUserBlacklist(userid.get(), types.getFiletypes());
          // Confirm senden
          UPDATEFILETYPES response = new UPDATEFILETYPES();
          response.setCommand(Command.UPDATEFILETYPES);
          response.setHeader(CONFIRM);
          response.setDataset(Protocol.DATASET);
          response.setPseudoid(types.getPseudoid());
          sendMessage(response, session);
          return;
        }
        else if (message instanceof FILETYPES) {
          FILETYPES filetypes = (FILETYPES) message;
          if (REQUEST != filetypes.getHeader()) return;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          Optional<String> userid = databaseservice.fetchUserByPseudoid(filetypes.getPseudoid());
          databaseservice.createUserBlacklist(userid.get());
          List<BlacklistTypes> sperrliste = databaseservice.fetchFiletypes(userid.get());
          FILETYPES response = new FILETYPES();
          response.setCommand(Command.FILETYPES);
          response.setHeader(RESPONSE);
          response.setDataset(Protocol.DATASET);
          response.setPseudoid(userid.get());
          response.setFiletypes(sperrliste.toArray(new BlacklistTypes[sperrliste.size()]));
          sendMessage(response, session);
          return;
        }
        else if (message instanceof SEARCHFILES) {
          // jetzt überträgt er alles
          SEARCHFILES searchfiles = (SEARCHFILES) message;
          List<Searchfile> foundFiles = searchfiles(searchfiles);

          final int blocksize = 64;
          int maxRound = foundFiles.size() / blocksize;
          ArrayList<Searchfile> part = new ArrayList<>();
          int round = 0;
          while (round <= maxRound) {
            part.clear();
            int m = 0;
            while (!foundFiles.isEmpty()) {
              if (m > blocksize) break;
              part.add(foundFiles.remove(0));
              m++;
            }
            // part senden // bestimmte Dateien filtern
            // part enthält bis 64 Dateien
            SEARCHFILES response = new SEARCHFILES();
            response.setCommand(Command.SEARCHFILES);
            response.setHeader(RESPONSE);
            response.setDataset(Protocol.DATASET);
            response.setSearchfiles(part.toArray(new Searchfile[part.size()]));
            response.setStarted(round == 0 ? true : false);
            response.setUserid(searchfiles.getUserid());
            sendMessage(response, session);
            round++;
          }
          return;
        }
        else if (message instanceof DOWNLOAD) {
          DOWNLOAD download = (DOWNLOAD) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          DOWNLOAD response = databaseservice.download(
              download.getPseudoid(), download.getIndex(), download.getSlot(), download.getFilename()
          );
          sendMessage(response, session);
          return;
        }
        else if (message instanceof UPLOADFILES) {
          UPLOADFILES uploadfiles = (UPLOADFILES) message;
          UPLOADFILES response = uploadfiles(uploadfiles);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof DELETEUPLOADFILES) {
          DELETEUPLOADFILES deletefiles = (DELETEUPLOADFILES) message;
          DELETEUPLOADFILES response = deleteuploadfiles(deletefiles.getPseudoid(), deletefiles.getIp());
          sendMessage(response, session);
          return;
        }
        else if (message instanceof PRIVATECHATFILE) {
          PRIVATECHATFILE privatechatfile = (PRIVATECHATFILE) message;
          log.info("<----- {}", privatechatfile.toString());
          if (REQUEST == privatechatfile.getHeader()) {
            DatabaseService databaseservice = WebdatabaseImpl.getInstance();
            String receiver_uid = databaseservice.fetchSessionByPseudoid(privatechatfile.getRemotePseudoid());
            String uid = databaseservice.fetchSessionByPseudoid(privatechatfile.getPseudoid());
            try {
              PrivateChatfile entity = databaseservice.uploadPrivateChatfileAttributes(
                  uid, receiver_uid, privatechatfile.getAbsoluteFilename(), privatechatfile.getFilename(),
                  privatechatfile.getFilesize(), privatechatfile.getMessage(),
                  privatechatfile.getRemoteNickname()
              );
              PRIVATECHATFILE response = new PRIVATECHATFILE();
              response.setCommand(Command.PRIVATECHATFILE);
              response.setHeader(RESPONSE);
              response.setDataset(Protocol.DATASET);
              response.setFilename(entity.getFilename());
              response.setAbsoluteFilename(privatechatfile.getAbsoluteFilename());
              response.setFilesize(entity.getFilesize());
              response.setMessage(entity.getMessage());
              response.setNumber(entity.getAnlage());
              response.setRemoteNickname(entity.getRemoteNickname());
              response.setRemotePseudoid(privatechatfile.getRemotePseudoid());
              response.setPseudoid(privatechatfile.getPseudoid());
              log.info("-----> {}", response.toString());
              sendMessage(response, session);
            }
            catch (DatabaseException e) {
              log.info(e.getMessage(), e);
            }
          }
          return;
        }
        else if (message instanceof PRIVATEMESSAGE) {
          PRIVATEMESSAGE privatemessage = (PRIVATEMESSAGE) message;
          log.info("<----- {}", privatemessage.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          if (REQUEST == privatemessage.getHeader()) {
            Optional<String> uid = databaseservice.fetchUserByPseudoid(privatemessage.getPseudoid());
            EntityUser entity = databaseservice.fetchUser(uid.get());
            ChatUser chatuser = new ChatUser();
            chatuser.setBackgroundColor(entity.getBackground());
            chatuser.setForegroundColor(entity.getForeground());
            chatuser.setNickname(entity.getNickname());
            // keine UserID in chatuser senden
            // TODO As of version 260501-PRE3 outdated
//            chatuser.setUserid(privatemessage.getSenderUID());
            privatemessage.setChatUser(chatuser);
            privatemessage.setDatetime(System.currentTimeMillis());
            log.info("-----> {}", privatemessage.toString());
            sendMessage(
                privatemessage, databaseservice.fetchSessionByPseudoid(privatemessage.getRemotePseudoid())
            );
          }
          else if (RESPONSE == privatemessage.getHeader()) {
            log.info("-----> {}", privatemessage.toString());
            sendMessage(privatemessage, databaseservice.fetchSessionByPseudoid(privatemessage.getPseudoid()));
          }
          return;
        }

        else if (message instanceof CHATUSERLIST) {
          // CHATUSERLIST response = chatuserlist((CHATUSERLIST)message);
          // Kann noch verbessert werden. Nur an die User senden, die
          // mit iherer SessionID im Raum sind
          // Aktuell sende ich an alle
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          List<String> userInRooms = databaseservice.fetchRoomlist(session.getId());
          // alle infos werden für den User aktualisiert
          broadcast(userInRooms);
          return;
        }
        else if (message instanceof HISTORYMESSAGE) {
          HISTORYMESSAGE response = historymessage((HISTORYMESSAGE) message);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof ROOMLIST) {
          if (message instanceof READTOPICROOMOWNER) return; // READTOPICROOMOWNER erweitert ROOMLIST
          // Eine Liste aller Chaträume wird angefordert
          // Der Server broadcastet eine Raumbelegung
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          List<String> roomlist = databaseservice.fetchRoomlist(session.getId());
          broadcast(roomlist);
          return;
        }
        else if (message instanceof READTOPICROOMOWNER) {
          READTOPICROOMOWNER readTopicOwner = (READTOPICROOMOWNER) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          READTOPICROOMOWNER response = databaseservice
              .fetchTopicroom(readTopicOwner.getOwner(), readTopicOwner.getPseudoid());
          sendMessage(response, session);
          return;
        }
        else if (message instanceof LEAVEROOM) {
          LEAVEROOM leaveroom = (LEAVEROOM) message;
          log.info("<----- " + leaveroom.toString());
          // Nach LEAVEROMM ist der User mit der SESSION_ID nicht mehr vorhanden */
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          // Der User mit der session.getID ist schon nicht mehr in dem Raum
          // Auch nicht so effizient, weil alle Räume ermittelt werden
          List<String> roomlist = databaseservice.fetchRoomlist(session.getId());

          // Die Raumliste muss vielleicht gekürzt werden, wenn der Raum
          // ein Tempraum war und leer ist

          // Ist der Chat ein Pausenraum oder ein Gruppenraum?

          Optional<String> userid = databaseservice.fetchUserByPseudoid(leaveroom.getPseudoid());
          try {
            switch(databaseservice.getLifetime(leaveroom.getRoom())) {
              case PERMANENT:
                databaseservice.remove(userid.get(), leaveroom.getRoom());
                break;
              case TEMPORARY:
                databaseservice.remove(userid.get(), leaveroom.getRoom());
                int count = databaseservice.countTemproomUser(leaveroom.getRoom());
                if (count == 0) {
                  // Temproom löschen
                  // und raumliste kürzen
                  // log.info(response.getRoom());
                  roomlist.remove(leaveroom.getRoom());
                  // Beim Löschen eines Temprooms, spielt die Userid keine Rolle.
                  // Der Raum muss gelöscht werden, wenn niemand anwesend ist
                  databaseservice.deleteTemproom(leaveroom.getRoom());
                }
                break;
              default:
                break;
            }

            // log.info("muss wieder angepasst werden !!!!!!!!!!!!!!!!!!!!!!");
            // Kann noch verbessert werden. Nur an die Senden, die
            // mit iherer SessionID im Raum sind
            // Aktuell sende ich an alle
            broadcast(roomlist);
          }
          catch (DatabaseException e) {
            log.fatal(e.getMessage());
          }
          return;
        }
        else if (message instanceof ENTERROOM) {
          ENTERROOM request = (ENTERROOM) message;
          log.info("<----- " + request.toString());
          if (request.getHeader() != REQUEST) return;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          try {
            Optional<String> userid = databaseservice.fetchUserByPseudoid(request.getPseudoid());
            boolean exists = databaseservice.isRoomExists(request.getRoom());
            ENTERROOM response;
            if (exists) {
              response = databaseservice
                  .enterRoom(userid.get(), request.getRoom(), request.getNickname(), request.getPseudoid());
            }
            else {
              // der Raum ist nicht vorhanden
              // ERROR
              String language = databaseservice.fetchLanguage(userid.get());
              MultilingualString chatraumNichtVorhanden = new MultilingualString(
                  KEY.SERVER_DER_CHATRAUM_IST_NICHT_VORHANDEN, ISO639.fromValue(language)
              );
              String errortext = chatraumNichtVorhanden.toString().replace("XXX", request.getRoom());
              response = new ENTERROOM();
              response.setCommand(Command.ENTERROOM);
              response.setHeader(ERROR);
              response.setDataset(Protocol.DATASET);
              response.setMultilingualkey(KEY.SERVER_DER_CHATRAUM_IST_NICHT_VORHANDEN);
              response.setText(errortext);
            }
            log.info("-----> " + response.toString());
            sendMessage(response, session);
          }
          catch (DatabaseException e) {
            log.fatal(e.getMessage());
            CloseReason reason = new CloseReason(CloseCodes.NORMAL_CLOSURE, e.getMessage());
            try {
              session.close(reason);
            }
            catch (IOException e1) {
              log.info(e1.getMessage(), e1);
            }
          }
          return;
        }
        else if (message instanceof CREATETEMPROOM) {
          CREATETEMPROOM temproom = (CREATETEMPROOM) message;
          CREATETEMPROOM response = createTemproom(temproom);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof LEAVEPRIVATECHAT) {
          // REQUEST kommt rein
          LEAVEPRIVATECHAT leavechat = (LEAVEPRIVATECHAT) message;
          log.info("<----- " + leavechat.toString());

          // sendTo muss ermittelt werden aus der Datenbank
          String participant = leavechat.getRemotePseudoid();

          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          String absender = databaseservice.fetchPseudoidBySessionid(session.getId());

          log.info("SinglePrivateChat geschlossen von {}, senden an ---> {}", absender, participant);

          String sessionPseudoid = databaseservice.fetchSessionByPseudoid(participant);
          databaseservice.deleteSinglePrivateChat(absender, participant);

          LEAVEPRIVATECHAT leaveprivatechat = new LEAVEPRIVATECHAT();
          leaveprivatechat.setCommand(Command.LEAVEPRIVATECHAT);
          leaveprivatechat.setHeader(RESPONSE);
          leaveprivatechat.setDataset(Protocol.DATASET);
          leaveprivatechat.setRemotePseudoid(absender);
          leaveprivatechat.setParticipant(participant);

          log.info("Aufgelegt sessionid=" + session.getId());
          log.info("-----> " + leaveprivatechat.toString());
          sendMessage(leaveprivatechat, sessionPseudoid);
          return;
        }
        else if (message instanceof CALLREMOTEUSER) {
          CALLREMOTEUSER remoteuser = (CALLREMOTEUSER) message;
          log.info("<----- " + remoteuser.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();

          // Ist der Absender ein Browser und hat keinen weiteren Chat offen?

          if (CONFIRM == remoteuser.getHeader()) {
            String uid = databaseservice.fetchUserByNickname(remoteuser.getLocalNickname());
            if (databaseservice.checkExhausted(uid)) {
              String isocode = databaseservice.fetchLanguage(uid);
              MultilingualString limit = new MultilingualString(
                  KEY.SERVER_CHAT_GEGENSEITE_LIMIT, ISO639.fromValue(isocode)
              );
              // ERROR senden
              CALLREMOTEUSER response = new CALLREMOTEUSER();
              response.setCommand(Command.CALLREMOTEUSER);
              response.setHeader(ERROR);
              response.setDataset(Protocol.DATASET);
              response.setMultilingualkey(KEY.SERVER_CHAT_GEGENSEITE_LIMIT);
              response.setErrorMessage(limit.toString());
              log.info("-----> {}", response.toString());
              sendMessage(response, session);
              return;
            }
            databaseservice.insertPrivateChat(remoteuser.getRemotePseudoid(), remoteuser.getPseudoid());
          }
          else if (RESPONSE == remoteuser.getHeader()) {
            databaseservice.deleteSinglePrivateChat(remoteuser.getPseudoid(), remoteuser.getRemotePseudoid());
          }
          else {
            log.error("unbekannter HEADER, " + remoteuser.getHeader());
          }
          // hier ist bereits der OK Fall
          CALLPRIVATECHAT callprivatechat = callPrivatechat(remoteuser);
          String localsession = databaseservice.getSessionidByNickname(remoteuser.getLocalNickname());
          log.info("-----> " + callprivatechat);
          sendMessage(callprivatechat, localsession);
          return;
        }
        else if (message instanceof CALLPRIVATECHAT) {
          CALLPRIVATECHAT callprivate = (CALLPRIVATECHAT) message;
          log.info("<----- " + callprivate.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();

          String isPresent = databaseservice.getSessionidByNickname(callprivate.getRemoteNickname());
          Optional<String> uid = databaseservice.fetchUserByPseudoid(callprivate.getPseudoid());
          if (isPresent == null) {
            // nicht online error

            String isocode = databaseservice.fetchLanguage(uid.get());
            CALLPRIVATECHAT call = new CALLPRIVATECHAT();
            call.setCommand(Command.CALLPRIVATECHAT);
            call.setHeader(ERROR);
            call.setDataset(Protocol.DATASET);
            call.setMultilingualkey(KEY.SERVER_USER_IST_OFFLINE);
            MultilingualString offline = new MultilingualString(
                KEY.SERVER_USER_IST_OFFLINE, ISO639.fromValue(isocode)
            );
            String text = offline.toString().replace("XXX", callprivate.getRemoteNickname());
            call.setErrorMessage(text);
            log.info("-----> " + call.toString());
            sendMessage(call, session);
            return;
          }

          try {
            CALLPRIVATECHAT check = databaseservice
                .checkExhausted(callprivate.getRemoteNickname(), uid.get());
            if (ERROR == check.getHeader()) {
              log.info("----->" + check.toString());
              sendMessage(check, session);
              return;
            }
          }
          catch (DatabaseException e) {
            log.error(e.getMessage(), e);
            CALLPRIVATECHAT call = new CALLPRIVATECHAT();
            call.setCommand(Command.CALLPRIVATECHAT);
            call.setHeader(ERROR);
            call.setDataset(Protocol.DATASET);
            call.setErrorMessage("Server Error - generischer Fehler");
            log.info("-----> " + call.toString());
            sendMessage(call, session);
            return;
          }

          // Wenn der CALLER selber bereits chattet und ein ein Browser ist,
          // Verbindungsversuch abbrechen
          if (databaseservice.checkExhausted(uid.get())) {
            // erschöpft
            String isocode = databaseservice.fetchLanguage(uid.get());
            MultilingualString exhausted = new MultilingualString(
                KEY.SERVER_CHAT_EXHAUSTED_LIMIT, ISO639.fromValue(isocode)
            );
            CALLPRIVATECHAT call = new CALLPRIVATECHAT();
            call.setCommand(Command.CALLPRIVATECHAT);
            call.setHeader(ERROR);
            call.setDataset(Protocol.DATASET);
            call.setMultilingualkey(KEY.SERVER_CHAT_EXHAUSTED_LIMIT);
            call.setErrorMessage(exhausted.toString());
            log.info("-----> " + call.toString());
            sendMessage(call, session);
            return;
          }

          // ist der REmoteUser ein Desktop?
          // kein Desktop, wie viele Chats hat er offen?
          // CallPrivateChat kennt den RemoteNickname
          // um die chats zu zählen, brauche ich die remote userid

          // TODO statt sessionId vom Anfrager wird die Pseudoid des Anfrgers übergeben

          String pseudoid = databaseservice.fetchPseudoidBySessionid(session.getId());

          CALLREMOTEUSER remoteuser = databaseservice.callRemoteuser(callprivate, pseudoid);
          if (ERROR == remoteuser.getHeader()) {
            log.info("-----> " + remoteuser.toString());
            sendMessage(remoteuser, session);
            return;
          }
          // der 1:1 Chat oder PrivateChat wird durch CALLPRIVATECHAT eingeleitet
          // localSessionID und remoteSessionid müssen verschieden sein

          if (remoteuser.getPseudoid().equals(remoteuser.getRemotePseudoid())) {
            // Sender und Empfänger sind identisch
            CALLPRIVATECHAT call = new CALLPRIVATECHAT();
            call.setCommand(Command.CALLPRIVATECHAT);
            call.setHeader(ERROR);
            call.setDataset(Protocol.DATASET);
            call.setLocalNickname(remoteuser.getRemoteNickname());
            call.setPseudoid(remoteuser.getRemotePseudoid());
            call.setRemoteNickname(remoteuser.getLocalNickname());
            call.setMultilingualkey(KEY.STRING_EINSAME_SEELEN);
            log.info("-----> " + call.toString());
            sendMessage(call, remoteuser.getPseudoid());
            return;
          }

          // private chat eintragen
          // remote SessionID und LocalSessionID

          // remote und local sind vertauscht!
          // der Anrufer trägt sich ein

          if (
            databaseservice.isPrivateChatEstablished(remoteuser.getPseudoid(), remoteuser.getRemotePseudoid())
          ) {
            CALLPRIVATECHAT call = new CALLPRIVATECHAT();
            call.setCommand(Command.CALLPRIVATECHAT);
            call.setHeader(ERROR);
            call.setDataset(Protocol.DATASET);
            call.setLocalNickname(remoteuser.getRemoteNickname());
            call.setPseudoid(remoteuser.getRemotePseudoid());
            call.setRemoteNickname(remoteuser.getLocalNickname());
            call.setErrorMessage("Der Chat läuft bereits.");
            call.setMultilingualkey(KEY.SERVER_DER_CHAT_LAEUFT_BEREITS);
            log.info("-----> " + call.toString());
            sendMessage(call, databaseservice.fetchSessionByPseudoid(call.getPseudoid()));
            return;
          }
          // sende an local, anaconda -> freigeist session id freigeist
          // database sessionByPSEUDOID
          log.info("-----> " + remoteuser.toString());
          sendMessage(remoteuser, databaseservice.fetchSessionByPseudoid(remoteuser.getPseudoid()));
          return;
        }
        else if (message instanceof DIAL) {
          DIAL request = (DIAL) message;
          log.info("<----- {}", request.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          DIAL dialResponse = databaseservice.dial(request);
          // War der RESPONSE positiv?
          // Falls ja
          // andere Seite benachrichtigen
          if (ERROR == dialResponse.getHeader()) {
            sendMessage(dialResponse, session);
            return;
          }
          else if (RESPONSE == dialResponse.getHeader()) {
            // Nachricht an dem Empfänger schicken, dass gleich eine Nachricht kommt
            // ich benötige die SessionID, damit die Nachricht einen Adressaten findet

            String sessionid = databaseservice.fetchSessionByPseudoid(dialResponse.getReceiverPseudoid());
            if (sessionid == null) {
              // der Anrufempfänger ist nicht mehr online
              DIAL dial = new DIAL();
              dial.setHeader(ERROR);
              dial.setCommand(Command.DIAL);
              dial.setDataset(Protocol.DATASET);
              dial.setReceiverNickname(dialResponse.getReceiverNickname());
              dial.setMultilingualkey(KEY.STRING_IST_NICHT_MEHR_ONLINE);
              sendMessage(dial, session);
              return;
            }
            // RESPONSE ist eine positive Antwort
            // SESSIONID wird für TCP-Übertragung optional hinzugepackt.
            dialResponse.setReceiverSession(sessionid); // die Sessionid vom Empfänger
            dialResponse.setCallerSession(session.getId()); // die Sessionid vom Caller
            log.info("-----> {}", dialResponse.toString());
            sendMessage(dialResponse, session);
          }
          return;
        }
        else if (message instanceof INCOMINGCALL) {
          INCOMINGCALL incoming = (INCOMINGCALL) message;
          log.info("<----- " + incoming.toString());
          if (RESPONSE == incoming.getHeader()) {
            DatabaseService databaseservice = WebdatabaseImpl.getInstance();

            Optional<String> incomingCallerUserid = databaseservice
                .fetchUserByPseudoid(incoming.getCallerPseudoid());
            final String callerSessionId = databaseservice.getSessionidByUserid(incomingCallerUserid.get());
            if (callerSessionId == null) {
              // exakte Fehlermeldung zurück an den Absender, Caller ist nicht mehr online
              // Protocoll anpassen error auch für CALLER_NICKNAME? oder RECEIVER_NICKNAME
              // komplett streichen
              // Fehler simulieren
              // Emepfänger hat den Hörer noch nicht abgenommen
              // und der Anrufer legt auf
              INCOMINGCALL error = new INCOMINGCALL();
              error.setCommand(Command.INCOMINGCALL);
              error.setHeader(ERROR);
              error.setDataset(Protocol.DATASET);
              error.setReceiverNickname(incoming.getCallerNickname()); // receiver und caller vertauschen
                                                                       // wegen response oder receiverNickname
                                                                       // optional
              error.setMultilingualkey(KEY.STRING_IST_NICHT_MEHR_ONLINE);
              // error senden
              log.info("-----> " + error.toString());
              sendMessage(error, session);
              return;
            }
            // response ist der CALLER telefonisch noch erreichbar?

            if (!databaseservice.isOnBusyByUid(incomingCallerUserid.get())) {
              // Gegenseite hat aufgelegt
              INCOMINGCALL error = new INCOMINGCALL();
              error.setCommand(Command.INCOMINGCALL);
              error.setHeader(ERROR);
              error.setDataset(Protocol.DATASET);
              error.setReceiverNickname(incoming.getCallerNickname()); // receiver und caller vertauschen
                                                                       // wegen response oder receiverNickname
                                                                       // optional
              error.setMultilingualkey(KEY.STRING_HAT_AUFGELEGT);
              // error senden
              log.info("-----> " + error.toString());
              sendMessage(error, session);
              return;
            }
            // Empfänger ist am telefonieren

            Optional<String> incomingReceiverUserid = databaseservice
                .fetchUserByPseudoid(incoming.getReceiverPseudoid());

            databaseservice.setOnBusy(incomingReceiverUserid.get(), true);
            // Beide Seiten als telefonierend eintragen
            databaseservice.insertToPhone(incomingCallerUserid.get(), incomingReceiverUserid.get());
            // 1. Der CONFERENCEKEY für dieses 1:1 Telefonat
            String confKey = incoming.getConferencekey();
            // 2. Beide Sessions in die conferenceMap eintragen
            // Der Anrufer (Caller)
            conferenceMap.computeIfAbsent(confKey, k -> new CopyOnWriteArrayList<>())
                .addIfAbsent(callerSessionId);
            // Der Angerufene (Receiver / aktuelle 'session')
            conferenceMap.get(confKey).addIfAbsent(session.getId());

            log.info(
                "[1:1 RAM] Mapping erstellt: Key {} -> Sessions [{}, {}]", confKey, callerSessionId,
                session.getId()
            );

            // an den Receiver CONFIRM senden, damit er den Lautsprecher starten kann
            INCOMINGCALL confirm = new INCOMINGCALL();
            confirm.setCommand(Command.INCOMINGCALL);
            confirm.setHeader(CONFIRM);
            confirm.setDataset(Protocol.DATASET);
            confirm.setCallerVoice(incoming.getCallerVoice());
            confirm.setCallerSession(callerSessionId);
            confirm.setConferencekey(incoming.getConferencekey());
            log.info("-----> " + confirm.toString());
            // CONFIRM wird an den Empfänger gesendet
            // Der Empfänger sendete RESPONSE und erhält ein CONFIRM zurück
            sendMessage(confirm, session);
            // an den CALLER wird die RESPONSE vom Empfänger weitergereicht
            // incoming ist hier eine RESPONSE an CALLER
            incoming.setCallerSession(null);
            incoming.setCallerNickname(null);
            incoming.setCallerPseudoid(null);
            incoming.setCallerVoice(null);
            incoming.setReceiverNickname(null);
            incoming.setReceiverPseudoid(null);
            log.info("-----> " + incoming.toString());
            sendMessage(incoming, callerSessionId);
            return;
          }
          else if (REQUEST == incoming.getHeader()) {
            // RECEIVER_SESSIONID habe ich aus incoming
            DatabaseService databaseservice = WebdatabaseImpl.getInstance();

            Optional<String> incomingReceiverUserid = databaseservice
                .fetchUserByPseudoid(incoming.getReceiverPseudoid());

            final String sessionid = databaseservice.getSessionidByUserid(incomingReceiverUserid.get());
            if (sessionid == null) {
              INCOMINGCALL error = new INCOMINGCALL();
              error.setCommand(Command.INCOMINGCALL);
              error.setHeader(ERROR);
              error.setDataset(Protocol.DATASET);
              error.setReceiverNickname(incoming.getReceiverNickname());
              error.setMultilingualkey(KEY.STRING_IST_NICHT_MEHR_ONLINE);
              log.info("-----> " + error.toString());
              sendMessage(error, session);
              return;
            }
            // Der CALLER telefoniert
            // TODO später zusammen mit dem Empfänger eintragen oder wieder löschen bei
            // error
            databaseservice.setOnBusy(incomingReceiverUserid.get(), true);
            // über den HEADER_RESPONSE sagt er, ich telefoniere
            log.info("-----> " + incoming.toString());
            sendMessage(incoming, sessionid);
            return;
          }
          return;
        }
        else if (message instanceof MICROERROR) {
          MICROERROR microerror = (MICROERROR) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          final String sessionid = databaseservice.getSessionidByUserid(microerror.getToUserid());
          if (sessionid == null) return;
          sendMessage(microerror, sessionid);
          return;
        }
        else if (message instanceof ONCALL) {
          ONCALL response = onCall((ONCALL) message);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof ONHOOK) {
          ONHOOK onhook = (ONHOOK) message;
          log.info("<----- " + onhook);
          final String sessionid;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          Optional<String> callerUserid;
          Optional<String> receiverUserid;
          switch(onhook.getHeader()) {
            case REQUEST:

              String confKey = onhook.getConferencekey();

              // 1. Sofortiger "Kill-Switch" für den Audio-Stream
              List<String> participants = conferenceMap.remove(confKey);

              if (participants != null) {
                // 2. Wer hat aufgelegt? (session.getId())
                // 3. Wer ist der Partner? (Der andere Eintrag in der Liste)
                for (String targetId : participants) {
                  if (!targetId.equals(session.getId())) {
                    // Sende ONHOOK an den Partner, damit sein Interface schließt

                    log.info("-----> {}", onhook.toString());
                    sendMessage(onhook, targetId);
                  }
                }

                // 4. Jetzt erst die DB-Hintergrundarbeit (isOnBusy = false etc.)
                // Da die Teilnehmer-IDs im RAM bekannt sind,
                // kann man die DB-Logik darauf basieren lassen.
                log.info("[CLEANUP] 1:1 Gespräch {} erfolgreich beendet.", confKey);
              }

              callerUserid = databaseservice.fetchUserByPseudoid(onhook.getCallerPseudoid());
              receiverUserid = databaseservice.fetchUserByPseudoid(onhook.getReceiverPseudoid());
              if (databaseservice.findPartnerUserid(callerUserid.get()) == null) {
//                Clientseite
//                1. Anrufen
//                2. Abnehmen
//                3. Wählen
//                4. Auflegen

                // An den Receiver trotzdem durchstellen, aber nicht klingeln
                onhook.setSignal(false);
                if (onhook.isTo()) {
                  // Caller hat aufgelegt an Receiver senden
                  sessionid = databaseservice.getSessionidByUserid(receiverUserid.get());
                }
                else {
                  // Receiver hat aufgeleg an Caller senden
                  sessionid = databaseservice.getSessionidByUserid(callerUserid.get());
                }
                if (sessionid == null) return;
//                sendMessage(onhook, sessionid); Neu
                return;
              }
              if (onhook.isTo()) {
                // Caller hat aufgelegt
//                log.info("der Caller hat aufgelegt");
                // Pärchen aus der Telefonierentabelle löschen
                databaseservice.deletePartner(callerUserid.get()); // TB_TOPHONE

                databaseservice.setOnBusy(callerUserid.get(), false); // TB_PHONE

//                // NEU: RAM-Bereinigung für 1:1
//                String confKey = onhook.getConferencekey();
//                if (confKey != null) {
//                  conferenceMap.remove(confKey);
//                  log.info("[1:1 RAM] Verbindung beendet und Key gelöscht: {}", confKey);
//                }

                sessionid = databaseservice.getSessionidByUserid(receiverUserid.get());

                // Telefoniert der Receiver?
                if (!databaseservice.isOnBusyByUid(receiverUserid.get())) { // TB_PHONE
                  // RECEIVER ist nicht am telefonieren
                  // ONHOOK muss trotzdem durchgestellt werden
                  // SIGNAL auf 1
                  log.info(onhook.toString());
                  return;
                }

              }
              else {

                // Receiver hat aufgelegt
//                log.info("der receiver hat aufgelegt");

                // Pärchen aus der Telefonierentabelle löschen
                databaseservice.deletePartner(receiverUserid.get());

                databaseservice.setOnBusy(receiverUserid.get(), false);
                sessionid = databaseservice.getSessionidByUserid(callerUserid.get());

                // Telefoniert der Caller?
                if (!databaseservice.isOnBusyByUid(callerUserid.get())) {
                  return;
                }

              }
//              if (sessionid == null) return; 3-Block Neu
//              log.info("-----> " + onhook.toString());
//              sendMessage(onhook, sessionid);
              break;
            default:
              break;
          }
          return;
        }
        else if (message instanceof UPDATEPHONE) {
          UPDATEPHONE request = (UPDATEPHONE) message;
          UPDATEPHONE response = updatePhone(request.getPseudoid(), request.isOnBusy()); // TB_ONLINE
          sendMessage(response, session);
          return;
        }
        else if (message instanceof UPDATEPORT) {
          UPDATEPORT request = (UPDATEPORT) message;
          UPDATEPORT response = updateport(request.getIp(), request.getPort(), request.getPseudoid());
          sendMessage(response, session);
          return;
        }
        else if (message instanceof USRLOGIN) {
          try {

            USRLOGIN login = (USRLOGIN) message;
            // IDENTITY ist RSA gecrypted
            // in IDENTIYT ist der AES Schlüssel, den wir für die Antwort brauchen
            // zurück wird IDENTITY AES geschickt

//            log.info("<----- " + login.toString());
            String RSA = login.getIdentity();
            DatabaseService databaseservice = WebdatabaseImpl.getInstance();

            String privatekeyRSA64 = databaseservice.fetchRsaPrivateKey();
            PrivateKey privateKey = Crypto.loadPrivateRSAKey(privatekeyRSA64);
            String klartext = Crypto.decryptRSA(RSA, privateKey);
            Token token = Token.toToken(klartext);

            String aes = token.getAES();
            String mail = token.getEMail();
            String password = token.getPassword();
            String userid = token.getUserid();
            String onetime = token.getOnetime();
            Agent agent = login.getAgent();

            USRLOGIN response = databaseservice
                .loginUser(session.getId(), aes, mail, password, agent, onetime);

            if (response.getHeader() == ERROR) {
              log.info("-----> " + response.toString());
              sendMessage(response, session);

              return;
            }
            if (response.getHeader() == CONFIRM) {
              session.getUserProperties().put("authorized", true);
              String pseudoid = databaseservice.fetchPseudoidBySessionid(session.getId());
              onlineMap.put(pseudoid, session.getId());

//              log.info("-----> " + response.toString());
              sendMessage(response, session);

              SecretKey secretKey = Crypto.getAESFromBase64(aes);
              String result = Crypto.decryptAES(response.getIdentity(), secretKey);
              userid = Token.toToken(result).getUserid();
              session.getUserProperties().put("userid", userid);
              databaseservice.updateConfirmationFlag(userid, mail, 0);
              USERONLINELIST userOnlinelist = databaseservice.fetchUseronlinelist();
              Thread.sleep(50);
              log.info("-----> " + userOnlinelist.toString());
              broadcastAll(userOnlinelist);
              return;
            }

          }
          catch (Exception e) {
            log.error(e.fillInStackTrace());
            CloseReason reason = new CloseReason(CloseCodes.NORMAL_CLOSURE, e.getMessage());
            try {
              session.close(reason);
            }
            catch (IOException e1) {
              log.info(e1.getMessage(), e1);
            }
          }
          return;
        }
        else if (message instanceof CHANGETOPICMEMBER) {
          CHANGETOPICMEMBER member = (CHANGETOPICMEMBER) message;
          CHANGETOPICMEMBER response = changeTopicmember(
              member.getPseudoid(), member.getRoom(), member.getRoomtype(), member.getChatUser()
          );
          sendMessage(response, session);
          return;
        }
        else if (message instanceof CANDIDATETOPICMEMBER) {
          CANDIDATETOPICMEMBER member = (CANDIDATETOPICMEMBER) message;
          CANDIDATETOPICMEMBER response = candidateMember(member.getPseudoid(), member.getRoom());
          sendMessage(response, session);
          return;
        }
        else if (message instanceof TOPICMEMBER) {
          TOPICMEMBER member = (TOPICMEMBER) message;
          log.info("<----- {}", member.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          TOPICMEMBER response = databaseservice.fetchTopicmember(member.getPseudoid(), member.getRoom());
          log.info("------> {}", response.toString());
          log.info("session {}", session.getId());
          sendMessage(response, session.getId());
          return;
        }
        else if (message instanceof DELETEROOM) {
          DELETEROOM member = (DELETEROOM) message;
          DELETEROOM response = deleteRoom(member.getPseudoid(), member.getRoom());
          sendMessage(response, session);
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          List<String> roomlist = databaseservice.fetchRoomlist(session.getId());
          broadcast(roomlist);
          return;
        }
        else if (message instanceof UPDATEUSER) {
          UPDATEUSER updateuser = (UPDATEUSER) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          String sessionid = session.getId();
          String aes = databaseservice.fetchAES(sessionid); // kann Exception auftreten
          SecretKey secretAES = Crypto.getAESFromBase64(aes);
          String identity = updateuser.getIdentity();
          String back = Crypto.decryptAES(identity, secretAES);
          Token backToken = Token.toToken(back);
          String userid = backToken.getUserid();
          String password = backToken.getPassword();

          // alten Nickname finden, falls der neue Nickname nicht erlaubt ist
          String alterNickname = databaseservice.fetchNickname(backToken.getUserid());
          String nickname = updateuser.getNickname();
          Integer fgColor = updateuser.getForegroundColor();
          Integer bgColor = updateuser.getBackgroundColor();
          String pseudoid = backToken.getPseudoid();

          log.info("<----- {}", updateuser.toString());
          log.info("pseudoid {}", pseudoid);

          if (pseudoid != null) {
            userid = databaseservice.fetchUserByPseudoid(pseudoid).get();
            log.info("userid über Pseudoid gefunden");
            // PseudoPassword muss nicht geprüft werden wegen der Abwärtskompatibilität
            // aus diesem Grund muss in der 260807-PRE1 das echte Anmeldepasswort benutzt
            // werden
            // Wurde keine userid gefunden, ist das Ergebnis null
          }
          if (userid == null) {
            try {
              session.close();
              log.warn("Verbindung wird geschlossen wegen unbekanntem Anwender, session {}", session.getId());
            }
            catch (IOException e) {}
            return;
          }

          // ONCALL und VOLUME fehlen
          UPDATEUSER response = databaseservice
              .updateUser(identity, userid, backToken.getEMail(), password, nickname, fgColor, bgColor);

          // true CONFIRM
          // false ERROR

          sendMessage(response, session);
          // nickname chaträume im Nachgang anpassen
          if (CONFIRM == response.getHeader()) {

            try {
              alterNickname = databaseservice.fetchNickname(userid);
              if (alterNickname == null) {
                log.warn(userid + " - Nickname nicht gefunden");
                return;
              }

              databaseservice.replacePrivateRoomname(response.getNickname(), alterNickname);
              boolean done = databaseservice.updateOWNER(userid, response.getNickname());
              if (done) {
                // broadcast roomlist
                List<String> roomlist = databaseservice.fetchRoomlist(session.getId());
                broadcast(roomlist);
              }
//             an alle User USERONLINELIST senden 
              USERONLINELIST userOnlinelist = databaseservice.fetchUseronlinelist();

              List<String> ids = sessionmap.values().stream().map(Session::getId)
                  .collect(Collectors.toList());
              List<String> sessionAsString = List.copyOf(ids);
              log.info("----->", userOnlinelist.toString());
              sendMessage(userOnlinelist, sessionAsString);
            }
            catch (DatabaseException e) {
              log.error(e.getMessage());
            }

          }

          return;
        }

        log.error("unbekannter Befehl, {}", message.toString());
      }
    });

  }



  /**
   * Löscht den User aus allen Onlinelisten.
   * 
   * @param sessionID
   *                  der Onlineschlüssel identifiziert einen Anwender
   */
  private void removeUser(String sessionID) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();

    String uid = databaseservice.getUserBySession(sessionID);
    if (uid == null) return;

    try {
      databaseservice.unmute(uid);
      log.info("löschen userid {}", uid);
      cleanUser(uid, sessionID);
      USERONLINELIST userOnlinelist = databaseservice.fetchUseronlinelist();
      log.info("-----> " + userOnlinelist.toString());
      broadcastAll(userOnlinelist);
    }
    catch (DatabaseException e) {
      log.error(e.getMessage());
    }

  }



  // TODO aktuell werden maximal 256 zurückgegeben.
  private List<Searchfile> searchfiles(SEARCHFILES request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    final int max_files = 256;
    List<Searchfile> foundFiles = databaseservice.selectSearchfiles(request.getPatterns(), max_files);
    return foundFiles;
  }



  private UPDATEPHONE updatePhone(String pseudoid, boolean onBusy) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    Optional<String> userid = databaseservice.fetchUserByPseudoid(pseudoid);
    databaseservice.setOnBusy(userid.get(), onBusy);
    UPDATEPHONE response = new UPDATEPHONE();
    response.setCommand(Command.UPDATEPHONE);
    response.setHeader(RESPONSE);
    response.setDataset(Protocol.DATASET);
    response.setPseudoid(pseudoid); // wirklich benötigt?
    response.setOnBusy(onBusy);
    return response;
  }



  private UPDATEPORT updateport(String ip, Integer port, String pseudoid) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    databaseservice.updateUpdateport(ip, port);
    UPDATEPORT response = new UPDATEPORT();
    response.setCommand(Command.UPDATEPORT);
    response.setHeader(RESPONSE);
    response.setDataset(Protocol.DATASET);
    response.setPseudoid(pseudoid);
    response.setIp(ip);
    response.setPort(port);
    return response;
  }



  private UPLOADFILES uploadfiles(UPLOADFILES request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    try {

      Optional<String> userid = databaseservice.fetchUserByPseudoid(request.getPseudoid());

      if (request.hasStarted()) databaseservice.deleteUploadfiles(userid.get(), request.getIp());

      databaseservice
          .insertUploadfiles(userid.get(), request.getIp(), request.getPort(), request.getUploadfile());

      UPLOADFILES uploadfiles = new UPLOADFILES();
      uploadfiles.setCommand(Command.UPLOADFILES);
      uploadfiles.setHeader(RESPONSE);
      uploadfiles.setDataset(Protocol.DATASET);
      uploadfiles.setPseudoid(request.getPseudoid());
      uploadfiles.setIp(request.getIp());
      uploadfiles.setStarted(request.hasStarted());
      return uploadfiles;
    }
    catch (SQLException e) {
      log.error(e.getMessage(), e.getCause());
      UPLOADFILES uploadfiles = new UPLOADFILES();
      uploadfiles.setCommand(Command.UPLOADFILES);
      uploadfiles.setHeader(ERROR);
      uploadfiles.setDataset(Protocol.DATASET);
      uploadfiles.setErrorCode(e.getErrorCode());
      uploadfiles.setErrorMessage(e.getMessage());
      return uploadfiles;
    }
  }

}
