#include "filetool.hpp"

#include <QFile>
#include <QFileInfo>
#include <QJsonArray>

namespace ZShell::llm {

namespace {

QJsonObject makeOutput(const QString& text) {
	QJsonObject obj;
	obj[QStringLiteral("output")] = text;
	return obj;
}

QJsonObject makeError(const QString& message) {
	QJsonObject obj;
	obj[QStringLiteral("error")] = message;
	return obj;
}

} // namespace

FileReadTool::FileReadTool(QObject* parent) : LlmTool(parent) {}

QString FileReadTool::name() const {
	return QStringLiteral("readfile");
}

QString FileReadTool::description() const {
	return QStringLiteral(
		"Read a text file from the local filesystem and return part of "
		"its content. At most a limited number of characters is returned "
		"per call; use offset and limit to page through larger files. "
		"Binary files are not supported. Full output of webfetch calls "
		"is stored under /tmp/zshell-llm/webfetch/ and can be read this "
		"way. This tool is read-only.");
}

QJsonObject FileReadTool::parameters() const {
	QJsonObject path;
	path[QStringLiteral("type")] = QStringLiteral("string");
	path[QStringLiteral("description")] =
		QStringLiteral("Path of the file to read");

	QJsonObject offset;
	offset[QStringLiteral("type")] = QStringLiteral("integer");
	offset[QStringLiteral("minimum")] = 0;
	offset[QStringLiteral("description")] =
		QStringLiteral("Byte offset to start reading from. Defaults to 0.");

	QJsonObject limit;
	limit[QStringLiteral("type")] = QStringLiteral("integer");
	limit[QStringLiteral("minimum")] = 1;
	limit[QStringLiteral("description")] =
		QStringLiteral("Maximum number of characters to return. Defaults "
					   "to a value that fits the model's context window.");

	QJsonObject properties;
	properties[QStringLiteral("path")] = path;
	properties[QStringLiteral("offset")] = offset;
	properties[QStringLiteral("limit")] = limit;

	QJsonObject schema;
	schema[QStringLiteral("type")] = QStringLiteral("object");
	schema[QStringLiteral("properties")] = properties;
	QJsonArray required;
	required.append(QStringLiteral("path"));
	schema[QStringLiteral("required")] = required;
	return schema;
}

void FileReadTool::execute(
	const QString& toolCallId,
	const QJsonObject& args,
	std::function<void(const QJsonObject&)> done) {
	Q_UNUSED(toolCallId);
	const QString path = args[QStringLiteral("path")].toString().trimmed();
	if (path.isEmpty()) {
		done(makeError(QStringLiteral("Missing required argument 'path'")));
		return;
	}
	const int offset = qMax(0, args[QStringLiteral("offset")].toInt(0));
	const int budget = inlineBudgetChars();
	const int limit =
		qBound(1, args[QStringLiteral("limit")].toInt(budget), budget);

	QFile file(path);
	if (!file.open(QIODevice::ReadOnly)) {
		const QFileInfo info(path);
		if (info.exists() && info.isDir()) {
			done(makeError(QStringLiteral("Path is a directory, not a "
										   "file: %1")
							   .arg(path)));
		} else {
			done(makeError(QStringLiteral("Cannot open file: %1").arg(path)));
		}
		return;
	}
	const qint64 fileSize = file.size();
	if (fileSize == 0) {
		file.close();
		done(makeOutput(QString()));
		return;
	}
	if (offset >= fileSize) {
		file.close();
		done(makeError(QStringLiteral("Offset %1 is beyond the end of the "
									   "file (%2 bytes)")
						   .arg(offset)
						   .arg(fileSize)));
		return;
	}
	if (!file.seek(offset)) {
		file.close();
		done(makeError(QStringLiteral("Cannot seek in file: %1").arg(path)));
		return;
	}
	// UTF-8 uses at most 4 bytes per character, so this many bytes always
	// cover `limit` characters.
	const qint64 wanted = qint64(limit) * 4 + 16;
	const QByteArray chunk = file.read(qMin(wanted, fileSize - offset));
	file.close();

	if (chunk.contains('\0')) {
		done(makeError(QStringLiteral("Binary files are not supported: %1")
						   .arg(path)));
		return;
	}
	const QString content = QString::fromUtf8(chunk);
	if (content.size() <= limit) {
		done(makeOutput(content));
		return;
	}
	const QString head = content.left(limit);
	const int nextOffset = offset + head.toUtf8().size();
	const QString output =
		head +
		QStringLiteral(
			"\n\n[... truncated: showing %1 characters (bytes %2-%3 of a "
			"%4 byte file). Continue with offset=%5.]")
			.arg(limit)
			.arg(offset)
			.arg(nextOffset)
			.arg(fileSize)
			.arg(nextOffset);
	done(makeOutput(output));
}

} // namespace ZShell::llm
