/*
 * BSD 2-Clause License
 *
 * Copyright (c) 2020, Agnieszka Cicha-Cisek
 * Copyright (c) 2020, Patryk Cisek
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include <sstream>

#include <boost/log/trivial.hpp>

#include "authenticatedialog.h"
#include "authentication.h"
#include "keytotpmodel.h"
#include "newtotpslotdialog.h"
#include "noemptyslotexception.h"
#include "totpconverter.h"
#include "unauthenticatedexception.h"

#include <QDateTime>
#include <QIcon>
#include <QMessageBox>
#include <QPainter>
#include <QPixmap>
#include <QMessageBox>

KeyTOTPModel::KeyTOTPModel(std::shared_ptr<NitrokeyProvider> keyProvider,
                           std::shared_ptr<AuthenticateDialog> adminAuthenticateDialog,
                           std::shared_ptr<Authentication> adminAuthentication,
                           std::shared_ptr<AuthenticateDialog> userAuthenticateDialog,
                           std::shared_ptr<Authentication> userAuthentication,
                           std::shared_ptr<NewTOTPSlotDialog> newTOTPSlotDialog,
                           QObject *parent)
    : QAbstractListModel(parent),
      keyProvider(keyProvider),
      connected(false),
      connectionState(),
      obtainedSlots(),
      itemSize(100, 100),
      parserForNewSlot(),
      postAdminAuthAction(PostAdminAuthAction::ADD_SLOT),
      postUserAuthAction(PostUserAuthAction::GET_TOTP_CODE),
      errorMessage(),
      adminAuthDialog(adminAuthenticateDialog),
      adminAuth(adminAuthentication),
      userAuthDialog(userAuthenticateDialog),
      userAuth(userAuthentication),
      newTOTPSlotDialog(newTOTPSlotDialog),
      newTOTPSlotProvider(),
      newTOTPSlotName(),
      removalConfirmed(false)
{
    connect(keyProvider.get(), SIGNAL(gotSlots(QList<TOTPSlot>)),
            this, SLOT(onGotSlots(QList<TOTPSlot>)), Qt::QueuedConnection);
    connect(keyProvider.get(), SIGNAL(totpSlotWritten(const TOTPSlot)),
            this, SLOT(onNewSlotWritten(const TOTPSlot)));
    connect(keyProvider.get(), &NitrokeyProvider::totpSlotErased,
            this, &KeyTOTPModel::onSelectedSlotErased);
    connect(keyProvider.get(), &NitrokeyProvider::gotTOTPCode,
            this, &KeyTOTPModel::onGotTOTPCode);

    connect(&connectionState, &ConnectionState::connectionStateChanged,
            this, &KeyTOTPModel::connectionStateChanged);

    keyProvider->connect();
    keyProvider->getSlots();

    connect(adminAuth.get(), &Authentication::authenticated,
            this, &KeyTOTPModel::onAdminAuthenticated, Qt::QueuedConnection);
    connect(adminAuth.get(), &Authentication::authenticationFailure,
            this, &KeyTOTPModel::onAdminAuthenticationError);
    connect(adminAuth.get(), &Authentication::authenticationCanceled,
            this, &KeyTOTPModel::onAdminAuthenticationCanceled);

    connect(userAuth.get(), &Authentication::authenticated,
            this, &KeyTOTPModel::onUserAuthenticated, Qt::QueuedConnection);
    connect(userAuth.get(), &Authentication::authenticationFailure,
            this, &KeyTOTPModel::onUserAuthenticationError);
    connect(userAuth.get(), &Authentication::authenticationCanceled,
            this, &KeyTOTPModel::onUserAuthenticationCanceled);

    connect(&tickTimer, &QTimer::timeout,
            this, &KeyTOTPModel::on1SecTick);
    tickTimer.setSingleShot(false);
    constexpr int ONE_SECOND = 1000;
    tickTimer.start(ONE_SECOND);
}

void KeyTOTPModel::onGotSlots(const QList<TOTPSlot> &s)
{
    BOOST_LOG_TRIVIAL(debug) << "onGotSlots(). Got " << s.size()
                             << " slots from the key.";
    beginResetModel();
    obtainedSlots = s;
    endResetModel();
}

void KeyTOTPModel::onAdminAuthenticated()
{
    if (PostAdminAuthAction::ADD_SLOT == postAdminAuthAction) {
        onAddNewSlot();
    } else if (PostAdminAuthAction::DELETE_SLOT == postAdminAuthAction) {
        onEraseSelectedSlot();
    }
}

void KeyTOTPModel::onAdminAuthenticationError()
{
    BOOST_LOG_TRIVIAL(error) << "onAdminAuthenticationError(). Error authenticating user.";
    QMessageBox::critical(nullptr, tr("Authentication failure"),
                          tr("Admin authentication has failed"));
    emit adminAuthenticationFailed();
}

void KeyTOTPModel::onAdminAuthenticationCanceled()
{
    BOOST_LOG_TRIVIAL(warning) << "onAdminAuthenticationCanceled(). Admin authentication has"
                                  " been canceled.";
    emit adminAuthenticationCanceled();
}

void KeyTOTPModel::onUserAuthenticated()
{
    if (PostUserAuthAction::GET_TOTP_CODE == postUserAuthAction) {
        onSlotSelected(currentSelected, previousSelected);
    }
}

void KeyTOTPModel::onUserAuthenticationError()
{
    BOOST_LOG_TRIVIAL(error) << "onUserAuthenticationError(). Error authenticating user.";
    QMessageBox::critical(nullptr, tr("Authentication failure"),
                          tr("User authentication has failed!"));
    emit userAuthenticationFailed();
}

void KeyTOTPModel::onUserAuthenticationCanceled()
{
    BOOST_LOG_TRIVIAL(warning) << "onUserAuthenticationCanceled(). Authentication canceled by the user..";
    emit userAuthenticationCanceled();
}

void KeyTOTPModel::onAddNewSlot()
{
    try {
        auto pass = adminAuth->password();
        auto newSlot = newTOTPSlotProvider.findFreeSlot(
                    obtainedSlots, newTOTPSlotName.toStdString());

        TotpConverter converter;
        const auto base32Secret = parserForNewSlot->getSecret();
        const auto hexSecret = converter.convertBase32ToHex(base32Secret);
        keyProvider->writeTotpSlot(newSlot, AUTHENTICATOR_PERIOD_SECONDS,
                                   hexSecret,
                                   pass);
    } catch (const UnauthenticatedException &e) {
        BOOST_LOG_TRIVIAL(warning) << "Not yet/anymore authenticated as admin. "
                                   "Authenticating.";
        postAdminAuthAction = PostAdminAuthAction::ADD_SLOT;
        adminAuth->authenticate();
    } catch (const NoEmptySlotException &e) {
        BOOST_LOG_TRIVIAL(warning) << "There is no empty slot available in the key.";
        QMessageBox::warning(nullptr, tr("No empty TOTP slot"),
                             tr("Cannot add new TOTP slot -- there is no empty slot in the key!"));
    }
}

void KeyTOTPModel::onNewSlotWritten(const TOTPSlot slot)
{
    BOOST_LOG_TRIVIAL(debug) << "New slot written. Name: " << slot.slotName()
                             << " number: " << static_cast<int>(slot.slotNumber());
    keyProvider->getSlots();
}

void KeyTOTPModel::onSelectedSlotErased(const TOTPSlot &erasedSlot)
{
    BOOST_LOG_TRIVIAL(debug) << "onSelectedSlotErased(). Erased slot with name: "
                             << erasedSlot.slotName() << " with number: "
                             << static_cast<int>(erasedSlot.slotNumber());
    keyProvider->getSlots();
}

void KeyTOTPModel::onGotTOTPCode(const GetTOTPCodeResponse &totpCode)
{
    BOOST_LOG_TRIVIAL(debug) << "Received TOTP code. Slot number: "
                             << static_cast<int>(totpCode.SLOT_NUMBER)
                             << " timestamp: " << totpCode.UNIX_TIMESTAMP;
    codeForSelectedSlot.reset(new GetTOTPCodeResponse(totpCode));
}

void KeyTOTPModel::on1SecTick()
{
    BOOST_LOG_TRIVIAL(debug) << "Number of entries in list: " << obtainedSlots.size();
    auto connected = keyProvider->syncIsConnected();
    connectionState.setCurrentState(connected);
    if (!connected) {
        codeForSelectedSlot.reset();
        keyProvider->connect();
        keyProvider->getSlots();
        emit totpCodePresent(false);
    }
    if (codeForSelectedSlot) {
        const auto IDX = index(
                    slotNumberToObtainedSlotsIndex(codeForSelectedSlot->SLOT_NUMBER));

        if (!selectedSlotStillValid()) {
            BOOST_LOG_TRIVIAL(info) << "Code not valid anymore. Acting as the slot got freshly selected.";
            emit totpCodePresent(false);
            onSlotSelected(currentSelected, previousSelected);
        }
        emit dataChanged(IDX, IDX);
    }
}

QVariant KeyTOTPModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid()) return QVariant();
    if (Qt::UserRole == role || Qt::DecorationRole == role)
    {
        if (codeForSelectedSlot &&
                index.row() == static_cast<int>(slotNumberToObtainedSlotsIndex(
                                                codeForSelectedSlot->SLOT_NUMBER))) {
            emit totpCodePresent(true);
            return iconForSelected();
        } else {
            QPixmap pic(itemSize.width(), itemSize.height());
            pic.fill(Qt::lightGray);
            return iconForUnSelected(index);
        }
    }

    return QVariant();
}

int KeyTOTPModel::rowCount(const QModelIndex &) const
{
    return obtainedSlots.size();
}

void KeyTOTPModel::addNewSlot(std::unique_ptr<TotpUriParser> &&uriParser)
{
    BOOST_LOG_TRIVIAL(info) << "About to add new slot. URI parser has the following data. "
                            "Issuer: " << uriParser->getIssuer().toStdString()
                            << "Account name: " << uriParser->getAccountName().toStdString();
    newTOTPSlotDialog->presetNameAndSecret(*uriParser);
    if (QDialog::Accepted != newTOTPSlotDialog->exec()) return;
    parserForNewSlot = std::move(uriParser);
    newTOTPSlotName = newTOTPSlotDialog->slotName();
    onAddNewSlot();
}

QString KeyTOTPModel::currentlySelectedTOTPCode() const
{
    if (codeForSelectedSlot && selectedSlotStillValid()) {
        return codeForSelectedSlot->TOTP_CODE;
    } else {
        return "";
    }
}

void KeyTOTPModel::onSlotSelected(const QModelIndex &current, const QModelIndex &previous)
{
    BOOST_LOG_TRIVIAL(info) << "Selected slot at row: " << current.row();
    emit totpCodePresent(false);
    codeForSelectedSlot.reset();
    try {
        currentSelected = current;
        previousSelected = previous;
        if (current.isValid()) {
            auto tempPass = userAuth->password();
            auto now = QDateTime::currentDateTimeUtc().toTime_t();
            const auto &slot = obtainedSlots[current.row()];
            BOOST_LOG_TRIVIAL(info) << "Getting TOTP code for slot with name: " << slot.slotName()
                                    << " with number: " << static_cast<int>(slot.slotNumber());
            keyProvider->getTOTPCode(slot, now, tempPass);
        }
    } catch (const UnauthenticatedException &) {
        postUserAuthAction = PostUserAuthAction::GET_TOTP_CODE;
        userAuth->authenticate();
    }
}

void KeyTOTPModel::onEraseSelectedSlot()
{
    try {
        const auto &slot = obtainedSlots[currentSelected.row()];
        const auto question = tr("Are you sure, you want to delete the following TOTP slot from the key?\n\n") +
                slot.slotName().c_str();
        if (removalConfirmed || QMessageBox::Yes == QMessageBox::question(nullptr, tr("Delete TOTP Slot"),
                                                      question)) {
            removalConfirmed = true;
            auto pass = adminAuth->password();
            BOOST_LOG_TRIVIAL(info) << "About to erase slot with name: " << slot.slotName()
                                        << " with number: " << static_cast<int>(slot.slotNumber());
            keyProvider->eraseTotpSlot(slot, pass);
            codeForSelectedSlot.reset();
            removalConfirmed = false;
        }

    } catch (const UnauthenticatedException &) {
        postAdminAuthAction = PostAdminAuthAction::DELETE_SLOT;
        adminAuth->authenticate();
    }
}

bool KeyTOTPModel::selectedSlotStillValid() const
{
    const auto NOW = QDateTime::currentDateTime().toTime_t();
    const auto NOW_PERIOD = NOW / AUTHENTICATOR_PERIOD_SECONDS;
    const auto CODE_PERIOD = codeForSelectedSlot->UNIX_TIMESTAMP / AUTHENTICATOR_PERIOD_SECONDS;
    BOOST_LOG_TRIVIAL(trace) << "Checking if code for timestamp: " << codeForSelectedSlot->UNIX_TIMESTAMP
                             << " is still valid now (" << NOW
                             << "). Current period: " << NOW_PERIOD
                             << ", TOTP code's period: " << CODE_PERIOD
                             << "NOW_PERIOD == CODE_PERIOD: " << (NOW_PERIOD == CODE_PERIOD);
    return NOW_PERIOD == CODE_PERIOD;
}

unsigned int KeyTOTPModel::selectedSlotSecondsRemaining() const
{
    const auto TIMESTAMP = QDateTime::currentDateTime().toTime_t();
    const auto SEC_REMAINING =
            AUTHENTICATOR_PERIOD_SECONDS - TIMESTAMP % AUTHENTICATOR_PERIOD_SECONDS;
    return SEC_REMAINING;
}

QIcon KeyTOTPModel::iconForSelected() const {
    QPixmap pic(itemSize.width(), itemSize.height());
    pic.fill(Qt::white);
    QPainter painter(&pic);

    std::stringstream slotTextStream;
    slotTextStream
            << obtainedSlots[
               slotNumberToObtainedSlotsIndex(codeForSelectedSlot->SLOT_NUMBER)].slotName()
            << " (";

    if (selectedSlotStillValid()) {
        painter.setPen(Qt::black);
        slotTextStream << selectedSlotSecondsRemaining() << ")";
    } else {
        painter.setPen(Qt::gray);
        slotTextStream << "0)";
    }
    painter.setFont(QFont("Adial", 14));

    QString slotText(slotTextStream.str().c_str());
    painter.drawText(QPoint(10, 20), slotText);

    painter.setFont(QFont("Adial", 20, QFont::Bold));
    painter.drawText(QPoint(10, 60), codeForSelectedSlot->TOTP_CODE);

    return QIcon(pic.scaled(itemSize.width(), itemSize.height(),
                            Qt::KeepAspectRatio, Qt::SmoothTransformation));
}

QIcon KeyTOTPModel::iconForUnSelected(const QModelIndex &index) const
{
    QPixmap pic(itemSize.width(), itemSize.height());

    auto slotNo = index.row();
    if (0 == slotNo%2) {
        pic.fill(Qt::white);
    } else {
        pic.fill(QColor(0xdc, 0xdc, 0xdc));
    }

    QString slotText(obtainedSlots[slotNo].slotName().c_str());
    QPainter painter(&pic);
    painter.setPen(Qt::black);
    painter.setFont(QFont("Adial", 14));
    painter.drawText(QPoint(10, 20), slotText);
    return QIcon(pic.scaled(itemSize.width(), itemSize.height(),
                            Qt::KeepAspectRatio, Qt::SmoothTransformation));
}

std::size_t KeyTOTPModel::slotNumberToObtainedSlotsIndex(const std::uint8_t slotNumber) const
{
    const auto SLOT_IT = std::find_if(std::begin(obtainedSlots),
                                  std::end(obtainedSlots),
                                  [&slotNumber](const TOTPSlot &slot)
    {
        return slotNumber == slot.slotNumber();
    });
    return SLOT_IT - std::begin(obtainedSlots);
}
