/**
 * Copyright (c) 2010-2019 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.binding.amazonechocontrol.internal.handler;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.storage.Storage;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.ThingUID;
import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.openhab.binding.amazonechocontrol.internal.AccountServlet;
import org.openhab.binding.amazonechocontrol.internal.Connection;
import org.openhab.binding.amazonechocontrol.internal.ConnectionException;
import org.openhab.binding.amazonechocontrol.internal.HttpException;
import org.openhab.binding.amazonechocontrol.internal.IWebSocketCommandHandler;
import org.openhab.binding.amazonechocontrol.internal.WebSocketConnection;
import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandler;
import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandlerSendMessage;
import org.openhab.binding.amazonechocontrol.internal.channelhandler.IAmazonThingHandler;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity.SourceDeviceId;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushActivity;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushActivity.Key;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushDevice;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushDevice.DopplerId;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushNotificationChange;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPushCommand;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWakeWords.WakeWord;
import org.osgi.service.http.HttpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

/**
 * Handles the connection to the amazon server.
 *
 * @author Michael Geramb - Initial Contribution
 */
@NonNullByDefault
public class AccountHandler extends BaseBridgeHandler implements IWebSocketCommandHandler, IAmazonThingHandler {

    private final Logger logger = LoggerFactory.getLogger(AccountHandler.class);
    private Storage<String> stateStorage;
    private @Nullable Connection connection;
    private @Nullable WebSocketConnection webSocketConnection;
    private final Set<EchoHandler> echoHandlers = new HashSet<>();
    private final Set<FlashBriefingProfileHandler> flashBriefingProfileHandlers = new HashSet<>();
    private final Object synchronizeConnection = new Object();
    private Map<String, Device> jsonSerialNumberDeviceMapping = new HashMap<>();
    private @Nullable ScheduledFuture<?> checkDataJob;
    private @Nullable ScheduledFuture<?> checkLoginJob;
    private @Nullable ScheduledFuture<?> refreshAfterCommandJob;
    private @Nullable ScheduledFuture<?> foceCheckDataJob;
    private String currentFlashBriefingJson = "";
    private final HttpService httpService;
    private @Nullable AccountServlet accountServlet;
    private final Gson gson;
    int checkDataCounter;
    private List<ChannelHandler> channelHandlers = new ArrayList<>();

    public AccountHandler(Bridge bridge, HttpService httpService, Storage<String> stateStorage, Gson gson) {
        super(bridge);
        this.gson = gson;
        this.httpService = httpService;
        this.stateStorage = stateStorage;
        channelHandlers.add(new ChannelHandlerSendMessage(this, this.gson));
    }

    @Override
    public void initialize() {
        logger.debug("amazon account bridge starting...");

        synchronized (synchronizeConnection) {
            Connection connection = this.connection;
            if (connection == null) {
                this.connection = new Connection(null, gson);
            }
        }
        if (this.accountServlet == null) {
            this.accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this, gson);
        }

        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login");

        checkLoginJob = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS);
        checkDataJob = scheduler.scheduleWithFixedDelay(this::checkData, 4, 60, TimeUnit.SECONDS);

        logger.debug("amazon account bridge handler started.");
    }

    @Override
    public void updateChannelState(String channelId, State state) {
        updateState(channelId, state);
    }

    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
        try {
            logger.trace("Command '{}' received for channel '{}'", command, channelUID);
            Connection connection = this.connection;
            if (connection == null) {
                return;
            }

            String channelId = channelUID.getId();
            for (ChannelHandler channelHandler : channelHandlers) {
                if (channelHandler.tryHandleCommand(new Device(), connection, channelId, command)) {
                    return;
                }
            }
            if (command instanceof RefreshType) {
                refreshData();
            }
        } catch (IOException | URISyntaxException e) {
            logger.info("handleCommand fails", e);
        }
    }

    public List<FlashBriefingProfileHandler> getFlashBriefingProfileHandlers() {
        return new ArrayList<>(this.flashBriefingProfileHandlers);
    }

    public List<Device> getLastKnownDevices() {
        return new ArrayList<>(jsonSerialNumberDeviceMapping.values());
    }

    public void addEchoHandler(EchoHandler echoHandler) {
        synchronized (echoHandlers) {
            if (!echoHandlers.add(echoHandler)) {
                return;
            }
        }
        forceCheckData();
    }

    public void forceCheckData() {
        if (foceCheckDataJob == null) {
            foceCheckDataJob = scheduler.schedule(this::forceCheckDataHandler, 1000, TimeUnit.MILLISECONDS);
        }
    }

    void forceCheckDataHandler() {
        this.checkData();
    }

    public @Nullable Thing findThingBySerialNumber(@Nullable String deviceSerialNumber) {
        EchoHandler echoHandler = findEchoHandlerBySerialNumber(deviceSerialNumber);
        if (echoHandler != null) {
            return echoHandler.getThing();
        }
        return null;
    }

    public @Nullable EchoHandler findEchoHandlerBySerialNumber(@Nullable String deviceSerialNumber) {
        synchronized (echoHandlers) {
            for (EchoHandler echoHandler : echoHandlers) {
                if (StringUtils.equals(echoHandler.findSerialNumber(), deviceSerialNumber)) {
                    return echoHandler;
                }
            }
        }
        return null;
    }

    public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) {
        synchronized (flashBriefingProfileHandlers) {
            flashBriefingProfileHandlers.add(flashBriefingProfileHandler);
        }
        Connection connection = this.connection;
        if (connection != null && connection.getIsLoggedIn()) {
            if (currentFlashBriefingJson.isEmpty()) {
                updateFlashBriefingProfiles(connection);
            }
            flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson);
        }
    }

    private void scheduleUpdate() {
        checkDataCounter = 999;
    }

    @Override
    public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
        super.childHandlerInitialized(childHandler, childThing);
        scheduleUpdate();
    }

    @Override
    public void handleRemoval() {
        cleanup();
        super.handleRemoval();
    }

    @Override
    public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
        // check for echo handler
        if (childHandler instanceof EchoHandler) {
            synchronized (echoHandlers) {
                echoHandlers.remove(childHandler);
            }
        }
        // check for flash briefing profile handler
        if (childHandler instanceof FlashBriefingProfileHandler) {
            synchronized (flashBriefingProfileHandlers) {
                flashBriefingProfileHandlers.remove(childHandler);
            }
        }
        super.childHandlerDisposed(childHandler, childThing);
    }

    @Override
    public void dispose() {
        AccountServlet accountServlet = this.accountServlet;
        if (accountServlet != null) {
            accountServlet.dispose();
        }
        this.accountServlet = null;
        cleanup();
        super.dispose();
    }

    private void cleanup() {
        logger.debug("cleanup {}", getThing().getUID().getAsString());
        @Nullable
        ScheduledFuture<?> refreshJob = this.checkDataJob;
        if (refreshJob != null) {
            refreshJob.cancel(true);
            this.checkDataJob = null;
        }
        @Nullable
        ScheduledFuture<?> refreshLogin = this.checkLoginJob;
        if (refreshLogin != null) {
            refreshLogin.cancel(true);
            this.checkLoginJob = null;
        }
        @Nullable
        ScheduledFuture<?> foceCheckDataJob = this.foceCheckDataJob;
        if (foceCheckDataJob != null) {
            foceCheckDataJob.cancel(true);
            this.foceCheckDataJob = null;
        }
        @Nullable
        ScheduledFuture<?> refreshDataDelayed = this.refreshAfterCommandJob;
        if (refreshDataDelayed != null) {
            refreshDataDelayed.cancel(true);
            this.refreshAfterCommandJob = null;
        }
        Connection connection = this.connection;
        if (connection != null) {
            connection.logout();
            this.connection = null;
        }
        closeWebSocketConnection();
    }

    private void checkLogin() {
        try {
            ThingUID uid = getThing().getUID();
            logger.debug("check login {}", uid.getAsString());

            synchronized (synchronizeConnection) {
                Connection currentConnection = this.connection;
                if (currentConnection == null) {
                    return;
                }

                try {
                    if (currentConnection.getIsLoggedIn()) {
                        if (currentConnection.checkRenewSession()) {
                            setConnection(currentConnection);
                        }
                    } else {

                        // read session data from property
                        String sessionStore = this.stateStorage.get("sessionStorage");

                        // try use the session data
                        if (currentConnection.tryRestoreLogin(sessionStore, null)) {
                            setConnection(currentConnection);
                        }
                    }
                    if (!currentConnection.getIsLoggedIn()) {
                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
                                "Please login in through web site: http(s)://<YOUROPENHAB>:<YOURPORT>/amazonechocontrol/"
                                        + URLEncoder.encode(uid.getId(), "UTF8"));
                    }
                } catch (ConnectionException e) {
                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
                } catch (HttpException e) {
                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
                } catch (UnknownHostException e) {
                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                            "Unknown host name '" + e.getMessage() + "'. Maybe your internet connection is offline");
                } catch (IOException e) {
                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
                } catch (URISyntaxException e) {
                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
                }
            }

        } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
            logger.error("check login fails with unexpected error", e);
        }
    }

    // used to set a valid connection from the web proxy login
    public void setConnection(@Nullable Connection connection) {
        this.connection = connection;
        if (connection != null) {
            String serializedStorage = connection.serializeLoginData();
            this.stateStorage.put("sessionStorage", serializedStorage);
        } else {
            this.stateStorage.put("sessionStorage", null);
            updateStatus(ThingStatus.OFFLINE);
        }
        closeWebSocketConnection();
        if (connection != null) {
            updateDeviceList();
            updateFlashBriefingHandlers();
            updateStatus(ThingStatus.ONLINE);
            scheduleUpdate();
            checkData();
        }
    }

    void closeWebSocketConnection() {
        WebSocketConnection webSocketConnection = this.webSocketConnection;
        this.webSocketConnection = null;
        if (webSocketConnection != null) {
            webSocketConnection.close();
        }
    }

    boolean checkWebSocketConnection() {
        WebSocketConnection webSocketConnection = this.webSocketConnection;
        if (webSocketConnection == null || webSocketConnection.isClosed()) {
            Connection connection = this.connection;
            if (connection != null && connection.getIsLoggedIn()) {
                try {
                    this.webSocketConnection = new WebSocketConnection(connection.getAmazonSite(),
                            connection.getSessionCookies(), this);
                } catch (IOException e) {
                    logger.warn("Web socket connection starting failed", e);
                }
            }
            return false;
        }
        return true;
    }

    private void checkData() {
        synchronized (synchronizeConnection) {
            try {
                Connection connection = this.connection;
                if (connection != null && connection.getIsLoggedIn()) {
                    checkDataCounter++;
                    if (checkDataCounter > 60 || foceCheckDataJob != null) {
                        checkDataCounter = 0;
                        foceCheckDataJob = null;
                    }
                    if (!checkWebSocketConnection() || checkDataCounter == 0) {
                        refreshData();
                    }
                }
                logger.debug("checkData {} finished", getThing().getUID().getAsString());
            } catch (HttpException | JsonSyntaxException | ConnectionException e) {
                logger.debug("checkData fails", e);
            } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
                logger.error("checkData fails with unexpected error", e);
            }
        }
    }

    private void refreshNotifications(@Nullable JsonCommandPayloadPushNotificationChange pushPayload) {
        Connection currentConnection = this.connection;
        if (currentConnection == null) {
            return;
        }
        if (!currentConnection.getIsLoggedIn()) {
            return;
        }
        JsonNotificationResponse[] notifications;
        ZonedDateTime timeStamp = ZonedDateTime.now();
        try {
            notifications = currentConnection.notifications();
        } catch (IOException | URISyntaxException e) {
            logger.debug("refreshNotifications failed", e);
            return;
        }
        ZonedDateTime timeStampNow = ZonedDateTime.now();

        for (EchoHandler child : echoHandlers) {
            child.updateNotifications(timeStamp, timeStampNow, pushPayload, notifications);
        }

    }

    private void refreshData() {
        synchronized (synchronizeConnection) {
            try {
                logger.debug("refreshing data {}", getThing().getUID().getAsString());

                // check if logged in
                Connection currentConnection = null;
                currentConnection = connection;
                if (currentConnection != null) {
                    if (!currentConnection.getIsLoggedIn()) {
                        return;
                    }
                }
                if (currentConnection == null) {
                    return;
                }

                // get all devices registered in the account
                updateDeviceList();
                updateFlashBriefingHandlers();

                DeviceNotificationState[] deviceNotificationStates = null;
                AscendingAlarmModel[] ascendingAlarmModels = null;
                JsonBluetoothStates states = null;
                List<JsonMusicProvider> musicProviders = null;
                if (currentConnection.getIsLoggedIn()) {
                    // update notification states
                    deviceNotificationStates = currentConnection.getDeviceNotificationStates();

                    // update ascending alarm
                    ascendingAlarmModels = currentConnection.getAscendingAlarm();

                    // update bluetooth states
                    states = currentConnection.getBluetoothConnectionStates();

                    // update music providers
                    if (currentConnection.getIsLoggedIn()) {
                        try {
                            musicProviders = currentConnection.getMusicProviders();
                        } catch (HttpException | JsonSyntaxException | ConnectionException e) {
                            logger.debug("Update music provider failed", e);
                        }
                    }
                }
                // forward device information to echo handler
                for (EchoHandler child : echoHandlers) {
                    Device device = findDeviceJson(child);

                    @Nullable
                    JsonNotificationSound[] notificationSounds = null;
                    JsonPlaylists playlists = null;
                    if (device != null && currentConnection.getIsLoggedIn()) {
                        // update notification sounds
                        try {
                            notificationSounds = currentConnection.getNotificationSounds(device);
                        } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
                            logger.debug("Update notification sounds failed", e);
                        }
                        // update playlists
                        try {
                            playlists = currentConnection.getPlaylists(device);
                        } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
                            logger.debug("Update playlist failed", e);
                        }
                    }

                    BluetoothState state = null;
                    if (states != null) {
                        state = states.findStateByDevice(device);
                    }
                    DeviceNotificationState deviceNotificationState = null;
                    AscendingAlarmModel ascendingAlarmModel = null;
                    if (device != null) {
                        if (ascendingAlarmModels != null) {
                            for (AscendingAlarmModel current : ascendingAlarmModels) {
                                if (StringUtils.equals(current.deviceSerialNumber, device.serialNumber)) {
                                    ascendingAlarmModel = current;
                                    break;
                                }
                            }
                        }

                        if (deviceNotificationStates != null) {
                            for (DeviceNotificationState current : deviceNotificationStates) {
                                if (StringUtils.equals(current.deviceSerialNumber, device.serialNumber)) {
                                    deviceNotificationState = current;
                                    break;
                                }
                            }
                        }
                    }
                    child.updateState(this, device, state, deviceNotificationState, ascendingAlarmModel, playlists,
                            notificationSounds, musicProviders);
                }

                // refresh notifications
                refreshNotifications(null);

                // update account state
                updateStatus(ThingStatus.ONLINE);

                logger.debug("refresh data {} finished", getThing().getUID().getAsString());
            } catch (HttpException | JsonSyntaxException | ConnectionException e) {
                logger.debug("refresh data fails", e);
            } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
                logger.error("refresh data fails with unexpected error", e);
            }
        }
    }

    public @Nullable Device findDeviceJson(EchoHandler echoHandler) {
        String serialNumber = echoHandler.findSerialNumber();
        return findDeviceJson(serialNumber);
    }

    public @Nullable Device findDeviceJson(@Nullable String serialNumber) {
        Device result = null;
        if (StringUtils.isNotEmpty(serialNumber)) {
            Map<String, Device> jsonSerialNumberDeviceMapping = this.jsonSerialNumberDeviceMapping;
            result = jsonSerialNumberDeviceMapping.get(serialNumber);
        }
        return result;
    }

    public @Nullable Device findDeviceJsonBySerialOrName(@Nullable String serialOrName) {
        if (StringUtils.isNotEmpty(serialOrName)) {
            Map<String, Device> currentJsonSerialNumberDeviceMapping = this.jsonSerialNumberDeviceMapping;
            for (Device device : currentJsonSerialNumberDeviceMapping.values()) {
                if (StringUtils.equalsIgnoreCase(device.serialNumber, serialOrName)) {
                    return device;
                }
            }
            for (Device device : currentJsonSerialNumberDeviceMapping.values()) {
                if (StringUtils.equalsIgnoreCase(device.accountName, serialOrName)) {
                    return device;
                }
            }
        }
        return null;
    }

    public List<Device> updateDeviceList() {

        Connection currentConnection = connection;
        if (currentConnection == null) {
            return new ArrayList<>();
        }

        List<Device> devices = null;
        try {
            if (currentConnection.getIsLoggedIn()) {
                devices = currentConnection.getDeviceList();
            }
        } catch (IOException | URISyntaxException e) {
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
        }
        if (devices != null) {
            Map<String, Device> newJsonSerialDeviceMapping = new HashMap<>();
            for (Device device : devices) {
                String serialNumber = device.serialNumber;
                if (serialNumber != null) {
                    newJsonSerialDeviceMapping.put(serialNumber, device);
                }

            }
            jsonSerialNumberDeviceMapping = newJsonSerialDeviceMapping;
        }
        WakeWord[] wakeWords = currentConnection.getWakeWords();

        synchronized (echoHandlers) {
            for (EchoHandler child : echoHandlers) {
                String serialNumber = child.findSerialNumber();
                String deviceWakeWord = null;
                for (WakeWord wakeWord : wakeWords) {
                    if (wakeWord != null) {
                        if (StringUtils.equals(wakeWord.deviceSerialNumber, serialNumber)) {
                            deviceWakeWord = wakeWord.wakeWord;
                            break;
                        }
                    }
                }
                child.setDeviceAndUpdateThingState(this, findDeviceJson(child), deviceWakeWord);
            }
        }
        if (devices != null) {
            return devices;
        }
        return new ArrayList<>();
    }

    public void setEnabledFlashBriefingsJson(String flashBriefingJson) {
        Connection currentConnection = connection;
        JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class);
        if (currentConnection != null) {
            try {
                currentConnection.setEnabledFlashBriefings(feeds);
            } catch (IOException | URISyntaxException e) {
                logger.warn("Set flashbriefing profile failed", e);
            }
        }
        updateFlashBriefingHandlers();
    }

    public String getNewCurrentFlashbriefingConfiguration() {
        return updateFlashBriefingHandlers();
    }

    public String updateFlashBriefingHandlers() {
        Connection currentConnection = connection;
        if (currentConnection != null) {
            return updateFlashBriefingHandlers(currentConnection);
        }
        return "";
    }

    private String updateFlashBriefingHandlers(Connection currentConnection) {
        synchronized (flashBriefingProfileHandlers) {
            if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) {
                updateFlashBriefingProfiles(currentConnection);
            }
            boolean flashBriefingProfileFound = false;
            for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) {
                flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson);
            }
            if (flashBriefingProfileFound) {
                return "";
            }
            return this.currentFlashBriefingJson;
        }
    }

    public @Nullable Connection findConnection() {
        return this.connection;
    }

    public String getEnabledFlashBriefingsJson() {
        Connection currentConnection = this.connection;
        if (currentConnection == null) {
            return "";
        }
        updateFlashBriefingProfiles(currentConnection);
        return this.currentFlashBriefingJson;
    }

    private void updateFlashBriefingProfiles(Connection currentConnection) {
        try {
            JsonFeed[] feeds = currentConnection.getEnabledFlashBriefings();
            // Make a copy and remove changeable parts
            JsonFeed[] forSerializer = new JsonFeed[feeds.length];
            for (int i = 0; i < feeds.length; i++) {
                JsonFeed source = feeds[i];
                JsonFeed copy = new JsonFeed();
                copy.feedId = source.feedId;
                copy.skillId = source.skillId;
                // Do not copy imageUrl here, because it will change
                forSerializer[i] = copy;
            }
            this.currentFlashBriefingJson = gson.toJson(forSerializer);
        } catch (HttpException | JsonSyntaxException | IOException | URISyntaxException | ConnectionException e) {
            logger.warn("get flash briefing profiles fails", e);
        }

    }

    @Override
    public void webSocketCommandReceived(JsonPushCommand pushCommand) {
        try {
            handleWebsocketCommand(pushCommand);
        } catch (Exception e) {
            // should never happen, but if the exception is going out of this function, the binding stop working.
            logger.warn("handling of websockets fails", e);
        }
    }

    void handleWebsocketCommand(JsonPushCommand pushCommand) {
        String command = pushCommand.command;
        if (command != null) {
            switch (command) {
                case "PUSH_ACTIVITY":
                    handlePushActivity(pushCommand.payload);
                    return;
                case "PUSH_DOPPLER_CONNECTION_CHANGE":
                case "PUSH_BLUETOOTH_STATE_CHANGE":
                    // refresh data 200ms after last command
                    @Nullable
                    ScheduledFuture<?> refreshDataDelayed = this.refreshAfterCommandJob;
                    if (refreshDataDelayed != null) {
                        refreshDataDelayed.cancel(false);
                    }
                    this.refreshAfterCommandJob = scheduler.schedule(this::refreshAfterCommand, 700,
                            TimeUnit.MILLISECONDS);
                    break;
                case "PUSH_NOTIFICATION_CHANGE":
                    JsonCommandPayloadPushNotificationChange pushPayload = gson.fromJson(pushCommand.payload,
                            JsonCommandPayloadPushNotificationChange.class);
                    refreshNotifications(pushPayload);
                    break;
                default:
                    String payload = pushCommand.payload;
                    if (payload != null && StringUtils.isNotEmpty(payload) && payload.startsWith("{")
                            && payload.endsWith("}")) {
                        JsonCommandPayloadPushDevice devicePayload = gson.fromJson(payload,
                                JsonCommandPayloadPushDevice.class);
                        @Nullable
                        DopplerId dopplerId = devicePayload.dopplerId;
                        if (dopplerId != null) {
                            handlePushDeviceCommand(dopplerId, command, payload);
                        }
                    }
                    break;
            }
        }
    }

    private void handlePushDeviceCommand(DopplerId dopplerId, String command, String payload) {
        @Nullable
        EchoHandler echoHandler = findEchoHandlerBySerialNumber(dopplerId.deviceSerialNumber);
        if (echoHandler != null) {
            echoHandler.handlePushCommand(command, payload);
        }
    }

    private void handlePushActivity(@Nullable String payload) {
        JsonCommandPayloadPushActivity pushActivity = gson.fromJson(payload, JsonCommandPayloadPushActivity.class);

        Key key = pushActivity.key;
        if (key == null) {
            return;
        }

        Connection connection = this.connection;
        if (connection == null || !connection.getIsLoggedIn()) {
            return;
        }
        Activity[] activities = connection.getActivities(10, pushActivity.timestamp);
        Activity currentActivity = null;
        String search = key.registeredUserId + "#" + key.entryId;
        for (Activity activity : activities) {
            if (StringUtils.equals(activity.id, search)) {
                currentActivity = activity;
                break;
            }
        }
        if (currentActivity == null) {
            return;
        }

        @Nullable
        SourceDeviceId @Nullable [] sourceDeviceIds = currentActivity.sourceDeviceIds;
        if (sourceDeviceIds != null) {
            for (SourceDeviceId sourceDeviceId : sourceDeviceIds) {
                if (sourceDeviceId != null) {
                    EchoHandler echoHandler = findEchoHandlerBySerialNumber(sourceDeviceId.serialNumber);
                    if (echoHandler != null) {
                        echoHandler.handlePushActivity(currentActivity);
                    }
                }
            }
        }
    }

    void refreshAfterCommand() {
        refreshData();
    }
}
