#include "tag.h"

#include <services/databaseservice.h>
#include <utils/misc.h>

#include <QDebug>
#include <QHash>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QStringBuilder>
#include <algorithm>

#include "note.h"
#include "notefolder.h"
#include "notesubfolder.h"
#include "services/settingsservice.h"

namespace {
struct MergeTagData {
    int id = 0;
    QString name;
    int parentId = 0;
    int priority = 0;
    QString color;
    QString darkColor;
};

struct MergeNoteTagLinkData {
    int sourceTagId = 0;
    QString noteFileName;
    QString noteSubFolderPath;
};

bool fetchMergeTags(QSqlDatabase &db, QHash<int, MergeTagData> &tagDataById) {
    QSqlQuery query(db);
    query.prepare(
        QStringLiteral("SELECT id, name, parent_id, priority, color, dark_color FROM tag"));

    if (!query.exec()) {
        qWarning() << __func__ << ":" << query.lastError();
        return false;
    }

    while (query.next()) {
        MergeTagData tagData;
        tagData.id = query.value(QStringLiteral("id")).toInt();
        tagData.name = query.value(QStringLiteral("name")).toString();
        tagData.parentId = query.value(QStringLiteral("parent_id")).toInt();
        tagData.priority = query.value(QStringLiteral("priority")).toInt();
        tagData.color = query.value(QStringLiteral("color")).toString();
        tagData.darkColor = query.value(QStringLiteral("dark_color")).toString();
        tagDataById.insert(tagData.id, tagData);
    }

    return true;
}

bool fetchMergeNoteTagLinks(QSqlDatabase &db, QVector<MergeNoteTagLinkData> &noteTagLinks) {
    QSqlQuery query(db);
    query.prepare(
        QStringLiteral("SELECT tag_id, note_file_name, note_sub_folder_path FROM noteTagLink"));

    if (!query.exec()) {
        qWarning() << __func__ << ":" << query.lastError();
        return false;
    }

    while (query.next()) {
        MergeNoteTagLinkData noteTagLinkData;
        noteTagLinkData.sourceTagId = query.value(QStringLiteral("tag_id")).toInt();
        noteTagLinkData.noteFileName = query.value(QStringLiteral("note_file_name")).toString();
        noteTagLinkData.noteSubFolderPath =
            query.value(QStringLiteral("note_sub_folder_path")).toString();
        noteTagLinks.append(noteTagLinkData);
    }

    return true;
}

int fetchMergedTagId(QSqlDatabase &db, const QString &name, int parentId) {
    QSqlQuery query(db);
    query.prepare(
        QStringLiteral("SELECT id FROM tag WHERE name = :name AND parent_id = :parentId"));
    query.bindValue(QStringLiteral(":name"), name);
    query.bindValue(QStringLiteral(":parentId"), parentId);

    if (!query.exec()) {
        qWarning() << __func__ << ":" << query.lastError();
        return -1;
    }

    return query.first() ? query.value(QStringLiteral("id")).toInt() : 0;
}

int insertMergedTag(QSqlDatabase &db, const MergeTagData &tagData, int targetParentId) {
    QSqlQuery query(db);
    query.prepare(
        QStringLiteral("INSERT INTO tag (name, priority, parent_id, color, dark_color) "
                       "VALUES (:name, :priority, :parentId, :color, :darkColor)"));
    query.bindValue(QStringLiteral(":name"), tagData.name);
    query.bindValue(QStringLiteral(":priority"), tagData.priority);
    query.bindValue(QStringLiteral(":parentId"), targetParentId);
    query.bindValue(QStringLiteral(":color"), tagData.color);
    query.bindValue(QStringLiteral(":darkColor"), tagData.darkColor);

    if (!query.exec()) {
        qWarning() << __func__ << ":" << query.lastError();
        return -1;
    }

    return query.lastInsertId().toInt();
}

int ensureMergedTagId(QSqlDatabase &targetDb, const QHash<int, MergeTagData> &sourceTagDataById,
                      QHash<int, int> &tagIdMap, int sourceTagId) {
    if (tagIdMap.contains(sourceTagId)) {
        return tagIdMap.value(sourceTagId);
    }

    const auto it = sourceTagDataById.constFind(sourceTagId);
    if (it == sourceTagDataById.constEnd()) {
        qWarning() << __func__ << ": missing tag for id" << sourceTagId;
        return -1;
    }

    const MergeTagData &tagData = it.value();
    int targetParentId = 0;

    if (tagData.parentId > 0) {
        targetParentId = ensureMergedTagId(targetDb, sourceTagDataById, tagIdMap, tagData.parentId);
        if (targetParentId < 0) {
            return -1;
        }
    }

    int targetTagId = fetchMergedTagId(targetDb, tagData.name, targetParentId);
    if (targetTagId < 0) {
        return -1;
    }

    if (targetTagId == 0) {
        targetTagId = insertMergedTag(targetDb, tagData, targetParentId);
        if (targetTagId < 0) {
            return -1;
        }
    }

    tagIdMap.insert(sourceTagId, targetTagId);
    return targetTagId;
}
}    // namespace

Tag::Tag() noexcept : _parentId(0), _priority(0) {}

int Tag::getParentId() const { return _parentId; }

void Tag::setParentId(const int id) { _parentId = id; }

QColor Tag::getColor() const { return _color; }

void Tag::setColor(const QColor &color) { _color = color; }

int Tag::getPriority() const { return _priority; }

void Tag::setPriority(const int value) { _priority = value; }

Tag Tag::fetch(const int id) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    Tag tag;

    query.prepare(QStringLiteral("SELECT * FROM tag WHERE id = :id"));
    query.bindValue(QStringLiteral(":id"), id);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else if (query.first()) {
        tag.fillFromQuery(query);
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return tag;
}

/**
 * Fetches a tag by name
 *
 * @param name
 * @param startsWith if true the tag only has to start with name
 * @return
 */
Tag Tag::fetchByName(QString name, const bool startsWith) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    Tag tag;
    const QString sql = QStringLiteral("SELECT * FROM tag WHERE name ") %
                        QString(startsWith ? QStringLiteral("LIKE") : QStringLiteral("=")) %
                        QStringLiteral(" :name ORDER BY name");
    query.prepare(sql);

    if (startsWith) {
        name += QStringLiteral("%");
    }

    query.bindValue(QStringLiteral(":name"), name);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else if (query.first()) {
        tag.fillFromQuery(query);
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return tag;
}

Tag Tag::fetchByName(const QString &name, const int parentId) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    Tag tag;

    query.prepare(
        QStringLiteral("SELECT * FROM tag WHERE name = :name AND "
                       "parent_id = :parent_id"));
    query.bindValue(QStringLiteral(":name"), name.toLower());
    query.bindValue(QStringLiteral(":parent_id"), parentId);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else if (query.first()) {
        tag.fillFromQuery(query);
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return tag;
}

int Tag::countAll() {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    query.prepare(QStringLiteral("SELECT COUNT(*) AS cnt FROM tag"));

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else if (query.first()) {
        int result = query.value(QStringLiteral("cnt")).toInt();
        DatabaseService::closeDatabaseConnection(db, query);

        return result;
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return 0;
}

/**
 * Removes the tag, their children and its note link items
 */
bool Tag::remove() const {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    // remove the tag
    query.prepare(QStringLiteral("DELETE FROM tag WHERE id = :id"));
    query.bindValue(QStringLiteral(":id"), _id);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
        DatabaseService::closeDatabaseConnection(db, query);
        return false;
    } else {
        // remove all children tags
        const auto tagList = fetchAllByParentId(_id);
        for (const Tag &tag : tagList) {
            tag.remove();
        }

        QSqlDatabase db2 = DatabaseService::getNoteFolderDatabase();
        QSqlQuery query2(db2);

        // remove the note tag links
        query2.prepare(QStringLiteral("DELETE FROM noteTagLink WHERE tag_id = :id"));
        query2.bindValue(QStringLiteral(":id"), _id);

        if (!query2.exec()) {
            qWarning() << __func__ << ": " << query2.lastError();
            DatabaseService::closeDatabaseConnection(db2, query2);

            return false;
        } else {
            DatabaseService::closeDatabaseConnection(db2, query2);
            return true;
        }
    }
}

Tag Tag::tagFromQuery(const QSqlQuery &query) { return Tag().fillFromQuery(query); }

Tag Tag::fillFromQuery(const QSqlQuery &query) {
    _id = query.value(QStringLiteral("id")).toInt();
    _name = query.value(QStringLiteral("name")).toString();
    _priority = query.value(QStringLiteral("priority")).toInt();
    _parentId = query.value(QStringLiteral("parent_id")).toInt();

    const QString colorName = query.value(colorFieldName()).toString();
    _color = colorName.isEmpty() ? QColor() : QColor(colorName);

    return *this;
}

QVector<Tag> Tag::fetchAll() {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QVector<Tag> tagList;

    if (!db.isValid() || !db.tables().contains(QStringLiteral("tag"), Qt::CaseInsensitive)) {
        return tagList;
    }

    QSqlQuery query(db);

    // query.prepare("SELECT * FROM tag ORDER BY priority ASC, name ASC");
    /*
     * We want to select all relevant fields from the tag table. (AS renames the
     * columns in the output table -- to be sure they have the same names as in
     * the original query, e.g. t.id = id). Since we want them ordered by time
     * of use, also consider the noteTagLink table and join it on the tag id.
     * (Now we get rows | tag.id | tag.updated | link.created | ... | for each
     * tag (with link.created = NULL, if it is not yet linked to a note) and
     * additional rows for each link of a tag after the first as an intermediate
     * result.) Now the CASE ... END selects only the highest created / updated
     * date. At this point there can still be multiple lines with the same
     * tag.id, hence we GROUP BY that to get rid of them and are only interested
     * in the latest / max() created time of every group, which can now be used
     * to sort the result by.
     *
     */
    query.prepare(
        QStringLiteral("SELECT t.id as id, t.name as name, t.priority as priority, max( "
                       "CASE "
                       "WHEN l.created > t.updated THEN l.created "
                       "ELSE t.updated "
                       "END "
                       ") AS created, t.parent_id as parent_id, "
                       "t.color as color, t.dark_color as dark_color "
                       "FROM tag t LEFT JOIN noteTagLink l ON t.id = l.tag_id "
                       "GROUP BY t.name "
                       "ORDER BY created DESC"));
    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            tagList.append(tagFromQuery(query));
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return tagList;
}

QVector<TagHeader> Tag::fetchAllTagHeadersByParentId(const int parentId) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    QVector<TagHeader> tagList;

    query.prepare(
        QStringLiteral("SELECT t.id as id, t.name as name "
                       "FROM tag t LEFT JOIN noteTagLink l ON t.id = l.tag_id "
                       "WHERE parent_id = :parentId "
                       "GROUP BY t.name"));
    query.bindValue(QStringLiteral(":parentId"), parentId);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            int id = query.value(QStringLiteral("id")).toInt();
            QString name = query.value(QStringLiteral("name")).toString();
            tagList.append(TagHeader{id, name});
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return tagList;
}

QVector<Tag> Tag::fetchAllByParentId(const int parentId, const QString &sortBy) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    QVector<Tag> tagList;

    // query.prepare("SELECT * FROM tag WHERE parent_id = :parentId ORDER BY "
    //                      "priority ASC, name ASC");
    /*
     * See fetchAll(), except we are only interested in tags with a specific
     * parent_id.
     */
    query.prepare(
        QStringLiteral("SELECT t.id as id, t.name as name, t.priority as priority, max( "
                       "CASE "
                       "WHEN l.created > t.updated THEN l.created "
                       "ELSE t.updated "
                       "END "
                       ") AS created, t.parent_id as parent_id, "
                       "t.color as color, t.dark_color as dark_color "
                       "FROM tag t LEFT JOIN noteTagLink l ON t.id = l.tag_id "
                       "WHERE parent_id = :parentId "
                       "GROUP BY t.name "
                       "ORDER BY :sortBy"));
    query.bindValue(QStringLiteral(":parentId"), parentId);
    query.bindValue(QStringLiteral(":sortBy"), sortBy);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            tagList.append(tagFromQuery(query));
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return tagList;
}

QVector<int> Tag::fetchAllIdsByParentId(const int parentId) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    query.prepare(
        QStringLiteral("SELECT t.id as id "
                       "FROM tag t LEFT JOIN noteTagLink l ON t.id = l.tag_id "
                       "WHERE parent_id = :parentId"));

    query.bindValue(QStringLiteral(":parentId"), parentId);

    QVector<int> tagList;
    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            int id = query.value(QStringLiteral("id")).toInt();
            tagList.append(id);
        }
    }
    DatabaseService::closeDatabaseConnection(db, query);
    return tagList;
}

/**
 * Fetches a list of all tags recursively by a parent id
 * The tag of the parent id is also included in the list
 *
 * @param parentId
 * @return
 */
QVector<Tag> Tag::fetchRecursivelyByParentId(const int parentId) {
    QVector<Tag> tagList = QVector<Tag>{fetch(parentId)};
    const auto tags = fetchAllByParentId(parentId);
    tagList.reserve(tags.size());

    for (const Tag &tag : tags) {
        tagList << fetchRecursivelyByParentId(tag.getId());
    }

    return tagList;
}

QVector<int> Tag::fetchTagIdsRecursivelyByParentId(const int parentId) {
    QVector<int> tagIdList{parentId};
    const auto tagIds = fetchAllIdsByParentId(parentId);
    tagIdList.reserve(tagIds.size());

    for (const int tagId : tagIds) {
        tagIdList << fetchTagIdsRecursivelyByParentId(tagId);
    }

    return tagIdList;
}

QStringList Tag::getParentTagNames() {
    const int parentId = _parentId;
    if (parentId == 0) {
        return QStringList();
    }

    Tag parentTag = Tag::fetch(parentId);

    if (!parentTag.isFetched()) {
        return QStringList();
    }

    const QString tagName = parentTag.getName();
    QStringList tagNames = {tagName};
    tagNames << parentTag.getParentTagNames();

    return tagNames;
}

/**
 * Checks if taggingShowNotesRecursively is set
 */
bool Tag::isTaggingShowNotesRecursively() {
    return SettingsService().value(QStringLiteral("taggingShowNotesRecursively")).toBool();
}

int Tag::countAllParentId(const int parentId) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    query.prepare(
        QStringLiteral("SELECT COUNT(*) AS cnt FROM tag "
                       "WHERE parent_id = :parentId "));
    query.bindValue(QStringLiteral(":parentId"), parentId);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else if (query.first()) {
        const int result = query.value(QStringLiteral("cnt")).toInt();
        DatabaseService::closeDatabaseConnection(db, query);
        return result;
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return 0;
}

/**
 * Checks if the current tag has a child with tagId
 */
bool Tag::hasChild(const int tagId) const {
    const auto tagList = fetchAllByParentId(_id);
    for (const Tag &tag : tagList) {
        qDebug() << __func__ << " - 'tag': " << tag;

        if ((tag.getId() == tagId) || tag.hasChild(tagId)) {
            return true;
        }
    }

    return false;
}

/**
 * Fetches all linked tags of a note
 */
QVector<Tag> Tag::fetchAllOfNote(const Note &note) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    QVector<Tag> tagList;

    query.prepare(
        QStringLiteral("SELECT t.* FROM tag t "
                       "JOIN noteTagLink l ON t.id = l.tag_id "
                       "WHERE l.note_file_name = :fileName AND "
                       "l.note_sub_folder_path = :noteSubFolderPath "
                       "ORDER BY t.priority ASC, t.name ASC"));
    query.bindValue(QStringLiteral(":fileName"), note.getName());
    query.bindValue(QStringLiteral(":noteSubFolderPath"), note.getNoteSubFolder().relativePath());

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            tagList.append(tagFromQuery(query));
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return tagList;
}

/**
 * Fetches all linked tags of a list of notes
 */
QVector<Tag> Tag::fetchAllOfNotes(const QVector<Note> &notes) {
    QVector<Tag> notesTagList;

    // get all tags for the notes list
    for (const Note &note : notes) {
        notesTagList.append(Tag::fetchAllOfNote(note));
    }
    // sort
    std::sort(notesTagList.begin(), notesTagList.end());
    // remove duplicates
    notesTagList.erase(std::unique(notesTagList.begin(), notesTagList.end()), notesTagList.end());

    return notesTagList;
}

/**
 * Fetches the names of all linked tags of a note
 */
QStringList Tag::fetchAllNamesOfNote(const Note &note) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    QStringList tagNameList;

    query.prepare(
        QStringLiteral("SELECT t.name FROM tag t "
                       "JOIN noteTagLink l ON t.id = l.tag_id "
                       "WHERE l.note_file_name = :fileName AND "
                       "l.note_sub_folder_path = :noteSubFolderPath "
                       "ORDER BY t.priority ASC, t.name ASC"));
    query.bindValue(QStringLiteral(":fileName"), note.getName());
    query.bindValue(QStringLiteral(":noteSubFolderPath"), note.getNoteSubFolder().relativePath());

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            tagNameList << query.value(QStringLiteral("name")).toString();
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return tagNameList;
}

QHash<QString, QStringList> Tag::fetchAllNamesByNoteFilePath() {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    QHash<QString, QStringList> tagNamesByNoteFilePath;

    query.prepare(
        QStringLiteral("SELECT l.note_file_name, l.note_sub_folder_path, t.name FROM tag t "
                       "JOIN noteTagLink l ON t.id = l.tag_id "
                       "ORDER BY l.note_sub_folder_path ASC, l.note_file_name ASC, "
                       "t.priority ASC, t.name ASC"));

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        while (query.next()) {
            const QString noteFileName = query.value(QStringLiteral("note_file_name")).toString();
            const QString noteSubFolderPath =
                query.value(QStringLiteral("note_sub_folder_path")).toString();
            const QString key = noteSubFolderPath + QStringLiteral("/") + noteFileName;
            tagNamesByNoteFilePath[key] << query.value(QStringLiteral("name")).toString();
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return tagNamesByNoteFilePath;
}

/**
 * Fetches the ids of all linked tags of a note
 */
QSet<int> Tag::fetchAllIdsByNote(const Note &note) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    QSet<int> tagIdList;

    query.prepare(
        QStringLiteral("SELECT tag_id FROM noteTagLink "
                       "WHERE note_file_name = :fileName AND "
                       "note_sub_folder_path = :noteSubFolderPath"));
    query.bindValue(QStringLiteral(":fileName"), note.getName());
    query.bindValue(QStringLiteral(":noteSubFolderPath"), note.getNoteSubFolder().relativePath());

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            tagIdList << query.value(QStringLiteral("tag_id")).toInt();
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return tagIdList;
}

/**
 * Fetches the names by substring searching for the name
 */
QStringList Tag::searchAllNamesByName(const QString &name) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    QStringList tagNameList;

    query.prepare(
        QStringLiteral("SELECT name FROM tag "
                       "WHERE name LIKE :name "
                       "ORDER BY priority ASC, name ASC"));
    query.bindValue(QStringLiteral(":name"), "%" + name + "%");

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            tagNameList << query.value(QStringLiteral("name")).toString();
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return tagNameList;
}

/**
 * Fetches one Tag of a note that has a color
 */
Tag Tag::fetchOneOfNoteWithColor(const Note &note) {
    const auto tagList = fetchAllOfNote(note);
    for (const Tag &tag : tagList) {
        if (tag.getColor().isValid()) {
            return tag;
        }
    }

    return Tag();
}

/**
 * Checks if a note has tags
 */
bool Tag::noteHasTags(const Note &note, const QString &path) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    query.prepare(
        QStringLiteral("SELECT "
                       "EXISTS (SELECT tag_id FROM noteTagLink "
                       "WHERE note_file_name=:fileName AND "
                       "note_sub_folder_path=:noteSubFolderPath) AS cnt"));
    query.bindValue(QStringLiteral(":fileName"), note.getName());
    query.bindValue(QStringLiteral(":noteSubFolderPath"),
                    path.isEmpty() ? note.getNoteSubFolder().relativePath() : path);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else if (query.first()) {
        int result = query.value(QStringLiteral("cnt")).toInt();
        DatabaseService::closeDatabaseConnection(db, query);

        return result == 1;
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return 0;
}

/**
 * Checks if tag is linked to a note
 */
bool Tag::isLinkedToNote(const Note &note) const {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    query.prepare(
        QStringLiteral("SELECT COUNT(*) AS cnt FROM noteTagLink "
                       "WHERE note_file_name = :fileName AND "
                       "note_sub_folder_path = :noteSubFolderPath "
                       "AND tag_id = :tagId"));
    query.bindValue(QStringLiteral(":fileName"), note.getName());
    query.bindValue(QStringLiteral(":noteSubFolderPath"), note.getNoteSubFolder().relativePath());
    query.bindValue(QStringLiteral(":tagId"), _id);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else if (query.first()) {
        bool result = query.value(QStringLiteral("cnt")).toInt() > 0;
        DatabaseService::closeDatabaseConnection(db, query);

        return result;
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return false;
}

/**
 * Returns all tags that are linked to certain note names
 */
QVector<Tag> Tag::fetchAllWithLinkToNoteNames(const QStringList &noteNameList) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    QVector<Tag> tagList;
    const QString noteIdListString = noteNameList.join(QStringLiteral("','"));

    const QString sql = QStringLiteral(
                            "SELECT t.* FROM tag t "
                            "JOIN noteTagLink l ON t.id = l.tag_id "
                            "WHERE l.note_file_name IN ('%1') AND "
                            "l.note_sub_folder_path = :noteSubFolderPath "
                            "GROUP BY t.id "
                            "ORDER BY t.priority ASC, t.name ASC")
                            .arg(noteIdListString);
    query.prepare(sql);
    query.bindValue(QStringLiteral(":noteSubFolderPath"),
                    NoteSubFolder::activeNoteSubFolder().relativePath());

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            tagList.append(tagFromQuery(query));
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return tagList;
}

/**
 * Fetches all linked note ids
 */
QVector<int> Tag::fetchAllLinkedNoteIds(int tagId, const bool fromAllSubfolders,
                                        const bool recursive) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    QVector<int> noteIdList;

    if (fromAllSubfolders) {
        // 'All notes' selected in note subfolder panel
        query.prepare(
            QStringLiteral("SELECT note_file_name, note_sub_folder_path "
                           "FROM noteTagLink WHERE tag_id = :id"));
    } else if (recursive) {
        query.prepare(
            QStringLiteral("SELECT note_file_name, note_sub_folder_path "
                           "FROM noteTagLink WHERE tag_id = :id "
                           "AND note_sub_folder_path LIKE :noteSubFolderPath"));
        query.bindValue(QStringLiteral(":noteSubFolderPath"),
                        NoteSubFolder::activeNoteSubFolder().relativePath() + "%");
    } else {
        query.prepare(
            QStringLiteral("SELECT note_file_name, note_sub_folder_path "
                           "FROM noteTagLink WHERE tag_id = :id "
                           "AND note_sub_folder_path = :noteSubFolderPath"));
        query.bindValue(QStringLiteral(":noteSubFolderPath"),
                        NoteSubFolder::activeNoteSubFolder().relativePath());
    }

    query.bindValue(QStringLiteral(":id"), tagId);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            // always keep in mind that note_file_name is no file name,
            // but the base name (so "my-note", instead of "my-note.md")
            const QString name = query.value(QStringLiteral("note_file_name")).toString();
            const QString noteSubFolderPathData =
                query.value(QStringLiteral("note_sub_folder_path")).toString();
            int noteSubFolderId =
                NoteSubFolder::fetchByPathData(noteSubFolderPathData, QStringLiteral("/")).getId();
            int noteId = Note::fetchNoteIdByName(name, noteSubFolderId);
            noteIdList.append(noteId);
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return noteIdList;
}

/**
 * Fetches all linked note ids for a given subfolder
 */
QVector<int> Tag::fetchAllLinkedNoteIdsForFolder(int tagId, const NoteSubFolder &noteSubFolder,
                                                 bool fromAllSubfolders, const bool recursive) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    QVector<int> noteIdList;

    if (fromAllSubfolders) {
        // 'All notes' selected in note subfolder panel
        query.prepare(
            QStringLiteral("SELECT note_file_name, note_sub_folder_path "
                           "FROM noteTagLink WHERE tag_id = :id"));
    } else if (recursive) {
        query.prepare(
            QStringLiteral("SELECT note_file_name, note_sub_folder_path "
                           "FROM noteTagLink WHERE tag_id = :id "
                           "AND note_sub_folder_path LIKE :noteSubFolderPath"));
        query.bindValue(QStringLiteral(":noteSubFolderPath"), noteSubFolder.relativePath() + "%");
    } else {
        query.prepare(
            QStringLiteral("SELECT note_file_name, note_sub_folder_path "
                           "FROM noteTagLink WHERE tag_id = :id "
                           "AND note_sub_folder_path = :noteSubFolderPath"));
        query.bindValue(QStringLiteral(":noteSubFolderPath"), noteSubFolder.relativePath());
    }

    query.bindValue(QStringLiteral(":id"), tagId);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            // always keep in mind that note_file_name is no file name,
            // but the base name (so "my-note", instead of "my-note.md")
            const QString name = query.value(QStringLiteral("note_file_name")).toString();
            const QString noteSubFolderPathData =
                query.value(QStringLiteral("note_sub_folder_path")).toString();
            int noteSubFolderId =
                NoteSubFolder::fetchByPathData(noteSubFolderPathData, QStringLiteral("/")).getId();
            int noteId = Note::fetchNoteIdByName(name, noteSubFolderId);
            noteIdList.append(noteId);
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return noteIdList;
}

/**
 * Fetches all linked notes
 */
QVector<Note> Tag::fetchAllLinkedNotes() const {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    QVector<Note> noteList;

    query.prepare(
        QStringLiteral("SELECT note_file_name, note_sub_folder_path FROM "
                       "noteTagLink WHERE tag_id = :id"));
    query.bindValue(QStringLiteral(":id"), _id);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            const QString fileName = query.value(QStringLiteral("note_file_name")).toString();
            const QString noteSubFolderPath =
                query.value(QStringLiteral("note_sub_folder_path")).toString();
            const auto noteSubFolder =
                NoteSubFolder::fetchByPathData(std::move(noteSubFolderPath), QStringLiteral("/"));
            const auto note = Note::fetchByName(fileName, noteSubFolder.getId());

            noteList << note;
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return noteList;
}

/**
 * Converts backslashes to slashes in the noteTagLink table to fix
 * problems with Windows
 */
void Tag::convertDirSeparator() {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    query.prepare(
        QStringLiteral("UPDATE noteTagLink SET note_sub_folder_path = replace("
                       "note_sub_folder_path, '\\', '/')"));

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    }

    DatabaseService::closeDatabaseConnection(db, query);
}

/**
 * Fetches all tag names
 */
QStringList Tag::fetchAllNames() {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    QStringList nameList;

    query.prepare(QStringLiteral("SELECT name FROM tag ORDER BY name"));

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            nameList.append(query.value(QStringLiteral("name")).toString());
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return nameList;
}

/**
 * Count the linked note file names for a note sub folder
 */
int Tag::countLinkedNoteFileNamesForNoteSubFolder(int tagId, const NoteSubFolder &noteSubFolder,
                                                  bool fromAllSubfolders, const bool recursive) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    if (fromAllSubfolders) {
        query.prepare(
            QStringLiteral("SELECT COUNT(note_file_name) AS cnt FROM noteTagLink "
                           "WHERE tag_id = :id"));
    } else if (recursive) {
        query.prepare(
            QStringLiteral("SELECT COUNT(note_file_name) AS cnt FROM noteTagLink "
                           "WHERE tag_id = :id AND "
                           "note_sub_folder_path LIKE :noteSubFolderPath"));
        query.bindValue(QStringLiteral(":noteSubFolderPath"),
                        noteSubFolder.relativePath() + QLatin1Char('%'));
    } else {
        query.prepare(
            QStringLiteral("SELECT COUNT(note_file_name) AS cnt FROM noteTagLink "
                           "WHERE tag_id = :id AND "
                           "note_sub_folder_path = :noteSubFolderPath"));
        query.bindValue(QStringLiteral(":noteSubFolderPath"),
                        noteSubFolder.relativePath() + QLatin1Char('%'));
    }
    query.bindValue(QStringLiteral(":id"), tagId);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else if (query.first()) {
        const int result = query.value(QStringLiteral("cnt")).toInt();
        DatabaseService::closeDatabaseConnection(db, query);

        return result;
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return 0;
}

/**
 * Count the linked note file names
 */
int Tag::countLinkedNoteFileNames(int tagId, bool fromAllSubfolders, bool recursive) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    if (fromAllSubfolders) {
        query.prepare(
            QStringLiteral("SELECT COUNT(note_file_name) AS cnt FROM noteTagLink "
                           "WHERE tag_id = :id"));
    } else if (recursive) {
        query.prepare(
            QStringLiteral("SELECT COUNT(note_file_name) AS cnt FROM noteTagLink "
                           "WHERE tag_id = :id AND "
                           "note_sub_folder_path LIKE :noteSubFolderPath"));
        query.bindValue(QStringLiteral(":noteSubFolderPath"),
                        NoteSubFolder::activeNoteSubFolder().relativePath() + "%");
    } else {
        query.prepare(
            QStringLiteral("SELECT COUNT(note_file_name) AS cnt FROM noteTagLink "
                           "WHERE tag_id = :id AND "
                           "note_sub_folder_path = :noteSubFolderPath"));
        query.bindValue(QStringLiteral(":noteSubFolderPath"),
                        NoteSubFolder::activeNoteSubFolder().relativePath());
    }
    query.bindValue(QStringLiteral(":id"), tagId);

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else if (query.first()) {
        const int result = query.value(QStringLiteral("cnt")).toInt();
        DatabaseService::closeDatabaseConnection(db, query);

        return result;
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return 0;
}

/**
 * Inserts or updates a Tag object in the database
 */
bool Tag::store() {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    const QString colorField = colorFieldName();

    const int id = _id;
    if (id > 0) {
        query.prepare(QStringLiteral("UPDATE tag SET name = :name, priority = :priority, "
                                     "parent_id = :parentId, ") %
                      colorField %
                      QStringLiteral(" = :color, "
                                     "updated = datetime('now') "
                                     "WHERE id = :id"));
        query.bindValue(QStringLiteral(":id"), id);
    } else {
        query.prepare(QStringLiteral("INSERT INTO tag (name, priority, parent_id, ") % colorField %
                      QStringLiteral(") VALUES (:name, :priority, :parentId, :color)"));
    }

    const int parentId = _parentId;

    query.bindValue(QStringLiteral(":name"), _name);
    query.bindValue(QStringLiteral(":priority"), _priority);
    query.bindValue(QStringLiteral(":parentId"), parentId);
    query.bindValue(QStringLiteral(":color"), _color.isValid() ? _color.name() : QLatin1String(""));

    if (!query.exec()) {
        // on error
        qWarning() << __func__ << ": " << query.lastError();

        DatabaseService::closeDatabaseConnection(db, query);
        return false;
    } else if (id == 0) {
        // on insert
        _id = query.lastInsertId().toInt();
    }

    // update the parent tag for correct sorting by last use
    if (parentId > 0) {
        QSqlQuery parentQuery(db);
        parentQuery.prepare(QStringLiteral("SELECT * FROM tag WHERE id = :parentId"));
        parentQuery.bindValue(QStringLiteral(":parentId"), parentId);

        if (!parentQuery.exec()) {
            qWarning() << __func__ << ": " << query.lastError();
        } else {
            if (parentQuery.next()) {
                Tag parent = tagFromQuery(parentQuery);
                parent.store();
            }
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return true;
}

/**
 * Returns the name of the color database field that should be used to load
 * and store tag colors
 *
 * @return
 */
QString Tag::colorFieldName() const {
    return SettingsService().value(QStringLiteral("darkMode")).toBool()
               ? QStringLiteral("dark_color")
               : QStringLiteral("color");
}

/**
 * Links a note to a tag
 */
bool Tag::linkToNote(const Note &note) const {
    if (!isFetched()) {
        return false;
    }

    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    query.prepare(
        QStringLiteral("INSERT INTO noteTagLink (tag_id, note_file_name, "
                       "note_sub_folder_path) "
                       "VALUES (:tagId, :noteFileName, "
                       ":noteSubFolderPath)"));

    query.bindValue(QStringLiteral(":tagId"), _id);
    query.bindValue(QStringLiteral(":noteFileName"), note.getName());
    query.bindValue(QStringLiteral(":noteSubFolderPath"), note.getNoteSubFolder().relativePath());

    if (!query.exec()) {
        // we should not show this warning, because we don't check if a
        // link to a note already exists before we try to create another link
        //        qWarning() << __func__ << ": " << query.lastError();

        DatabaseService::closeDatabaseConnection(db, query);

        return false;
    }

    // update the parent tag for correct sorting by last use
    if (_parentId > 0) {
        QSqlQuery parentQuery(db);
        parentQuery.prepare(QStringLiteral("SELECT * FROM tag WHERE id = :parentId"));
        parentQuery.bindValue(QStringLiteral(":parentId"), _parentId);

        if (!parentQuery.exec()) {
            qWarning() << __func__ << ": " << query.lastError();
        } else {
            if (parentQuery.next()) {
                Tag parent = tagFromQuery(parentQuery);
                parent.store();
            }
        }
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return true;
}

/**
 * Removes the link to a note
 */
bool Tag::removeLinkToNote(const Note &note) const {
    if (!isFetched()) {
        return false;
    }

    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    query.prepare(
        QStringLiteral("DELETE FROM noteTagLink WHERE tag_id = :tagId AND "
                       "note_file_name = :noteFileName AND "
                       "note_sub_folder_path = :noteSubFolderPath"));

    query.bindValue(QStringLiteral(":tagId"), _id);
    query.bindValue(QStringLiteral(":noteFileName"), note.getName());
    query.bindValue(QStringLiteral(":noteSubFolderPath"), note.getNoteSubFolder().relativePath());

    if (!query.exec()) {
        // on error
        qWarning() << __func__ << ": " << query.lastError();

        DatabaseService::closeDatabaseConnection(db, query);

        return false;
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return true;
}

/**
 * Removes all links to a note
 */
bool Tag::removeAllLinksToNote(const Note &note) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    query.prepare(
        QStringLiteral("DELETE FROM noteTagLink WHERE "
                       "note_file_name = :noteFileName AND "
                       "note_sub_folder_path = :noteSubFolderPath"));

    query.bindValue(QStringLiteral(":noteFileName"), note.getName());
    query.bindValue(QStringLiteral(":noteSubFolderPath"), note.getNoteSubFolder().relativePath());

    if (!query.exec()) {
        // on error
        qWarning() << __func__ << ": " << query.lastError();

        DatabaseService::closeDatabaseConnection(db, query);

        return false;
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return true;
}

/**
 * Removes all broken note tag links
 */
void Tag::removeBrokenLinks() {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    // Check all note tag links if note is missing to set the link stale
    query.prepare(QStringLiteral("SELECT * FROM notetaglink"));
    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            Note note = getNoteFromNoteTagLinkQuery(query);

            // set note tag link stale if note doesn't exist
            if (!note.isFetched()) {
                const int id = query.value(QStringLiteral("id")).toInt();
                setNoteLinkByIdStale(id);
            }
        }
    }

    // Check all stale note tag links if note is still missing to remove the
    // stale date if the note came back
    query.prepare(QStringLiteral("SELECT * FROM notetaglink WHERE stale_DATE IS NOT NULL"));
    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
    } else {
        for (int r = 0; query.next(); r++) {
            Note note = getNoteFromNoteTagLinkQuery(query);

            // set note tag link to not stale if note exists again
            if (note.isFetched()) {
                const int id = query.value(QStringLiteral("id")).toInt();
                setNoteLinkByIdNotStale(id);
            }
        }
    }

    // Remove all note links that were stale for too long
    removeExpiredStaleNoteLinkBy();

    DatabaseService::closeDatabaseConnection(db, query);
}

Note Tag::getNoteFromNoteTagLinkQuery(const QSqlQuery &query) {
    const QString noteFileName = query.value(QStringLiteral("note_file_name")).toString();
    const QString noteSubFolderPath =
        query.value(QStringLiteral("note_sub_folder_path")).toString();

    const NoteSubFolder noteSubFolder =
        NoteSubFolder::fetchByPathData(noteSubFolderPath, QStringLiteral("/"));

    return Note::fetchByName(noteFileName, noteSubFolder.getId());
}

/**
 * Removes a note tag link by its id
 *
 * @param id
 * @return
 */
bool Tag::removeNoteLinkById(int id) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    query.prepare(QStringLiteral("DELETE FROM noteTagLink WHERE id = :id"));
    query.bindValue(QStringLiteral(":id"), id);

    if (!query.exec()) {
        // on error
        qWarning() << __func__ << ": " << query.lastError();

        DatabaseService::closeDatabaseConnection(db, query);
        return false;
    }

    DatabaseService::closeDatabaseConnection(db, query);
    return true;
}

/**
 * Sets a note tag link stale by its id (if it wasn't already stale)
 *
 * @param id
 * @return
 */
bool Tag::setNoteLinkByIdStale(int id) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    query.prepare(QStringLiteral(
        "UPDATE noteTagLink SET stale_date = :date WHERE id = :id AND stale_date IS NULL"));
    query.bindValue(QStringLiteral(":id"), id);
    query.bindValue(QStringLiteral(":date"), QDateTime::currentDateTime());

    if (!query.exec()) {
        // on error
        qWarning() << __func__ << ": " << query.lastError();

        DatabaseService::closeDatabaseConnection(db, query);
        return false;
    }

    DatabaseService::closeDatabaseConnection(db, query);
    return true;
}

/**
 * Sets a note tag link to not stale by its id
 *
 * @param id
 * @return
 */
bool Tag::setNoteLinkByIdNotStale(int id) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    query.prepare(QStringLiteral("UPDATE noteTagLink SET stale_date = NULL WHERE id = :id"));
    query.bindValue(QStringLiteral(":id"), id);

    if (!query.exec()) {
        // on error
        qWarning() << __func__ << ": " << query.lastError();

        DatabaseService::closeDatabaseConnection(db, query);
        return false;
    }

    DatabaseService::closeDatabaseConnection(db, query);
    return true;
}

/**
 * Remove all note links that were stale for too long (10 days)
 *
 * @return
 */
bool Tag::removeExpiredStaleNoteLinkBy() {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    query.prepare(QStringLiteral("DELETE FROM noteTagLink WHERE stale_date < :date"));
    query.bindValue(QStringLiteral(":date"), QDateTime::currentDateTime().addDays(-10));

    if (!query.exec()) {
        // on error
        qWarning() << __func__ << ": " << query.lastError();

        DatabaseService::closeDatabaseConnection(db, query);
        return false;
    }

    DatabaseService::closeDatabaseConnection(db, query);
    return true;
}

/**
 * Renames the note file name of note links
 */
bool Tag::renameNoteFileNamesOfLinks(const QString &oldFileName, const QString &newFileName,
                                     const NoteSubFolder &noteSubFolder) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    query.prepare(
        QStringLiteral("UPDATE noteTagLink SET note_file_name = :newFileName WHERE "
                       "note_file_name = :oldFileName AND "
                       "note_sub_folder_path = :noteSubFolderPath"));

    query.bindValue(QStringLiteral(":oldFileName"), oldFileName);
    query.bindValue(QStringLiteral(":newFileName"), newFileName);
    query.bindValue(QStringLiteral(":noteSubFolderPath"), noteSubFolder.relativePath());

    if (!query.exec()) {
        // on error
        qWarning() << __func__ << ": " << query.lastError();

        DatabaseService::closeDatabaseConnection(db, query);
        return false;
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return true;
}

/**
 * Renames the note sub folder paths of note links
 *
 * Uses prefix-only substitution to avoid accidentally modifying sibling paths
 * that share the same prefix (e.g. renaming "work" must not affect "workplace"),
 * and to prevent the global replace() from corrupting paths that contain the
 * old folder name in a deeper component (e.g. "A/A/note" when renaming "A").
 */
bool Tag::renameNoteSubFolderPathsOfLinks(const QString &oldPath, const QString &newPath) {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);
    // Replace only the prefix: match the exact path or direct/deep children
    // (separated by "/"), then concatenate newPath with the remainder of the
    // original string so that inner occurrences of the old name are untouched.
    query.prepare(
        QStringLiteral("UPDATE noteTagLink SET note_sub_folder_path = "
                       ":newPath || substr(note_sub_folder_path, length(:oldPath2) + 1) "
                       "WHERE note_sub_folder_path = :oldPath "
                       "OR note_sub_folder_path LIKE :oldPathChildLike"));

    query.bindValue(QStringLiteral(":oldPath"), oldPath);
    query.bindValue(QStringLiteral(":oldPath2"), oldPath);
    query.bindValue(QStringLiteral(":oldPathChildLike"), oldPath + "/%");
    query.bindValue(QStringLiteral(":newPath"), newPath);

    if (!query.exec()) {
        // on error
        qWarning() << __func__ << ": " << query.lastError();

        DatabaseService::closeDatabaseConnection(db, query);
        return false;
    }

    DatabaseService::closeDatabaseConnection(db, query);

    return true;
}

/**
 * Checks if the active tag still exists in the database
 */
bool Tag::exists() const { return Tag::fetch(_id)._id > 0; }

bool Tag::isFetched() const { return (_id > 0); }

void Tag::setAsActive() { Tag::setAsActive(_id); }

void Tag::setAsActive(const int tagId) {
    NoteFolder noteFolder = NoteFolder::currentNoteFolder();
    noteFolder.setActiveTagId(tagId);
    noteFolder.store();
}

/**
 * Checks if this note folder is the active one
 */
bool Tag::isActive() const { return activeTagId() == _id; }

/**
 * Returns the id of the active note folder in the settings
 */
int Tag::activeTagId() {
    const NoteFolder noteFolder = NoteFolder::currentNoteFolder();
    return noteFolder.getActiveTagId();
}

/**
 * Returns the active note folder
 */
Tag Tag::activeTag() { return Tag::fetch(activeTagId()); }

/**
 * Sets the non-darkMode colors as darkMode colors for all tags
 */
void Tag::migrateDarkColors() {
    QSqlDatabase db = DatabaseService::getNoteFolderDatabase();
    QSqlQuery query(db);

    // This migration runs before the tag table has the `updated` column, so don't use
    // Tag::fetchAll() / Tag::store() here. Those helpers already expect the final schema.
    query.prepare(QStringLiteral("SELECT id, color FROM tag"));

    if (!query.exec()) {
        qWarning() << __func__ << ": " << query.lastError();
        DatabaseService::closeDatabaseConnection(db, query);
        return;
    }

    QVector<QPair<int, QString>> tagColors;
    while (query.next()) {
        const QString colorName = query.value(QStringLiteral("color")).toString();
        const QColor color = colorName.isEmpty() ? QColor() : QColor(colorName);
        tagColors.append({query.value(QStringLiteral("id")).toInt(),
                          color.isValid() ? color.name() : QLatin1String("")});
    }

    query.finish();
    query.clear();

    QSqlQuery updateQuery(db);
    updateQuery.prepare(QStringLiteral("UPDATE tag SET dark_color = :color WHERE id = :id"));

    for (const auto &tagColor : Utils::asConst(tagColors)) {
        updateQuery.bindValue(QStringLiteral(":id"), tagColor.first);
        updateQuery.bindValue(QStringLiteral(":color"), tagColor.second);

        if (!updateQuery.exec()) {
            qWarning() << __func__ << ": " << updateQuery.lastError();
            break;
        }
    }

    DatabaseService::closeDatabaseConnection(db, updateQuery);
}

bool Tag::mergeFromDatabase(QSqlDatabase &db) {
    QSqlDatabase noteFolderDB = DatabaseService::getNoteFolderDatabase();

    const bool isSameTagTable =
        DatabaseService::generateDatabaseTableSha1Signature(db, "tag") ==
        DatabaseService::generateDatabaseTableSha1Signature(noteFolderDB, "tag");

    const bool isSameNoteTagLinkTable =
        DatabaseService::generateDatabaseTableSha1Signature(db, "noteTagLink") ==
        DatabaseService::generateDatabaseTableSha1Signature(noteFolderDB, "noteTagLink");

    // if those tables are the same everything is ok
    if (isSameTagTable && isSameNoteTagLinkTable) {
        qDebug() << "Tag and tagLinkTable were the same in conflicting note "
                    "folder database";

        return true;
    }

    QHash<int, MergeTagData> sourceTagDataById;
    QVector<MergeNoteTagLinkData> noteTagLinks;

    if (!fetchMergeTags(db, sourceTagDataById) || !fetchMergeNoteTagLinks(db, noteTagLinks)) {
        return false;
    }

    if (!noteFolderDB.transaction()) {
        qWarning() << __func__ << ":" << noteFolderDB.lastError();
        return false;
    }

    QHash<int, int> tagIdMap;

    for (auto it = sourceTagDataById.constBegin(); it != sourceTagDataById.constEnd(); ++it) {
        if (ensureMergedTagId(noteFolderDB, sourceTagDataById, tagIdMap, it.key()) < 0) {
            noteFolderDB.rollback();
            return false;
        }
    }

    QSqlQuery linkInsertQuery(noteFolderDB);
    linkInsertQuery.prepare(
        QStringLiteral("INSERT OR IGNORE INTO noteTagLink (tag_id, note_file_name, "
                       "note_sub_folder_path) VALUES (:tagId, :noteFileName, "
                       ":noteSubFolderPath)"));

    for (const MergeNoteTagLinkData &noteTagLink : noteTagLinks) {
        const int targetTagId =
            ensureMergedTagId(noteFolderDB, sourceTagDataById, tagIdMap, noteTagLink.sourceTagId);
        if (targetTagId < 0) {
            noteFolderDB.rollback();
            return false;
        }

        linkInsertQuery.bindValue(QStringLiteral(":tagId"), targetTagId);
        linkInsertQuery.bindValue(QStringLiteral(":noteFileName"), noteTagLink.noteFileName);
        linkInsertQuery.bindValue(QStringLiteral(":noteSubFolderPath"),
                                  noteTagLink.noteSubFolderPath);

        if (!linkInsertQuery.exec()) {
            qWarning() << __func__ << ":" << linkInsertQuery.lastError();
            noteFolderDB.rollback();
            return false;
        }
    }

    if (!noteFolderDB.commit()) {
        qWarning() << __func__ << ":" << noteFolderDB.lastError();
        noteFolderDB.rollback();
        return false;
    }

    return true;
}

/**
 * Fetches a tag by its "breadcrumb list" of tag names.
 * Element nameList[0] would be highest in the tree (with parentId: 0)
 *
 * @param nameList
 * @param createMissing {bool} if true (default) all missing tags will be
 * created
 * @return Tag object of deepest tag of the name breadcrumb list
 */
Tag Tag::getTagByNameBreadcrumbList(const QStringList &nameList, bool createMissing) {
    int parentId = 0;
    Tag tag;

    for (const QString &tagName : nameList) {
        tag = Tag::fetchByName(tagName, parentId);

        if (!tag.isFetched()) {
            if (!createMissing) {
                return Tag();
            }

            tag.setName(tagName);
            tag.setParentId(parentId);
            tag.store();

            if (!tag.isFetched()) {
                return Tag();
            }
        }
        parentId = tag.getId();
    }
    return tag;
}

bool Tag::operator==(const Tag &tag) const { return _id == tag._id; }

bool Tag::operator<(const Tag &tag) const { return _name < tag._name; }

QDebug operator<<(QDebug dbg, const Tag &tag) {
    dbg.nospace() << "Tag: <id>" << tag._id << " <name>" << tag._name << " <parentId>"
                  << tag._parentId;
    return dbg.space();
}
