#include "nightlightmanager.hpp"

#include <qfileinfo.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qlogging.h>
#include <qtenvironmentvariables.h>

namespace ZShell::services {

namespace {
constexpr const char* NL_EVENT = "zshell-nightlight";
constexpr int RECONNECT_MS = 2000;
} // namespace

NightlightManager::NightlightManager(QObject* parent) : QObject(parent) {
	connect(&m_timer, &QTimer::timeout, this, &NightlightManager::apply);
	connect(&m_manualTimer, &QTimer::timeout, this, [this] {
		m_manualToggle = false;
		emit manualToggleChanged();
		apply();
	});
	connect(&m_startCooldown, &QTimer::timeout, this, [this] {
		m_startAllowed = true;
		apply();
	});
	connect(&m_reconnectTimer, &QTimer::timeout, this, [this] {
		if (!m_useNativeNightlight) return;

		connectEventSocket();
		m_pluginLoadAttempted = false;
		m_pluginLoadedByUs = false;
		bootstrapState();
	});

	m_startCooldown.start(2000);
	m_manualTimer.setSingleShot(true);
	m_reconnectTimer.setSingleShot(true);
	m_timer.start(60000);

	m_process.setStandardInputFile(QProcess::nullDevice());
	m_process.setStandardOutputFile(QProcess::nullDevice());
}

int NightlightManager::startTime() const {
	return m_startTime;
}

int NightlightManager::endTime() const {
	return m_endTime;
}

bool NightlightManager::manualToggle() const {
	return m_manualToggle;
}

bool NightlightManager::enabled() const {
	return m_enabled;
}

bool NightlightManager::activeAuto() const {
	return m_activeAuto;
}

int NightlightManager::temp() const {
	return m_temp;
}

double NightlightManager::fadeDuration() const {
	return m_fadeDuration;
}

bool NightlightManager::animating() const {
	return m_animating;
}

int NightlightManager::lastTemp() const {
	return m_lastTemp;
}

int NightlightManager::currentTemp() const {
	return m_currentTemp;
}

void NightlightManager::setActiveAuto(bool activeAuto) {
	if (activeAuto == m_activeAuto) return;

	m_activeAuto = activeAuto;
	emit activeAutoChanged();
}

void NightlightManager::setManualToggle(bool toggle) {
	if (toggle == m_manualToggle) return;

	m_manualToggle = toggle;
	emit manualToggleChanged();

	m_manualTimer.start(60 * 60 * 1000);
}

void NightlightManager::setEndTime(const int& time) {
	if (time == m_endTime) return;

	m_endTime = time;
	emit endTimeChanged();
	apply();
}

void NightlightManager::setStartTime(const int& time) {
	if (time == m_startTime) return;

	m_startTime = time;
	emit startTimeChanged();
	apply();
}

void NightlightManager::setTemp(const int& temp) {
	if (temp == m_temp) return;

	m_temp = temp;
	emit tempChanged();
	apply();
}

void NightlightManager::setFadeDuration(double duration) {
	if (duration < 0.0) duration = 0.0;

	m_fadeDuration = duration;
	emit fadeDurationChanged();
}

void NightlightManager::toggle() {
	if (m_enabled) {
		end();
	} else {
		start();
	}
}

void NightlightManager::start() {
	if (m_enabled && m_initialized) return;

	m_initialized = true;
	m_enabled = true;

	emit enabledChanged();

	if (m_useNativeNightlight) {
		m_process.setProgram("hyprctl");
		m_process.setArguments(
			{"dispatch",
			 QString("hl.plugin.zshell.nlSet(%1, %2)")
				 .arg(m_temp)
				 .arg(m_fadeDuration)});
		m_process.startDetached();
	} else {
		m_process.setProgram("hyprctl");
		m_process.setArguments(
			{"hyprsunset", "temperature", QString::number(m_temp)});
		m_process.startDetached();
	}
}

void NightlightManager::end() {
	if (!m_enabled && m_initialized) return;

	m_initialized = true;
	m_enabled = false;

	emit enabledChanged();

	if (m_useNativeNightlight) {
		m_process.setProgram("hyprctl");
		m_process.setArguments(
			{"dispatch",
			 QString("hl.plugin.zshell.nlDisable(%1)").arg(m_fadeDuration)});
		m_process.startDetached();
	} else {
		m_process.setProgram("hyprctl");
		m_process.setArguments({"hyprsunset", "identity"});
		m_process.startDetached();
	}
}

void NightlightManager::apply() {
	if (m_manualToggle || !m_activeAuto || !m_startAllowed) return;

	const auto current = QTime::currentTime();
	const auto currentMin = current.hour() * 60 + current.minute();
	bool isDarkTime = false;

	if (m_startTime <= m_endTime) {
		isDarkTime = (currentMin >= m_startTime && currentMin < m_endTime);
	} else {
		isDarkTime = (currentMin >= m_startTime || currentMin < m_endTime);
	}

	if (isDarkTime) {
		start();
	} else {
		end();
	}
}

bool NightlightManager::useNativeNightlight() const {
	return m_useNativeNightlight;
}

QString NightlightManager::pluginPath() const {
	return m_pluginPath;
}

void NightlightManager::setPluginPath(const QString& path) {
	if (path == m_pluginPath) return;

	m_pluginPath = path;
	emit pluginPathChanged();
}

void NightlightManager::setUseNativeNightlight(bool native) {
	if (native == m_useNativeNightlight) return;

	m_useNativeNightlight = native;
	emit useNativeNightlightChanged();

	if (native) {
		disconnect(&m_eventSocket, nullptr, this, nullptr);
		connect(
			&m_eventSocket,
			&QLocalSocket::readyRead,
			this,
			&NightlightManager::handleEventLines);
		connect(
			&m_eventSocket,
			&QLocalSocket::errorOccurred,
			this,
			&NightlightManager::scheduleReconnect);
		connectEventSocket();
		bootstrapState();
	} else {
		m_reconnectTimer.stop();
		disconnect(&m_eventSocket, nullptr, this, nullptr);
		if (m_eventSocket.state() != QLocalSocket::UnconnectedState)
			m_eventSocket.disconnectFromServer();
		m_socketBuffer.clear();
		m_animating = false;
		emit animatingChanged();
		m_pluginLoadAttempted = false;
		m_pluginLoadedByUs = false;
	}
}

QString NightlightManager::hyprlandDir() const {
	const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE");
	if (his.isEmpty()) return QString();

	auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
	auto dir = runtimeDir + "/hypr/" + his;
	if (!QFileInfo(dir).isDir()) dir = "/tmp/hypr/" + his;

	if (!QFileInfo(dir).isDir()) return QString();

	return dir;
}

QString NightlightManager::eventSocketPath() const {
	const auto dir = hyprlandDir();
	return dir.isEmpty() ? QString() : dir + "/.socket2.sock";
}

QString NightlightManager::resolvePluginPath() const {
	if (!m_pluginPath.isEmpty()) return m_pluginPath;

	const QString name = QStringLiteral("libzshell-nightlight.so");
	const QStringList dirs = {
		"/usr/lib/ZShell/plugins",
		"/usr/local/lib/ZShell/plugins",
	};
	for (const auto& dir : dirs) {
		const QString candidate = dir + "/" + name;
		if (QFileInfo::exists(candidate)) return candidate;
	}

	const QString home = qEnvironmentVariable("HOME");
	if (!home.isEmpty()) {
		const QString candidate = home + "/.local/lib/ZShell/plugins/" + name;
		if (QFileInfo::exists(candidate)) return candidate;
	}

	return QString();
}

void NightlightManager::tryLoadPlugin() {
	if (m_pluginLoadAttempted) return;

	const auto path = resolvePluginPath();
	if (path.isEmpty()) {
		qWarning() << "NightlightManager: plugin not found; searched "
					  "pluginPath and <prefix>/usr/lib/ZShell/plugins";
		m_pluginLoadAttempted = true;
		return;
	}

	m_pluginLoadAttempted = true;
	m_pluginLoadedByUs = true;
	qInfo() << "NightlightManager: plugin not loaded, loading" << path;
	QProcess::startDetached("hyprctl", {"plugin", "load", path});

	QTimer::singleShot(1500, this, [this] { bootstrapState(); });
}

void NightlightManager::bootstrapState() {
	if (!m_useNativeNightlight) return;

	const auto dir = hyprlandDir();
	if (dir.isEmpty()) return;

	auto* sock = new QLocalSocket(this);
	auto* watchdog = new QTimer(this);
	watchdog->setSingleShot(true);

	const auto finish = [sock, watchdog]() {
		watchdog->stop();
		sock->abort();
		sock->deleteLater();
	};

	connect(sock, &QLocalSocket::connected, sock, [sock] {
		sock->write("repl return hl.plugin.zshell.nlState()");
		sock->flush();
	});
	connect(sock, &QLocalSocket::readyRead, sock, [this, sock, finish]() {
		const auto raw = sock->readAll();
		const auto doc = QJsonDocument::fromJson(raw);
		finish();
		if (!doc.isObject()) {
			if (m_pluginLoadAttempted)
				qWarning() << "NightlightManager: plugin load failed, state "
						   << "query still answered:" << raw;

			tryLoadPlugin();
			return;
		}

		if (m_pluginLoadedByUs) {
			qInfo() << "NightlightManager: plugin was not loaded, manager "
					<< "loaded it; state:" << raw;
			m_pluginLoadedByUs = false;
		} else {
			qInfo() << "NightlightManager: plugin already loaded, state:"
					<< raw;
		}

		const auto obj = doc.object();
		updateState(
			obj.value("enabled").toBool(),
			obj.value("animating").toBool(),
			obj.value("last").toInt(),
			obj.value("current").toInt());
	});
	connect(sock, &QLocalSocket::errorOccurred, sock, finish);
	connect(watchdog, &QTimer::timeout, sock, finish);
	watchdog->start(2500);

	sock->connectToServer(dir + "/.socket.sock");
}

void NightlightManager::connectEventSocket() {
	if (!m_useNativeNightlight) return;

	if (m_eventSocket.state() != QLocalSocket::UnconnectedState) return;

	const auto path = eventSocketPath();
	if (path.isEmpty()) {
		scheduleReconnect();
		return;
	}

	m_socketBuffer.clear();
	m_eventSocket.connectToServer(path, QLocalSocket::ReadOnly);
}

void NightlightManager::scheduleReconnect() {
	if (!m_useNativeNightlight) return;

	m_reconnectTimer.start(RECONNECT_MS);
}

void NightlightManager::handleEventLines() {
	m_socketBuffer.append(m_eventSocket.readAll());

	qsizetype idx = 0;
	while ((idx = m_socketBuffer.indexOf('\n')) >= 0) {
		const auto line = m_socketBuffer.left(idx);
		m_socketBuffer.remove(0, idx + 1);
		handleEventLine(line);
	}
}

void NightlightManager::handleEventLine(const QByteArray& line) {
	const qsizetype sep = line.indexOf(">>");
	if (sep <= 0) return;

	if (line.left(sep) != NL_EVENT) return;

	const auto fields = line.mid(sep + 2).split(',');
	if (fields.size() < 4) return;

	bool ok = false;
	const auto enabled = fields.at(0).toInt(&ok);
	if (!ok) return;
	const auto animating = fields.at(1).toInt(&ok);
	if (!ok) return;
	const auto last = fields.at(2).toInt(&ok);
	if (!ok) return;
	const auto current = fields.at(3).toInt(&ok);
	if (!ok) return;

	updateState(enabled == 1, animating == 1, last, current);
}

void NightlightManager::updateState(
	bool enabled, bool animating, int last, int current) {
	if (enabled != m_enabled) {
		m_enabled = enabled;
		emit enabledChanged();
	}
	if (animating != m_animating) {
		m_animating = animating;
		emit animatingChanged();
	}
	if (last != m_lastTemp) {
		m_lastTemp = last;
		emit lastTempChanged();
	}
	if (current != m_currentTemp) {
		m_currentTemp = current;
		emit currentTempChanged();
	}
}

}; // namespace ZShell::services
