// project includes
#include <api/JsonAPI.h>
#include <api/JsonInfo.h>

// Qt includes
#include <QResource>
#include <QDateTime>
#include <QImage>
#include <QBuffer>
#include <QByteArray>
#include <QTimer>
#include <QHostInfo>
#include <QMultiMap>
#include <QRegularExpression>
#include <QStringList>

// hyperion includes
#include <leddevice/LedDeviceWrapper.h>
#include <leddevice/LedDevice.h>
#include <leddevice/LedDeviceFactory.h>

#include <HyperionConfig.h> // Required to determine the cmake options

#include <utils/WeakConnect.h>
#include <events/EventEnum.h>

#include <utils/jsonschema/QJsonFactory.h>
#include <utils/jsonschema/QJsonSchemaChecker.h>
#include <utils/ColorSys.h>
#include <utils/Process.h>
#include <utils/JsonUtils.h>

// ledmapping int <> string transform methods
#include <hyperion/ImageProcessor.h>

// api includes
#include <api/JsonCallbacks.h>
#include <events/EventHandler.h>

// auth manager
#include <hyperion/AuthManager.h>
#include <db/DBConfigManager.h>

#ifdef ENABLE_MDNS
// mDNS discover
#include <mdns/MdnsBrowser.h>
#include <mdns/MdnsServiceRegister.h>
#else
// ssdp discover
#include <ssdp/SSDPDiscover.h>
#endif

#include <chrono>
#include <utility>

using namespace hyperion;

// Constants
namespace {

constexpr std::chrono::milliseconds NEW_TOKEN_REQUEST_TIMEOUT{ 180000 };

const char TOKEN_TAG[] = "token";
constexpr int TOKEN_TAG_LENGTH = sizeof(TOKEN_TAG) - 1;
const char BEARER_TOKEN_TAG[] = "Bearer";
constexpr int BEARER_TOKEN_TAG_LENGTH = sizeof(BEARER_TOKEN_TAG) - 1;

const int MIN_PASSWORD_LENGTH = 8;
const int APP_TOKEN_LENGTH = 36;

const char SETTINGS_UI_SCHEMA_FILE[] = ":/schema-settings-ui.json";

const bool verbose = false;
}

JsonAPI::JsonAPI(QString peerAddress, Logger *log, bool localConnection, QObject *parent, bool noListener)
	: API(log, localConnection, parent)
	,_noListener(noListener)
	,_peerAddress (std::move(peerAddress))
	,_jsonCB (nullptr)
{
	Q_INIT_RESOURCE(JSONRPC_schemas);

	qRegisterMetaType<Event>("Event");
	_jsonCB = QSharedPointer<JsonCallbacks>(new JsonCallbacks( _log, _peerAddress, parent));
}

void JsonAPI::initialize()
{
	// init API, REQUIRED!
	API::init();

	// setup auth interface
	connect(this, &API::onPendingTokenRequest, this, &JsonAPI::issueNewPendingTokenRequest);
	connect(this, &API::onTokenResponse, this, &JsonAPI::handleTokenResponse);

	// listen for killed instances
	connect(_instanceManager, &HyperionIManager::instanceStateChanged, this, &JsonAPI::handleInstanceStateChange);

	// pipe callbacks from subscriptions to parent
	connect(_jsonCB.data(), &JsonCallbacks::newCallback, this, &JsonAPI::callbackMessage);

	// notify hyperion about a jsonMessageForward
	if (_hyperion != nullptr)
	{
		// Initialise jsonCB with current instance
		_jsonCB->setSubscriptionsTo(_hyperion);
		connect(this, &JsonAPI::forwardJsonMessage, _hyperion, &Hyperion::forwardJsonMessage);
	}

	//notify eventhadler on suspend/resume/idle requests
	connect(this, &JsonAPI::signalEvent, EventHandler::getInstance().data(), &EventHandler::handleEvent);
}

bool JsonAPI::handleInstanceSwitch(quint8 inst, bool /*forced*/)
{
	if (API::setHyperionInstance(inst))
	{
		Debug(_log, "Client '%s' switch to Hyperion instance %d", QSTRING_CSTR(_peerAddress), inst);
		// the JsonCB creates json messages you can subscribe to e.g. data change events
		_jsonCB->setSubscriptionsTo(_hyperion);
		return true;
	}
	return false;
}

void JsonAPI::handleMessage(const QString &messageString, const QString &httpAuthHeader)
{
	const QString ident = "JsonRpc@" + _peerAddress;
	QJsonObject message;

	//parse the message
	QPair<bool, QStringList> parsingResult = JsonUtils::parse(ident, messageString, message, _log);
	if (!parsingResult.first)
	{
		//Try to find command and tan, even parsing failed
		QString command = findCommand(messageString);
		int tan = findTan(messageString);

		sendErrorReply("Parse error", parsingResult.second, command, tan);
		return;
	}

	DebugIf(verbose, _log, "message: [%s]", QJsonDocument(message).toJson(QJsonDocument::Compact).constData() );

	// check specific message
	const QString command = message.value("command").toString();
	const QString subCommand = message.value("subcommand").toString();

	int tan {0};
	if (message.value("tan") != QJsonValue::Undefined)
	{
		tan = message["tan"].toInt();
	}

	// check basic message
	QJsonObject schemaJson = QJsonFactory::readSchema(":schema");
	QPair<bool, QStringList> validationResult = JsonUtils::validate(ident, message, schemaJson, _log);
	if (!validationResult.first)
	{
		sendErrorReply("Invalid command", validationResult.second, command, tan);
		return;
	}

	JsonApiCommand cmd = ApiCommandRegister::getCommandInfo(command, subCommand);
	cmd.tan = tan;

	if (cmd.command == Command::Unknown)
	{
		const QStringList errorDetails (subCommand.isEmpty() ? "subcommand is missing" : QString("Invalid subcommand: %1").arg(subCommand));
		sendErrorReply("Invalid command", errorDetails, command, tan);
		return;
	}

	if (_noListener)
	{
		setAuthorization(false);
		if(cmd.isNolistenerCmd == NoListenerCmd::No)
		{
			sendErrorReply("Command not supported via single API calls using HTTP/S", cmd);
			return;
		}

		// Check authorization for HTTP requests
		if (!httpAuthHeader.isEmpty())
		{
			int bearTokenLenght {0};
			if (httpAuthHeader.startsWith(BEARER_TOKEN_TAG, Qt::CaseInsensitive)) {
				bearTokenLenght = BEARER_TOKEN_TAG_LENGTH;
			}
			else if (httpAuthHeader.startsWith(TOKEN_TAG, Qt::CaseInsensitive)) {
				bearTokenLenght = TOKEN_TAG_LENGTH;
			}

			if (bearTokenLenght == 0)
			{
				sendErrorReply("No bearer token found in Authorization header", cmd);
				return;
			}

			QString cToken =httpAuthHeader.mid(bearTokenLenght).trimmed();
			API::isTokenAuthorized(cToken); // _authorized && _adminAuthorized are set
		}

		if (islocalConnection() && !_authManager->isLocalAuthRequired())
		{
			// if the request comes via a local network connection, plus authorization is disabled for local request,
			// no token authorization is required for non-admin requests
			setAuthorization(true);
		}
	}

	if (cmd.authorization != Authorization::No )
	{
		if (!isAuthorized() || (cmd.authorization == Authorization::Admin && !isAdminAuthorized()))
		{
			sendNoAuthorization(cmd);
			return;
		}
	}

	schemaJson = QJsonFactory::readSchema(QString(":schema-%1").arg(command));
	validationResult = JsonUtils::validate(ident, message, schemaJson, _log);
	if (!validationResult.first)
	{
		sendErrorReply("Invalid params", validationResult.second, cmd);
		return;
	}

	if (_hyperion == nullptr)
	{
		sendErrorReply("Service Unavailable", cmd);
		return;
	}

	if (!message.contains("instance") || cmd.isInstanceCmd == InstanceCmd::No)
	{
		handleCommand(cmd, message);
	}
	else
	{
		handleInstanceCommand(cmd, message);
	}
}

void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject &message)
{
	const QJsonValue instanceElement = message.value("instance");
	QJsonArray instances;
	if (instanceElement.isDouble())
	{
		instances.append(instanceElement);
	} else if (instanceElement.isArray())
	{
		instances = instanceElement.toArray();
	}

	QList<quint8> runningInstanceIdxs = _instanceManager->getRunningInstanceIdx();

	QList<quint8> instanceIdxList;
	QStringList errorDetails;
	if (instances.contains("all"))
	{
		for (const auto& instanceIdx : runningInstanceIdxs)
		{
			instanceIdxList.append(instanceIdx);
		}
	}
	else
	{
		for (const auto &instance : std::as_const(instances)) {

			quint8 instanceIdx = static_cast<quint8>(instance.toInt());
			if (instance.isDouble() && runningInstanceIdxs.contains(instanceIdx))
			{
				instanceIdxList.append(instanceIdx);
			}
			else
			{
				errorDetails.append("Not a running or valid instance: " + instance.toVariant().toString());
			}
		}
	}

	if (instanceIdxList.isEmpty() || !errorDetails.isEmpty() )
	{
		sendErrorReply("Invalid instance(s) given", errorDetails, cmd);
		return;
	}

	quint8 currentInstanceIdx = getCurrentInstanceIndex();
	if (instanceIdxList.size() > 1)
	{
		if (cmd.isInstanceCmd != InstanceCmd::Multi)
		{
			sendErrorReply("Command does not support multiple instances", cmd);
			return;
		}
	}

	for (const auto &instanceIdx : instanceIdxList)
	{
		if (setHyperionInstance(instanceIdx))
		{
			handleCommand(cmd, message);
		}
	}

	setHyperionInstance(currentInstanceIdx);
}

void JsonAPI::handleCommand(const JsonApiCommand& cmd, const QJsonObject &message)
{
	switch (cmd.command) {
	case Command::Authorize:
		handleAuthorizeCommand(message, cmd);
	break;
	case Command::Color:
		handleColorCommand(message, cmd);
	break;
	case Command::Image:
		handleImageCommand(message, cmd);
	break;
#if defined(ENABLE_EFFECTENGINE)
	case Command::Effect:
		handleEffectCommand(message, cmd);
	break;
	case Command::CreateEffect:
		handleCreateEffectCommand(message, cmd);
	break;
	case Command::DeleteEffect:
		handleDeleteEffectCommand(message, cmd);
	break;
#endif
	case Command::SysInfo:
		handleSysInfoCommand(message, cmd);
	break;
	case Command::ServerInfo:
		handleServerInfoCommand(message, cmd);
	break;
	case Command::Clear:
		handleClearCommand(message, cmd);
	break;
	case Command::Adjustment:
		handleAdjustmentCommand(message, cmd);
	break;
	case Command::SourceSelect:
		handleSourceSelectCommand(message, cmd);
	break;
	case Command::Config:
		handleConfigCommand(message, cmd);
	break;
	case Command::ComponentState:
		handleComponentStateCommand(message, cmd);
	break;
	case Command::LedColors:
		handleLedColorsCommand(message, cmd);
	break;
	case Command::Logging:
		handleLoggingCommand(message, cmd);
	break;
	case Command::Processing:
		handleProcessingCommand(message, cmd);
	break;
	case Command::VideoMode:
		handleVideoModeCommand(message, cmd);
	break;
	case Command::Instance:
		handleInstanceCommand(message, cmd);
	break;
	case Command::LedDevice:
		handleLedDeviceCommand(message, cmd);
	break;
	case Command::InputSource:
		handleInputSourceCommand(message, cmd);
	break;
	case Command::Service:
		handleServiceCommand(message, cmd);
	break;
	case Command::System:
		handleSystemCommand(message, cmd);
	break;
	case Command::ClearAll:
		handleClearallCommand(message, cmd);
	break;
		// BEGIN | The following commands are deprecated but used to ensure backward compatibility with Hyperion Classic remote control
	case Command::Transform:
	case Command::Correction:
	case Command::Temperature:
		sendErrorReply("The command is deprecated, please use the Hyperion Web Interface to configure", cmd);
	break;
		// END
	default:
	break;
	}
}

void JsonAPI::handleColorCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	emit forwardJsonMessage(message);
	int priority = message["priority"].toInt();
	int duration = message["duration"].toInt(-1);
	const QString origin = message["origin"].toString("JsonRpc") + "@" + _peerAddress;

	const QJsonArray &jsonColor = message["color"].toArray();
	std::vector<uint8_t> colors;
	colors.reserve(static_cast<std::vector<uint8_t>::size_type>(jsonColor.size()));
	// Transform each entry in jsonColor to uint8_t and append to colors
	std::transform(jsonColor.begin(), jsonColor.end(), std::back_inserter(colors),
				   [](const QJsonValue &value) { return static_cast<uint8_t>(value.toInt()); });

	API::setColor(priority, colors, duration, origin);
	sendSuccessReply(cmd);
}

void JsonAPI::handleImageCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	emit forwardJsonMessage(message);

	API::ImageCmdData idata;
	idata.priority = message["priority"].toInt();
	idata.origin = message["origin"].toString("JsonRpc") + "@" + _peerAddress;
	idata.duration = message["duration"].toInt(-1);
	idata.width = message["imagewidth"].toInt();
	idata.height = message["imageheight"].toInt();
	idata.scale = message["scale"].toInt(-1);
	idata.format = message["format"].toString();
	idata.imgName = message["name"].toString("");
	idata.data = QByteArray::fromBase64(QByteArray(message["imagedata"].toString().toUtf8()));
	QString replyMsg;

	if (API::setImage(idata, COMP_IMAGE, replyMsg)) {
		sendSuccessReply(cmd);
	} else {
		sendErrorReply(replyMsg, cmd);
	}
}

#if defined(ENABLE_EFFECTENGINE)
void JsonAPI::handleEffectCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	emit forwardJsonMessage(message);

	EffectCmdData dat;
	dat.priority = message["priority"].toInt();
	dat.duration = message["duration"].toInt(-1);
	dat.pythonScript = message["pythonScript"].toString();
	dat.origin = message["origin"].toString("JsonRpc") + "@" + _peerAddress;
	dat.effectName = message["effect"].toObject()["name"].toString();
	dat.data = message["imageData"].toString("").toUtf8();
	dat.args = message["effect"].toObject()["args"].toObject();

	if (API::setEffect(dat)) {
		sendSuccessReply(cmd);
	} else {
		sendErrorReply("Effect '" + dat.effectName + "' not found", cmd);
	}
}

void JsonAPI::handleCreateEffectCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	const QString resultMsg = API::saveEffect(message);
	resultMsg.isEmpty() ? sendSuccessReply(cmd) : sendErrorReply(resultMsg, cmd);
}

void JsonAPI::handleDeleteEffectCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	const QString res = API::deleteEffect(message["name"].toString());
	res.isEmpty() ? sendSuccessReply(cmd) : sendErrorReply(res, cmd);
}
#endif

void JsonAPI::handleSysInfoCommand(const QJsonObject & /*unused*/, const JsonApiCommand& cmd)
{
	sendSuccessDataReply(JsonInfo::getSystemInfo(_hyperion), cmd);
}

void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	QJsonObject info {};
	QStringList errorDetails;

	switch (cmd.getSubCommand()) {
	case SubCommand::Empty:
	case SubCommand::GetInfo:
		info["priorities"] = JsonInfo::getPrioritiestInfo(_hyperion);
		info["priorities_autoselect"] = _hyperion->sourceAutoSelectEnabled();
		info["adjustment"] = JsonInfo::getAdjustmentInfo(_hyperion, _log);
		info["ledDevices"] = JsonInfo::getAvailableLedDevices();
		info["grabbers"] = JsonInfo::getGrabbers(_hyperion);
		info["videomode"] = QString(videoMode2String(_hyperion->getCurrentVideoMode()));
		info["cec"] = JsonInfo::getCecInfo();
		info["services"] = JsonInfo::getServices();
		info["components"] = JsonInfo::getComponents(_hyperion);
		info["imageToLedMappingType"] = ImageProcessor::mappingTypeToStr(_hyperion->getLedMappingType());
		info["instance"] = JsonInfo::getInstanceInfo();
		info["leds"] = _hyperion->getSetting(settings::LEDS).array();
		info["activeLedColor"] =  JsonInfo::getActiveColors(_hyperion);

#if defined(ENABLE_EFFECTENGINE)
		info["effects"] = JsonInfo::getEffects(_hyperion);
		info["activeEffects"] = JsonInfo::getActiveEffects(_hyperion);
#endif

		// BEGIN | The following entries are deprecated but used to ensure backward compatibility with hyperion Classic or up to Hyperion 2.0.16
		info["hostname"] = QHostInfo::localHostName();
		info["transform"] = JsonInfo::getTransformationInfo(_hyperion);

		if (!_noListener && message.contains("subscribe"))
		{
			const QJsonArray &subscriptions = message["subscribe"].toArray();
			QStringList invaliCommands = _jsonCB->subscribe(subscriptions);
			if (!invaliCommands.isEmpty())
			{
				errorDetails.append("subscribe - Invalid commands provided: " +  invaliCommands.join(','));
			}
		}
		// END

	break;

	case SubCommand::Subscribe:
	case SubCommand::Unsubscribe:
	{
		const QJsonObject &params = message["data"].toObject();
		const QJsonArray &subscriptions = params["subscriptions"].toArray();
		if (subscriptions.isEmpty()) {
			sendErrorReply("Invalid params", {"No subscriptions provided"}, cmd);
			return;
		}

		QStringList invaliCommands;
		if (cmd.subCommand == SubCommand::Subscribe)
		{
			invaliCommands = _jsonCB->subscribe(subscriptions);
		}
		else
		{
			invaliCommands = _jsonCB->unsubscribe(subscriptions);
		}

		if (!invaliCommands.isEmpty())
		{
			errorDetails.append("subscriptions - Invalid commands provided: " +  invaliCommands.join(','));
		}
	}
	break;

	case SubCommand::GetSubscriptions:
		info["subscriptions"] = QJsonArray::fromStringList(_jsonCB->getSubscribedCommands());
	break;

	case SubCommand::GetSubscriptionCommands:
		info["commands"] = QJsonArray::fromStringList(_jsonCB->getCommands());
	break;

	default:
	break;
	}

	sendSuccessDataReplyWithError(info, cmd, errorDetails);
}

void JsonAPI::handleClearCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	emit forwardJsonMessage(message);
	int priority = message["priority"].toInt();
	QString replyMsg;

	if (!API::clearPriority(priority, replyMsg))
	{
		sendErrorReply(replyMsg, cmd);
		return;
	}
	sendSuccessReply(cmd);
}

void JsonAPI::handleClearallCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	emit forwardJsonMessage(message);
	QString replyMsg;
	API::clearPriority(-1, replyMsg);
	sendSuccessReply(cmd);
}

void JsonAPI::handleAdjustmentCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	const QJsonObject &adjustment = message["adjustment"].toObject();

	const QList<QString> adjustmentIds = _hyperion->getAdjustmentIds();
	if (adjustmentIds.isEmpty()) {
		sendErrorReply("No adjustment data available", cmd);
		return;
	}

	const QString adjustmentId = adjustment["id"].toString(adjustmentIds.first());
	ColorAdjustment *colorAdjustment = _hyperion->getAdjustment(adjustmentId);
	if (colorAdjustment == nullptr) {
		Warning(_log, "Incorrect adjustment identifier: %s", adjustmentId.toStdString().c_str());
		return;
	}

	applyColorAdjustments(adjustment, colorAdjustment);
	applyTransforms(adjustment, colorAdjustment);
	_hyperion->adjustmentsUpdated();
	sendSuccessReply(cmd);
}

void JsonAPI::applyColorAdjustments(const QJsonObject &adjustment, ColorAdjustment *colorAdjustment)
{
	applyColorAdjustment("red", adjustment, colorAdjustment->_rgbRedAdjustment);
	applyColorAdjustment("green", adjustment, colorAdjustment->_rgbGreenAdjustment);
	applyColorAdjustment("blue", adjustment, colorAdjustment->_rgbBlueAdjustment);
	applyColorAdjustment("cyan", adjustment, colorAdjustment->_rgbCyanAdjustment);
	applyColorAdjustment("magenta", adjustment, colorAdjustment->_rgbMagentaAdjustment);
	applyColorAdjustment("yellow", adjustment, colorAdjustment->_rgbYellowAdjustment);
	applyColorAdjustment("white", adjustment, colorAdjustment->_rgbWhiteAdjustment);
}

void JsonAPI::applyColorAdjustment(const QString &colorName, const QJsonObject &adjustment, RgbChannelAdjustment &rgbAdjustment)
{
	if (adjustment.contains(colorName)) {
		const QJsonArray &values = adjustment[colorName].toArray();
		if (values.size() >= 3) {
			rgbAdjustment.setAdjustment(static_cast<uint8_t>(values[0U].toInt()),
					static_cast<uint8_t>(values[1U].toInt()),
					static_cast<uint8_t>(values[2U].toInt()));
		}
	}
}

void JsonAPI::applyTransforms(const QJsonObject &adjustment, ColorAdjustment *colorAdjustment)
{
	applyGammaTransform("gammaRed", adjustment, colorAdjustment->_rgbTransform, 'r');
	applyGammaTransform("gammaGreen", adjustment, colorAdjustment->_rgbTransform, 'g');
	applyGammaTransform("gammaBlue", adjustment, colorAdjustment->_rgbTransform, 'b');
	applyTransform("backlightThreshold", adjustment, colorAdjustment->_rgbTransform, &RgbTransform::setBacklightThreshold);
	applyTransform("backlightColored", adjustment, colorAdjustment->_rgbTransform, &RgbTransform::setBacklightColored);
	applyTransform("brightness", adjustment, colorAdjustment->_rgbTransform, &RgbTransform::setBrightness);
	applyTransform("brightnessCompensation", adjustment, colorAdjustment->_rgbTransform, &RgbTransform::setBrightnessCompensation);
	applyTransform("saturationGain", adjustment, colorAdjustment->_okhsvTransform, &OkhsvTransform::setSaturationGain);
	applyTransform("brightnessGain", adjustment, colorAdjustment->_okhsvTransform, &OkhsvTransform::setBrightnessGain);
}

void JsonAPI::applyGammaTransform(const QString &transformName, const QJsonObject &adjustment, RgbTransform &rgbTransform, char channel)
{
	if (adjustment.contains(transformName)) {
		rgbTransform.setGamma(channel == 'r' ? adjustment[transformName].toDouble() : rgbTransform.getGammaR(),
							  channel == 'g' ? adjustment[transformName].toDouble() : rgbTransform.getGammaG(),
							  channel == 'b' ? adjustment[transformName].toDouble() : rgbTransform.getGammaB());
	}
}

template<typename T>
void JsonAPI::applyTransform(const QString &transformName, const QJsonObject &adjustment, T &transform, void (T::*setFunction)(bool))
{
	if (adjustment.contains(transformName)) {
		(transform.*setFunction)(adjustment[transformName].toBool());
	}
}

template<typename T>
void JsonAPI::applyTransform(const QString &transformName, const QJsonObject &adjustment, T &transform, void (T::*setFunction)(double))
{
	if (adjustment.contains(transformName)) {
		(transform.*setFunction)(adjustment[transformName].toDouble());
	}
}

template<typename T>
void JsonAPI::applyTransform(const QString &transformName, const QJsonObject &adjustment, T &transform, void (T::*setFunction)(uint8_t))
{
	if (adjustment.contains(transformName)) {
		(transform.*setFunction)(static_cast<uint8_t>(adjustment[transformName].toInt()));
	}
}

void JsonAPI::handleSourceSelectCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	if (message.contains("auto"))
	{
		API::setSourceAutoSelect(message["auto"].toBool(false));
	}
	else if (message.contains("priority"))
	{
		API::setVisiblePriority(message["priority"].toInt());
	}
	else
	{
		sendErrorReply("Priority request is invalid", cmd);
		return;
	}
	sendSuccessReply(cmd);
}

void JsonAPI::handleConfigCommand(const QJsonObject& message, const JsonApiCommand& cmd)
{
	switch (cmd.subCommand) {
	case SubCommand::GetSchema:
		handleSchemaGetCommand(message, cmd);
	break;

	case SubCommand::GetConfig:	
		handleConfigGetCommand(message, cmd);
	break;

	case SubCommand::GetConfigOld:
		sendSuccessDataReply(_hyperion->getQJsonConfig(), cmd);
	break;

	case SubCommand::SetConfig:
		handleConfigSetCommand(message, cmd);
	break;

	case SubCommand::RestoreConfig:
		handleConfigRestoreCommand(message, cmd);
	break;

	case SubCommand::Reload:
		Debug(_log, "Restarting due to RPC command");
		emit signalEvent(Event::Reload);
		sendSuccessReply(cmd);
	break;

	default:
	break;
	}
}

void JsonAPI::handleConfigSetCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	if (DBManager::isReadOnly())
	{
		sendErrorReply("Database Error", {"Hyperion is running in read-only mode","Configuration updates are not possible"}, cmd);
		return;
	}

	QJsonObject config = message["config"].toObject();
	if (config.isEmpty())
	{
		sendErrorReply("Update configuration failed", {"No configuration data provided!"}, cmd);
		return;
	}

	QStringList errorDetails;

	QMap<quint8, QJsonObject> instancesNewConfigs;

	const QJsonArray instances = config["instances"].toArray();
	if (!instances.isEmpty())
	{
		QList<quint8> configuredInstanceIds = _instanceManager->getInstanceIds();
		for (const auto &instance : instances)
		{
			QJsonObject instanceObject = instance.toObject();
			const QJsonValue idx = instanceObject["id"];
			if (idx.isDouble())
			{
				quint8 instanceId = static_cast<quint8>(idx.toInt());
				if (configuredInstanceIds.contains(instanceId))
				{
					instancesNewConfigs.insert(instanceId,instanceObject.value("settings").toObject());
				}
				else
				{
					errorDetails.append(QString("Given instance id '%1' does not exist. Configuration item will be ignored").arg(instanceId));
				}
			}
		}
	}

	const QJsonObject globalSettings = config["global"].toObject().value("settings").toObject();
	if (!globalSettings.isEmpty())
	{
		const QJsonObject instanceZeroConfig = instancesNewConfigs.value(0);
		instancesNewConfigs.insert(0, JsonUtils::mergeJsonObjects(instanceZeroConfig, globalSettings));
	}

	QMapIterator<quint8, QJsonObject> i (instancesNewConfigs);
	while (i.hasNext()) {
		i.next();

		quint8 idx = i.key();
		Hyperion* instance = HyperionIManager::getInstance()->getHyperionInstance(idx);

		QPair<bool, QStringList> isSaved = instance->saveSettings(i.value());
		errorDetails.append(isSaved.second);
	}

	if (!errorDetails.isEmpty())
	{
		sendErrorReply("Update configuration failed", errorDetails, cmd);
		return;
	}

	sendSuccessReply(cmd);
}

void JsonAPI::handleConfigGetCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	QJsonObject settings;
	QStringList errorDetails;

	QJsonObject filter = message["configFilter"].toObject();
	if (!filter.isEmpty())
	{
		QStringList globalFilterTypes;

		const QJsonObject globalConfig = filter["global"].toObject();
		if (!globalConfig.isEmpty())
		{
			const QJsonArray globalTypes = globalConfig["types"].toArray();
			for (const auto &type : globalTypes) {
				if (type.isString()) {
					globalFilterTypes.append(type.toString());
				}
			}
		}

		QList<quint8> instanceListFilter;
		QStringList instanceFilterTypes;

		const QJsonObject instances = filter["instances"].toObject();
		if (!instances.isEmpty())
		{
			QList<quint8> configuredInstanceIds = _instanceManager->getInstanceIds();
			const QJsonArray instanceIds = instances["ids"].toArray();
			for (const auto &idx : instanceIds) {
				if (idx.isDouble()) {
					quint8 instanceId = static_cast<quint8>(idx.toInt());
					if (configuredInstanceIds.contains(instanceId))
					{
						instanceListFilter.append(instanceId);
					}
					else
					{
						errorDetails.append(QString("Given instance number '%1' does not exist.").arg(instanceId));
					}
				}
			}

			const QJsonArray instanceTypes = instances["types"].toArray();
			for (const auto &type : instanceTypes) {
				if (type.isString()) {
					instanceFilterTypes.append(type.toString());
				}
			}
		}

		settings = JsonInfo::getConfiguration(instanceListFilter, instanceFilterTypes, globalFilterTypes);
	}
	else
	{
		//Get complete configuration
		settings = JsonInfo::getConfiguration();
	}

	if (!settings.empty())
	{
		sendSuccessDataReplyWithError(settings, cmd, errorDetails);
	}
	else
	{
		sendErrorReply("Generating full config failed", cmd);
	}
}

void JsonAPI::handleConfigRestoreCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	QJsonObject config = message["config"].toObject();
	if (API::isHyperionEnabled())
	{
		DBConfigManager configManager;
		QPair<bool, QStringList> result = configManager.updateConfiguration(config, false);
		if (result.first)
		{
			QString infoMsg {"Restarting after importing configuration successfully."};
			sendSuccessDataReply(infoMsg, cmd);
			Info(_log, "%s", QSTRING_CSTR(infoMsg));
			emit signalEvent(Event::Restart);
		}
		else
		{
			sendErrorReply("Restore configuration failed", result.second, cmd);
		}
	}
	else
	{
		sendErrorReply("Restoring configuration while Hyperion is disabled is not possible", cmd);
	}
}

void JsonAPI::handleSchemaGetCommand(const QJsonObject& /*message*/, const JsonApiCommand& cmd)
{
	// create result
	QJsonObject schemaJson;
	QJsonObject alldevices;
	QJsonObject properties;

	// make sure the resources are loaded (they may be left out after static linking)
	Q_INIT_RESOURCE(resource);

	// read the hyperion json schema from the resource
	QString schemaFile = SETTINGS_UI_SCHEMA_FILE;

	try
	{
		schemaJson = QJsonFactory::readSchema(schemaFile);
	}
	catch (const std::runtime_error &error)
	{
		throw std::runtime_error(error.what());
	}

	// collect all LED Devices
	properties = schemaJson["properties"].toObject();
	alldevices = LedDeviceWrapper::getLedDeviceSchemas();
	properties.insert("alldevices", alldevices);

	// Add infor about the type of setting elements
	QJsonObject settingTypes;
	QJsonArray globalSettingTypes;
	for (const QString &type : SettingsTable().getGlobalSettingTypes()) {
		globalSettingTypes.append(type);
	}
	settingTypes.insert("globalProperties", globalSettingTypes);

	QJsonArray instanceSettingTypes;
	for (const QString &type : SettingsTable().getInstanceSettingTypes()) {
		instanceSettingTypes.append(type);
	}
	settingTypes.insert("instanceProperties", instanceSettingTypes);
	properties.insert("propertiesTypes", settingTypes);

#if defined(ENABLE_EFFECTENGINE)
	// collect all available effect schemas
	QJsonArray schemaList;
	const std::list<EffectSchema>& effectsSchemas = _hyperion->getEffectSchemas();
	for (const EffectSchema& effectSchema : effectsSchemas)
	{
		QJsonObject schema;
		schema.insert("script", effectSchema.pyFile);
		schema.insert("schemaLocation", effectSchema.schemaFile);
		schema.insert("schemaContent", effectSchema.pySchema);
		if (effectSchema.pyFile.startsWith(':'))
		{
			schema.insert("type", "system");
		}
		else
		{
			schema.insert("type", "custom");
		}
		schemaList.append(schema);
	}
	properties.insert("effectSchemas", schemaList);
#endif

	schemaJson.insert("properties", properties);

	// send the result
	sendSuccessDataReply(schemaJson, cmd);
}

void JsonAPI::handleComponentStateCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	const QJsonObject &componentState = message["componentstate"].toObject();
	QString comp = componentState["component"].toString("invalid");
	bool compState = componentState["state"].toBool(true);
	QString replyMsg;

	if (API::setComponentState(comp, compState, replyMsg)) {
		sendSuccessReply(cmd);
	} else {
		sendErrorReply(replyMsg, cmd);
	}
}

void JsonAPI::handleLedColorsCommand(const QJsonObject& /*message*/, const JsonApiCommand& cmd)
{
	switch (cmd.subCommand) {
	case SubCommand::LedStreamStart:
		_jsonCB->subscribe( Subscription::LedColorsUpdate);
		// push once
		_hyperion->update();
		sendSuccessReply(cmd);
	break;

	case SubCommand::LedStreamStop:
		_jsonCB->unsubscribe( Subscription::LedColorsUpdate);
		sendSuccessReply(cmd);
	break;

	case SubCommand::ImageStreamStart:
		_jsonCB->subscribe(Subscription::ImageUpdate);
		sendSuccessReply(cmd);
	break;

	case SubCommand::ImageStreamStop:
		_jsonCB->unsubscribe(Subscription::ImageUpdate);
		sendSuccessReply(cmd);
	break;

	default:
	break;
	}
}

void JsonAPI::handleLoggingCommand(const QJsonObject& /*message*/, const JsonApiCommand& cmd)
{
	switch (cmd.subCommand) {
	case SubCommand::Start:
		_jsonCB->subscribe("logmsg-update");
		sendSuccessReply(cmd);
	break;

	case SubCommand::Stop:
		_jsonCB->unsubscribe("logmsg-update");
		sendSuccessReply(cmd);
	break;
	default:
	break;
	}
}

void JsonAPI::handleProcessingCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	API::setLedMappingType(ImageProcessor::mappingTypeToInt(message["mappingType"].toString("multicolor_mean")));
	sendSuccessReply(cmd);
}

void JsonAPI::handleVideoModeCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	API::setVideoMode(parse3DMode(message["videoMode"].toString("2D")));
	sendSuccessReply(cmd);
}

void JsonAPI::handleAuthorizeCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	switch (cmd.subCommand) {
	case SubCommand::TokenRequired:
		handleTokenRequired(cmd);
	break;
	case SubCommand::AdminRequired:
		handleAdminRequired(cmd);
	break;
	case SubCommand::NewPasswordRequired:
		handleNewPasswordRequired(cmd);
	break;
	case SubCommand::Logout:
		handleLogout(cmd);
	break;
	case SubCommand::NewPassword:
		handleNewPassword(message, cmd);
	break;
	case SubCommand::CreateToken:
		handleCreateToken(message, cmd);
	break;
	case SubCommand::RenameToken:
		handleRenameToken(message, cmd);
	break;
	case SubCommand::DeleteToken:
		handleDeleteToken(message, cmd);
	break;
	case SubCommand::RequestToken:
		handleRequestToken(message, cmd);
	break;
	case SubCommand::GetPendingTokenRequests:
		handleGetPendingTokenRequests(cmd);
	break;
	case SubCommand::AnswerRequest:
		handleAnswerRequest(message, cmd);
	break;
	case SubCommand::GetTokenList:
		handleGetTokenList(cmd);
	break;
	case SubCommand::Login:
		handleLogin(message, cmd);
	break;
	default:
	return;
	}
}

void JsonAPI::handleTokenRequired(const JsonApiCommand& cmd)
{
	bool isTokenRequired = !islocalConnection() || _authManager->isLocalAuthRequired();
	QJsonObject response { { "required", isTokenRequired} };
	sendSuccessDataReply(response, cmd);
}

void JsonAPI::handleAdminRequired(const JsonApiCommand& cmd)
{
	bool isAdminAuthRequired = true;
	QJsonObject response { { "adminRequired", isAdminAuthRequired} };
	sendSuccessDataReply(response, cmd);
}

void JsonAPI::handleNewPasswordRequired(const JsonApiCommand& cmd)
{
	QJsonObject response { { "newPasswordRequired", API::hasHyperionDefaultPw() } };
	sendSuccessDataReply(response, cmd);
}

void JsonAPI::handleLogout(const JsonApiCommand& cmd)
{
	API::logout();
	sendSuccessReply(cmd);
}

void JsonAPI::handleNewPassword(const QJsonObject &message, const JsonApiCommand& cmd)
{
	const QString password = message["password"].toString().trimmed();
	const QString newPassword = message["newPassword"].toString().trimmed();
	if (API::updateHyperionPassword(password, newPassword)) {
		sendSuccessReply(cmd);
	} else {
		sendErrorReply("Failed to update user password", cmd);
	}
}

void JsonAPI::handleCreateToken(const QJsonObject &message, const JsonApiCommand& cmd)
{
	const QString &comment = message["comment"].toString().trimmed();
	AuthManager::AuthDefinition def;
	const QString createTokenResult = API::createToken(comment, def);
	if (createTokenResult.isEmpty()) {
		QJsonObject newTok;
		newTok["comment"] = def.comment;
		newTok["id"] = def.id;
		newTok["token"] = def.token;

		sendSuccessDataReply(newTok, cmd);
	} else {
		sendErrorReply("Token creation failed", {createTokenResult}, cmd);
	}
}

void JsonAPI::handleRenameToken(const QJsonObject &message, const JsonApiCommand& cmd)
{
	const QString &identifier = message["id"].toString().trimmed();
	const QString &comment = message["comment"].toString().trimmed();
	const QString renameTokenResult = API::renameToken(identifier, comment);
	if (renameTokenResult.isEmpty()) {
		sendSuccessReply(cmd);
	} else {
		sendErrorReply("Token rename failed", {renameTokenResult}, cmd);
	}
}

void JsonAPI::handleDeleteToken(const QJsonObject &message, const JsonApiCommand& cmd)
{
	const QString &identifier = message["id"].toString().trimmed();
	const QString deleteTokenResult = API::deleteToken(identifier);
	if (deleteTokenResult.isEmpty()) {
		sendSuccessReply(cmd);
	} else {
		sendErrorReply("Token deletion failed", {deleteTokenResult}, cmd);
	}
}

void JsonAPI::handleRequestToken(const QJsonObject &message, const JsonApiCommand& cmd)
{
	const QString &identifier = message["id"].toString().trimmed();
	const QString &comment = message["comment"].toString().trimmed();
	const bool &acc = message["accept"].toBool(true);
	if (acc) {
		API::setNewTokenRequest(comment, identifier, cmd.tan);
	} else {
		API::cancelNewTokenRequest(comment, identifier);
		// client should wait for answer
	}
}

void JsonAPI::handleGetPendingTokenRequests(const JsonApiCommand& cmd)
{
	QVector<AuthManager::AuthDefinition> vec;
	if (API::getPendingTokenRequests(vec)) {
		QJsonArray pendingTokeRequests;
		for (const auto &entry : std::as_const(vec))
		{
			QJsonObject obj;
			obj["comment"] = entry.comment;
			obj["id"] = entry.id;
			obj["timeout"] = int(entry.timeoutTime);
			obj["tan"] = entry.tan;
			pendingTokeRequests.append(obj);
		}
		sendSuccessDataReply(pendingTokeRequests, cmd);
	}
}

void JsonAPI::handleAnswerRequest(const QJsonObject &message, const JsonApiCommand& cmd)
{
	const QString &identifier = message["id"].toString().trimmed();
	const bool &accept = message["accept"].toBool(false);
	if (API::handlePendingTokenRequest(identifier, accept)) {
		sendSuccessReply(cmd);
	} else {
		sendErrorReply("Unable to handle token acceptance or denial", cmd);
	}
}

void JsonAPI::handleGetTokenList(const JsonApiCommand& cmd)
{
	QVector<AuthManager::AuthDefinition> defVect;
	if (API::getTokenList(defVect))
	{
		QJsonArray tokenList;
		for (const auto &entry : std::as_const(defVect))
		{
			QJsonObject token;
			token["comment"] = entry.comment;
			token["id"] = entry.id;
			token["last_use"] = entry.lastUse;

			tokenList.append(token);
		}
		sendSuccessDataReply(tokenList, cmd);
	}
}

void JsonAPI::handleLogin(const QJsonObject &message, const JsonApiCommand& cmd)
{
	const QString &token = message["token"].toString().trimmed();
	if (!token.isEmpty())
	{
		// userToken is longer than app token
		if (token.size() > APP_TOKEN_LENGTH)
		{
			if (API::isUserTokenAuthorized(token)) {
				sendSuccessReply(cmd);
			} else {
				sendNoAuthorization(cmd);
			}

			return;
		}

		if (token.size() == APP_TOKEN_LENGTH)
		{
			if (API::isTokenAuthorized(token)) {
				sendSuccessReply(cmd);
			} else {
				sendNoAuthorization(cmd);
			}
		}
		return;
	}

	// password
	const QString &password = message["password"].toString().trimmed();
	if (password.size() >= MIN_PASSWORD_LENGTH)
	{
		QString userTokenRep;
		if (API::isUserAuthorized(password) && API::getUserToken(userTokenRep))
		{
			// Return the current valid Hyperion user token
			QJsonObject response { { "token", userTokenRep } };
			sendSuccessDataReply(response, cmd);
		}
		else
		{
			sendNoAuthorization(cmd);
		}
	}
	else
	{
		sendErrorReply(QString("Password is too short. Minimum length: %1 characters").arg(MIN_PASSWORD_LENGTH), cmd);
	}
}

void JsonAPI::issueNewPendingTokenRequest(const QString &identifier, const QString &comment)
{
	QJsonObject tokenRequest;
	tokenRequest["comment"] = comment;
	tokenRequest["id"] = identifier;
	tokenRequest["timeout"] = static_cast<int>(NEW_TOKEN_REQUEST_TIMEOUT.count());

	sendNewRequest(tokenRequest, "authorize-tokenRequest");
}

void JsonAPI::handleTokenResponse(bool success, const QString &token, const QString &comment, const QString &identifier, const int &tan)
{
	const QString cmd = "authorize-requestToken";
	QJsonObject result;
	result["token"] = token;
	result["comment"] = comment;
	result["id"] = identifier;

	if (success) {
		sendSuccessDataReply(result, cmd, tan);
	} else {
		sendErrorReply("Token request timeout or denied", {}, cmd, tan);
	}
}

void JsonAPI::handleInstanceCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	QString replyMsg;

	const quint8 inst = static_cast<quint8>(message["instance"].toInt());
	const QString &name = message["name"].toString();

	switch (cmd.subCommand) {
	case SubCommand::SwitchTo:
		if (handleInstanceSwitch(inst))
		{
			QJsonObject response { { "instance", inst } };
			sendSuccessDataReply(response, cmd);
		}
		else
		{
			sendErrorReply("Selected Hyperion instance is not running", cmd);
		}
	break;

	case SubCommand::StartInstance:
		//Only send update once
		weakConnect(this, &API::onStartInstanceResponse, [this, cmd] ()
		{
			sendSuccessReply(cmd);
		});

		if (!API::startInstance(inst, cmd.tan))
		{
			sendErrorReply("Cannot start Hyperion instance index " + QString::number(inst), cmd);
		}
	break;
	case SubCommand::StopInstance:
		// silent fail
		API::stopInstance(inst);
		sendSuccessReply(cmd);
	break;

	case SubCommand::DeleteInstance:
		if (API::deleteInstance(inst, replyMsg))
		{
			sendSuccessReply(cmd);
		}
		else
		{
			sendErrorReply(replyMsg, cmd);
		}
	break;

	case SubCommand::CreateInstance:
	case SubCommand::SaveName:
		// create and save name requires name
		if (name.isEmpty()) {
			sendErrorReply("Name string required for this command", cmd);
			return;
		}

		if (cmd.subCommand == SubCommand::CreateInstance) {
			replyMsg = API::createInstance(name);
		} else {
			replyMsg = API::setInstanceName(inst, name);
		}

		if (replyMsg.isEmpty()) {
			sendSuccessReply(cmd);
		} else {
			sendErrorReply(replyMsg, cmd);
		}
	break;
	default:
	break;
	}
}

void JsonAPI::handleLedDeviceCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	const QString &devType = message["ledDeviceType"].toString().trimmed();
	const LedDeviceRegistry& ledDevices = LedDeviceWrapper::getDeviceMap();

	if (ledDevices.count(devType) == 0) {
		sendErrorReply(QString("Unknown LED-Device type: %1").arg(devType), cmd);
		return;
	}

	QJsonObject config { { "type", devType } };
	LedDevice* ledDevice = LedDeviceFactory::construct(config);

	switch (cmd.subCommand) {
	case SubCommand::Discover:
		handleLedDeviceDiscover(*ledDevice, message, cmd);
	break;
	case SubCommand::GetProperties:
		handleLedDeviceGetProperties(*ledDevice, message, cmd);
	break;
	case SubCommand::Identify:
		handleLedDeviceIdentify(*ledDevice, message, cmd);
	break;
	case SubCommand::AddAuthorization:
		handleLedDeviceAddAuthorization(*ledDevice, message, cmd);
	break;
	default:
	break;
	}

	delete ledDevice;
}

void JsonAPI::handleLedDeviceDiscover(LedDevice& ledDevice, const QJsonObject& message, const JsonApiCommand& cmd)
{
	const QJsonObject &params = message["params"].toObject();
	const QJsonObject devicesDiscovered = ledDevice.discover(params);
	Debug(_log, "response: [%s]", QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact).constData() );
	sendSuccessDataReply(devicesDiscovered, cmd);
}

void JsonAPI::handleLedDeviceGetProperties(LedDevice& ledDevice, const QJsonObject& message, const JsonApiCommand& cmd)
{
	const QJsonObject &params = message["params"].toObject();
	const QJsonObject deviceProperties = ledDevice.getProperties(params);
	Debug(_log, "response: [%s]", QJsonDocument(deviceProperties).toJson(QJsonDocument::Compact).constData() );
	sendSuccessDataReply(deviceProperties, cmd);
}

void JsonAPI::handleLedDeviceIdentify(LedDevice& ledDevice, const QJsonObject& message, const JsonApiCommand& cmd)
{
	const QJsonObject &params = message["params"].toObject();
	ledDevice.identify(params);
	sendSuccessReply(cmd);
}

void JsonAPI::handleLedDeviceAddAuthorization(LedDevice& ledDevice, const QJsonObject& message, const JsonApiCommand& cmd)
{
	const QJsonObject& params = message["params"].toObject();
	const QJsonObject response = ledDevice.addAuthorization(params);
	sendSuccessDataReply(response, cmd);
}

void JsonAPI::handleInputSourceCommand(const QJsonObject& message, const JsonApiCommand& cmd) {
	const QString& sourceType = message["sourceType"].toString().trimmed();
	const QStringList sourceTypes {"screen", "video", "audio"};

	if (!sourceTypes.contains(sourceType)) {
		sendErrorReply(QString("Unknown input source type: %1").arg(sourceType), cmd);
		return;
	}

	if (cmd.subCommand == SubCommand::Discover) {

		const QJsonObject& params = message["params"].toObject();
		QJsonObject inputSourcesDiscovered = JsonInfo().discoverSources(sourceType, params);

		DebugIf(verbose, _log, "response: [%s]", QJsonDocument(inputSourcesDiscovered).toJson(QJsonDocument::Compact).constData());

		sendSuccessDataReply(inputSourcesDiscovered, cmd);
	}
}

void JsonAPI::handleServiceCommand(const QJsonObject &message, const JsonApiCommand& cmd)
{
	if (cmd.subCommand == SubCommand::Discover)
	{
		QByteArray serviceType;
		const QString type = message["serviceType"].toString().trimmed();
#ifdef ENABLE_MDNS
		QString discoveryMethod("mDNS");
		serviceType = MdnsServiceRegister::getServiceType(type);
#else
		QString discoveryMethod("ssdp");
#endif
		if (!serviceType.isEmpty())
		{
			QJsonArray serviceList;
#ifdef ENABLE_MDNS
			QMetaObject::invokeMethod(MdnsBrowser::getInstance().data(), "browseForServiceType",
									  Qt::QueuedConnection, Q_ARG(QByteArray, serviceType));

			serviceList = MdnsBrowser::getInstance().data()->getServicesDiscoveredJson(serviceType, MdnsServiceRegister::getServiceNameFilter(type), DEFAULT_DISCOVER_TIMEOUT);
#endif
			QJsonObject servicesDiscovered;
			QJsonObject servicesOfType;

			servicesOfType.insert(type, serviceList);

			servicesDiscovered.insert("discoveryMethod", discoveryMethod);
			servicesDiscovered.insert("services", servicesOfType);

			sendSuccessDataReply(servicesDiscovered, cmd);
		}
		else
		{
			sendErrorReply(QString("Discovery of service type [%1] via %2 not supported").arg(type, discoveryMethod), cmd);
		}
	}
}

void JsonAPI::handleSystemCommand(const QJsonObject& /*message*/, const JsonApiCommand& cmd)
{
	switch (cmd.subCommand) {
	case SubCommand::Suspend:
		emit signalEvent(Event::Suspend);
	break;
	case SubCommand::Resume:
		emit signalEvent(Event::Resume);
	break;
	case SubCommand::Restart:
		emit signalEvent(Event::Restart);
	break;
	case SubCommand::ToggleSuspend:
		emit signalEvent(Event::ToggleSuspend);
	break;
	case SubCommand::Idle:
		emit signalEvent(Event::Idle);
	break;
	case SubCommand::ToggleIdle:
		emit signalEvent(Event::ToggleIdle);
	break;
	default:
	return;
	}
	sendSuccessReply(cmd);
}

QJsonObject JsonAPI::getBasicCommandReply(bool success, const QString &command, int tan, InstanceCmd::Type isInstanceCmd) const
{
	QJsonObject reply;
	reply["success"] = success;
	reply["command"] = command;
	reply["tan"] = tan;

	if (isInstanceCmd == InstanceCmd::Yes || ( isInstanceCmd == InstanceCmd::Multi && !_noListener))
	{
		reply["instance"] = _hyperion->getInstanceIndex();
	}
	return reply;
}

void JsonAPI::sendSuccessReply(const JsonApiCommand& cmd)
{
	sendSuccessReply(cmd.toString(), cmd.tan, cmd.isInstanceCmd);
}

void JsonAPI::sendSuccessReply(const QString &command, int tan, InstanceCmd::Type isInstanceCmd)
{
	emit callbackMessage(getBasicCommandReply(true, command, tan , isInstanceCmd));
}

void JsonAPI::sendSuccessDataReply(const QJsonValue &infoData, const JsonApiCommand& cmd)
{
	sendSuccessDataReplyWithError(infoData, cmd.toString(), cmd.tan, {}, cmd.isInstanceCmd);
}

void JsonAPI::sendSuccessDataReply(const QJsonValue &infoData, const QString &command, int tan, InstanceCmd::Type isInstanceCmd)
{
	sendSuccessDataReplyWithError(infoData, command, tan, {}, isInstanceCmd);
}

void JsonAPI::sendSuccessDataReplyWithError(const QJsonValue &infoData, const JsonApiCommand& cmd, const QStringList& errorDetails)
{
	sendSuccessDataReplyWithError(infoData, cmd.toString(), cmd.tan, errorDetails, cmd.isInstanceCmd);
}

void JsonAPI::sendSuccessDataReplyWithError(const QJsonValue &infoData, const QString &command, int tan, const QStringList& errorDetails, InstanceCmd::Type isInstanceCmd)
{
	QJsonObject reply {getBasicCommandReply(true, command, tan , isInstanceCmd)};
	reply["info"] = infoData;

	if (!errorDetails.isEmpty())
	{
		QJsonArray errorsArray;
		for (const QString& errorString : errorDetails)
		{
			QJsonObject errorObject;
			errorObject["description"] = errorString;
			errorsArray.append(errorObject);
		}
		reply["errorData"] = errorsArray;
	}

	emit callbackMessage(reply);
}

void JsonAPI::sendErrorReply(const QString &error, const JsonApiCommand& cmd)
{
	sendErrorReply(error, {}, cmd.toString(), cmd.tan, cmd.isInstanceCmd);
}

void JsonAPI::sendErrorReply(const QString &error, const QStringList& errorDetails, const JsonApiCommand& cmd)
{
	sendErrorReply(error, errorDetails, cmd.toString(), cmd.tan, cmd.isInstanceCmd);
}

void JsonAPI::sendErrorReply(const QString &error, const QStringList& errorDetails, const QString &command, int tan, InstanceCmd::Type isInstanceCmd)
{
	QJsonObject reply {getBasicCommandReply(false, command, tan , isInstanceCmd)};
	reply["error"] = error;
	if (!errorDetails.isEmpty())
	{
		QJsonArray errorsArray;
		for (const QString& errorString : errorDetails)
		{
			QJsonObject errorObject;
			errorObject["description"] = errorString;
			errorsArray.append(errorObject);
		}
		reply["errorData"] = errorsArray;
	}

	emit callbackMessage(reply);
}

void JsonAPI::sendNewRequest(const QJsonValue &infoData, const JsonApiCommand& cmd)
{
	sendSuccessDataReplyWithError(infoData, cmd.toString(), cmd.isInstanceCmd);
}

void JsonAPI::sendNewRequest(const QJsonValue &infoData, const QString &command, InstanceCmd::Type isInstanceCmd)
{
	QJsonObject request;
	request["command"] = command;

	if (isInstanceCmd != InstanceCmd::No)
	{
		request["instance"] = _hyperion->getInstanceIndex();
	}

	request["info"] = infoData;

	emit callbackMessage(request);
}

void JsonAPI::sendNoAuthorization(const JsonApiCommand& cmd)
{
	sendErrorReply(NO_AUTHORIZATION, cmd);
}

void JsonAPI::handleInstanceStateChange(InstanceState state, quint8 instance, const QString& /*name */)
{
	switch (state)
	{
	case InstanceState::H_ON_STOP:
		if (_hyperion->getInstanceIndex() == instance)
		{
			handleInstanceSwitch();
		}
	break;

	case InstanceState::H_STARTED:
	case InstanceState::H_STOPPED:
	case InstanceState::H_CREATED:
	case InstanceState::H_DELETED:
	break;
	}
}

void JsonAPI::stopDataConnections()
{
	_jsonCB->resetSubscriptions();
	LoggerManager::getInstance()->disconnect();
}

QString JsonAPI::findCommand (const QString& jsonString)
{
	QString commandValue {"unknown"};

	// Define a regular expression pattern to match the value associated with the key "command"
	static QRegularExpression regex("\"command\"\\s*:\\s*\"([^\"]+)\"");
	QRegularExpressionMatch match = regex.match(jsonString);

	if (match.hasMatch()) {
		commandValue = match.captured(1);
	}
	return commandValue;
}

int JsonAPI::findTan (const QString& jsonString)
{
	int tanValue {0};
	static QRegularExpression regex("\"tan\"\\s*:\\s*(\\d+)");
	QRegularExpressionMatch match = regex.match(jsonString);

	if (match.hasMatch()) {
		QString valueStr = match.captured(1);
		tanValue = valueStr.toInt();
	}
	return tanValue;
}