/*
    SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.com>
    SPDX-FileCopyrightText: 2018 Claudio Maradonna <penguyman@stronzi.org>
    SPDX-FileCopyrightText: 2019 Maciej S. Szmigiero <mail@maciej.szmigiero.name>
    SPDX-FileCopyrightText: 2023 g10 Code GmbH
    SPDX-FileContributor: Sune Stolborg Vuorela <sune@vuorela.dk>

    SPDX-License-Identifier: GPL-3.0-or-later
*/

#include "storemodel.h"

#include "addfileinfoproxy.h"
#include "job/filereencryptjob.h"
#include "rootfolderconfig.h"
#include "rootfoldersmanager.h"
#include "util.h"

#include <Libkleo/Formatting>
#include <Libkleo/KeyCache>
#include <gpgme++/key.h>

#include <KLocalizedString>
#include <KSelectionProxyModel>

#include <QDir>
#include <QFileSystemModel>
#include <QItemSelectionModel>
#include <QMessageBox>
#include <QMimeData>
#include <QRegularExpression>

static const QString mimeType = QStringLiteral("application/vnd+gnupgpass.dragAndDropInfoPasswordStore");

/// \brief holds values to share beetween drag and drop on the passwordstorage view.
struct DragAndDropInfoPasswordStore {
    bool isDir = false;
    bool isFile = false;
    QString path;
};

QDataStream &operator<<(QDataStream &out, const DragAndDropInfoPasswordStore &dragAndDropInfoPasswordStore)
{
    out << dragAndDropInfoPasswordStore.isDir << dragAndDropInfoPasswordStore.isFile << dragAndDropInfoPasswordStore.path;
    return out;
}

QDataStream &operator>>(QDataStream &in, DragAndDropInfoPasswordStore &dragAndDropInfoPasswordStore)
{
    in >> dragAndDropInfoPasswordStore.isDir >> dragAndDropInfoPasswordStore.isFile >> dragAndDropInfoPasswordStore.path;
    return in;
}

StoreModel::StoreModel(QObject *parent)
    : QSortFilterProxyModel(parent)
    , m_fileSystemModel(new QFileSystemModel(this))
    , m_addRoleModel(new AddFileInfoProxy(this))
    , m_itemSelectionModel(new QItemSelectionModel(m_addRoleModel, this))
    , m_selectionProxyModel(new KSelectionProxyModel(m_itemSelectionModel, this))
{
    m_fileSystemModel->setNameFilters({QStringLiteral("*.gpg")});
    m_fileSystemModel->setNameFilterDisables(false);

    m_addRoleModel->setSourceModel(m_fileSystemModel);
    m_selectionProxyModel->setFilterBehavior(KSelectionProxyModel::SubTrees);
    m_selectionProxyModel->setSourceModel(m_addRoleModel);

    setObjectName(QStringLiteral("StoreModel"));
    setRecursiveFilteringEnabled(true);
    setSourceModel(m_selectionProxyModel);
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
    setAutoAcceptChildRows(true);
#endif

    connect(m_fileSystemModel, &QFileSystemModel::directoryLoaded, this, &StoreModel::directoryLoaded);
}

int StoreModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return 1;
}

RootFoldersManager *StoreModel::rootFoldersManager() const
{
    return m_rootFoldersManager;
}

void StoreModel::setRootFoldersManager(RootFoldersManager *rootFoldersManager)
{
    if (m_rootFoldersManager) {
        disconnect(m_rootFoldersManager, &RootFoldersManager::rootFoldersChanged, this, &StoreModel::updateRootFolders);
    }

    m_rootFoldersManager = rootFoldersManager;

    if (m_rootFoldersManager) {
        updateRootFolders();
        connect(m_rootFoldersManager, &RootFoldersManager::rootFoldersChanged, this, &StoreModel::updateRootFolders);
    }
}

void StoreModel::updateRootFolders()
{
    m_itemSelectionModel->clear();
    const auto rootFolders = m_rootFoldersManager->rootFolders();
    for (const auto &rootFolder : rootFolders) {
        QDir dir(rootFolder->path());
        if (!dir.exists()) {
            dir.mkpath(QStringLiteral("."));
        }
        QModelIndex rootDirIndex = m_fileSystemModel->setRootPath(rootFolder->path());
        m_fileSystemModel->fetchMore(rootDirIndex);
        m_itemSelectionModel->select(m_addRoleModel->mapFromSource(rootDirIndex), QItemSelectionModel::Select);
    }
    Q_EMIT rootFoldersSizeChanged();
}

QVariant StoreModel::data(const QModelIndex &index, int role) const
{
    Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid));

    const auto initialValue = QSortFilterProxyModel::data(index, role);

    if (index.column() == 0 && !index.parent().isValid() && m_rootFoldersManager) {
        if (role == Qt::DisplayRole && m_rootFoldersManager->rootFolders().count() > 1) {
            const auto uuid = initialValue.toString();
            if (!uuid.isEmpty()) {
                return m_rootFoldersManager->rootFolderName(uuid);
            }
        } else if (role == Qt::DecorationRole) {
            return {};
        }
    }

    if (role == Qt::ToolTipRole) {
        const auto fileInfo = index.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>();
        const bool isDir = fileInfo.isDir();
        if (isDir) {
            const auto recipients = recipientsForFile(fileInfo);
            QString tooltip = i18nc("@info:tooltip", "<p>This directory is encrypted for the following users:</p><ul>");
            auto keyCache = Kleo::KeyCache::instance();
            for (const auto &recipient : recipients) {
                auto key = keyCache->findByKeyIDOrFingerprint(recipient.data());
                if (key.isNull()) {
                    tooltip += QStringLiteral("<li>") + QString::fromUtf8(recipient) + QStringLiteral("</li>");
                } else {
                    tooltip += QStringLiteral("<li>") + Kleo::Formatting::summaryLine(key) + QStringLiteral("</li>");
                }
            }
            tooltip += QStringLiteral("</ul>");
            return tooltip;
        }
    } else if (role == Qt::DisplayRole) {
        QString name = initialValue.toString();
        name.replace(Util::endsWithGpg(), QString{});
        return name;
    }
    return initialValue;
}

Qt::DropActions StoreModel::supportedDropActions() const
{
    return Qt::CopyAction | Qt::MoveAction;
}

Qt::DropActions StoreModel::supportedDragActions() const
{
    return Qt::CopyAction | Qt::MoveAction;
}

Qt::ItemFlags StoreModel::flags(const QModelIndex &index) const
{
    Qt::ItemFlags defaultFlags = QSortFilterProxyModel::flags(index);

    if (index.isValid()) {
        return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
    }
    return Qt::ItemIsDropEnabled | defaultFlags;
}

QStringList StoreModel::mimeTypes() const
{
    QStringList types;
    types << mimeType;
    return types;
}

QMimeData *StoreModel::mimeData(const QModelIndexList &indexes) const
{
    DragAndDropInfoPasswordStore info;

    QByteArray encodedData;
    // only use the first, otherwise we should enable multiselection
    QModelIndex index = indexes.at(0);
    if (index.isValid()) {
        auto fileInfo = index.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>();

        info.isDir = fileInfo.isDir();
        info.isFile = fileInfo.isFile();
        info.path = fileInfo.absoluteFilePath();
        QDataStream stream(&encodedData, QIODevice::WriteOnly);
        stream << info;
    }

    auto *mimeData = new QMimeData();
    mimeData->setData(mimeType, encodedData);
    return mimeData;
}

bool StoreModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const
{
#ifdef QT_DEBUG
    qDebug() << action << row;
#else
    Q_UNUSED(action)
    Q_UNUSED(row)
#endif
    if (!sourceModel()) {
        return false;
    }

    const QModelIndex idx = index(parent.row(), parent.column(), parent.parent());
    QByteArray encodedData = data->data(mimeType);
    QDataStream stream(&encodedData, QIODevice::ReadOnly);
    DragAndDropInfoPasswordStore info;
    stream >> info;
    if (!data->hasFormat(mimeType))
        return false;

    if (column > 0) {
        return false;
    }

    auto fileInfo = idx.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>();
    const auto rootDir = fileInfo.absoluteFilePath().isEmpty();

    // you can drop a folder on a folder
    if ((fileInfo.isDir() || rootDir) && info.isDir) {
        return true;
    }
    // you can drop a file on a folder
    if ((fileInfo.isDir() || rootDir) && info.isFile) {
        return true;
    }
    // you can drop a file on a file
    if (fileInfo.isFile() && info.isFile) {
        return true;
    }

    return false;
}

bool StoreModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
    if (!canDropMimeData(data, action, row, column, parent))
        return false;

    if (action == Qt::IgnoreAction) {
        return true;
    }
    QByteArray encodedData = data->data(mimeType);

    QDataStream stream(&encodedData, QIODevice::ReadOnly);
    DragAndDropInfoPasswordStore info;
    stream >> info;
    QModelIndex destIndex = this->index(parent.row(), parent.column(), parent.parent());
    QFileInfo destFileinfo = destIndex.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>();
    if (destFileinfo.absoluteFilePath().isEmpty()) {
        auto rootFolder = m_rootFoldersManager->rootFolders().constFirst();
        destFileinfo = QFileInfo(rootFolder->path());
    }
    QFileInfo srcFileInfo = QFileInfo(info.path);
    QString cleanedSrc = QDir::cleanPath(srcFileInfo.absoluteFilePath());
    QString cleanedDest = QDir::cleanPath(destFileinfo.absoluteFilePath());
    if (info.isDir) {
        // dropped dir onto dir
        if (destFileinfo.isDir()) {
            QDir destDir = QDir(cleanedDest).filePath(srcFileInfo.fileName());
            QString cleanedDestDir = QDir::cleanPath(destDir.absolutePath());
            if (action == Qt::MoveAction) {
                move(cleanedSrc, cleanedDestDir);
            } else if (action == Qt::CopyAction) {
                copy(cleanedSrc, cleanedDestDir);
            }
        }
    } else if (info.isFile) {
        // dropped file onto a directory
        if (destFileinfo.isDir()) {
            if (action == Qt::MoveAction) {
                move(cleanedSrc, cleanedDest);
            } else if (action == Qt::CopyAction) {
                copy(cleanedSrc, cleanedDest);
            }
        } else if (destFileinfo.isFile()) {
            // dropped file onto a file
            int answer = QMessageBox::question(nullptr,
                                               i18n("Force overwrite?"),
                                               i18nc("Overwrite DestinationFile with SourceFile", "Overwrite %1 with %2?", cleanedDest, cleanedSrc),
                                               QMessageBox::Yes | QMessageBox::No);
            bool force = answer == QMessageBox::Yes;
            if (action == Qt::MoveAction) {
                move(cleanedSrc, cleanedDest, force);
            } else if (action == Qt::CopyAction) {
                copy(cleanedSrc, cleanedDest, force);
            }
        }
    }
    return true;
}

bool StoreModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
/* matches logic in QFileSystemModelSorter::compareNodes() */
#ifndef Q_OS_MAC
    if (source_left.column() == 0 || source_left.column() == 1) {
        bool leftD = source_left.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>().isDir();
        bool rightD = source_right.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>().isDir();

        if (leftD ^ rightD)
            return leftD;
    }
#endif

    return QSortFilterProxyModel::lessThan(source_left, source_right);
}

QList<QByteArray> StoreModel::recipientsForFile(const QFileInfo &fileInfo) const
{
    QDir gpgIdPath(fileInfo.isDir() ? fileInfo.absoluteFilePath() : fileInfo.absoluteDir());
    bool found = false;
    QString rootDir;
    while (gpgIdPath.exists()) {
        bool isInRootFolder = false;
        const auto rootFolders = m_rootFoldersManager->rootFolders();
        for (const auto &rootFolder : rootFolders) {
            auto rootFolderPath = rootFolder->path();
            rootFolderPath.chop(1); // remove / at end
#ifdef Q_OS_WINDOWS
            if (gpgIdPath.absolutePath().startsWith(rootFolderPath, Qt::CaseInsensitive)) {
#else
            if (gpgIdPath.absolutePath().startsWith(rootFolderPath)) {
#endif
                isInRootFolder = true;
                rootDir = rootFolder->path();
                break;
            }
        }

        if (!isInRootFolder) {
            break;
        }
        if (QFile(gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id"))).exists()) {
            found = true;
            break;
        }
        if (!gpgIdPath.cdUp())
            break;
    }
    QFile gpgId(found ? gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id")) : rootDir + QStringLiteral(".gpg-id"));
    if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qWarning() << "Unable to open" << (found ? gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id")) : rootDir + QStringLiteral(".gpg-id"));
        return {};
    }

    QList<QByteArray> recipients;
    while (!gpgId.atEnd()) {
        const auto recipient(gpgId.readLine().trimmed());
        if (!recipient.isEmpty()) {
            recipients += recipient;
        }
    }
    std::sort(recipients.begin(), recipients.end());
    return recipients;
}

void StoreModel::move(const QString &source, const QString &destination, const bool force)
{
    QFileInfo srcFileInfo(source);
    QFileInfo destFileInfo(destination);
    QString destFile;
    QString srcFileBaseName = srcFileInfo.fileName();

    if (srcFileInfo.isFile()) {
        if (destFileInfo.isFile()) {
            if (!force) {
                return;
            }
        } else if (destFileInfo.isDir()) {
            destFile = QDir(destination).filePath(srcFileBaseName);
        } else {
            destFile = destination;
        }

        if (destFile.endsWith(QStringLiteral(".gpg"), Qt::CaseInsensitive))
            destFile.chop(4); // make sure suffix is lowercase
        destFile.append(QStringLiteral(".gpg"));
    } else if (srcFileInfo.isDir()) {
        if (destFileInfo.isDir()) {
            destFile = QDir(destination).filePath(srcFileBaseName);
        } else if (destFileInfo.isFile()) {
            return;
        } else {
            destFile = destination;
        }
    } else {
        return;
    }

    QDir qDir;
    if (force) {
        qDir.remove(destFile);
    }
    qDir.rename(source, destFile);
}

void StoreModel::copy(const QString &source, const QString &destination, const bool force)
{
    QDir qDir;
    if (force) {
        qDir.remove(destination);
    }

    if (!QFile::copy(source, destination)) {
        Q_EMIT errorOccurred(QStringLiteral("Failed to copy file"));
    }

    const QFileInfo destinationInfo(destination);
    const QFileInfo sourceInfo(source);
    if (destinationInfo.isDir()) {
        const auto recipients = recipientsForFile(destinationInfo);
        auto fileReencryptJob = new FileReencryptJob(destinationInfo.absoluteFilePath() + u'/' + sourceInfo.fileName(), recipients);
        fileReencryptJob->start();
    } else if (destinationInfo.isFile()) {
        const auto recipients = recipientsForFile(destinationInfo);
        auto fileReencryptJob = new FileReencryptJob(destinationInfo.absoluteFilePath(), recipients);
        fileReencryptJob->start();
    }
}

QModelIndex StoreModel::indexForPath(const QString &filePath)
{
    const auto sourceIndex = m_fileSystemModel->index(filePath);
    auto proxyIndex = m_addRoleModel->mapFromSource(sourceIndex);
    proxyIndex = m_selectionProxyModel->mapFromSource(proxyIndex);
    return mapFromSource(proxyIndex);
}
