#include "config.hpp"

#include "appearance.hpp"
#include "background.hpp"
#include "bar.hpp"
#include "clipboard.hpp"
#include "colors.hpp"
#include "dashboard.hpp"
#include "display.hpp"
#include "dock.hpp"
#include "general.hpp"
#include "launcher.hpp"
#include "llm.hpp"
#include "lock.hpp"
#include "migration.hpp"
#include "notifs.hpp"
#include "osd.hpp"
#include "screenshot.hpp"
#include "services.hpp"
#include "sidebar.hpp"
#include "utilities.hpp"

#include <QFile>
#include <QSaveFile>
#include <QJsonDocument>
#include <QStandardPaths>
#include <QDir>
#include <QDebug>
#include <QFutureWatcher>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <qcontainerfwd.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qthreadpool.h>

namespace ZShell::config {

Config* Config::s_instance = nullptr;

Config::Config(QObject* parent)
	: ConfigObject(parent)
	, m_appearance(new Appearance(this))
	, m_background(new Background(this))
	, m_bar(new Bar(this))
	, m_clipboard(new Clipboard(this))
	, m_colors(new Colors(this))
	, m_dashboard(new Dashboard(this))
	, m_display(new Display(this))
	, m_dock(new Dock(this))
	, m_general(new General(this))
	, m_launcher(new Launcher(this))
	, m_llm(new Llm(this))
	, m_lock(new Lock(this))
	, m_notifs(new Notifs(this))
	, m_osd(new Osd(this))
	, m_screenshot(new Screenshot(this))
	, m_services(new Services(this))
	, m_sidebar(new Sidebar(this))
	, m_utilities(new Utilities(this)) {
	s_instance = this;
	connect(this, &ConfigObject::propertiesChanged, this, &Config::scheduleSave);

	m_saveTimer.setSingleShot(true);
	m_saveTimer.setInterval(300);
	connect(&m_saveTimer, &QTimer::timeout, this, &Config::flushAsync);

	m_reloadTimer.setSingleShot(true);
	m_reloadTimer.setInterval(50);
	connect(&m_reloadTimer, &QTimer::timeout, this, &Config::reloadAsync);

	connectAutoSave(this);

	connect(
		&m_watcher,
		&QFileSystemWatcher::directoryChanged,
		this,
		&Config::onWatcherEvent);

	connect(
		&m_watcher,
		&QFileSystemWatcher::fileChanged,
		this,
		&Config::onWatcherEvent);

	loadSync();
	updateWatch();

	m_firstLoadDone = true;
}

Config* Config::instance() {
	return s_instance;
}

Config* Config::create(QQmlEngine*, QJSEngine*) {
	if (!s_instance) s_instance = new Config();
	return s_instance;
}

QString Config::filePath() const {
	const QString dir =
		QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
		QStringLiteral("/zshell");
	QDir().mkpath(dir);
	return dir + QStringLiteral("/config.json");
}

void Config::loadSync() {
	m_loading = true;

	QFile f(filePath());
	QJsonObject before;

	bool existed = false;
	bool migrated = false;

	if (f.open(QIODevice::ReadOnly)) {
		const auto doc = QJsonDocument::fromJson(f.readAll());
		if (doc.isObject()) {
			const QJsonObject raw = doc.object();
			before = ConfigMigrations::apply(raw);
			migrated = before != raw;
			existed = true;
		} else {
			qInfo() << "Config: existing config at" << filePath()
					<< "is empty or not a valid JSON object - using defaults";
		}
	} else {
		qInfo() << "Config: no existing config at" << filePath()
				<< "- using defaults";
	}

	loadFromJson(QJsonValue(before));
	m_loading = false;

	const auto after = toJson().toObject();
	if (!existed || migrated || after != before) saveNow();
}

void Config::updateWatch() {
	const QFileInfo info(filePath());

	const QString dir = info.absolutePath();
	const QString file = info.absoluteFilePath();

	if (!m_watcher.directories().contains(dir)) m_watcher.addPath(dir);
	if (info.exists() && !m_watcher.files().contains(file))
		m_watcher.addPath(file);
}

void Config::onWatcherEvent(const QString&) {
	updateWatch();

	const QFileInfo info(filePath());
	if (!info.exists()) return;

	QFile file(info.absoluteFilePath());
	if (!file.open(QIODevice::ReadOnly)) return;

	const QByteArray hash =
		QCryptographicHash::hash(file.readAll(), QCryptographicHash::Sha256);

	{
		QMutexLocker lock(&m_writeMutex);

		if (hash == m_lastWriteHash) return;
	}

	m_reloadPending = true;

	if (!m_loading) m_reloadTimer.start();
}

void Config::loadAsync() {
	m_loading = true;

	auto future = QtConcurrent::run([this]() {
		QFile f(filePath());

		if (f.open(QIODevice::ReadOnly)) {
			const auto doc = QJsonDocument::fromJson(f.readAll());
			const bool valid = doc.isObject();
			const QJsonObject raw = valid ? doc.object() : QJsonObject();
			const QJsonObject before = ConfigMigrations::apply(raw);
			const bool migrated = valid && before != raw;

			QMetaObject::invokeMethod(
				this,
				[this, valid, migrated, before]() {
					loadFromJson(QJsonValue(before));
					m_loading = false;

					const auto after = toJson().toObject();
					if (!valid || migrated || after != before) saveNow();

					if (m_reloadPending) m_reloadTimer.start();
				},
				Qt::QueuedConnection);
		} else {
			qInfo() << "Config: failed to reload from" << filePath()
					<< "- using in-memory values";

			QMetaObject::invokeMethod(
				this,
				[this]() {
					m_loading = false;

					if (m_reloadPending) m_reloadTimer.start();
				},
				Qt::QueuedConnection);
		}
	});

	m_loadFuture = future;
}

void Config::load() {
	if (!m_firstLoadDone) {
		loadSync();
	} else {
		loadAsync();
	}
}

void Config::reloadAsync() {
	if (m_loading) return;

	m_reloadPending = false;
	loadAsync();
}

void Config::scheduleSave() {
	if (m_loading) return;
	m_saveTimer.start();
}

void Config::flushAsync() {
	const QByteArray data =
		QJsonDocument(toJson().toObject()).toJson(QJsonDocument::Indented);

	const QByteArray hash =
		QCryptographicHash::hash(data, QCryptographicHash::Sha256);

	QThreadPool::globalInstance()->start([this, data, hash]() {
		const bool success = writeAtomically(data);

		QMetaObject::invokeMethod(
			this,
			[this, success, hash]() {
				if (!success) return;

				QMutexLocker lock(&m_writeMutex);
				m_lastWriteHash = hash;
			},
			Qt::QueuedConnection);
	});
}

void Config::saveNow() {
	m_saveTimer.stop();
	flushAsync();
}

void Config::connectAutoSave(ConfigNode* node) {
	connect(node, &ConfigNode::propertiesChanged, this, [this] {
		scheduleSave();
	});

	for (auto* child : node->childNodes())
		connectAutoSave(child);
}

bool Config::writeAtomically(const QByteArray& data) {
	QSaveFile f(filePath());

	if (!f.open(QIODevice::WriteOnly)) {
		qWarning() << "Config: failed to open for atomic write:"
				   << f.errorString();
		return false;
	}

	if (f.write(data) != data.size()) {
		qWarning() << "Config: failed to write:" << f.errorString();
		return false;
	}

	if (!f.commit()) {
		qWarning() << "Config: commit failed:" << f.errorString();
		return false;
	}

	return true;
}

}; // namespace ZShell::config
