/*
 * 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 <algorithm>

#include <boost/log/trivial.hpp>

#include "authenticationexception.h"
#include "nitrokeyprovider.h"

#include <QtConcurrent/QtConcurrent>
#include <QUuid>

NitrokeyProvider::NitrokeyProvider(std::shared_ptr<NitrokeyBase> nitrokey)
    : QObject(nullptr), nitrokey(nitrokey), threadPool(nullptr)
{
    threadPool.setMaxThreadCount(1);
}

void NitrokeyProvider::connect()
{
    BOOST_LOG_TRIVIAL(debug) << "Connecting to the key...";
    QtConcurrent::run(&threadPool, this, &NitrokeyProvider::asyncConnect);
}

bool NitrokeyProvider::syncIsConnected()
{
    return nitrokey->isConnected();
}

void NitrokeyProvider::getSlots()
{
    BOOST_LOG_TRIVIAL(debug) << "Getting all slots...";
    QtConcurrent::run(&threadPool, this, &NitrokeyProvider::asyncGetSlots);
}

void NitrokeyProvider::adminAuth(const QString &adminPin,
                                  const RandomPasswordGenerator::KeyPassword &tempPassword)
{
    BOOST_LOG_TRIVIAL(debug) << "Performing admin authentication...";
    QtConcurrent::run(&threadPool, this, &NitrokeyProvider::asyncAdminAuth,
                      adminPin, tempPassword);
}

void NitrokeyProvider::userAuth(const QString &userPin,
                                 const RandomPasswordGenerator::KeyPassword &tempPassword)
{
    BOOST_LOG_TRIVIAL(debug) << "Performing user authentication...";
    QtConcurrent::run(&threadPool, this, &NitrokeyProvider::asyncUserAuth,
                      userPin, tempPassword);
}

void NitrokeyProvider::writeTotpSlot(const TOTPSlot &slot,
                                      std::uint16_t timeWindow,
                                      const QString &hexSecret,
                                      const RandomPasswordGenerator::KeyPassword temporaryPassword)
{
    BOOST_LOG_TRIVIAL(debug) << "Writing TOTP slot to the key...";
    QtConcurrent::run(&threadPool, this, &NitrokeyProvider::asyncWriteTotpSlot,
                      slot, timeWindow, hexSecret, temporaryPassword);
}

void NitrokeyProvider::eraseTotpSlot(const TOTPSlot &slot,
                                      const RandomPasswordGenerator::KeyPassword temporaryPassword)
{
    BOOST_LOG_TRIVIAL(debug) << "Erasing TOTP slot from the key...";
    QtConcurrent::run(&threadPool, this, &NitrokeyProvider::asyncEraseTotpSlot,
                      slot, temporaryPassword);
}

void NitrokeyProvider::getTOTPCode(const TOTPSlot &slot,
                                    const unsigned int unixTimestamp,
                                    const RandomPasswordGenerator::KeyPassword &temporaryPassword)
{
    BOOST_LOG_TRIVIAL(debug) << "Getting TOTP slot from the key...";
    QtConcurrent::run(&threadPool, this, &NitrokeyProvider::asyncGetTOTPCode,
                      slot, unixTimestamp, temporaryPassword);
}

void NitrokeyProvider::asyncConnect()
{
    BOOST_LOG_TRIVIAL(debug) << "asyncConnect(). Connecting.";
    if (nitrokey->connect())
    {
        BOOST_LOG_TRIVIAL(debug) << "asyncConnect(). Connected.";
        emit connected();
    }
}

void NitrokeyProvider::asyncGetSlots()
{
    BOOST_LOG_TRIVIAL(debug) << "asyncGetSlots(). Reading all slots.";
    auto slotsVector = nitrokey->getSlots();

    // Would be nice to just emit std::vector<TOTPSlot>, alas it would be really cumbersome.
    // In order to be able to emit custom (non-Qt) types in signals, one has to jump thru
    // hoops with Qt's object MetaType: https://doc.qt.io/qt-5/custom-types.html
    // Since we're talking about ~15 slots max, it's perfectly fine to quickly convert
    // std::vector to QList, which is natively supported by QVariant and therefore is
    // compatible with queued signals/slots (signals connected to slots in different
    // threads).
    QList<TOTPSlot> slotsList;
    std::for_each(slotsVector.begin(), slotsVector.end(), [&slotsList](const TOTPSlot s) mutable
    {
        BOOST_LOG_TRIVIAL(debug) << "Appending slot with name: " << s.slotName()
                                 << " and number: " << static_cast<int>(s.slotNumber());
        slotsList.append(s);
    });

    emit gotSlots(slotsList);
}

void NitrokeyProvider::asyncAdminAuth(const QString &adminPin,
                                       const RandomPasswordGenerator::KeyPassword &tempPassword)
{
    try {
        BOOST_LOG_TRIVIAL(debug) << "asyncAdminAuth().";
        nitrokey->firstAuth(adminPin.toStdString(), keyPassToString(tempPassword));
        BOOST_LOG_TRIVIAL(debug) << "asyncAdminAuth(). Successfully authenticated.";
        emit adminAuthenticated();
    } catch (const AuthenticationException &e) {
        BOOST_LOG_TRIVIAL(error) << "asyncAdminAuth(). Admin authentication error occured: "
                                 << e.what();
        emit adminAuthenticationFailure();
    }
}

void NitrokeyProvider::asyncUserAuth(const QString &userPin,
                                      const RandomPasswordGenerator::KeyPassword &tempPassword)
{
    try {
        BOOST_LOG_TRIVIAL(debug) << "asyncUserAuth().";
        nitrokey->userAuth(userPin.toStdString(), keyPassToString(tempPassword));
        BOOST_LOG_TRIVIAL(debug) << "asyncUserAuth(). Successfully authenticated.";
        emit userAuthenticated();
    } catch (const AuthenticationException &e) {
        BOOST_LOG_TRIVIAL(error) << "asyncUserAuth(). User authentication error occured: "
                                 << e.what();
        emit userAuthenticationFailure();
    }
}

void NitrokeyProvider::asyncWriteTotpSlot(const TOTPSlot &slot,
                                           std::uint16_t timeWindow,
                                           const QString &hexSecret,
                                           const RandomPasswordGenerator::KeyPassword temporaryPassword)
{
    BOOST_LOG_TRIVIAL(debug) << "asyncWriteTotpSlot(), slotName:" << slot.slotName()
                             << " slotNumber: " << static_cast<int>(slot.slotNumber());
    nitrokey->writeTotpSlot(slot.slotNumber(),
                             slot.slotName(),
                             hexSecret.toStdString(),
                             timeWindow,
                             keyPassToString(temporaryPassword));
    BOOST_LOG_TRIVIAL(debug) << "asyncWriteTotpSlot(). Added slot.";
    emit totpSlotWritten(slot);
}

void NitrokeyProvider::asyncEraseTotpSlot(const TOTPSlot &slot,
                                           const RandomPasswordGenerator::KeyPassword temporaryPassword)
{
    BOOST_LOG_TRIVIAL(debug) << "asyncEraseTotpSlot(). Erasing slot with name: " << slot.slotName()
                             << " and number: " << static_cast<int>(slot.slotNumber());
    nitrokey->eraseTotpSlot(slot.slotNumber(), keyPassToString(temporaryPassword));
    emit totpSlotErased(slot);
}

void NitrokeyProvider::asyncGetTOTPCode(const TOTPSlot &slot,
                                         const unsigned int unixTimestamp,
                                         const RandomPasswordGenerator::KeyPassword &temporaryPassword)
{
    BOOST_LOG_TRIVIAL(debug) << "asyncGetTOTPCode. Getting code for slot with name:"
                             << slot.slotName() << " and number:" << static_cast<int>(slot.slotNumber())
                             << " for unix timestamp: " << unixTimestamp;
    nitrokey->setTimeSoft(unixTimestamp);
    auto code = nitrokey->getTOTPCode(slot, keyPassToString(temporaryPassword));
    emit gotTOTPCode(GetTOTPCodeResponse(code.c_str(), slot.slotNumber(), unixTimestamp));
}

std::string NitrokeyProvider::keyPassToString(const RandomPasswordGenerator::KeyPassword &password) const
{
    std::string strPass;
    std::copy(password.begin(), password.end(), std::back_inserter(strPass));
    return strPass;
}
