/*
    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 "util.h"
#include <KLocalizedString>
#include <KSelectionProxyModel>
#include <QDebug>
#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::SubTreesWithoutRoots);
    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
}

QString StoreModel::rootPath() const
{
    return m_fileSystemModel->rootPath();
}

void StoreModel::setRootPath(const QString &rootPath)
{
    QModelIndex rootDirIndex = m_fileSystemModel->setRootPath(rootPath);
    m_fileSystemModel->fetchMore(rootDirIndex);
    m_itemSelectionModel->select(m_addRoleModel->mapFromSource(rootDirIndex), QItemSelectionModel::ClearAndSelect);
}

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

    auto initial_value = QSortFilterProxyModel::data(index, role);

    if (role == Qt::DisplayRole) {
        QString name = initial_value.toString();
        name.replace(Util::endsWithGpg(), QString{});
        return name;
    }
    return initial_value;
}

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()) {
        destFileinfo = QFileInfo(rootPath());
    }
    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.absoluteDir());
    bool found = false;
    while (gpgIdPath.exists() && gpgIdPath.absolutePath().startsWith(rootPath())) {
        if (QFile(gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id"))).exists()) {
            found = true;
            break;
        }
        if (!gpgIdPath.cdUp())
            break;
    }
    QFile gpgId(found ? gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id")) : rootPath() + QStringLiteral(".gpg-id"));
    if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
        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();
    }
}
