/*
    identitydialog.cpp

    This file is part of KMail, the KDE mail client.
    SPDX-FileCopyrightText: 2002 Marc Mutz <mutz@kde.org>
    SPDX-FileCopyrightText: 2014-2023 Laurent Montel <montel@kde.org>

    SPDX-License-Identifier: GPL-2.0-only
*/

#include "identitydialog.h"
#include "addressvalidationjob.h"
#include "identitymanager.h"
#include "kleo_util.h"

#include <QGpgME/Job>
#include <QGpgME/Protocol>

// other KMail headers:
#include <KEditListWidget>

#include <Sonnet/DictionaryComboBox>

// other kdepim headers:
#include "identity/identity.h"
#include "identity/signatureconfigurator.h"

#include <KLineEditEventHandler>

// libkleopatra:
#include <Libkleo/DefaultKeyFilter>
#include <Libkleo/DefaultKeyGenerationJob>
#include <Libkleo/KeySelectionCombo>
#include <Libkleo/ProgressDialog>

// gpgme++
#include <gpgme++/keygenerationresult.h>

// other KDE headers:
#include <KEmailAddress>
#include <KEmailValidator>
#include <KLineEdit>
#include <KLocalizedString>
#include <KMessageBox>

// Qt headers:
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFormLayout>
#include <QGroupBox>
#include <QHostInfo>
#include <QIcon>
#include <QLabel>
#include <QPushButton>
#include <QStandardPaths>
#include <QTabWidget>
#include <QToolButton>
#include <QVBoxLayout>

// other headers:
#include <algorithm>
#include <gpgme++/key.h>
#include <iterator>

namespace KMail
{
class KeySelectionCombo : public Kleo::KeySelectionCombo
{
    Q_OBJECT

public:
    enum KeyType {
        SigningKey,
        EncryptionKey,
    };

    explicit KeySelectionCombo(KeyType keyType, GpgME::Protocol protocol, QWidget *parent);
    ~KeySelectionCombo() override;

    void setIdentity(const QString &name, const QString &email);

    void init() override;

private:
    void onCustomItemSelected(const QVariant &type);
    QString mEmail;
    QString mName;
    const KeyType mKeyType;
    const GpgME::Protocol mProtocol;
};

class KeyGenerationJob : public QGpgME::Job
{
    Q_OBJECT

public:
    explicit KeyGenerationJob(const QString &name, const QString &email, KeySelectionCombo *parent);
    ~KeyGenerationJob() override;

    void slotCancel() override;
    void start();

private:
    void keyGenerated(const GpgME::KeyGenerationResult &result);
    const QString mName;
    const QString mEmail;
    QGpgME::Job *mJob = nullptr;
};

KeyGenerationJob::KeyGenerationJob(const QString &name, const QString &email, KeySelectionCombo *parent)
    : QGpgME::Job(parent)
    , mName(name)
    , mEmail(email)
{
}

KeyGenerationJob::~KeyGenerationJob() = default;

void KeyGenerationJob::slotCancel()
{
    if (mJob) {
        mJob->slotCancel();
    }
}

void KeyGenerationJob::start()
{
    auto job = new Kleo::DefaultKeyGenerationJob(this);
    connect(job, &Kleo::DefaultKeyGenerationJob::result, this, &KeyGenerationJob::keyGenerated);
    job->start(mEmail, mName);
    mJob = job;
}

void KeyGenerationJob::keyGenerated(const GpgME::KeyGenerationResult &result)
{
    mJob = nullptr;
    if (result.error()) {
        KMessageBox::error(qobject_cast<QWidget *>(parent()),
                           i18n("Error while generating new key pair: %1", QString::fromUtf8(result.error().asString())),
                           i18n("Key Generation Error"));
        Q_EMIT done();
        return;
    }

    auto combo = qobject_cast<KeySelectionCombo *>(parent());
    combo->setDefaultKey(QLatin1String(result.fingerprint()));
    connect(combo, &KeySelectionCombo::keyListingFinished, this, &KeyGenerationJob::done);
    combo->refreshKeys();
}

KeySelectionCombo::KeySelectionCombo(KeyType keyType, GpgME::Protocol protocol, QWidget *parent)
    : Kleo::KeySelectionCombo(parent)
    , mKeyType(keyType)
    , mProtocol(protocol)
{
}

KeySelectionCombo::~KeySelectionCombo() = default;

void KeySelectionCombo::setIdentity(const QString &name, const QString &email)
{
    mName = name;
    mEmail = email;
    setIdFilter(email);
}

void KeySelectionCombo::init()
{
    Kleo::KeySelectionCombo::init();

    std::shared_ptr<Kleo::DefaultKeyFilter> keyFilter(new Kleo::DefaultKeyFilter);
    keyFilter->setIsOpenPGP(mProtocol == GpgME::OpenPGP ? Kleo::DefaultKeyFilter::Set : Kleo::DefaultKeyFilter::NotSet);
    if (mKeyType == SigningKey) {
        keyFilter->setCanSign(Kleo::DefaultKeyFilter::Set);
        keyFilter->setHasSecret(Kleo::DefaultKeyFilter::Set);
    } else {
        keyFilter->setCanEncrypt(Kleo::DefaultKeyFilter::Set);
    }
    setKeyFilter(keyFilter);
    prependCustomItem(QIcon(), i18n("No key"), QStringLiteral("no-key"));
    if (mProtocol == GpgME::OpenPGP) {
        appendCustomItem(QIcon::fromTheme(QStringLiteral("password-generate")), i18n("Generate a new key pair"), QStringLiteral("generate-new-key"));
    }

    connect(this, &KeySelectionCombo::customItemSelected, this, &KeySelectionCombo::onCustomItemSelected);
}

void KeySelectionCombo::onCustomItemSelected(const QVariant &type)
{
    if (type == QLatin1String("no-key")) {
        return;
    } else if (type == QLatin1String("generate-new-key")) {
        auto job = new KeyGenerationJob(mName, mEmail, this);
        auto dlg = new Kleo::ProgressDialog(job, i18n("Generating new key pair..."), parentWidget());
        dlg->setModal(true);
        setEnabled(false);
        connect(job, &KeyGenerationJob::done, this, [this]() {
            setEnabled(true);
        });
        job->start();
    }
}

IdentityDialog::IdentityDialog(QWidget *parent)
    : QDialog(parent)
{
    setWindowTitle(i18nc("@title:window", "Edit Identity"));
    auto mainLayout = new QVBoxLayout(this);
    mainLayout->setContentsMargins({});

    auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
    QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
    okButton->setDefault(true);
    okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
    connect(buttonBox, &QDialogButtonBox::accepted, this, &IdentityDialog::slotAccepted);
    connect(buttonBox, &QDialogButtonBox::rejected, this, &IdentityDialog::reject);

    //
    // Tab Widget: General
    //
    auto page = new QWidget(this);
    mainLayout->addWidget(page);
    auto buttonBoxLayout = new QVBoxLayout;
    buttonBoxLayout->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
                                        style()->pixelMetric(QStyle::PM_LayoutTopMargin),
                                        style()->pixelMetric(QStyle::PM_LayoutRightMargin),
                                        style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
    buttonBoxLayout->addWidget(buttonBox);
    mainLayout->addLayout(buttonBoxLayout);
    auto vlay = new QVBoxLayout(page);
    vlay->setContentsMargins({});
    mTabWidget = new QTabWidget(page);
    mTabWidget->tabBar()->setExpanding(true);
    mTabWidget->setDocumentMode(true);
    mTabWidget->setObjectName(QStringLiteral("config-identity-tab"));
    vlay->addWidget(mTabWidget);

    auto tab = new QWidget(mTabWidget);
    mTabWidget->addTab(tab, i18nc("@title:tab General identity settings.", "General"));

    auto formLayout = new QFormLayout(tab);

    // "Name" line edit and label:
    mNameEdit = new QLineEdit(tab);
    KLineEditEventHandler::catchReturnKey(mNameEdit);
    auto label = new QLabel(i18n("&Your name:"), tab);
    formLayout->addRow(label, mNameEdit);
    label->setBuddy(mNameEdit);

    QString msg = i18n(
        "<qt><h3>Your name</h3>"
        "<p>This field should contain your name as you would like "
        "it to appear in the email header that is sent out;</p>"
        "<p>if you leave this blank your real name will not "
        "appear, only the email address.</p></qt>");
    label->setWhatsThis(msg);
    mNameEdit->setWhatsThis(msg);

    // "Organization" line edit and label:
    mOrganizationEdit = new QLineEdit(tab);
    KLineEditEventHandler::catchReturnKey(mOrganizationEdit);
    label = new QLabel(i18n("Organi&zation:"), tab);
    formLayout->addRow(label, mOrganizationEdit);
    label->setBuddy(mOrganizationEdit);

    msg = i18n(
        "<qt><h3>Organization</h3>"
        "<p>This field should have the name of your organization "
        "if you would like it to be shown in the email header that "
        "is sent out.</p>"
        "<p>It is safe (and normal) to leave this blank.</p></qt>");
    label->setWhatsThis(msg);
    mOrganizationEdit->setWhatsThis(msg);

    // "Dictionary" combo box and label:
    mDictionaryCombo = new Sonnet::DictionaryComboBox(tab);
    label = new QLabel(i18n("D&ictionary:"), tab);
    label->setBuddy(mDictionaryCombo);
    formLayout->addRow(label, mDictionaryCombo);

    // "Email Address" line edit and label:
    // (row 3: spacer)
    mEmailEdit = new QLineEdit(tab);
    mEmailEdit->setEnabled(false);
    KLineEditEventHandler::catchReturnKey(mEmailEdit);
    label = new QLabel(i18n("&Email address:"), tab);
    formLayout->addRow(label, mEmailEdit);
    label->setBuddy(mEmailEdit);

    msg = i18n(
        "<qt><h3>Email address</h3>"
        "<p>This field should have your full email address.</p>"
        "<p>This address is the primary one, used for all outgoing mail. "
        "If you have more than one address, either create a new identity, "
        "or add additional alias addresses in the field below.</p>"
        "<p>If you leave this blank, or get it wrong, people "
        "will have trouble replying to you.</p></qt>");
    label->setWhatsThis(msg);
    mEmailEdit->setWhatsThis(msg);

    auto emailValidator = new KEmailValidator(this);
    mEmailEdit->setValidator(emailValidator);

    //
    // Tab Widget: Security
    //
    mCryptographyTab = new QWidget(mTabWidget);
    mTabWidget->addTab(mCryptographyTab, i18nc("@title:tab", "Security"));
    formLayout = new QFormLayout(mCryptographyTab);

    // "OpenPGP Signature Key" requester and label:
    mPGPSigningKeyRequester = new KeySelectionCombo(KeySelectionCombo::SigningKey, GpgME::OpenPGP, mCryptographyTab);
    mPGPSigningKeyRequester->setObjectName("PGP Signing Key Requester");
    msg = i18n(
        "<qt><p>The OpenPGP key you choose here will be used "
        "to digitally sign messages. You can also use GnuPG keys.</p>"
        "<p>You can leave this blank, but KMail will not be able "
        "to digitally sign emails using OpenPGP; "
        "normal mail functions will not be affected.</p>"
        "<p>You can find out more about keys at <a>https://www.gnupg.org</a></p></qt>");
    label = new QLabel(i18n("OpenPGP signing key:"), mCryptographyTab);
    label->setBuddy(mPGPSigningKeyRequester);
    mPGPSigningKeyRequester->setWhatsThis(msg);
    label->setWhatsThis(msg);

    auto vbox = new QVBoxLayout;
    mPGPSameKey = new QCheckBox(i18n("Use same key for encryption and signing"));
    vbox->addWidget(mPGPSigningKeyRequester);
    vbox->addWidget(mPGPSameKey);
    formLayout->addRow(label, vbox);

    connect(mPGPSameKey, &QCheckBox::toggled, this, [=](bool checked) {
        mPGPEncryptionKeyRequester->setVisible(!checked);
        formLayout->labelForField(mPGPEncryptionKeyRequester)->setVisible(!checked);
        const auto label = qobject_cast<QLabel *>(formLayout->labelForField(vbox));
        if (checked) {
            label->setText(i18n("OpenPGP key:"));
            const auto key = mPGPSigningKeyRequester->currentKey();
            if (!key.isBad()) {
                mPGPEncryptionKeyRequester->setCurrentKey(key);
            } else if (mPGPSigningKeyRequester->currentData() == QLatin1String("no-key")) {
                mPGPEncryptionKeyRequester->setCurrentIndex(mPGPSigningKeyRequester->currentIndex());
            }
        } else {
            label->setText(i18n("OpenPGP signing key:"));
        }
    });
    connect(mPGPSigningKeyRequester, &KeySelectionCombo::currentKeyChanged, this, [&](const GpgME::Key &key) {
        if (mPGPSameKey->isChecked()) {
            mPGPEncryptionKeyRequester->setCurrentKey(key);
        }
    });
    connect(mPGPSigningKeyRequester, &KeySelectionCombo::customItemSelected, this, [&](const QVariant &type) {
        if (mPGPSameKey->isChecked() && type == QLatin1String("no-key")) {
            mPGPEncryptionKeyRequester->setCurrentIndex(mPGPSigningKeyRequester->currentIndex());
        }
    });
    connect(mPGPSigningKeyRequester, &KeySelectionCombo::keyListingFinished, this, [this] {
        slotKeyListingFinished(mPGPSigningKeyRequester);
    });

    // "OpenPGP Encryption Key" requester and label:
    mPGPEncryptionKeyRequester = new KeySelectionCombo(KeySelectionCombo::EncryptionKey, GpgME::OpenPGP, mCryptographyTab);
    msg = i18n(
        "<qt><p>The OpenPGP key you choose here will be used "
        "to encrypt messages to yourself and for the \"Attach My Public Key\" "
        "feature in the composer. You can also use GnuPG keys.</p>"
        "<p>You can leave this blank, but KMail will not be able "
        "to encrypt copies of outgoing messages to you using OpenPGP; "
        "normal mail functions will not be affected.</p>"
        "<p>You can find out more about keys at <a>https://www.gnupg.org</a></p></qt>");
    label = new QLabel(i18n("OpenPGP encryption key:"), mCryptographyTab);
    label->setBuddy(mPGPEncryptionKeyRequester);
    label->setWhatsThis(msg);
    mPGPEncryptionKeyRequester->setWhatsThis(msg);

    formLayout->addRow(label, mPGPEncryptionKeyRequester);

    // "S/MIME Signature Key" requester and label:
    mSMIMESigningKeyRequester = new KeySelectionCombo(KeySelectionCombo::SigningKey, GpgME::CMS, mCryptographyTab);
    mSMIMESigningKeyRequester->setObjectName("SMIME Signing Key Requester");
    msg = i18n(
        "<qt><p>The S/MIME (X.509) certificate you choose here will be used "
        "to digitally sign messages.</p>"
        "<p>You can leave this blank, but KMail will not be able "
        "to digitally sign emails using S/MIME; "
        "normal mail functions will not be affected.</p></qt>");
    label = new QLabel(i18n("S/MIME signing certificate:"), mCryptographyTab);
    label->setBuddy(mSMIMESigningKeyRequester);
    mSMIMESigningKeyRequester->setWhatsThis(msg);
    label->setWhatsThis(msg);
    formLayout->addRow(label, mSMIMESigningKeyRequester);
    connect(mSMIMESigningKeyRequester, &KeySelectionCombo::keyListingFinished, this, [this] {
        slotKeyListingFinished(mSMIMESigningKeyRequester);
    });

    const QGpgME::Protocol *smimeProtocol = QGpgME::smime();

    label->setEnabled(smimeProtocol);
    mSMIMESigningKeyRequester->setEnabled(smimeProtocol);

    // "S/MIME Encryption Key" requester and label:
    mSMIMEEncryptionKeyRequester = new KeySelectionCombo(KeySelectionCombo::EncryptionKey, GpgME::CMS, mCryptographyTab);
    mSMIMEEncryptionKeyRequester->setObjectName("SMIME Encryption Key Requester");
    msg = i18n(
        "<qt><p>The S/MIME certificate you choose here will be used "
        "to encrypt messages to yourself and for the \"Attach My Certificate\" "
        "feature in the composer.</p>"
        "<p>You can leave this blank, but KMail will not be able "
        "to encrypt copies of outgoing messages to you using S/MIME; "
        "normal mail functions will not be affected.</p></qt>");
    label = new QLabel(i18n("S/MIME encryption certificate:"), mCryptographyTab);
    label->setBuddy(mSMIMEEncryptionKeyRequester);
    mSMIMEEncryptionKeyRequester->setWhatsThis(msg);
    connect(mSMIMEEncryptionKeyRequester, &KeySelectionCombo::keyListingFinished, this, [this] {
        slotKeyListingFinished(mSMIMEEncryptionKeyRequester);
    });
    label->setWhatsThis(msg);

    formLayout->addRow(label, mSMIMEEncryptionKeyRequester);

    label->setEnabled(smimeProtocol);
    mSMIMEEncryptionKeyRequester->setEnabled(smimeProtocol);

    mWarnNotEncrypt = new QCheckBox(i18n("Warn when trying to send unencrypted messages"));
    formLayout->addRow(QString(), mWarnNotEncrypt);

    //
    // Tab Widget: Signature
    //
    mSignatureConfigurator = new KIdentityManagementWidgets::SignatureConfigurator(mTabWidget);
    mTabWidget->addTab(mSignatureConfigurator, i18n("Signature"));
}

IdentityDialog::~IdentityDialog() = default;

void IdentityDialog::slotAccepted()
{
    // Validate email addresses
    const QString email = mEmailEdit->text().trimmed();
    if (email.isEmpty()) {
        KMessageBox::error(this, i18n("You must provide an email for this identity."), i18nc("@title:window", "Empty Email Address"));
        return;
    }
    if (!KEmailAddress::isValidSimpleAddress(email)) {
        const QString errorMsg(KEmailAddress::simpleEmailAddressErrorMsg());
        KMessageBox::error(this, errorMsg, i18n("Invalid Email Address"));
        return;
    }

    const GpgME::Key &pgpSigningKey = mPGPSigningKeyRequester->currentKey();
    const GpgME::Key &pgpEncryptionKey = mPGPEncryptionKeyRequester->currentKey();
    const GpgME::Key &smimeSigningKey = mSMIMESigningKeyRequester->currentKey();
    const GpgME::Key &smimeEncryptionKey = mSMIMEEncryptionKeyRequester->currentKey();

    QString msg;
    bool err = false;
    if (!keyMatchesEmailAddress(pgpSigningKey, email)) {
        msg = i18n(
            "One of the configured OpenPGP signing keys does not contain "
            "any user ID with the configured email address for this "
            "identity (%1).\n"
            "This might result in warning messages on the receiving side "
            "when trying to verify signatures made with this configuration.",
            email);
        err = true;
    } else if (!keyMatchesEmailAddress(pgpEncryptionKey, email)) {
        msg = i18n(
            "One of the configured OpenPGP encryption keys does not contain "
            "any user ID with the configured email address for this "
            "identity (%1).",
            email);
        err = true;
    } else if (!keyMatchesEmailAddress(smimeSigningKey, email)) {
        msg = i18n(
            "One of the configured S/MIME signing certificates does not contain "
            "the configured email address for this "
            "identity (%1).\n"
            "This might result in warning messages on the receiving side "
            "when trying to verify signatures made with this configuration.",
            email);
        err = true;
    } else if (!keyMatchesEmailAddress(smimeEncryptionKey, email)) {
        msg = i18n(
            "One of the configured S/MIME encryption certificates does not contain "
            "the configured email address for this "
            "identity (%1).",
            email);
        err = true;
    }

    if (err
        && KMessageBox::warningContinueCancel(this,
                                              msg,
                                              i18nc("@title:window", "Email Address Not Found in Key/Certificates"),
                                              KStandardGuiItem::cont(),
                                              KStandardGuiItem::cancel(),
                                              QStringLiteral("warn_email_not_in_certificate"))
            != KMessageBox::Continue) {
        return;
    }

    accept();
}

bool IdentityDialog::keyMatchesEmailAddress(const GpgME::Key &key, const QString &email_)
{
    if (key.isNull()) {
        return true;
    }
    const QString email = email_.trimmed().toLower();
    const auto uids = key.userIDs();
    for (const auto &uid : uids) {
        QString em = QString::fromUtf8(uid.email() ? uid.email() : uid.id());
        if (em.isEmpty()) {
            continue;
        }
        if (em[0] == QLatin1Char('<')) {
            em = em.mid(1, em.length() - 2);
        }
        if (em.toLower() == email) {
            return true;
        }
    }

    return false;
}

void IdentityDialog::setIdentity(KIdentityManagementCore::Identity &ident)
{
    setWindowTitle(i18nc("@title:window", "Edit Identity \"%1\"", ident.identityName()));

    // "General" tab:
    mNameEdit->setText(ident.fullName());
    mOrganizationEdit->setText(ident.organization());
    mEmailEdit->setText(ident.primaryEmailAddress());
    mDictionaryCombo->setCurrentByDictionaryName(ident.dictionary());

    // "Cryptography" tab:
    mPGPSigningKeyRequester->setDefaultKey(QLatin1String(ident.pgpSigningKey()));
    mPGPEncryptionKeyRequester->setDefaultKey(QLatin1String(ident.pgpEncryptionKey()));

    mPGPSameKey->setChecked(ident.pgpSigningKey() == ident.pgpEncryptionKey());

    mSMIMESigningKeyRequester->setDefaultKey(QLatin1String(ident.smimeSigningKey()));
    mSMIMEEncryptionKeyRequester->setDefaultKey(QLatin1String(ident.smimeEncryptionKey()));

    mWarnNotEncrypt->setChecked(ident.warnNotEncrypt());

    // "Signature" tab:
    mSignatureConfigurator->setImageLocation(ident);
    mSignatureConfigurator->setSignature(ident.signature());

    // set the configured email address as initial query of the key
    // requesters:
    const QString name = mNameEdit->text().trimmed();
    const QString email = mEmailEdit->text().trimmed();

    mPGPEncryptionKeyRequester->setIdentity(name, email);
    mPGPSigningKeyRequester->setIdentity(name, email);
    mSMIMEEncryptionKeyRequester->setIdentity(name, email);
    mSMIMESigningKeyRequester->setIdentity(name, email);
}

void IdentityDialog::updateIdentity(KIdentityManagementCore::Identity &ident)
{
    // "General" tab:
    ident.setFullName(mNameEdit->text());
    ident.setOrganization(mOrganizationEdit->text());
    QString email = mEmailEdit->text().trimmed();
    ident.setPrimaryEmailAddress(email);

    // "Cryptography" tab:
    ident.setPGPSigningKey(mPGPSigningKeyRequester->currentKey().primaryFingerprint());
    ident.setPGPEncryptionKey(mPGPEncryptionKeyRequester->currentKey().primaryFingerprint());
    ident.setSMIMESigningKey(mSMIMESigningKeyRequester->currentKey().primaryFingerprint());
    ident.setSMIMEEncryptionKey(mSMIMEEncryptionKeyRequester->currentKey().primaryFingerprint());
    ident.setEncryptionOverride(true);
    ident.setWarnNotEncrypt(mWarnNotEncrypt->isChecked());
    ident.setWarnNotEncrypt(mWarnNotEncrypt->isChecked());

    // "Advanced" tab:
    ident.setDictionary(mDictionaryCombo->currentDictionaryName());

    // "Signature" tab:
    ident.setSignature(mSignatureConfigurator->signature());
}

void IdentityDialog::slotKeyListingFinished(KeySelectionCombo *combo)
{
    mInitialLoadingFinished << combo;

    if (mInitialLoadingFinished.count() == 2) {
        Q_EMIT keyListingFinished();
    }
}

}

#include "identitydialog.moc"

#include "moc_identitydialog.cpp"
