#include "mqttitem.h"

#include <QJsonArray>
#include <QJsonObject>
#include <QJsonDocument>
#include <QtMqtt/QMqttClient>

#include "mqttclient.h"
#include "programmode.h"

MqttItem::MqttItem(QString name, uint8_t value, QObject *parent)
	: Item(0, name, value, parent),
	  topic_(""),
	  valueKey_("state"),
	  valueOn_("ON"),
	  valueOff_("OFF")
{
	hashId();
	std::shared_ptr<MqttClient> workClient = client.lock();
	assert(workClient || programMode == PROGRAM_MODE_UI_ONLY);

	if(workClient)
		connect(workClient->getClient().get(), &QMqttClient::stateChanged, this, &MqttItem::onClientStateChanged);
}

MqttItem::~MqttItem()
{
	qDebug()<<__func__;
	std::shared_ptr<MqttClient> workClient = client.lock();
	if(!workClient)
		return;

	if(subscription)
	{
		disconnect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
		workClient->unsubscribe(subscription);
	}
	
	if(devicesSubscription)
	{
		disconnect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived);
		workClient->unsubscribe(devicesSubscription);
	}
}

void MqttItem::onClientStateChanged(QMqttClient::ClientState state)
{
	if(state == QMqttClient::Connected)
	{
		refreshSubscription();
		// Subscribe to bridge/devices to get exposes
		std::shared_ptr<MqttClient> workClient = client.lock();
		if(workClient && !exposeLoaded_)
		{
			devicesSubscription = workClient->subscribe(workClient->getBaseTopic() + "/bridge/devices");
			connect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived);
		}
	}
}

void MqttItem::refreshSubscription()
{
	std::shared_ptr<MqttClient> workClient = client.lock();
	if(!workClient || topic_.isEmpty())
		return;

	if(workClient->getClient()->state() != QMqttClient::Connected)
		return;

	if(subscription)
	{
		disconnect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
		workClient->unsubscribe(subscription);
	}

	subscription = workClient->subscribe(workClient->getBaseTopic() + "/" + getTopic());
	connect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
}

void MqttItem::onDevicesMessageReceived(const QMqttMessage& message)
{
	if(exposeLoaded_)
		return;
	
	QJsonDocument doc = QJsonDocument::fromJson(message.payload());
	if(!doc.isArray())
		return;
	
	QJsonArray devices = doc.array();
	for(const QJsonValue& deviceValue : devices)
	{
		if(!deviceValue.isObject())
			continue;
		
		QJsonObject device = deviceValue.toObject();
		QString ieeeAddr = device["ieee_address"].toString();
		
		// Check if this device matches our topic (friendly_name)
		QString friendlyName = device["friendly_name"].toString();
		if(friendlyName == topic_)
		{
			loadExposeFromDevice(device);
			exposeLoaded_ = true;
			Q_EMIT exposeLoaded();
			
			// Unsubscribe from devices topic since we found our device
			std::shared_ptr<MqttClient> workClient = client.lock();
			if(workClient && devicesSubscription)
			{
				disconnect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived);
				workClient->unsubscribe(devicesSubscription);
				devicesSubscription = nullptr;
			}
			break;
		}
	}
}

void MqttItem::loadExposeFromDevice(const QJsonObject& device)
{
	// Get definition - may be null for unsupported devices
	QJsonObject definition = device["definition"].toObject();
	if(definition.isEmpty())
	{
		qWarning() << "MqttItem" << topic_ << "device has no definition (unsupported)";
		return;
	}
	
	// Get exposes from definition
	QJsonArray exposes = definition["exposes"].toArray();
	if(exposes.isEmpty())
	{
		qWarning() << "MqttItem" << topic_ << "device has no exposes";
		return;
	}
	
	for(const QJsonValue& exposeValue : exposes)
	{
		if(!exposeValue.isObject())
			continue;
		
		QJsonObject expose = exposeValue.toObject();
		QString property = expose["property"].toString();
		
		// Check if this expose matches our valueKey
		if(property == valueKey_)
		{
			setFromExpose(expose);
			qDebug() << "MqttItem" << topic_ << "detected type" << expose["type"].toString() << "for property" << valueKey_;
			return;
		}
		
		// Check if it's a composite type with features
		if(expose["type"].toString() == "composite" || expose["type"].toString() == "light")
		{
			QJsonArray features = expose["features"].toArray();
			for(const QJsonValue& featureValue : features)
			{
				if(!featureValue.isObject())
					continue;
				
				QJsonObject feature = featureValue.toObject();
				if(feature["property"].toString() == valueKey_)
				{
					setFromExpose(feature);
					qDebug() << "MqttItem" << topic_ << "detected type" << feature["type"].toString() << "for property" << valueKey_;
					return;
				}
			}
		}
	}
	
	qWarning() << "MqttItem" << topic_ << "could not find expose for property" << valueKey_;
}

void MqttItem::onMessageReceived(const QMqttMessage& message)
{
	QJsonDocument doc = QJsonDocument::fromJson(message.payload());
	if(doc.isObject())
	{
		QJsonObject obj = doc.object();
		if(obj.contains(getValueKey()))
		{
			QJsonValue value = obj[getValueKey()];
			ItemUpdateRequest req = createValueUpdateRequest(ITEM_UPDATE_BACKEND);
			req.changes.value = true;

			if(getValueType() == ITEM_VALUE_UINT)
			{
				// Numeric value
				req.payload.setValueData(value.toInt(0));
			}
			else if(getValueType() == ITEM_VALUE_ENUM)
			{
				// Enum value - find index
				QString strValue = value.toString();
				int index = valueNameToIndex(strValue);
				if(index >= 0)
					req.payload.setValueData(index);
			}
			else
			{
				// Binary value
				QString strValue = value.toString();
				if(strValue == getValueOn() || strValue == "ON" || strValue == "true")
					req.payload.setValueData(true);
				else
					req.payload.setValueData(false);
			}
			requestUpdate(req);
		}
	}
}

void MqttItem::hashId()
{
	QString hashString = topic_ + "/" + valueKey_;
	itemId_ = qHash(hashString.toLatin1());
}

void MqttItem::setTopic(const QString& topic)
{
	topic_ = topic;
	hashId();
	refreshSubscription();
}

void MqttItem::setValueKey(const QString& valueKey)
{
	valueKey_ = valueKey;
	hashId();
}

void MqttItem::setValueOn(const QString& valueOn)
{
	valueOn_ = valueOn;
}

void MqttItem::setValueOff(const QString& valueOff)
{
	valueOff_ = valueOff;
}

void MqttItem::setValueMin(int min)
{
	valueMin_ = min;
}

void MqttItem::setValueMax(int max)
{
	valueMax_ = max;
}

void MqttItem::setValueStep(int step)
{
	valueStep_ = step;
}

void MqttItem::setValueType(item_value_type_t type)
{
	type_ = type;
}

void MqttItem::setFromExpose(const QJsonObject& expose)
{
	QString type = expose["type"].toString();
	QString property = expose["property"].toString();
	
	setValueKey(property);
	
	if(type == "binary")
	{
		type_ = ITEM_VALUE_BOOL;
		setValueOn(expose["value_on"].toString("ON"));
		setValueOff(expose["value_off"].toString("OFF"));
	}
	else if(type == "numeric")
	{
		type_ = ITEM_VALUE_UINT;
		setValueMin(expose["value_min"].toInt(0));
		setValueMax(expose["value_max"].toInt(255));
		setValueStep(expose["value_step"].toInt(1));
	}
	else if(type == "enum")
	{
		type_ = ITEM_VALUE_ENUM;
		QJsonArray values = expose["values"].toArray();
		std::vector<QString> valueNames;
		for(const QJsonValue& v : values)
			valueNames.push_back(v.toString());
		setValueNames(valueNames);
	}
	
	hashId();
}

void MqttItem::triggerExposeLookup()
{
	if(exposeLoaded_)
		return;
	
	std::shared_ptr<MqttClient> workClient = client.lock();
	if(!workClient)
		return;
	
	// Reset expose loaded flag to allow re-detection
	exposeLoaded_ = false;
	
	// Subscribe to bridge/devices
	if(devicesSubscription)
	{
		disconnect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived);
		workClient->unsubscribe(devicesSubscription);
		devicesSubscription = nullptr;
	}
	
	devicesSubscription = workClient->subscribe(workClient->getBaseTopic() + "/bridge/devices");
	connect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived);
}

QString MqttItem::getTopic() const
{
	return topic_;
}

QString MqttItem::getValueKey() const
{
	return valueKey_;
}

QString MqttItem::getValueOn() const
{
	return valueOn_;
}

QString MqttItem::getValueOff() const
{
	return valueOff_;
}

int MqttItem::getValueMin() const
{
	return valueMin_;
}

int MqttItem::getValueMax() const
{
	return valueMax_;
}

int MqttItem::getValueStep() const
{
	return valueStep_;
}

bool MqttItem::getExposeLoaded() const
{
	return exposeLoaded_;
}

void MqttItem::store(QJsonObject& json)
{
	Item::store(json);
	json["Type"] = "Mqtt";
	json["Topic"] = topic_;
	json["ValueKey"] = valueKey_;
	json["ValueOn"] = valueOn_;
	json["ValueOff"] = valueOff_;
	json["ValueMin"] = valueMin_;
	json["ValueMax"] = valueMax_;
	json["ValueStep"] = valueStep_;
}

void MqttItem::load(const QJsonObject& json, const bool preserve)
{
	Item::load(json, preserve);
	topic_ = json["Topic"].toString();
	valueKey_ = json["ValueKey"].toString("state");
	valueOn_ = json["ValueOn"].toString("ON");
	valueOff_ = json["ValueOff"].toString("OFF");
	valueMin_ = json["ValueMin"].toInt(0);
	valueMax_ = json["ValueMax"].toInt(255);
	valueStep_ = json["ValueStep"].toInt(1);
	exposeLoaded_ = json["ExposeLoaded"].toBool(false);
	hashId();
	refreshSubscription();
}

void MqttItem::enactValue(uint8_t value)
{
	std::shared_ptr<MqttClient> workClient = client.lock();
	if(!workClient || topic_.isEmpty())
		return;

	QString fullTopic = workClient->getBaseTopic() + "/" + topic_ + "/set";
	QJsonObject payload;
	
	if(getValueType() == ITEM_VALUE_UINT)
	{
		payload[valueKey_] = static_cast<int>(value);
	}
	else if(getValueType() == ITEM_VALUE_ENUM)
	{
		payload[valueKey_] = indexToValueName(value);
	}
	else
	{
		payload[valueKey_] = value ? valueOn_ : valueOff_;
	}

	QJsonDocument doc(payload);
	QByteArray data = doc.toJson(QJsonDocument::Compact);
	
	qDebug() << "MqttItem publishing to" << fullTopic << ":" << data;
	workClient->getClient()->publish(fullTopic, data);
}
