/*
 * 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 "webeiddialog.hpp"
#include "application.hpp"
#include "languageselect.hpp"

#include "ui_dialog.h"

#include <QActionGroup>
#include <QButtonGroup>
#include <QDesktopServices>
#include <QFile>
#include <QMenu>
#include <QMessageBox>
#include <QMutexLocker>
#include <QRegularExpressionValidator>
#include <QSettings>
#include <QStyle>
#include <QTimeLine>

#ifdef Q_OS_LINUX
#include <stdio.h>
#include <unistd.h>
#endif

#if QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
constexpr inline QLatin1String operator"" _L1(const char* str, size_t size) noexcept
{
    return QLatin1String(str, int(size));
}

inline QString operator""_s(const char16_t* str, size_t size) noexcept
{
    return QString(QStringPrivate(nullptr, const_cast<char16_t*>(str), qsizetype(size)));
}
#else
using namespace Qt::Literals::StringLiterals;
#endif

using namespace electronic_id;

class WebEidDialog::Private : public Ui::WebEidDialog
{
public:
    observer_ptr<QRegularExpressionValidator> pinInputValidator;
    observer_ptr<QTimeLine> pinTimeoutTimer;
    observer_ptr<QButtonGroup> selectionGroup;
    observer_ptr<QToolButton> langButton;
};

WebEidDialog::WebEidDialog(QWidget* parent) : WebEidUI(parent), ui(new Private)
{
    // close() deletes the dialog automatically if the Qt::WA_DeleteOnClose flag is set.
    setAttribute(Qt::WA_DeleteOnClose);
    ui->setupUi(this);
    ui->lockedWarning->hide();
    if (Application::isDarkTheme()) {
        if (QFile f(u":dark.qss"_s); f.open(QFile::ReadOnly | QFile::Text)) {
            setStyleSheet(styleSheet() + QTextStream(&f).readAll());
            ui->cardChipIcon->setPixmap(pixmap("id-card-err"_L1));
        }
    }
    setWindowFlag(Qt::CustomizeWindowHint);
    setWindowFlag(Qt::WindowTitleHint);
    setWindowTitle(QApplication::applicationDisplayName());
    setTrText(ui->aboutVersion, []() -> QString {
        return tr("Version: %1").arg(QApplication::applicationVersion());
    });

    ui->langButton = new QToolButton(this);
    ui->langButton->setObjectName("langButton");
    ui->langButton->setText(tr("EN", "Active language"));
    ui->langButton->setAccessibleName(tr("English", "Active language accessible"));
    ui->langButton->setIcon(pixmap("lang"_L1));
    ui->langButton->setIconSize(QSize(20, 20));
    ui->langButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    ui->langButton->setCursor(QCursor(Qt::PointingHandCursor));
    ui->langButton->setLayoutDirection(Qt::RightToLeft);
    connect(ui->langButton, &QToolButton::clicked, this,
            [this] { (new LanguageSelect(this))->open(); });

    ui->pinInput->setAttribute(Qt::WA_MacShowFocusRect, false);
    auto pinInputFont = ui->pinInput->font();
    pinInputFont.setLetterSpacing(QFont::AbsoluteSpacing, 2);
    ui->pinInput->setFont(pinInputFont);

    ui->waitingSpinner->load(Application::isDarkTheme() ? u":/images/wait_dark.svg"_s
                                                        : u":/images/wait.svg"_s);

    ui->selectionGroup = new QButtonGroup(this);
    ui->fatalError->hide();
    ui->fatalHelp->hide();
    ui->selectAnotherCertificate->hide();

    connect(ui->pageStack, &QStackedWidget::currentChanged, this, &WebEidDialog::resizeHeight);
    connect(ui->selectionGroup, qOverload<QAbstractButton*>(&QButtonGroup::buttonClicked), this,
            [this] {
                ui->okButton->setEnabled(true);
                if (auto* button =
                        qobject_cast<CertificateButton*>(ui->selectionGroup->checkedButton())) {
                    setupWarning(button->certificateInfo());
                    ui->okButton->setEnabled(isCardActive(button->certificateInfo()));
                }
                ui->okButton->setFocus();
            });
    connect(ui->cancelButton, &QPushButton::clicked, this, &WebEidDialog::reject);
    connect(ui->helpButton, &QPushButton::clicked, this, [this] {
        ui->helpButton->setDown(false);
#ifdef Q_OS_LINUX
        // Launching Chrome in Linux causes the message "Opening in existing browser session." to be
        // printed to stdout, which ruins the browser-app communication channel. Redirect stdout to
        // pipe before launching the browser and restore it after to avoid this.
        std::array<int, 2> unusedPipe {};
        int pipeFailed = pipe(unusedPipe.data());
        int savedStdout {};
        if (!pipeFailed) {
            savedStdout = dup(1); // Save the original stdout.
            dup2(unusedPipe[1], 1); // Redirect stdout to pipe.
        }
#endif
        QDesktopServices::openUrl(
            tr("https://www.id.ee/en/article/how-to-check-that-your-id-card-reader-is-working/"));
#ifdef Q_OS_LINUX
        if (!pipeFailed) {
            fflush(stdout);
            if (savedStdout >= 0) {
                dup2(savedStdout, 1); // Restore the original stdout.
                ::close(savedStdout);
            }
            ::close(unusedPipe[1]);
            ::close(unusedPipe[0]);
        }
#endif
    });

    // Hide PIN-related widgets by default.
    ui->pinErrorLabel->hide();
    ui->pinInput->hide();
    ui->pinEntryTimeoutProgressBar->hide();
    ui->pinTimeRemaining->hide();

    ui->pinInputValidator = new QRegularExpressionValidator(ui->pinInput);
    ui->pinInput->setValidator(ui->pinInputValidator);
    connect(ui->pinInput, &QLineEdit::textChanged, ui->okButton, [this] {
        showPinInputWarning(false);
        ui->okButton->setEnabled(ui->pinInput->hasAcceptableInput());
    });
#ifdef Q_OS_MAC
    // Fix pressing Enter in PIN input field in macOS.
    connect(ui->pinInput, &QLineEdit::returnPressed, ui->okButton, &QPushButton::click);
#endif

    ui->pinEntryTimeoutProgressBar->setMaximum(PinInfo::PIN_PAD_PIN_ENTRY_TIMEOUT);
    ui->pinTimeoutTimer = new QTimeLine(ui->pinEntryTimeoutProgressBar->maximum() * 1000,
                                        ui->pinEntryTimeoutProgressBar);
    ui->pinTimeoutTimer->setEasingCurve(QEasingCurve::Linear);
    ui->pinTimeoutTimer->setFrameRange(ui->pinEntryTimeoutProgressBar->minimum(),
                                       ui->pinEntryTimeoutProgressBar->maximum());
    connect(ui->pinTimeoutTimer, &QTimeLine::frameChanged, ui->pinEntryTimeoutProgressBar,
            &QProgressBar::setValue);
    connect(ui->pinTimeoutTimer, &QTimeLine::frameChanged, ui->pinTimeRemaining, [this](int value) {
        ui->pinTimeRemaining->setText(
            tr("Time remaining: <b>%1</b>").arg(ui->pinEntryTimeoutProgressBar->maximum() - value));
    });

    connect(this, &WebEidDialog::languageChange, this,
            [this] { ui->pinInputCertificateInfo->languageChange(); });
}

WebEidDialog::~WebEidDialog()
{
    delete ui;
}

void WebEidDialog::forceClose()
{
    ui->pinTimeoutTimer->stop();
    ui->pinEntryTimeoutProgressBar->hide();
    close();
}

void WebEidDialog::showAboutPage()
{
    auto* d = new WebEidDialog();
    d->ui->helpButton->hide();
    d->ui->aboutAlert->hide();
    auto* app = qApp;
    if (app->isSafariExtensionContainingApp()) {
        d->setupOK([app] { app->showSafariSettings(); }, QT_TR_NOOP("Open Safari settings..."),
                   true);
        connect(app, &Application::safariExtensionEnabled, d, [d](bool value) {
            d->ui->aboutAlert->setHidden(value);
            d->resizeHeight();
        });
        app->requestSafariExtensionState();
    } else {
        d->ui->okButton->hide();
    }
    d->ui->pageStack->setCurrentIndex(int(Page::ABOUT));
    d->resizeHeight();
    d->open();
    connect(d, &WebEidDialog::finished, qApp, &QApplication::quit);
}

void WebEidDialog::showFatalErrorPage()
{
    auto* d = new WebEidDialog();
    d->setTrText(d->ui->messagePageTitleLabel, QT_TR_NOOP("Operation failed"));
    d->ui->fatalError->show();
    d->ui->fatalHelp->show();
    d->ui->connectCardLabel->hide();
    d->ui->cardChipIcon->hide();
    d->ui->helpButton->show();
    d->ui->cancelButton->show();
    d->ui->okButton->hide();
    d->ui->pageStack->setCurrentIndex(int(Page::ALERT));
    d->resizeHeight();
    d->exec();
}

void WebEidDialog::showWaitingForCardPage(const CommandType commandType)
{
    currentCommand = commandType;

    // Don't show OK button while waiting for card operation or connect card.
    ui->okButton->hide();

    ui->pageStack->setCurrentIndex(int(Page::WAITING));
}

QString WebEidDialog::getPin()
{
    // getPin() is called from background threads and must be thread-safe.
    // QString uses QAtomicPointer internally and is thread-safe.
    // There should be only single reference and this is transferred to the caller for safety
    QString ret = pin;
    // Cannot use eraseData because we have to force Qt copy-on-write
    pin.fill('\0');
    pin.clear();
    return ret;
}

void WebEidDialog::onSmartCardStatusUpdate(const RetriableError status)
{
    currentCommand = CommandType::INSERT_CARD;

    setTrText(ui->connectCardLabel, std::get<0>(retriableErrorToTextTitleAndIcon(status)));
    setTrText(ui->messagePageTitleLabel, std::get<1>(retriableErrorToTextTitleAndIcon(status)));
    ui->cardChipIcon->setPixmap(pixmap(std::get<2>(retriableErrorToTextTitleAndIcon(status))));

    // In case the insert card page is not shown, switch back to it.
    ui->helpButton->show();
    ui->cancelButton->show();
    ui->okButton->hide();
    ui->pageStack->setCurrentIndex(int(Page::ALERT));
}

/** This slot is used by the get certificate and authenticate commands in case there are multiple
 * certificates available. It displays the certificate selection view with multiple certificates.
 *
 * Get certificate exits the flow on OK with the selected certificate from here,
 * authenticate continues to onSingleCertificateReady().
 */
void WebEidDialog::onMultipleCertificatesReady(
    const QUrl& origin, const std::vector<EidCertificateAndPinInfo>& certAndPinInfos)
{
    setupOrigin(origin);
    setupCertificateAndPinInfo(certAndPinInfos);

    switch (currentCommand) {
    case CommandType::GET_SIGNING_CERTIFICATE:
        setupOK([this] {
            ui->okButton->setDisabled(true);
            if (auto* button =
                    qobject_cast<CertificateButton*>(ui->selectionGroup->checkedButton())) {
                emit accepted(button->certificateInfo());
            } else {
                emit failure(u"CertificateButton not found"_s);
            }
        });
        ui->pageStack->setCurrentIndex(int(Page::SELECT_CERTIFICATE));
        break;
    case CommandType::AUTHENTICATE:
        ui->selectAnotherCertificate->disconnect();
        ui->selectAnotherCertificate->setVisible(certAndPinInfos.size() > 1);
        connect(ui->selectAnotherCertificate, &QPushButton::clicked, this,
                [this, origin, certAndPinInfos] {
                    // We set pinInput to empty text instead of clear() to also reset undo buffer
                    ui->pinInput->setText({});
                    onMultipleCertificatesReady(origin, certAndPinInfos);
                });
        setupOK([this, origin] {
            ui->okButton->setDisabled(true);
            // Authenticate continues with the selected certificate to onSingleCertificateReady().
            if (auto* button =
                    qobject_cast<CertificateButton*>(ui->selectionGroup->checkedButton())) {
                onSingleCertificateReady(origin, button->certificateInfo());
            } else {
                emit failure(u"CertificateButton not found"_s);
            }
        });
        ui->pageStack->setCurrentIndex(int(Page::SELECT_CERTIFICATE));
        break;
    default:
        emit failure("Command %1 not allowed here"_L1.arg(QString::fromStdString(currentCommand)));
    }
}

/** This slot is used by all commands in case there is only a single certificate available. It
 * displays the certificate confirmation view and, in case of authenticate or sign, the PIN input
 * widgets.
 *
 * Authenticate enters here also from onCertificatesReady() after a certificate has been selected.
 *
 * All of the commands exit the flow on OK with the selected certificate from here.
 */
void WebEidDialog::onSingleCertificateReady(const QUrl& origin,
                                            const EidCertificateAndPinInfo& certAndPinInfo)
{
    setupOrigin(origin);
    ui->pinInputOriginLabel->setText(ui->selectCertificateOriginLabel->text());
    const bool useExternalPinDialog = certAndPinInfo.eid->providesExternalPinDialog();

    switch (currentCommand) {
    case CommandType::GET_SIGNING_CERTIFICATE:
        setupCertificateAndPinInfo({certAndPinInfo});
        setupOK([this, certAndPinInfo] {
            ui->okButton->setDisabled(true);
            emit accepted(certAndPinInfo);
        });
        ui->selectionGroup->buttons().at(0)->click();
        ui->pageStack->setCurrentIndex(int(Page::SELECT_CERTIFICATE));
        return;
    case CommandType::AUTHENTICATE:
        ui->pinInputCertificateInfo->setCertificateInfo(certAndPinInfo);
        setTrText(ui->pinInputPageTitleLabel, QT_TR_NOOP("Authenticate"));
        setTrText(ui->pinInputDescriptionLabel,
                  QT_TR_NOOP("By authenticating, I agree to the transfer of my name and personal "
                             "identification code to the service provider."));
        setTrText(
            ui->pinTitleLabel,
            useExternalPinDialog
                ? QT_TR_NOOP(
                      "Please enter PIN for authentication in the PIN dialog window that opens.")
                : QT_TR_NOOP("Enter PIN1 for authentication"));
        break;
    case CommandType::SIGN:
        ui->pinInputCertificateInfo->setCertificateInfo(certAndPinInfo);
        setTrText(ui->pinInputPageTitleLabel, QT_TR_NOOP("Signing"));
        setTrText(
            ui->pinInputDescriptionLabel,
            QT_TR_NOOP("By signing, I agree to the transfer of my name and personal identification "
                       "code to the service provider."));
        setTrText(
            ui->pinTitleLabel,
            useExternalPinDialog
                ? QT_TR_NOOP("Please enter PIN for signing in the PIN dialog window that opens.")
                : QT_TR_NOOP("Enter PIN2 for signing"));
        break;
    default:
        emit failure(u"Only SELECT_CERTIFICATE, AUTHENTICATE or SIGN allowed"_s);
        return;
    }

    if (certAndPinInfo.pinInfo.pinIsBlocked()) {
        displayPinBlockedError();
    } else if (certAndPinInfo.certInfo.isExpired || certAndPinInfo.certInfo.notEffective) {
        ui->pinTitleLabel->hide();
    } else if (useExternalPinDialog) {
        connectOkToCachePinAndEmitSelectedCertificate(certAndPinInfo);
        ui->okButton->setEnabled(true);
    } else if (certAndPinInfo.pinInfo.readerHasPinPad) {
        setupPinPadProgressBarAndEmitWait(certAndPinInfo);
    } else {
        setupPinInput(certAndPinInfo);
    }

    ui->pageStack->setCurrentIndex(int(Page::PIN_INPUT));
}

void WebEidDialog::onRetry(const RetriableError error)
{
    onRetryImpl(std::get<0>(retriableErrorToTextTitleAndIcon(error)));
}

void WebEidDialog::onSigningCertificateMismatch()
{
    onRetryImpl(
        QT_TR_NOOP("The certificate of the ID card in the reader does not match the originally "
                   "submitted certificate. Please insert the original ID card."));
}

void WebEidDialog::onVerifyPinFailed(const VerifyPinFailed::Status status, const qint8 retriesLeft)
{
    using Status = VerifyPinFailed::Status;

    std::function<QString()> message;

    switch (status) {
    case Status::RETRY_ALLOWED:
        message = [retriesLeft]() -> QString {
            return retriesLeft == -1 ? tr("Incorrect PIN.")
                                     : tr("Incorrect PIN, %n attempts left.", nullptr, retriesLeft);
        };
        showPinInputWarning(true);
        break;
    case Status::PIN_BLOCKED:
        displayPinBlockedError();
        return;
    case Status::INVALID_PIN_LENGTH:
        message = [] { return tr("Invalid PIN length"); };
        break;
    case Status::PIN_ENTRY_TIMEOUT:
        message = [] { return tr("PinPad timed out waiting for customer interaction."); };
        break;
    case Status::PIN_ENTRY_CANCEL:
        message = [] { return tr("PIN entry cancelled."); };
        break;
    case Status::PIN_ENTRY_DISABLED:
        message = [] { return tr("PIN entry disabled"); };
        break;
    case Status::UNKNOWN_ERROR:
        message = [] { return tr("Technical error"); };
        displayFatalError(std::move(message));
        return;
    }

    ui->pinErrorLabel->setVisible(bool(message));
    setTrText(ui->pinErrorLabel, message);

    if (ui->pinEntryTimeoutProgressBar->isVisible()) {
        onRetryImpl(std::move(message));
    } else {
        ui->pinInput->show();
        ui->pinInput->setFocus();
        ui->pinTitleLabel->show();
        ui->okButton->setDisabled(true);
        ui->cancelButton->setEnabled(true);
        resizeHeight();
    }
}

void WebEidDialog::reject()
{
    if (!ui->pinEntryTimeoutProgressBar->isVisible()) {
        WebEidUI::reject();
    }
}

bool WebEidDialog::event(QEvent* event)
{
    switch (event->type()) {
    case QEvent::LanguageChange:
        ui->retranslateUi(this);
        ui->langButton->setText(tr("EN", "Active language"));
        ui->langButton->setAccessibleName(tr("English", "Active language accessible"));
        emit languageChange();
        resizeHeight();
        break;
    case QEvent::Resize:
        ui->langButton->move(width() - ui->langButton->width() - 20, ui->pageStack->pos().y() - 20);
        break;
    default:
        break;
    }
    return WebEidUI::event(event);
}

template <typename Text>
void WebEidDialog::onRetryImpl(Text text)
{
    setTrText(ui->connectCardLabel, std::forward<Text>(text));
    setTrText(ui->messagePageTitleLabel, QT_TR_NOOP("Operation failed"));
    ui->cardChipIcon->setPixmap(pixmap("id-card-err"_L1));
    setupOK(&WebEidDialog::retry, QT_TR_NOOP("Try again"), true);
    ui->pageStack->setCurrentIndex(int(Page::ALERT));
}

template <typename Text>
void WebEidDialog::setTrText(QWidget* label, Text text) const
{
    disconnect(this, &WebEidDialog::languageChange, label, nullptr);
    if constexpr (std::is_same_v<Text, const char*>) {
        label->setProperty("text", tr(text));
        connect(this, &WebEidDialog::languageChange, label,
                [label, text] { label->setProperty("text", tr(text)); });
    } else {
        label->setProperty("text", text());
        connect(this, &WebEidDialog::languageChange, label,
                [label, text = std::forward<Text>(text)] { label->setProperty("text", text()); });
    }
}

void WebEidDialog::connectOkToCachePinAndEmitSelectedCertificate(
    const EidCertificateAndPinInfo& certAndPin)
{
    setupOK([this, certAndPin] {
        ui->pinInput->hide();
        ui->pinTitleLabel->hide();
        ui->pinErrorLabel->hide();
        ui->okButton->setDisabled(true);
        ui->cancelButton->setDisabled(true);
        // Cache the PIN in an instance variable for later use in getPin().
        // This is required as accessing widgets from background threads is not allowed,
        // so getPin() cannot access pinInput directly.
        // QString uses QAtomicPointer internally and is thread-safe.
        pin = ui->pinInput->text();

        // Use setText() instead of clear() to clear undo/redo history as well.
        ui->pinInput->setText({});
        // TODO: Implement a custom widget to ensure no copy of the PIN text remains in memory.
        // The widget should:
        // - store the PIN in a locked byte vector,
        // - prevent content leaks via the accessibility interface.

        emit accepted(certAndPin);
    });
}

void WebEidDialog::setupCertificateAndPinInfo(
    const std::vector<EidCertificateAndPinInfo>& cardCertAndPinInfos)
{
    qDeleteAll(ui->selectCertificatePage->findChildren<CertificateButton*>());
    for (const EidCertificateAndPinInfo& certAndPin : cardCertAndPinInfos) {
        QWidget* previous = ui->selectCertificateOriginLabel;
        if (!ui->selectionGroup->buttons().isEmpty()) {
            previous = ui->selectionGroup->buttons().last();
        }
        auto* button = new CertificateButton(certAndPin, ui->selectCertificatePage);
        ui->selectCertificateInfo->addWidget(button);
        ui->selectionGroup->addButton(button);
        connect(this, &WebEidDialog::languageChange, button,
                [button] { button->languageChange(); });
        setTabOrder(previous, button);
    }
}

void WebEidDialog::setupOrigin(const QUrl& origin)
{
    const auto host = origin.host(QUrl::PrettyDecoded);
    ui->selectCertificateOriginLabel->setText(QStringLiteral("%2%3").arg(
        host, origin.port() == -1 ? QString() : QStringLiteral(":%1").arg(origin.port())));
}

void WebEidDialog::setupPinPrompt(PinInfo pinInfo, bool cardActive)
{
    ui->okButton->setHidden(pinInfo.readerHasPinPad || !cardActive);
    ui->cancelButton->setHidden(pinInfo.readerHasPinPad && cardActive);
    ui->pinTitleLabel->setVisible(cardActive);
    ui->pinInput->setHidden(pinInfo.readerHasPinPad || !cardActive);
    ui->pinTimeRemaining->setVisible(pinInfo.readerHasPinPad && cardActive);
    ui->pinEntryTimeoutProgressBar->setVisible(pinInfo.readerHasPinPad && cardActive);
    bool showPinError = pinInfo.pinRetriesCount.second != -1
        && pinInfo.pinRetriesCount.first != pinInfo.pinRetriesCount.second;
    ui->pinErrorLabel->setVisible(showPinError);
    showPinInputWarning(showPinError);
    if (showPinError) {
        setTrText(ui->pinErrorLabel, [count = pinInfo.pinRetriesCount.first]() -> QString {
            return tr("The PIN has been entered incorrectly at least once. %n attempts left.",
                      nullptr, count);
        });
    }
}

void WebEidDialog::setupPinPadProgressBarAndEmitWait(const EidCertificateAndPinInfo& certAndPin)
{
    setupWarning(certAndPin);
    bool cardActive = isCardActive(certAndPin);
    setupPinPrompt(certAndPin.pinInfo, cardActive);
    if (!cardActive) {
        return;
    }
    hide();
    setWindowFlag(Qt::WindowCloseButtonHint, false);
    show();

    ui->selectAnotherCertificate->hide();
    ui->pinTimeRemaining->setText(
        tr("Time remaining: <b>%1</b>").arg(ui->pinEntryTimeoutProgressBar->maximum()));
    setTrText(ui->pinTitleLabel, [this]() -> QString {
        return tr("Please enter %1 in PinPad reader")
            .arg(currentCommand == CommandType::AUTHENTICATE ? tr("PIN1 for authentication")
                                                             : tr("PIN2 for signing"));
    });

    ui->pinEntryTimeoutProgressBar->reset();
    // To be strictly correct, the timeout timer should be started after the handler thread
    // has triggered the PIN pad internal timeout timer. However, that would involve extra
    // complexity in signal-slot setup that would bring little value as the difference between
    // timers is undetectable to the user, so we simply start the timer here, slightly earlier
    // than the PIN pad timer.
    ui->pinTimeoutTimer->start();

    emit waitingForPinPad(certAndPin);
}

void WebEidDialog::setupPinInput(const EidCertificateAndPinInfo& certAndPinInfo)
{
    setupWarning(certAndPinInfo);
    setupPinPrompt(certAndPinInfo.pinInfo, isCardActive(certAndPinInfo));
    // The allowed character ranges are from the SafeNet eToken guide:
    // 1. English uppercase letters (ASCII 0x41...0x5A).
    // 2. English lowercase letters (ASCII 0x61...0x7A).
    // 3. Numeric (ASCII 0x30...0x39).
    // 4. Special characters
    //    (ASCII 0x20...0x2F, space../ + 0x3A...0x40, :..@ + 0x5B...0x60, [..` + 0x7B...0x7F, {..~).
    // 5. We additionally allow uppercase and lowercase Unicode letters.
    const auto& regexpWithOrWithoutLetters =
        certAndPinInfo.eid->allowsUsingLettersAndSpecialCharactersInPin()
        ? QStringLiteral("[0-9 -/:-@[-`{-~\\p{L}]{%1,%2}")
        : QStringLiteral("[0-9]{%1,%2}");
    const QRegularExpression numericMinMaxRegexp(
        regexpWithOrWithoutLetters.arg(certAndPinInfo.pinInfo.pinMinMaxLength.first)
            .arg(certAndPinInfo.pinInfo.pinMinMaxLength.second));
    ui->pinInputValidator->setRegularExpression(numericMinMaxRegexp);
    ui->pinInput->setMaxLength(int(certAndPinInfo.pinInfo.pinMinMaxLength.second));
    ui->pinInput->setFocus();
    connectOkToCachePinAndEmitSelectedCertificate(certAndPinInfo);
}

template <typename Func>
void WebEidDialog::setupOK(Func func, const char* text, bool enabled)
{
    ui->okButton->disconnect();
    connect(ui->okButton, &QPushButton::clicked, this, std::forward<Func>(func));
    ui->okButton->show();
    ui->okButton->setEnabled(enabled);
    setTrText(ui->okButton, text ? text : QT_TR_NOOP("Confirm"));
    ui->cancelButton->show();
    ui->cancelButton->setEnabled(true);
    ui->helpButton->hide();
}

void WebEidDialog::setupWarning(const EidCertificateAndPinInfo& certAndPinInfo)
{
    ui->lockedWarning->setHidden(certAndPinInfo.pin1Active && certAndPinInfo.pin2Active);
    if (!certAndPinInfo.pin1Active) {
        setTrText(
            ui->lockedWarning,
            QT_TR_NOOP(
                "Authentication and signing with the ID-card isn't possible yet. "
                "ID-card must be activated in the Police and Border Guard Board’s self-service "
                "portal in order to use it. "
                "<a href=\"https://www.politsei.ee/en/self-service-portal\">Activate ID-card</a>"));
    } else if (!certAndPinInfo.pin2Active) {
        setTrText(
            ui->lockedWarning,
            QT_TR_NOOP(
                "Signing with an ID-card isn't possible yet. PIN2 code must be changed in DigiDoc4 "
                "application in order to sign. "
                "<a "
                "href=\"https://www.id.ee/en/article/changing-id-card-pin-codes-and-puk-code/"
                "\">Additional information</a>"));
    }
    resizeHeight();
}

void WebEidDialog::displayPinBlockedError()
{
    displayFatalError([] { return tr("PIN is locked. Unblock and try again."); });
}

template <typename Text>
void WebEidDialog::displayFatalError(Text message)
{
    ui->pinTitleLabel->hide();
    ui->pinInput->hide();
    ui->pinTimeoutTimer->stop();
    ui->pinTimeRemaining->hide();
    ui->pinEntryTimeoutProgressBar->hide();
    setTrText(ui->pinErrorLabel, std::forward<Text>(message));
    ui->pinErrorLabel->show();
    ui->okButton->hide();
    ui->cancelButton->setEnabled(true);
    ui->cancelButton->show();
    ui->helpButton->show();
    resizeHeight();
}

void WebEidDialog::showPinInputWarning(bool show)
{
    style()->unpolish(ui->pinInput);
    ui->pinInput->setProperty("warning", show);
    style()->polish(ui->pinInput);
}

void WebEidDialog::resizeHeight()
{
    ui->pageStack->setFixedHeight(ui->pageStack->currentWidget()->sizeHint().height());
    adjustSize();
}

bool WebEidDialog::isCardActive(const EidCertificateAndPinInfo& certAndPinInfo) const noexcept
{
    if (!certAndPinInfo.pin1Active) {
        return false;
    }
    return currentCommand == CommandType::AUTHENTICATE || certAndPinInfo.pin2Active;
}

QPixmap WebEidDialog::pixmap(QLatin1String name)
{
    return {":/images/%1%2.svg"_L1.arg(name,
                                       Application::isDarkTheme() ? "_dark"_L1 : QLatin1String())};
}

constexpr std::tuple<const char*, const char*, QLatin1String>
WebEidDialog::retriableErrorToTextTitleAndIcon(const RetriableError error) noexcept
{
    switch (error) {
    case RetriableError::SMART_CARD_SERVICE_IS_NOT_RUNNING:
        return {
            QT_TR_NOOP("The smart card service required to use the ID-card is not running. Please "
                       "start the smart card service and try again."),
            QT_TR_NOOP("Launch the Smart Card service"), "id-card-err"_L1};

    case RetriableError::NO_SMART_CARD_READERS_FOUND:
        return {QT_TR_NOOP("<b>Card reader not connected.</b> Please connect the card reader to "
                           "the computer."),
                QT_TR_NOOP("Connect the card reader"), "id-card-err"_L1};

    case RetriableError::NO_SMART_CARDS_FOUND:
    case RetriableError::PKCS11_TOKEN_NOT_PRESENT:
        return {QT_TR_NOOP("<b>ID-card not found.</b> Please insert the ID-card into the reader."),
                QT_TR_NOOP("Insert the ID-card"), "id-card-err"_L1};

    case RetriableError::SMART_CARD_WAS_REMOVED:
    case RetriableError::PKCS11_TOKEN_REMOVED:
        return {QT_TR_NOOP(
                    "The ID-card was removed from the reader. Please insert the ID-card into the "
                    "reader."),
                QT_TR_NOOP("Insert the ID-card"), "id-card-err"_L1};

    case RetriableError::SMART_CARD_TRANSACTION_FAILED:
        return {
            QT_TR_NOOP(
                "Operation failed. Make sure that the ID-card and the card reader are connected "
                "correctly."),
            QT_TR_NOOP("Check the ID-card and the reader connection"), "id-card-err"_L1};

    case RetriableError::FAILED_TO_COMMUNICATE_WITH_CARD_OR_READER:
        return {
            QT_TR_NOOP(
                "Connection to the ID-card or reader failed. Make sure that the ID-card and the "
                "card reader are connected correctly."),
            QT_TR_NOOP("Check the ID-card and the reader connection"), "id-card-err"_L1};

    case RetriableError::SMART_CARD_CHANGE_REQUIRED:
        return {
            QT_TR_NOOP(
                "The desired operation cannot be performed with the inserted ID-card. Make sure "
                "that the ID-card is supported by the Web eID application."),
            QT_TR_NOOP("Operation not supported"), "id-card-err"_L1};

    case RetriableError::SMART_CARD_COMMAND_ERROR:
        return {QT_TR_NOOP("Error communicating with the card."), QT_TR_NOOP("Operation failed"),
                "id-card-err"_L1};

    case RetriableError::PKCS11_ERROR:
        return {QT_TR_NOOP("Card driver error. Please try again."), QT_TR_NOOP("Card driver error"),
                "id-card-err"_L1};

    case RetriableError::SCARD_ERROR:
        return {QT_TR_NOOP(
                    "An error occurred in the Smart Card service required to use the ID-card. Make "
                    "sure that the ID-card and the card reader are connected correctly or relaunch "
                    "the Smart Card service."),
                QT_TR_NOOP("Operation failed"), "id-card-err"_L1};

    case RetriableError::UNSUPPORTED_CARD:
        return {
            QT_TR_NOOP(
                "The card in the reader is not supported. Make sure that the entered ID-card is "
                "supported by the Web eID application."),
            QT_TR_NOOP("Operation not supported"), "id-card-err"_L1};

    case RetriableError::NO_VALID_CERTIFICATE_AVAILABLE:
        return {QT_TR_NOOP(
                    "The inserted ID-card does not contain a certificate for the requested "
                    "operation. Please insert an ID-card that supports the requested operation."),
                QT_TR_NOOP("Operation not supported"), "id-card-err"_L1};

    case RetriableError::PIN_VERIFY_DISABLED:
        return {
            QT_TR_NOOP(
                "Operation failed. Make sure that the driver of the corresponding card reader is "
                "used. Read more <a "
                "href=\"https://www.id.ee/en/article/using-pinpad-card-reader-drivers/\">here</"
                "a>."),
            QT_TR_NOOP("Card driver error"), "id-card-err"_L1};

    case RetriableError::UNKNOWN_ERROR:
        return {QT_TR_NOOP("Unknown error"), QT_TR_NOOP("Unknown error"), "id-card-err"_L1};
    }

    return {QT_TR_NOOP("Unknown error"), QT_TR_NOOP("Unknown error"), "id-card-err"_L1};
}
