/* Copyright (C) 2018 by Intevation GmbH <info@intevation.de>
 *
 * This file is free software under the GNU GPL (v>=2)
 * and comes with ABSOLUTELY NO WARRANTY!
 * See LICENSE.txt for details.
 */

#include "gpgolkeyadder.h"
#include "w32-gettext.h"
#include "w32-util.h"
#include "w32-qt-util.h"
#include "stdinreader.h"

#include <QLabel>
#include <QWidget>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QCommandLineParser>
#include <QCheckBox>
#include <QDebug>
#include <QTextEdit>
#include <QFont>
#include <QFontMetrics>
#include <QMessageBox>
#include <QDateTime>
#include <QLocale>
#include <QTabWidget>
#include <QToolTip>
#include <QThread>
#include <QProgressDialog>

#include <gpgme++/context.h>
#include <gpgme++/keylistresult.h>
#include <gpgme++/importresult.h>
#include <gpgme++/data.h>
#include <gpgme++/key.h>

#include <iostream>

namespace
{

static char **vector_to_cArray(const std::vector<std::string> &vec)
{
  char ** ret = (char**) malloc (sizeof (char*) * (vec.size() + 1));
  for (size_t i = 0; i < vec.size(); i++)
    {
      ret[i] = strdup (vec[i].c_str());
    }
  ret[vec.size()] = NULL;
  return ret;
}

static void release_cArray (char **carray)
{
  if (carray)
    {
      for (int idx = 0; carray[idx]; idx++)
        {
          free (carray[idx]);
        }
      free (carray);
    }
}

class CMSImportThread: public QThread
{
    Q_OBJECT

    public:
        explicit CMSImportThread(const GpgME::Data &data):
            mData(data)
        {
        }

        std::vector<GpgME::Key> certs()
        {
            return mCerts;
        }

        std::string error()
        {
            return mError.asString();
        }

    protected:
        void run() override
        {
            auto ctx = GpgME::Context::create(GpgME::CMS);

            if (!ctx) {
                qDebug () << "No ctx";
                return;
            }

            const auto result = ctx->importKeys(mData);
            mError = result.error ();
            if (result.error ()) {
                qDebug() << "Import failed: " << result.error().asString();
            }
            std::vector<std::string> fingerprints;

            for (const auto import: result.imports()) {
                if (import.error()){
                    qDebug() << "Error importing:" << import.error().asString();
                    continue;
                }
                const char *fpr = import.fingerprint ();
                if (!fpr)
                {
                    qDebug() << "Import with no fpr.";
                    continue;
                }
                fingerprints.push_back (fpr);
                qDebug () << "imported: " << fpr;
            }
            if (!fingerprints.size()) {
                qDebug () << "Nothing imported";
                return;
            }
            ctx = GpgME::Context::create(GpgME::CMS);
            ctx->setKeyListMode (GpgME::KeyListMode::Local |
                                 GpgME::KeyListMode::Validate);

            char **patterns = vector_to_cArray(fingerprints);
            mError = ctx->startKeyListing((const char**)patterns, false);
            release_cArray(patterns);
            if (mError) {
                qDebug() << "Failed to start keylisting err:" << mError.asString();
                return;
            }

            GpgME::Error err;
            while (!err) {
                const auto key = ctx->nextKey(err);
                if (err || key.isNull()) {
                    break;
                }
                mCerts.push_back(key);
            }
            return;
        }

    private:
        GpgME::Error mError;
        GpgME::Data mData;
        std::vector<GpgME::Key> mCerts;
};
} // Namespace


GpgOLKeyAdder::GpgOLKeyAdder(const QCommandLineParser &parser):
  QDialog(nullptr),
  mEdit(new QTextEdit),
  mCMSEdit(new QTextEdit),
  mAlwaysSec(new QCheckBox)
{
    setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint));

    mName = parser.value(QStringLiteral("username"));
    mShowCMS = parser.isSet(QStringLiteral("cms"));

    setWindowTitle(_("GpgOL") + QStringLiteral(" - ") + _("Settings for:") +
                   QStringLiteral(" ") + mName);

    setWindowIcon(QIcon(":/gpgol-icon.svg"));

    const auto hwnd = parser.value(QStringLiteral("hwnd"));
    if (!hwnd.isEmpty()) {
        bool ok;
        WId id = (WId) hwnd.toInt(&ok);
        if (!ok) {
            qDebug() << "invalid hwnd value";
        } else {
            W32::setupForeignParent(id, this, true);
            setModal(true);
        }
    }

    mAlwaysSec->setChecked(parser.isSet("encrypt") && parser.isSet("sign"));

    /* FIXME: This should be implemented in GpgOL */
    mAlwaysSec->setVisible(false);

    setupGUI();

    // Deletes itself
    auto reader = new StdinReader;

    connect (reader, &StdinReader::stdinRead, this, [this] (const QByteArray &data) {
            if (data.size() > 1) {
                handleInput(data);
            }
        });
    reader->start();
}

void GpgOLKeyAdder::handleInput(const QByteArray &data) {
    const auto stringData = QString::fromUtf8(data);
    const auto splitData = stringData.split("BEGIN CMS DATA\n");
    if (splitData.size() != 2) {
        qDebug() << "Failed to split data. Old GpgOL Version?";
        mEdit->setPlainText(stringData);
    } else {
        mEdit->setPlainText(splitData[0].trimmed());
        mCMSEdit->setPlainText(splitData[1].trimmed());
    }
}

void GpgOLKeyAdder::setupGUI()
{
    /* Setup Edits */
    auto fixedFont = QFont("Monospace", 10);
    fixedFont.setStyleHint(QFont::TypeWriter);

    resize(QFontMetrics(fixedFont).averageCharWidth() * 80,
           QFontMetrics(fixedFont).height() * 30);


    mEdit->setFont(fixedFont);
    mEdit->setPlaceholderText(QString::fromUtf8(_("Paste a public key export here. It should look like:")) +
                              QStringLiteral("\n\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n"
                                "mQENBEzJavsBCADG/guWL6AxGgngUxp/DcmoitJjaJMqcJkBtD3uKrW81Pbnm3LI\n"
                                "...\n"
                                "dCl8hHggB9x2\n"
                                "=oShe\n"
                                "-----END PGP PUBLIC KEY BLOCK-----"));
    mEdit->setUndoRedoEnabled(true);

    mCMSEdit->setFont(fixedFont);
    mCMSEdit->setPlaceholderText(QString::fromUtf8(_("Paste certificates here. They should look like:")) +
                              QStringLiteral("\n\n-----BEGIN CERTIFICATE-----\n\n"
                                "MIICeDCCAeGgAwIBAgIJANNFIDoYY4XJMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV\n"
                                "...\n"
                                "dCl8hHggB9x2\n"
                                "-----END CERTIFICATE-----"));
    mCMSEdit->setUndoRedoEnabled(true);

    /* Setup buttons */
    QDialogButtonBox *buttonBox = new QDialogButtonBox();
    buttonBox->setStandardButtons(QDialogButtonBox::Cancel |
                                  QDialogButtonBox::Ok);

    connect(buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked,
            this, [this] () {
        checkAccept();
    });
    connect(buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked,
            this, [this] () {
        qApp->quit();
    });

    auto okBtn = buttonBox->button(QDialogButtonBox::Ok);
    okBtn->setEnabled(false);
    connect(mEdit, &QTextEdit::textChanged, this, [this, okBtn] () {
        const auto text = mEdit->toPlainText().trimmed().toUtf8();
        if (!text.size()) {
            okBtn->setEnabled(true);
            return;
        }
        GpgME::Data data(text.constData(), text.size(), false);

        okBtn->setEnabled(data.type() == GpgME::Data::PGPKey);
    });
    connect(mCMSEdit, &QTextEdit::textChanged, this, [this, okBtn] () {
        const auto text = mCMSEdit->toPlainText().trimmed().toUtf8();
        if (!text.size()) {
            okBtn->setEnabled(true);
            return;
        }
        GpgME::Data data(text.constData(), text.size(), false);

        okBtn->setEnabled(data.type() == GpgME::Data::X509Cert);
    });

    /* The always secure box */
    mAlwaysSec->setText(QString::fromUtf8(_("Always secure mails")));

    /* Setup layout */
    auto layout = new QVBoxLayout;
    setLayout(layout);

    layout->addWidget(mAlwaysSec);

    auto hBox = new QHBoxLayout;

    hBox->addWidget(new QLabel(
        _("Use these keys for this contact:")));

    auto infoBtn = new QPushButton;
    infoBtn->setIcon(QIcon::fromTheme("help-contextual"));
    infoBtn->setFlat(true);
    hBox->addWidget(infoBtn);
    hBox->addStretch(1);

    connect(infoBtn, &QPushButton::clicked, this, [this, infoBtn] () {
            const QString generalMsg = QString::fromUtf8(_("You can use this to override the keys "
                                          "for this contact. The keys will be imported and used "
                                          "regardless of their trust level."));
            const QString smimeMsg = QString::fromUtf8(_("For S/MIME the root certificate has to be trusted."));
            const QString multiMsg = QString::fromUtf8(_("Place multiple keys in here to encrypt to all of them."));
            QToolTip::showText(infoBtn->mapToGlobal(QPoint()) + QPoint(infoBtn->width(), 0),
                    QStringLiteral("%1<br/><br/>%2").arg(generalMsg).arg(
                      (mShowCMS ? smimeMsg + QStringLiteral("<br/><br/>") + multiMsg :
                       multiMsg)), infoBtn, QRect(), 30000);
        });


    layout->addLayout(hBox);

    if (mShowCMS) {
        auto tab = new QTabWidget;
        tab->addTab(mEdit, QStringLiteral("OpenPGP"));
        tab->addTab(mCMSEdit, QString::fromUtf8(_("S/MIME (X509 Certificates)")));
        layout->addWidget(tab);
    } else {
        layout->addWidget(mEdit);
    }
    layout->addWidget(buttonBox);
}

void GpgOLKeyAdder::save()
{
    std::cout << "BEGIN KEYADDER PGP DATA" << std::endl;
    const QByteArray &pgpData = mEdit->toPlainText().trimmed().toUtf8();
    if (!pgpData.size()) {
        /* Empty is a special case which can mean that an
         * existing key should be removed. */
        std::cout << "empty" << std::endl;
    } else {
        std::cout << pgpData.constData() << std::endl;
    }
    std::cout << "END KEYADDER PGP DATA" << std::endl;

    std::cout << "BEGIN KEYADDER CMS DATA" << std::endl;
    const QByteArray &cmsData = mCMSEdit->toPlainText().trimmed().toUtf8();
    if (!cmsData.size()) {
        /* Empty is a special case which can mean that an
         * existing key should be removed. */
        std::cout << "empty" << std::endl;
    } else {
        std::cout << cmsData.constData() << std::endl;
    }
    std::cout << "END KEYADDER CMS DATA" << std::endl;
    std::cout << "BEGIN KEYADDER OPTIONS" << std::endl;
    std::cout << "secure=" << (mAlwaysSec->isChecked() ? "3" : "0") << std::endl;
    std::cout << "END KEYADDER OPTIONS" << std::endl;

    qApp->quit();
}

static QString prettyNameAndEMail(const QString &id, const QString &name, const QString &email, const QString &comment)
{
    if (name.isEmpty()) {
        if (email.isEmpty()) {
            return QString();
        } else if (comment.isEmpty()) {
            return QStringLiteral("<%1>").arg(email);
        } else {
            return QStringLiteral("(%2) <%1>").arg(email, comment);
        }
    }
    if (email.isEmpty()) {
        if (comment.isEmpty()) {
            return name;
        } else {
            return QStringLiteral("%1 (%2)").arg(name, comment);
        }
    }
    if (comment.isEmpty()) {
        return QStringLiteral("%1 <%2>").arg(name, email);
    } else {
        return QStringLiteral("%1 (%3) <%2>").arg(name, email, comment);
    }
    return QString();
}

static QString prettyNameAndEMail(const char *id, const char *name_, const std::string &email_, const char *comment_)
{
    return prettyNameAndEMail(QString::fromUtf8(id), QString::fromUtf8(name_), QString::fromStdString(email_),
            QString::fromUtf8(comment_));
}

static QString prettyNameAndEMail(const GpgME::UserID &uid)
{
    return prettyNameAndEMail(uid.id(), uid.name(), uid.addrSpec(), uid.comment());
}

static QString time_t2string(time_t t)
{
    QDateTime dt;
    dt.setSecsSinceEpoch(t);
    return QLocale().toString(dt, QLocale::ShortFormat);
}

static QStringList
buildKeyInfos(const std::vector<GpgME::Key> keys, QWidget *parent,
              bool *bOk)
{
    QStringList keyInfos;

    if (!bOk) {
        qDebug() << "Invalid call";
        return keyInfos;
    }
    *bOk = false;

    for (const auto &key: keys) {
        if (key.isNull() || !key.numSubkeys()) {
            qDebug() << "Null key?";
            continue;
        }

        if (key.hasSecret() && key.protocol() == GpgME::OpenPGP) {
            QMessageBox::warning(parent, QString::fromUtf8 (_("Error")),
                    QStringLiteral("<b>%1</b><br/><br/>").arg(QString::fromUtf8(_("Secret key detected."))) +
                    QString::fromUtf8(_("You can only configure public keys in Outlook."
                            " Import secret keys with Kleopatra.")));
            return QStringList();
        }
        if (key.isBad() || !key.canEncrypt()) {
            QMessageBox::warning(parent, QString::fromUtf8 (_("Error")),
                    QStringLiteral("<b>%1</b><br/><br/>%2<br/><br/>").arg(QString::fromUtf8(_("Invalid key detected."))).arg(
                    key.primaryFingerprint()) +
                    QString::fromUtf8(_("The key is unusable for Outlook."
                            " Please check Kleopatra for more information.")));
            return QStringList();
        }
        const auto subkey = key.subkey(0);
        QString info = QString::fromLatin1(key.primaryFingerprint()) + "<br/>" +
                QString::fromUtf8(_("Created:")) + " " + time_t2string(subkey.creationTime()) + "<br/>" +
                QString::fromUtf8(_("User Ids:")) + "<br/>";

        for (const auto &uid: key.userIDs()) {
            if (uid.isNull() || uid.isRevoked() || uid.isInvalid()) {
                continue;
            }
            info += "&nbsp;&nbsp;" + prettyNameAndEMail(uid).toHtmlEscaped() + "<br/>";
        }
        keyInfos << info;
    }
    *bOk = true;
    return keyInfos;
}

void GpgOLKeyAdder::checkAcceptBottom(const std::vector<GpgME::Key> &pgpKeys,
                                      const std::vector<GpgME::Key> &cmsCerts,
                                      const std::string &errString)
{
    bool ok;
    QString msg;

    if (!pgpKeys.size() && (mShowCMS && !pgpKeys.size() && !cmsCerts.size())) {
        QMessageBox::warning(this, QString::fromUtf8 (_("Error")),
                QStringLiteral("<b>%1</b><br/><br/>").arg(QString::fromUtf8(_("Failed to parse any public key.")) +
                    (errString.empty() ? QStringLiteral("") : QStringLiteral("<br><br>%1").arg(QString::fromStdString(errString)))));
        return;
    }

    if (pgpKeys.size())
    {
        const auto keyInfos = buildKeyInfos (pgpKeys, this, &ok);
        if (!ok) {
            return;
        }
        msg += QString::fromUtf8(_("You are about to configure the following OpenPGP %1 for:")).arg(
                (keyInfos.size() > 1 ? QString::fromUtf8(_("keys")) : QString::fromUtf8(_("key")))) +
            QStringLiteral("<br/>\t\"%1\"<br/><br/>").arg(mName.toHtmlEscaped()) +
            keyInfos.join("<br/><br/>");

    }
    if (mShowCMS && cmsCerts.size())
    {
        std::vector<GpgME::Key> leaves;
        std::remove_copy_if(cmsCerts.begin(), cmsCerts.end(),
                std::back_inserter(leaves),
                [cmsCerts] (const auto &k) {
                    /* Check if a key has this fingerprint in the
                     * chain ID. Meaning that there is any child of
                     * this certificate. In that case remove it. */
                    for (const auto &c: cmsCerts) {
                        if (!c.chainID()) {
                            continue;
                        }
                        if (!k.primaryFingerprint() || !c.primaryFingerprint()) {
                            /* WTF? */
                            continue;
                        }
                        if (!strcmp (c.chainID(), k.primaryFingerprint())) {
                            qDebug() << "Filtering out non leaf cert" << k.primaryFingerprint();
                            return true;
                        }
                    }
                    return false;
                });

        const auto certInfos = buildKeyInfos (leaves, this, &ok);
        if (!ok) {
            return;
        }
        msg += QString::fromUtf8(_("You are about to configure the following S/MIME %1 for:")).arg(
                (certInfos.size() > 1 ? QString::fromUtf8(_("certificates")) : QString::fromUtf8(_("certificate")))) +
                QStringLiteral("<br/>\t\"%1\"<br/><br/>").arg(mName.toHtmlEscaped()) +
            certInfos.join("<br/><br/>");
    }

    if (!msg.isEmpty())
    {
        msg += "<br/><br/>" + QString::fromUtf8 (_("Continue?"));
        const auto ret = QMessageBox::question(this, QString::fromUtf8(_("Confirm")), msg,
                QMessageBox::Yes | QMessageBox::Abort, QMessageBox::Yes);
        if (ret == QMessageBox::Yes) {
            save();
            return;
        }
    }

    save();
    return;
}

void GpgOLKeyAdder::checkAccept()
{
    const auto openpgpText = mEdit->toPlainText().trimmed().toUtf8();
    auto cmsText = mCMSEdit->toPlainText().trimmed().toUtf8();
    if (!openpgpText.size() && !cmsText.size()) {
        save();
        return;
    }

    /* Otherwise we get an BER error */
    cmsText += '\n';

    GpgME::Data pgp(openpgpText.constData(), openpgpText.size(), false);
    GpgME::Data cms(cmsText.constData(), cmsText.size(), false);

    const auto keys = pgp.toKeys(GpgME::OpenPGP);

    if (!mShowCMS || !cmsText.size()) {
        checkAcceptBottom (keys, std::vector<GpgME::Key>(), std::string());
        return;
    }

    auto progress = new QProgressDialog(this, Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::Dialog);
    progress->setAutoClose(true);
    progress->setMinimumDuration(0);
    progress->setMaximum(0);
    progress->setMinimum(0);
    progress->setModal(true);
    progress->setCancelButton(nullptr);
    progress->setWindowTitle(_("Validating S/MIME certificates"));
    progress->setLabel(new QLabel(_("This may take several minutes...")));
    auto workerThread = new CMSImportThread(cms);
    connect(workerThread, &QThread::finished, this, [this, workerThread, progress, keys] {
            progress->accept();
            progress->deleteLater();
            checkAcceptBottom(keys, workerThread->certs(), workerThread->error());
            delete workerThread;
        });
    workerThread->start();
    progress->exec();
}

#include "gpgolkeyadder.moc"
