/*
 * Copyright (C) 2014-2026 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <QByteArray>
#include <QDateTime>
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QStringBuilder>

#include "src/datovka_shared/io/filesystem.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/utility/date_time.h"

bool createDirRecursive(const QString &dirPath)
{
	if (Q_UNLIKELY(dirPath.isEmpty())) {
		return false;
	}

	QDir dir(dirPath);
	if (dir.exists()) {
		return true;
	}

	return dir.mkpath(".");
}

bool copyDirRecursively(const QString &fromDirPath, const QString &toPath,
    bool deleteOnError)
{
	if (Q_UNLIKELY(fromDirPath.isEmpty() || toPath.isEmpty())) {
		return false;
	}

	{
		const QFileInfo fromInfo(fromDirPath);
		if (Q_UNLIKELY(((!fromInfo.isDir()) || (!fromInfo.isReadable())))) {
			/* Not a readable directory. */
			return false;
		}

		const QFileInfo toInfo(toPath);
		if (Q_UNLIKELY((!toInfo.isDir()) || (!toInfo.isWritable()))) {
			/* Not a writeable directory. */
			return false;
		}

		const QString canonicalFrom = fromInfo.canonicalPath();
		const QString canonicalTo = toInfo.canonicalPath();
		if (Q_UNLIKELY(canonicalFrom.isEmpty() || canonicalTo.isEmpty())) {
			Q_ASSERT(0);
			return false;
		}

		if (Q_UNLIKELY(toPath.startsWith(fromDirPath))) {
			/* Cannot copy from into its own subdirectory. */
			return false;
		}
	}

	const QString fromDirName = QFileInfo(fromDirPath).fileName();

	QStringList toBecreatedDirs;
	{
		/* Create target root subdirectory if not present. */
		const QString tgtDir = toPath + QStringLiteral("/")
		    + fromDirName;
		const QFileInfo info(tgtDir);
		if (info.exists() && ((!info.isDir()) || (!info.isWritable()))) {
			logErrorNL("The path '%s' is not a writeable directory.",
			    tgtDir.toUtf8().constData());
			return false;
		} else if (!info.exists()) {
			toBecreatedDirs.append(tgtDir);
		}
	}
	{
		/* Search for subdirectories to be created. */
		QDirIterator dirIt(fromDirPath, QStringList(),
		    QDir::AllDirs | QDir::NoDotAndDotDot,
		    QDirIterator::Subdirectories);
		while (dirIt.hasNext()) {
			dirIt.next();

			const QString srcDirRelative =
			    QDir(fromDirPath).relativeFilePath(dirIt.filePath());
			const QString tgtDir = toPath + QStringLiteral("/") +
			    fromDirName + QStringLiteral("/") + srcDirRelative;
			{
				const QFileInfo info(tgtDir);
				if (info.exists() && ((!info.isDir()) || (!info.isWritable()))) {
					logErrorNL("The path '%s' is not a writeable directory.",
					    tgtDir.toUtf8().constData());
					return false;
				} else if (!info.exists()) {
					toBecreatedDirs.append(tgtDir);
				}
			}
		}
	}

	/* Create missing subdirectories. */
	for (const QString &toPath : toBecreatedDirs) {
		if (Q_UNLIKELY(!createDirRecursive(toPath))) {
			logErrorNL("Cannot create directory '%s'.",
			    toPath.toUtf8().constData());
			goto fail;
		}
	}

	{
		/* Copy all files. */
		QDirIterator dirIt(fromDirPath, QStringList(),
		    QDir::Files | QDir::NoDotAndDotDot,
		    QDirIterator::Subdirectories);
		while (dirIt.hasNext()) {
			dirIt.next();

			const QString srcFileRelative =
			    QDir(fromDirPath).relativeFilePath(dirIt.filePath());
			const QString tgtFile = toPath + QStringLiteral("/") +
			    fromDirName + QStringLiteral("/") + srcFileRelative;

			QFile::remove(tgtFile); /* Just remove target if already exists. */
			if (Q_UNLIKELY(!QFile::copy(dirIt.filePath(), tgtFile))) {
				logErrorNL("Cannot copy file '%s' to '%s'.",
				    dirIt.filePath().toUtf8().constData(),
				    tgtFile.toUtf8().constData());
				goto fail;
			}
		}
	}

	return true;

fail:
	if (deleteOnError) {
		for (const QString &toPath : toBecreatedDirs) {
			QDir(toPath).removeRecursively();
		}
		/* It doesn't delete newly copied files. */
	}
	return false;
}

QString nonconflictingFileName(QString filePath)
{
	if (Q_UNLIKELY(filePath.isEmpty())) {
		return QString();
	}

	if (QFile::exists(filePath)) {

		int fileCnt = 0;
		QFileInfo fi(filePath);

		const QString baseName(fi.baseName());
		const QString path(fi.path());
		const QString suffix(fi.completeSuffix());

		do {
			++fileCnt;
			QString newName(
			    baseName + "_" + QString::number(fileCnt));
			filePath =
			    path + QDir::separator() + newName + "." + suffix;
		} while (QFile::exists(filePath));
	}

	return filePath;
}

enum WriteFileState writeFile(const QString &filePath, const QByteArray &data,
    bool deleteOnError)
{
	if (Q_UNLIKELY(filePath.isEmpty())) {
		Q_ASSERT(0);
		return WF_ERROR;
	}

	QFile fout(filePath);
	if (Q_UNLIKELY(!fout.open(QIODevice::WriteOnly))) {
		return WF_CANNOT_CREATE;
	}

	qint64 written = fout.write(data);
	bool flushed = fout.flush();
	fout.close();

	if (Q_UNLIKELY((written != data.size()) || !flushed)) {
		if (deleteOnError) {
			QFile::remove(filePath);
		}
		return WF_CANNOT_WRITE_WHOLE;
	}

	return WF_SUCCESS;
}

QString auxConfFilePath(const QString &filePath)
{
	return filePath % QStringLiteral(".aux.")
	    % QDateTime::currentDateTimeUtc().toString(
	        Utility::dateTimeFileSuffixFormat);
}
