// SPDX-License-Identifier: GPL-2.0
#include "uploadDiveLogsDE.h"
#include <QDir>
#include <QDebug>
#include <zip.h>
#include <errno.h>
#include "core/display.h"
#include "core/errorhelper.h"
#include "core/qthelper.h"
#include "core/dive.h"
#include "core/membuffer.h"
#include "core/divesite.h"
#include "core/cloudstorage.h"
#ifndef SUBSURFACE_MOBILE
#include "core/selection.h"
#endif // SUBSURFACE_MOBILE
#include "core/settings/qPrefCloudStorage.h"


uploadDiveLogsDE *uploadDiveLogsDE::instance()
{
	static uploadDiveLogsDE *self = new uploadDiveLogsDE;

	return self;
}


uploadDiveLogsDE::uploadDiveLogsDE():
	reply(NULL),
	multipart(NULL)
{
	timeout.setSingleShot(true);
}


void uploadDiveLogsDE::doUpload(bool selected, const QString &userid, const QString &password)
{
	QString err;


	/* generate a temporary filename and create/open that file with zip_open */
	QString filename(QDir::tempPath() + "/divelogsde-upload.dld");

	// delete file if it exist

	QFile f(filename);
	if (f.open(QIODevice::ReadOnly)) {
		f.close();
		f.remove();
	}

	// Make zip file, with all dives, in divelogs.de format
	if (!prepareDives(filename, selected)) {
		emit uploadFinish(false, tr("Cannot prepare dives, none selected?"));
		timeout.stop();
		return;
	}

	// And upload it
	uploadDives(filename, userid, password);
}


bool uploadDiveLogsDE::prepareDives(const QString &tempfile, bool selected)
{
	static const char errPrefix[] = "divelog.de-upload:";
#ifndef SUBSURFACE_MOBILE
	if (!amount_selected) {
		report_error(tr("No dives were selected").toUtf8());
		return false;
	}
#endif // SUBSURFACE_MOBILE

	xsltStylesheetPtr xslt = NULL;
	struct zip *zip;

	emit uploadStatus(tr("building zip file to upload"));

	xslt = get_stylesheet("divelogs-export.xslt");
	if (!xslt) {
		qDebug() << errPrefix << "missing stylesheet";
		report_error(tr("Stylesheet to export to divelogs.de is not found").toUtf8());
		return false;
	}

	// Prepare zip file
	int error_code;
	zip = zip_open(QFile::encodeName(QDir::toNativeSeparators(tempfile)), ZIP_CREATE, &error_code);
	if (!zip) {
		char buffer[1024];
		zip_error_to_str(buffer, sizeof buffer, error_code, errno);
		report_error(tr("Failed to create zip file for upload: %s").toUtf8(), buffer);
		return false;
	}

	/* walk the dive list in chronological order */
	int i;
	struct dive *dive;
	for_each_dive (i, dive) {
		char filename[PATH_MAX];
		int streamsize;
		const char *membuf;
		xmlDoc *transformed;
		struct zip_source *s;
		struct membuffer mb = {};

		/*
		 * Get the i'th dive in XML format so we can process it.
		 * We need to save to a file before we can reload it back into memory...
		 */
		if (selected && !dive->selected)
			continue;

		/* make sure the buffer is empty and add the dive */
		mb.len = 0;

		struct dive_site *ds = dive->dive_site;

		if (ds) {
			put_format(&mb, "<divelog><divesites><site uuid='%8x' name='", ds->uuid);
			put_quoted(&mb, ds->name, 1, 0);
			put_format(&mb, "'");
			put_location(&mb, &ds->location, " gps='", "'");
			put_format(&mb, ">\n");
			if (ds->taxonomy.nr) {
				for (int j = 0; j < ds->taxonomy.nr; j++) {
					struct taxonomy *t = &ds->taxonomy.category[j];
					if (t->category != TC_NONE && t->category == prefs.geocoding.category[j] && t->value) {
						put_format(&mb, "  <geo cat='%d'", t->category);
						put_format(&mb, " origin='%d' value='", t->origin);
						put_quoted(&mb, t->value, 1, 0);
						put_format(&mb, "'/>\n");
					}
				}
			}
			put_format(&mb, "</site>\n</divesites>\n");
		}

		save_one_dive_to_mb(&mb, dive, false);

		if (ds) {
			put_format(&mb, "</divelog>\n");
		}
		membuf = mb_cstring(&mb);
		streamsize = strlen(membuf);
		/*
		 * Parse the memory buffer into XML document and
		 * transform it to divelogs.de format, finally dumping
		 * the XML into a character buffer.
		 */
		xmlDoc *doc = xmlReadMemory(membuf, streamsize, "divelog", NULL, 0);
		if (!doc) {
			qWarning() << errPrefix << "could not parse back into memory the XML file we've just created!";
			report_error(tr("internal error").toUtf8());
			zip_close(zip);
			QFile::remove(tempfile);
			xsltFreeStylesheet(xslt);
			return false;
		}
		free_buffer(&mb);

		transformed = xsltApplyStylesheet(xslt, doc, NULL);
		if (!transformed) {
			qWarning() << errPrefix << "XSLT transform failed for dive: " << i;
			report_error(tr("Conversion of dive %1 to divelogs.de format failed").arg(i).toUtf8());
			continue;
		}
		xmlDocDumpMemory(transformed, (xmlChar **)&membuf, &streamsize);
		xmlFreeDoc(doc);
		xmlFreeDoc(transformed);

		/*
		 * Save the XML document into a zip file.
		 */
		snprintf(filename, PATH_MAX, "%d.xml", i + 1);
		s = zip_source_buffer(zip, membuf, streamsize, 1);
		if (s) {
			int64_t ret = zip_add(zip, filename, s);
			if (ret == -1)
				qDebug() << errPrefix << "failed to include dive:" << i;
		}
	}
	xsltFreeStylesheet(xslt);
	if (zip_close(zip)) {
		int ze, se;
#if LIBZIP_VERSION_MAJOR >= 1
		zip_error_t *error = zip_get_error(zip);
		ze = zip_error_code_zip(error);
		se = zip_error_code_system(error);
#else
		zip_error_get(zip, &ze, &se);
#endif
		report_error(qPrintable(tr("error writing zip file: %s zip error %d system error %d - %s")),
			     qPrintable(QDir::toNativeSeparators(tempfile)), ze, se, zip_strerror(zip));
		return false;
	}
	return true;
}


void uploadDiveLogsDE::uploadDives(const QString &filename, const QString &userid, const QString &password)
{
	QHttpPart part1, part2, part3;
	static QNetworkRequest request;
	QString args;

	// Check if there is an earlier request open
	if (reply != NULL && reply->isOpen()) {
		reply->abort();
		delete reply;
		reply = NULL;
	}
	if (multipart != NULL) {
		delete multipart;
		multipart = NULL;
	}
	multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType);

	emit uploadStatus(tr("Uploading dives"));

	// prepare header with filename (of all dives) and pointer to file
	args = "form-data; name=\"userfile\"; filename=\"" + filename + "\"";
	part1.setRawHeader("Content-Disposition", args.toLatin1());
	QFile *f = new QFile(filename);
	if (!f->open(QIODevice::ReadOnly)) {
		qDebug() << "ERROR opening zip file: " << filename;
		return;
	}
	part1.setBodyDevice(f);
	multipart->append(part1);

	// Add userid
	args = "form-data; name=\"user\"";
	part2.setRawHeader("Content-Disposition", args.toLatin1());
	part2.setBody(qPrefCloudStorage::divelogde_user().toUtf8());
	multipart->append(part2);

	// Add password
	args = "form-data; name=\"pass\"";
	part3.setRawHeader("Content-Disposition", args.toLatin1());
	part3.setBody(qPrefCloudStorage::divelogde_pass().toUtf8());
	multipart->append(part3);

	// Prepare network request
	request.setUrl(QUrl("https://divelogs.de/DivelogsDirectImport.php"));
	request.setRawHeader("Accept", "text/xml, application/xml");
	request.setRawHeader("User-Agent", getUserAgent().toUtf8());

	// Execute async.
	reply = manager()->post(request, multipart);

	// connect signals from upload process
	connect(reply, SIGNAL(finished()), this, SLOT(uploadFinishedSlot()));
	connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this,
		SLOT(uploadErrorSlot(QNetworkReply::NetworkError)));
	connect(reply, SIGNAL(uploadProgress(qint64, qint64)), this,
		SLOT(updateProgressSlot(qint64, qint64)));
	connect(&timeout, SIGNAL(timeout()), this, SLOT(uploadTimeoutSlot()));

	timeout.start(30000); // 30s
}


void uploadDiveLogsDE::updateProgressSlot(qint64 current, qint64 total)
{
	if (!reply)
		return;
	
	if (total <= 0 || current <= 0)
		return;

	// Calculate percentage as 0.x (values between 0.0 and 1.0)
	// And signal whoever wants to know
	qreal percentage = (float)current / (float)total;
	emit uploadProgress(percentage, 1.0);

	// reset the timer: 30 seconds after we last got any data
	timeout.start();
}


void uploadDiveLogsDE::uploadFinishedSlot()
{
	QString err;

	if (!reply)
		return;

	// check what the server sent us: it might contain
	// an error condition, such as a failed login
	QByteArray xmlData = reply->readAll();
	reply->deleteLater();
	reply = NULL;
	char *resp = xmlData.data();
	if (resp) {
		char *parsed = strstr(resp, "<Login>");
		if (parsed) {
			if (strstr(resp, "<Login>succeeded</Login>")) {
				if (strstr(resp, "<FileCopy>failed</FileCopy>")) {
					report_error(tr("Upload failed").toUtf8());
					return;
				}
				timeout.stop();
				err = tr("Upload successful");
				emit uploadFinish(true, err);
				return;
			}
			timeout.stop();
			err = tr("Login failed");
			report_error(err.toUtf8());
			emit uploadFinish(false, err);
			return;
		}
		timeout.stop();
		err = tr("Cannot parse response");
		report_error(tr("Cannot parse response").toUtf8());
		emit uploadFinish(false, err);
	}
}


void uploadDiveLogsDE::uploadTimeoutSlot()
{
	timeout.stop();
	if (reply) {
		reply->deleteLater();
		reply = NULL;
	}
	QString err(tr("divelogs.de not responding"));
	report_error(err.toUtf8());
	emit uploadFinish(false, err);
}


void uploadDiveLogsDE::uploadErrorSlot(QNetworkReply::NetworkError error)
{
	timeout.stop();
	if (reply) {
		reply->deleteLater();
		reply = NULL;
	}
	QString err(tr("network error %1").arg(error));
	report_error(err.toUtf8());
	emit uploadFinish(false, err);
}
