/*
    SPDX-FileCopyrightText: 2012, 2013 Daniel Vrátil <dvratil@redhat.com>

    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/

#include "authwidget_p.h"
#include "account.h"
#include "accountinfo/accountinfo.h"
#include "accountinfo/accountinfofetchjob.h"
#include "private/newtokensfetchjob_p.h"
#include "../../debug.h"

#include <QWebEngineProfile>
#include <QWebEngineCertificateError>
#include <QContextMenuEvent>

#include <QtGlobal>
#include <QVBoxLayout>
#include <QLabel>
#include <QTimer>
#include <QMessageBox>
#include <QAbstractSocket>
#include <QUrlQuery>
#include <QDateTime>
#include <QWebEngineView>

using namespace KGAPI2;

namespace
{

class WebView : public QWebEngineView
{
    Q_OBJECT
public:
    explicit WebView(QWidget *parent = nullptr)
        : QWebEngineView(parent)
    {
        // Don't store cookies, so that subsequent invocations of AuthJob won't remember
        // the previous accounts.
        QWebEngineProfile::defaultProfile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies);
    }

    void contextMenuEvent(QContextMenuEvent *e) override
    {
        // No menu
        e->accept();
    }
};

class WebPage : public QWebEnginePage
{
    Q_OBJECT
public:
    explicit WebPage(QObject *parent = nullptr)
        : QWebEnginePage(parent)
        , mLastError(nullptr)
    {
    }

    QWebEngineCertificateError *lastCertificateError() const
    {
        return mLastError;
    }

    bool certificateError(const QWebEngineCertificateError &err) override
    {
        if (mLastError) {
            delete mLastError;
        }
        mLastError = new QWebEngineCertificateError(err.error(), err.url(), err.isOverridable(), err.errorDescription());
        Q_EMIT sslError();

        return false; // don't let it through
    }

Q_SIGNALS:
    void sslError();

private:
    QWebEngineCertificateError *mLastError;
};

}




AuthWidgetPrivate::AuthWidgetPrivate(AuthWidget *parent):
    QObject(),
    showProgressBar(true),
    progress(AuthWidget::None),
    q(parent)
{
}

AuthWidgetPrivate::~AuthWidgetPrivate()
{
}

void AuthWidgetPrivate::setSslIcon(const QString &iconName)
{
    // FIXME: workaround for silly Breeze icons: the small 22x22 icons are
    // monochromatic, which is absolutely useless since we are trying to security
    // information here, so instead we force use the bigger 48x48 icons which
    // have colors and downscale them
    sslIndicator->setIcon(QIcon::fromTheme(iconName).pixmap(48));
}

void AuthWidgetPrivate::setupUi()
{
    vbox = new QVBoxLayout(q);
    q->setLayout(vbox);

    label = new QLabel(q);
    label->setText(QLatin1String("<b>") % tr("Authorizing token. This should take just a moment...") % QLatin1String("</b>"));
    label->setWordWrap(true);
    label->setAlignment(Qt::AlignCenter);
    label->setVisible(false);
    vbox->addWidget(label);

    auto hbox = new QHBoxLayout;
    hbox->setSpacing(0);
    sslIndicator = new QToolButton(q);
    connect(sslIndicator, &QToolButton::clicked,
            this, [this]() {
                auto page = qobject_cast<WebPage*>(webview->page());
                if (auto err = page->lastCertificateError()) {
                    QMessageBox msg;
                    msg.setIconPixmap(QIcon::fromTheme(QStringLiteral("security-low")).pixmap(64));
                    msg.setText(err->errorDescription());
                    msg.addButton(QMessageBox::Ok);
                    msg.exec();
                }
            });
    hbox->addWidget(sslIndicator);
    urlEdit = new QLineEdit(q);
    urlEdit->setReadOnly(true);
    hbox->addWidget(urlEdit);
    vbox->addLayout(hbox);

    progressbar = new QProgressBar(q);
    progressbar->setMinimum(0);
    progressbar->setMaximum(100);
    progressbar->setValue(0);
    vbox->addWidget(progressbar);

    webview = new WebView(q);

    auto webpage = new WebPage(webview);
    connect(webpage, &WebPage::sslError,
            this, [this]() {
                setSslIcon(QStringLiteral("security-low"));
            });
    webview->setPage(webpage);

    vbox->addWidget(webview);
    connect(webview, &QWebEngineView::loadProgress, progressbar, &QProgressBar::setValue);
    connect(webview, &QWebEngineView::urlChanged, this, &AuthWidgetPrivate::webviewUrlChanged);
    connect(webview, &QWebEngineView::loadFinished, this, &AuthWidgetPrivate::webviewFinished);
}

void AuthWidgetPrivate::setUrl(const QUrl &url)
{
    webview->setUrl(url);
    webview->setFocus();
}

void AuthWidgetPrivate::setVisible(bool visible)
{
    sslIndicator->setVisible(visible);
    urlEdit->setVisible(visible);
    webview->setVisible(visible);
    if (showProgressBar && visible) {
        progressbar->setVisible(visible);
    } else {
        progressbar->setVisible(visible);
    }
}



void AuthWidgetPrivate::setProgress(AuthWidget::Progress progress)
{
    qCDebug(KGAPIDebug) << progress;
    this->progress = progress;
    Q_EMIT q->progress(progress);
}


void AuthWidgetPrivate::emitError(const enum Error errCode, const QString& msg)
{
    label->setVisible(true);
    sslIndicator->setVisible(false);
    urlEdit->setVisible(false);
    webview->setVisible(false);
    progressbar->setVisible(false);

    label->setText(QLatin1String("<b>") % msg % QLatin1String("</b>"));

    Q_EMIT q->error(errCode, msg);
    setProgress(AuthWidget::Error);
}


void AuthWidgetPrivate::webviewUrlChanged(const QUrl &url)
{
    qCDebug(KGAPIDebug) << "URLChange:" << url;

    // Whoa! That should not happen!
    if (url.scheme() != QLatin1String("https")) {
        QTimer::singleShot(0, this, [this, url]() {
            QUrl sslUrl = url;
            sslUrl.setScheme(QStringLiteral("https"));
            webview->setUrl(sslUrl);
        });
        return;
    }

    if (!isGoogleHost(url)) {
        // We handled SSL above, so we are secure. We are however outside of
        // accounts.google.com, which is a little suspicious in context of this class
        setSslIcon(QStringLiteral("security-medium"));
        return;
    }

    if (qobject_cast<WebPage*>(webview->page())->lastCertificateError()) {
        setSslIcon(QStringLiteral("security-low"));
    } else {
        // We have no way of obtaining current SSL certificate from QWebEngine, but we
        // handled SSL and accounts.google.com cases above and QWebEngine did not report
        // any SSL error to us, so we can assume we are safe.
        setSslIcon(QStringLiteral("security-high"));
    }


    // Username and password inputs are loaded dynamically, so we only get
    // urlChanged, but not urlFinished.
    if (isUsernameFrame(url)) {
        if (!username.isEmpty()) {
            webview->page()->runJavaScript(QStringLiteral("document.getElementById(\"identifierId\").value = \"%1\";").arg(username));
        }
    } else if (isPasswordFrame(url)) {
        if (!password.isEmpty()) {
            webview->page()->runJavaScript(QStringLiteral("var elems = document.getElementsByTagName(\"input\");"
                                                          "for (var i = 0; i < elems.length; i++) {"
                                                          "  if (elems[i].type == \"password\" && elems[i].name == \"password\") {"
                                                          "      elems[i].value = \"%1\";"
                                                          "      break;"
                                                          "  }"
                                                          "}").arg(password));
        }
    }
}

void AuthWidgetPrivate::webviewFinished(bool ok)
{
    if (!ok) {
        qCWarning(KGAPIDebug) << "Failed to load" << webview->url();
    }

    const QUrl url = webview->url();
    urlEdit->setText(url.toDisplayString(QUrl::PrettyDecoded));
    urlEdit->setCursorPosition(0);
    qCDebug(KGAPIDebug) << "URLFinished:" << url;
}

void AuthWidgetPrivate::socketError(QAbstractSocket::SocketError socketError)
{
    if (connection)
        connection->deleteLater();
    qCDebug(KGAPIDebug) << QStringLiteral("Socket error when receiving response: %1").arg(socketError);
    emitError(InvalidResponse, tr("Error receiving response: %1").arg(socketError));
}

void AuthWidgetPrivate::socketReady()
{
    Q_ASSERT(connection);
    const QByteArray data = connection->readLine();
    connection->write("HTTP/1.1 200 OK\n");
    connection->flush();
    connection->deleteLater();
    qCDebug(KGAPIDebug) << QStringLiteral("Got connection on socket");
    if (webview) { // when running in tests we don't have webview or any other widgets
        webview->stop();
    }
    setVisible(false);
    if (label) {
        label->setVisible(true);
    }

    const auto line = data.split(' ');
    if (line.size() != 3 || line.at(0) != QByteArray("GET") || !line.at(2).startsWith(QByteArray("HTTP/1.1"))) {
        qCDebug(KGAPIDebug) << QStringLiteral("Token response invalid");
        emitError(InvalidResponse, tr("Token response invalid"));
        return;
    }

    //qCDebug(KGAPIDebug) << "Receiving data on socket: " << data;
    const QUrl url(QString::fromLatin1(line.at(1)));
    const QUrlQuery query(url);
    const QString code = query.queryItemValue(QStringLiteral("code"));
    if (code.isEmpty()) {
        const QString error = query.queryItemValue(QStringLiteral("error"));
        if (!error.isEmpty()) {
            emitError(UnknownError, error);
            qCDebug(KGAPIDebug) << error;
        } else {
            qCDebug(KGAPIDebug) << QStringLiteral("Could not extract token from HTTP answer");
            emitError(InvalidResponse, tr("Could not extract token from HTTP answer"));
        }
        return;
    }

    Q_ASSERT(serverPort != -1);
    auto fetch = new KGAPI2::NewTokensFetchJob(code, apiKey, secretKey, serverPort);
    connect(fetch, &Job::finished, this, &AuthWidgetPrivate::tokensReceived);
}

void AuthWidgetPrivate::tokensReceived(KGAPI2::Job* job)
{
    KGAPI2::NewTokensFetchJob *tokensFetchJob = qobject_cast<KGAPI2::NewTokensFetchJob*>(job);

    account->setAccessToken(tokensFetchJob->accessToken());
    account->setRefreshToken(tokensFetchJob->refreshToken());
    account->setExpireDateTime(QDateTime::currentDateTime().addSecs(tokensFetchJob->expiresIn()));
    tokensFetchJob->deleteLater();

    KGAPI2::AccountInfoFetchJob *fetchJob = new KGAPI2::AccountInfoFetchJob(account, this);

    connect(fetchJob, &Job::finished,
            this, &AuthWidgetPrivate::accountInfoReceived);
    qCDebug(KGAPIDebug) << "Requesting AccountInfo";
}

void AuthWidgetPrivate::accountInfoReceived(KGAPI2::Job* job)
{
    if (job->error()) {
        qCDebug(KGAPIDebug) << "Error when retrieving AccountInfo:" << job->errorString();
        emitError((enum Error) job->error(), job->errorString());
        return;
    }

    KGAPI2::ObjectsList objects = qobject_cast<KGAPI2::AccountInfoFetchJob*>(job)->items();
    Q_ASSERT(!objects.isEmpty());

    KGAPI2::AccountInfoPtr accountInfo = objects.first().staticCast<KGAPI2::AccountInfo>();
    account->setAccountName(accountInfo->email());

    job->deleteLater();

    Q_EMIT q->authenticated(account);
    setProgress(AuthWidget::Finished);
}


#include "authwidget_p.moc"
