/**************************************************************************
** Piperka Client
** Copyright (C) 2019  Kari Pahula
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License along
** with this program; if not, write to the Free Software Foundation, Inc.,
** 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
**************************************************************************/

#include <QJsonDocument>
#include <QJsonObject>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkCookie>
#include <QtNetwork/QNetworkCookieJar>
#include <QtNetwork/QNetworkReply>
#include <QtQuick/QQuickView>
#include <QSettings>

#include "page.h"
#include "subscription.h"
#include "user.h"

User::User(Download &download, QObject *parent)
    : QObject(parent)
    , m_download(download)
    , timer(this)
    , syncAvailableTimer(this)
{
    QSettings settings;
    m_browseHelpSeen = settings.value("help/browseOffsetSeen", false).toBool();
    m_offsetBack = settings.value("app/offsetBack", false).toBool();
    QString name = settings.value("account/name").value<QString>();
    if (!name.isEmpty()) {
        m_name = name;
        m_token = settings.value("account/token").value<QByteArray>();
        QByteArray session = settings.value("account/session").value<QByteArray>();
        m_download.setSession(session);
    } else {
        QMap<QString, QVariant> tmp = settings.value("local/bookmarks").value<QMap<QString, QVariant>>();
        for (QMap<QString, QVariant>::const_iterator iter = tmp.cbegin(); iter != tmp.cend(); ++iter) {
            int cid = iter.key().toInt();
            bool ordOk;
            int ord = iter.value().toInt(&ordOk);
            if (cid > 0 && ordOk)
                localBM[cid] = ord;
        }
    }
    timer.setSingleShot(true);
    connect(&timer, &QTimer::timeout, this,
            [=]() {
        User::syncNow(false);
    });
    connect(&syncAvailableTimer, &QTimer::timeout, this,
            [=]() {
        m_syncAvailable = true;
        emit syncAvailableChanged();
    });
}

void User::emitIfLoggedin()
{
    if (!m_name.isEmpty()) {
        emit loggedChange();
    }
}

void User::setBrowseHelpSeen(const bool &seen)
{
    QSettings settings;
    m_browseHelpSeen = seen;
    settings.setValue("help/browseOffsetSeen", true);
}

void User::createAccount(const QString &user, const QString &email, const QString &password, bool remember)
{
    m_storedCreateEmail = email;
    m_storedCreatePassword = password;
    networkActionBegin();
    QNetworkReply *reply = m_download.createAccount(user, email, password, localBM);
    connect(reply, &QNetworkReply::finished, this,
            [=]() {
        QByteArray data = reply->readAll();
        QJsonDocument doc = QJsonDocument::fromJson(data);
        QJsonObject obj = doc.object();
        if (reply->error() != QNetworkReply::NoError) {
            QString code = obj.value("code").toString();
            if (code == "DuplicateName") {
                emit createAccountNameReserved();
            } else if (!code.isEmpty()) {
                m_networkErrorMessage.clear();
                m_networkErrorMessage.append(reply->errorString());
                m_networkErrorMessage.append(" / ");
                m_networkErrorMessage.append(code);
                emit networkError();
            } else {
                m_networkErrorMessage = reply->errorString();
                emit networkError();
            }
        } else if (!extractAccountData(reply, obj)){
            m_networkErrorMessage.clear();
            emit networkError();
        }

        networkActionEnd();
        reply->deleteLater();
    });
    m_remember = remember;
}

void User::login(const QString &user, const QString &password, bool remember, bool importBookmarks)
{
    m_storedLoginName = user;
    networkActionBegin();
    QNetworkReply *reply = m_download.login(user, password, importBookmarks ? &localBM : nullptr);
    connect(reply, &QNetworkReply::finished, this,
            [=]() {
        QByteArray data = reply->readAll();
        QJsonDocument doc = QJsonDocument::fromJson(data);
        QJsonObject obj = doc.object();

        if (!extractAccountData(reply, obj))
            emit loginFailed();
        else
            syncNow(true);

        networkActionEnd();
        reply->deleteLater();
    });
    m_remember = remember;
}

void User::logout()
{
    if (!m_token.isEmpty()) {
        QSettings settings;
        networkActionBegin();
        QNetworkReply *reply = m_download.logout(QString::fromLatin1(m_token));
        // Ignore network failures and just remove local data
        connect(reply, &QNetworkReply::finished, this,
                [=]() {
            networkActionEnd();
            m_token = QByteArray();
            reply->deleteLater();
        });
        m_name = QString();
        settings.remove("account");
        int oldCount = subs_set.size();
        subs_set.clear();
        if (oldCount > 0)
            emit noSubscriptionsChanged();
        if (oldCount > 5)
            emit recSubscriptionsChanged();
        deleteSubscriptions();
        emit loggedOut();
        emit loggedChange();
        emit subscriptionChange();
    } else {
        fprintf(stderr, "Logout with empty token, not doing anything");
    }
}

void User::subscribe(const int &cid, const bool &bookmarkFirst)
{
    subscribeAt(cid, bookmarkFirst ? 0 : INT_MAX);
}

void User::subscribeAt(const int &cid, const int &ord)
{
    networkActionBegin();
    if (logged()) {
        QNetworkReply *reply = m_download.subscribe(m_token, cid, ord);
        connect(reply, &QNetworkReply::finished, this,
                [=](){
            reply->deleteLater();
            if (reply->error() != QNetworkReply::NoError)
                handleNetworkError(reply, false);
            else
                parseAndSaveSubscription("stat_row", cid, reply);
        });
    } else {
        QMap<int, int> pos;
        pos[cid] = ord == INT_MAX ? -1 : ord;
        QNetworkReply *reply = m_download.bookmarkPositions(pos);
        connect(reply, &QNetworkReply::finished, this,
                [=](){
            reply->deleteLater();
            if (reply->error() != QNetworkReply::NoError) {
                handleNetworkError(reply, false);
                return;
            }

            Subscription *subs = parseAndSaveSubscription("subscriptions", cid, reply);
            if (subs) {
                localBM[cid] = subs->ord();
                storeLocalSubscriptions();
            }
        });
    }
}

void User::unsubscribe(const int &cid)
{
    if (logged()) {
        networkActionBegin();
        QNetworkReply *reply = m_download.unsubscribe(m_token, cid);
        connect(reply, &QNetworkReply::finished, this,
                [=]() {
            reply->deleteLater();
            if (reply->error() != QNetworkReply::NoError) {
                handleNetworkError(reply, false);
                return;
            }
            QByteArray data = reply->readAll();
            QJsonDocument doc = QJsonDocument::fromJson(data);
            QJsonObject obj = doc.object();
            QJsonValue ok = obj.value("ok");
            if (ok.toBool(false)) {
                int oldCount = subs_set.size();
                Subscription *ptr = subs_set.take(cid);
                if (subs_set.empty() && oldCount > 0)
                    emit noSubscriptionsChanged();
                else if (oldCount >= 5 && subs_set.size() < 5)
                    emit recSubscriptionsChanged();
                if (ptr) {
                    emit ptr->unsubscribing();
                    ptr->deleteLater();
                }
            }
            emit subscriptionChange();

            networkActionEnd();
        });
    } else {
        int oldCount = subs_set.size();
        Subscription *subs = subs_set.take(cid);
        if (subs) {
            if (subs_set.empty() && oldCount > 0)
                emit noSubscriptionsChanged();
            else if (oldCount >= 5 && subs_set.size() < 5)
                emit recSubscriptionsChanged();
            emit subs->unsubscribing();
            subs->deleteLater();
            localBM.remove(cid);
            storeLocalSubscriptions();
            emit subscriptionChange();
        }
    }
}

void User::resetStoredAccountDetails()
{
    m_storedLoginName.clear();
    m_storedCreateEmail.clear();
    m_storedCreatePassword.clear();
}

void User::syncNow(bool interactive)
{
    lastSync.start();
    if (interactive && m_syncAvailable) {
        m_syncAvailable = false;
        syncAvailableTimer.start(interactiveSyncInterval);
        emit syncAvailableChanged();
    }
    if (m_silentSyncFailure && interactive) {
        m_silentSyncFailure = 0;
        emit silentSyncFailureChanged();
    }
    m_interactiveSync = interactive;
    m_reportedSyncError = false;
    m_countedSilentFailure = false;
    emit synchronize();
    fetchSubscriptions();
    timer.start(syncInterval);
}

void User::unlockSync()
{
    if (lastSync.elapsed() > unlockSyncInterval)
        syncNow(false);
}

void User::setOffsetBack(const bool &offsetBack)
{
    QSettings settings;
    settings.setValue("app/offsetBack", offsetBack);
    m_offsetBack = offsetBack;
    emit offsetBackChanged();
}

void User::networkActionBegin()
{
    if (!m_loading++)
        emit loadingChange();
}

void User::networkActionEnd()
{
    if (!--m_loading)
        emit loadingChange();
}

void User::syncError(QNetworkReply *reply)
{
    networkActionEnd();
    if (m_interactiveSync) {
        if (!m_reportedSyncError) {
            m_reportedSyncError = true;
            m_networkErrorMessage = reply->errorString();
            emit networkError();
        }
    } else if (!m_countedSilentFailure){
        m_countedSilentFailure = true;
        emit silentSyncFailureChanged();
    }
    reply->deleteLater();
}

void User::genericNetworkError(QNetworkReply *reply)
{
    networkActionEnd();
    m_networkErrorMessage = reply->errorString();
    emit networkError();
}

void User::setCursor()
{
    PageModel *model = qobject_cast<PageModel *>(sender());
    if (!model) {
        return;
    }
    Subscription *sub = subs_set.value(model->cid());
    model->setSubscription(sub);
    int ord = sub == nullptr ? 0 : sub->ord();
    if (m_offsetBack && ord > 0)
        --ord;
    model->setCursor(ord);
}

bool User::extractAccountData(QNetworkReply *reply, QJsonObject &obj)
{
    QString name = obj.value("name").toString();
    QByteArray token = obj.value("csrf_ham").toString().toLatin1();

    if (name.isEmpty() || token.isEmpty())
        return false;
    QSettings settings;

    if (m_remember) {
        QNetworkCookieJar *jar = reply->manager()->cookieJar();
        QByteArray session;
        QList<QNetworkCookie> cookies = jar->cookiesForUrl(QUrl("https://" PIPERKA_HOST "/"));
        for (QList<QNetworkCookie>::const_iterator iter = cookies.constBegin();
             iter != cookies.constEnd(); ++iter) {
            if (iter->name() == "p_session") {
                session = iter->value();
                break;
            }
        }
        if (session.isEmpty()) {
            fprintf(stderr, "Session cookie extraction from jar failed\n");
            // Non-fatal. It just means that user needs to log in again.
        } else {
            settings.setValue("account/name", name);
            settings.setValue("account/token", token);
            settings.setValue("account/session", session);
        }
    }

    localBM.clear();
    settings.remove("local/bookmarks");

    m_name = name;
    m_token = token;
    fetchSubscriptions();
    emit loggedChange();
    return true;
}

void User::deleteSubscriptions()
{
    subs_set.clear();
    QList<Subscription *> subs = findChildren<Subscription *>();
    foreach(Subscription *sub, subs) {
        emit sub->unsubscribing();
        sub->deleteLater();
    }
}

Subscription *User::addSubscription(const QJsonArray &val)
{
    Subscription *subs = new Subscription(val, this);
    int cid = subs->cid();
    if (cid != -1) {
        int oldCount = subs_set.size();
        subs_set.insert(cid, subs);
        if (oldCount == 0)
            emit noSubscriptionsChanged();
        else if (oldCount < 5)
            emit recSubscriptionsChanged();
        connect(subs, &Subscription::requestMove, this,
                [=](const int &ord) {
            Subscription *s = qobject_cast<Subscription *>(sender());
            if (s == nullptr)
                return;
            if (ord == INT_MAX)
                subscribe(s->cid(), false);
            else
                subscribeAt(s->cid(), ord);
        });
        emit newSubscription(QPointer<Subscription>(subs));
    } else {
        delete subs;
    }
    return subs;
}

void User::fetchSubscriptions()
{
    QNetworkReply *reply = nullptr;

    if (logged())
        reply = m_download.userPrefs();
    else if (!localBM.empty())
        reply = m_download.bookmarkPositions(localBM);
    else {
        emit fetchSubscriptionsEnd();
        return;
    }

    networkActionBegin();
    connect(reply, &QNetworkReply::finished, this,
            [=]() {
        reply->deleteLater();
        if (reply->error() != QNetworkReply::NoError) {
            // Emit all old subscriptions for comic model
            QList<Subscription *> subs = findChildren<Subscription *>();
            foreach(Subscription *sub, subs) {
                emit refreshSubscription(QPointer<Subscription>(sub));
            }
            emit fetchSubscriptionsEnd();

            handleNetworkError(reply, true);
            return;
        }
        networkActionEnd();
        QByteArray data = reply->readAll();
        QJsonDocument doc = QJsonDocument::fromJson(data);
        QJsonObject mainObj = doc.object();

        QJsonArray subsArr = mainObj.value("subscriptions").toArray();
        QSet<int> oldSubsCids;
        for (QHash<int, Subscription *>::const_iterator iter = subs_set.cbegin(); iter != subs_set.cend(); ++iter) {
            oldSubsCids.insert(iter.key());
        }
        for (QJsonArray::const_iterator iter = subsArr.constBegin(); iter != subsArr.constEnd(); ++iter) {
            QJsonArray array = (*iter).toArray();
            if (array.empty())
                continue;
            int cid = array.at(0).toInt();
            if (cid == 0)
                continue;
            oldSubsCids.remove(cid);
            Subscription *subs = subs_set.value(cid);
            if (subs) {
                subs->update(array);
                refreshSubscription(QPointer<Subscription>(subs));
            } else {
                addSubscription(array);
            }
        }
        foreach(int cid, oldSubsCids) {
            Subscription *subs = subs_set.take(cid);
            if (subs) {
                emit subs->unsubscribing();
                delete subs;
            }
        }

        emit subscriptionChange();
        emit fetchSubscriptionsEnd();
    });
}

Subscription *User::parseAndSaveSubscription(const QString &fieldName, const int &cid, QNetworkReply *reply)
{
    QByteArray data = reply->readAll();
    QJsonDocument doc = QJsonDocument::fromJson(data);
    QJsonObject obj = doc.object();
    QJsonArray statsRow = obj.value(fieldName).toArray();
    if (statsRow.at(0).isArray())
        statsRow = statsRow.at(0).toArray();

    Subscription *subs = subs_set.value(cid);
    if (subs)
        subs->update(statsRow);
    else
        subs = addSubscription(statsRow);
    emit subscriptionChange();

    networkActionEnd();
    reply->deleteLater();
    return subs;
}

void User::storeLocalSubscriptions()
{
    QSettings settings;
    QMap<QString, QVariant> tmp;
    for (QMap<int, int>::const_iterator iter = localBM.cbegin(); iter != localBM.cend(); ++iter)
        tmp[QString::number(iter.key())] = iter.value();
    settings.setValue("local/bookmarks", QVariant(tmp));
}

void User::handleNetworkError(QNetworkReply *reply, const bool sync)
{
    if (logged()
            && (reply->error() == QNetworkReply::ContentOperationNotPermittedError
                || reply->error() == QNetworkReply::ContentAccessDenied)) {
        emit forceLogout();
        logout();
        networkActionEnd();
    } else if (sync) {
        syncError(reply);
    } else {
        m_networkErrorMessage = reply->errorString();
        emit networkError();
    }
}
