/*
 * Copyright (c) 2020-2024 Estonian Information System Authority
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include "certificatereader.hpp"

#include "application.hpp"
#include "signauthutils.hpp"
#include "utils/utils.hpp"

using namespace electronic_id;

namespace
{

EidCertificateAndPinInfo getCertificateWithStatusAndInfo(ElectronicID::ptr&& eid,
                                                         const CertificateType certificateType)
{
    const auto certificateBytes = eid->getCertificate(certificateType);

    QByteArray certificateDer(reinterpret_cast<const char*>(certificateBytes.data()),
                              int(certificateBytes.size()));
    QSslCertificate certificate(certificateDer, QSsl::Der);
    if (certificate.isNull()) {
        THROW(SmartCardChangeRequiredError,
              "Invalid certificate returned by electronic ID " + eid->name());
    }

    auto subject = certificate.subjectInfo(QSslCertificate::CommonName).join(' ');
    auto givenName = certificate.subjectInfo("GN").join(' ');
    auto surName = certificate.subjectInfo("SN").join(' ');
    auto serialNumber = certificate.subjectInfo(QSslCertificate::SerialNumber).join(' ');

    // http://www.etsi.org/deliver/etsi_en/319400_319499/31941201/01.01.01_60/en_31941201v010101p.pdf
    if (serialNumber.size() > 6 && serialNumber.startsWith(QLatin1String("PNO"))
        && serialNumber[5] == '-')
        serialNumber.remove(0, 6);

    if (!givenName.isEmpty() && !surName.isEmpty() && !serialNumber.isEmpty()) {
        subject = QStringLiteral("%1, %2, %3").arg(surName, givenName, serialNumber);
    }

    CertificateInfo certInfo {
        certificateType, certificate.expiryDate() < QDateTime::currentDateTimeUtc(),
        certificate.effectiveDate() > QDateTime::currentDateTimeUtc(), std::move(subject)};
    auto info = certificateType.isAuthentication() ? eid->authPinInfo() : eid->signingPinInfo();
    PinInfo pinInfo {.pinMinMaxLength = certificateType.isAuthentication()
                         ? eid->authPinMinMaxLength()
                         : eid->signingPinMinMaxLength(),
                     .pinRetriesCount {
                         info.retryCount,
                         info.maxRetry,
                     },
                     .readerHasPinPad = eid->smartcard().readerHasPinPad()};
    bool pin1Active = true;
    bool pin2Active = true;
    if (eid->type() == ElectronicID::EstEID && eid->name() == "EstEIDThales") {
        auto infoOther =
            certificateType.isAuthentication() ? eid->signingPinInfo() : eid->authPinInfo();
        pin1Active = certificateType.isAuthentication() ? info.pinActive : infoOther.pinActive;
        pin2Active = certificateType.isAuthentication() ? infoOther.pinActive : info.pinActive;
    }

    return {
        .eid = std::move(eid),
        .certificateBytesInDer = std::move(certificateDer),
        .certificate = certificate,
        .certInfo = std::move(certInfo),
        .pinInfo = std::move(pinInfo),
        .pin1Active = pin1Active,
        .pin2Active = pin2Active,
    };
}

} // namespace

CertificateReader::CertificateReader(const CommandWithArguments& cmd) : CommandHandler(cmd)
{
    validateAndStoreOrigin(cmd.second);
    if (auto* app = qobject_cast<Application*>(qApp)) {
        app->loadTranslations(cmd.second.value(QStringLiteral("lang")).toString());
    }
}

void CertificateReader::run(std::vector<ElectronicID::ptr>&& eids)
{
    REQUIRE_NOT_EMPTY_CONTAINS_NON_NULL_PTRS(eids)

    certificateType = command.first == CommandType::AUTHENTICATE ? CertificateType::AUTHENTICATION
                                                                 : CertificateType::SIGNING;

    std::vector<EidCertificateAndPinInfo> certAndPinInfos;
    certAndPinInfos.reserve(eids.size());
    for (auto& eid : eids) {
        try {
            certAndPinInfos.push_back(
                getCertificateWithStatusAndInfo(std::move(eid), certificateType));
        } catch (const WrongCertificateTypeError&) {
            // Ignore eIDs that don't support the given ceritifcate type.
        }
    }

    if (certAndPinInfos.empty()) {
        emit retry(RetriableError::NO_VALID_CERTIFICATE_AVAILABLE);
    } else {
        emitCertificatesReady(certAndPinInfos);
    }
}

void CertificateReader::connectSignals(const WebEidUI* window)
{
    window->disconnect(this);
    connect(this, &CertificateReader::multipleCertificatesReady, window,
            &WebEidUI::onMultipleCertificatesReady);
    connect(this, &CertificateReader::singleCertificateReady, window,
            &WebEidUI::onSingleCertificateReady);
}

void CertificateReader::emitCertificatesReady(
    const std::vector<EidCertificateAndPinInfo>& certAndPinInfos)
{
    if (certAndPinInfos.size() == 1) {
        emit singleCertificateReady(origin, certAndPinInfos[0]);
    } else {
        emit multipleCertificatesReady(origin, certAndPinInfos);
    }
}

void CertificateReader::validateAndStoreOrigin(const QVariantMap& arguments)
{
    const auto originStr = validateAndGetArgument<QString>(QStringLiteral("origin"), arguments);
    if (originStr.size() > 255) {
        THROW(CommandHandlerInputDataError, "origin length cannot exceed 255 characters");
    }

    origin = QUrl(originStr, QUrl::ParsingMode::StrictMode);

    if (!origin.isValid()) {
        THROW(CommandHandlerInputDataError, "origin is not a valid URL");
    }
    if (origin.isRelative() || !origin.path().isEmpty() || origin.hasQuery()
        || origin.hasFragment()) {
        THROW(CommandHandlerInputDataError, "origin is not in <scheme>://<host>[:<port>] format");
    }
    if (origin.scheme() != QLatin1String("https") && origin.scheme() != QLatin1String("wss")) {
        THROW(CommandHandlerInputDataError, "origin scheme has to be https or wss");
    }
}
