// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include "DeviceVerificationFlow.h"

#include <tuple>

#include <QDateTime>
#include <QTimer>

#include <fmt/ranges.h>
#include <nlohmann/json.hpp>

#include "Cache.h"
#include "Cache_p.h"
#include "ChatPage.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h"
#include "timeline/TimelineModel.h"

static constexpr int TIMEOUT = 2 * 60 * 1000; // 2 minutes

static constexpr std::string_view mac_method_alg_v1 = "hkdf-hmac-sha256";
static constexpr std::string_view mac_method_alg_v2 = "hkdf-hmac-sha256.v2";

DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
                                               DeviceVerificationFlow::Type flow_type,
                                               TimelineModel *model,
                                               const QString &userID,
                                               const std::vector<QString> &deviceIds_)
  : sender(false)
  , type(flow_type)
  , deviceIds(std::move(deviceIds_))
  , model_(model)
{
    nhlog::crypto()->debug("CREATING NEW FLOW, {}, {}", static_cast<int>(flow_type), (void *)this);
    if (deviceIds.size() == 1)
        deviceId = deviceIds.front();

    timeout = new QTimer(this);
    timeout->setSingleShot(true);
    this->sas           = olm::client()->sas_init();
    this->isMacVerified = false;

    auto user_id_  = userID.toStdString();
    this->toClient = mtx::identifiers::parse<mtx::identifiers::User>(user_id_);
    cache::client()->query_keys(
      user_id_, [user_id_, this](const UserKeyCache &res, mtx::http::RequestErr err) {
          if (err) {
              nhlog::net()->warn("failed to query device keys: {},{}",
                                 mtx::errors::to_string(err->matrix_error.errcode),
                                 static_cast<int>(err->status_code));
              return;
          }

          if (!this->deviceId.isEmpty() &&
              (res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) {
              nhlog::net()->warn("no devices retrieved {}", user_id_);
              return;
          }

          this->their_keys = res;
      });

    cache::client()->query_keys(
      http::client()->user_id().to_string(),
      [this](const UserKeyCache &res, mtx::http::RequestErr err) {
          if (err) {
              nhlog::net()->warn("failed to query device keys: {},{}",
                                 mtx::errors::to_string(err->matrix_error.errcode),
                                 static_cast<int>(err->status_code));
              return;
          }

          if (res.master_keys.keys.empty())
              return;

          if (auto status = cache::verificationStatus(http::client()->user_id().to_string());
              status && status->user_verified == crypto::Trust::Verified)
              this->our_trusted_master_key = res.master_keys.keys.begin()->second;
      });

    if (model) {
        connect(this->model_,
                &TimelineModel::updateFlowEventId,
                this,
                [this](const std::string &event_id_) {
                    this->relation.rel_type = mtx::common::RelationType::Reference;
                    this->relation.event_id = event_id_;
                    this->transaction_id    = event_id_;
                });
    }

    connect(timeout, &QTimer::timeout, this, [this]() {
        nhlog::crypto()->info("verification: timeout");
        if (state_ != Success && state_ != Failed)
            this->cancelVerification(DeviceVerificationFlow::Error::Timeout);
    });

    connect(ChatPage::instance(),
            &ChatPage::receivedDeviceVerificationStart,
            this,
            &DeviceVerificationFlow::handleStartMessage);
    connect(ChatPage::instance(),
            &ChatPage::receivedDeviceVerificationAccept,
            this,
            [this](const mtx::events::msg::KeyVerificationAccept &msg) {
                if (state_ == Failed || state_ == Success)
                    return;

                nhlog::crypto()->info("verification: received accept with mac methods {}",
                                      fmt::join(msg.message_authentication_code, ", "));
                if (msg.transaction_id.has_value()) {
                    if (msg.transaction_id.value() != this->transaction_id)
                        return;
                } else if (msg.relations.references()) {
                    if (msg.relations.references() != this->relation.event_id)
                        return;
                }

                if (msg.key_agreement_protocol == "curve25519-hkdf-sha256" &&
                    msg.hash == "sha256" &&
                    (msg.message_authentication_code == mac_method_alg_v1 ||
                     msg.message_authentication_code == mac_method_alg_v2)) {
                    this->commitment = msg.commitment;
                    if (std::find(msg.short_authentication_string.begin(),
                                  msg.short_authentication_string.end(),
                                  mtx::events::msg::SASMethods::Emoji) !=
                        msg.short_authentication_string.end()) {
                        this->method = mtx::events::msg::SASMethods::Emoji;
                    } else {
                        this->method = mtx::events::msg::SASMethods::Decimal;
                    }
                    this->mac_method = msg.message_authentication_code;
                    this->sendVerificationKey();
                } else {
                    this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
                }
            });

    connect(ChatPage::instance(),
            &ChatPage::receivedDeviceVerificationCancel,
            this,
            [this](const mtx::events::msg::KeyVerificationCancel &msg) {
                nhlog::crypto()->info(
                  "verification: received cancel, {} : {}", msg.code, msg.reason);
                if (msg.transaction_id.has_value()) {
                    if (msg.transaction_id.value() != this->transaction_id)
                        return;
                } else if (msg.relations.references()) {
                    if (msg.relations.references() != this->relation.event_id)
                        return;
                }
                error_ = User;
                emit errorChanged();
                setState(Failed);
            });

    connect(
      ChatPage::instance(),
      &ChatPage::receivedDeviceVerificationKey,
      this,
      [this](const mtx::events::msg::KeyVerificationKey &msg) {
          if (state_ == Failed || state_ == Success)
              return;

          nhlog::crypto()->info(
            "verification: received key, sender {}, state {}", sender, state().toStdString());
          if (msg.transaction_id.has_value()) {
              if (msg.transaction_id.value() != this->transaction_id)
                  return;
          } else if (msg.relations.references()) {
              if (msg.relations.references() != this->relation.event_id)
                  return;
          }

          if (sender) {
              if (state_ != WaitingForOtherToAccept && state_ != WaitingForKeys) {
                  this->cancelVerification(OutOfOrder);
                  return;
              }
          } else {
              if (state_ != WaitingForKeys) {
                  this->cancelVerification(OutOfOrder);
                  return;
              }
          }

          this->sas->set_their_key(msg.key);
          std::string info;
          if (this->sender == true) {
              info = "MATRIX_KEY_VERIFICATION_SAS|" + http::client()->user_id().to_string() + "|" +
                     http::client()->device_id() + "|" + this->sas->public_key() + "|" +
                     this->toClient.to_string() + "|" + this->deviceId.toStdString() + "|" +
                     msg.key + "|" + this->transaction_id;
          } else {
              info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() + "|" +
                     this->deviceId.toStdString() + "|" + msg.key + "|" +
                     http::client()->user_id().to_string() + "|" + http::client()->device_id() +
                     "|" + this->sas->public_key() + "|" + this->transaction_id;
          }

          nhlog::ui()->info("Info is: '{}'", info);

          if (this->sender == false) {
              this->sendVerificationKey();
          } else {
              if (this->commitment != mtx::crypto::bin2base64_unpadded(
                                        mtx::crypto::sha256(msg.key + this->canonical_json))) {
                  this->cancelVerification(DeviceVerificationFlow::Error::MismatchedCommitment);
                  return;
              }
          }

          if (this->method == mtx::events::msg::SASMethods::Emoji) {
              this->sasList = this->sas->generate_bytes_emoji(info);
              setState(CompareEmoji);
          } else if (this->method == mtx::events::msg::SASMethods::Decimal) {
              this->sasList = this->sas->generate_bytes_decimal(info);
              setState(CompareNumber);
          }
      });

    connect(
      ChatPage::instance(),
      &ChatPage::receivedDeviceVerificationMac,
      this,
      [this](const mtx::events::msg::KeyVerificationMac &msg) {
          if (state_ == Failed || state_ == Success)
              return;

          nhlog::crypto()->info("verification: received mac");
          if (msg.transaction_id.has_value()) {
              if (msg.transaction_id.value() != this->transaction_id)
                  return;
          } else if (msg.relations.references()) {
              if (msg.relations.references() != this->relation.event_id)
                  return;
          }

          std::map<std::string, std::string> key_list;
          std::string key_string;
          for (const auto &mac : msg.mac) {
              for (const auto &[deviceid, key] : their_keys.device_keys) {
                  (void)deviceid;
                  if (key.keys.count(mac.first))
                      key_list[mac.first] = key.keys.at(mac.first);
              }

              if (their_keys.master_keys.keys.count(mac.first))
                  key_list[mac.first] = their_keys.master_keys.keys[mac.first];
              if (their_keys.user_signing_keys.keys.count(mac.first))
                  key_list[mac.first] = their_keys.user_signing_keys.keys[mac.first];
              if (their_keys.self_signing_keys.keys.count(mac.first))
                  key_list[mac.first] = their_keys.self_signing_keys.keys[mac.first];
          }
          auto macs = sas->calculate_mac(mac_method,
                                         toClient,
                                         this->deviceId.toStdString(),
                                         http::client()->user_id(),
                                         http::client()->device_id(),
                                         this->transaction_id,
                                         key_list);

          for (const auto &[key, mac] : macs.mac) {
              if (mac != msg.mac.at(key)) {
                  this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch);
                  return;
              }
          }

          if (msg.keys == macs.keys) {
              mtx::requests::KeySignaturesUpload req;
              if (utils::localUser().toStdString() == this->toClient.to_string()) {
                  // self verification, sign master key with device key, if we
                  // verified it
                  for (const auto &mac : msg.mac) {
                      if (their_keys.master_keys.keys.count(mac.first)) {
                          nlohmann::json j = their_keys.master_keys;
                          j.erase("signatures");
                          j.erase("unsigned");
                          mtx::crypto::CrossSigningKeys master_key =
                            j.get<mtx::crypto::CrossSigningKeys>();
                          master_key.signatures[utils::localUser().toStdString()]
                                               ["ed25519:" + http::client()->device_id()] =
                            olm::client()->sign_message(j.dump());
                          req.signatures[utils::localUser().toStdString()]
                                        [master_key.keys.at(mac.first)] = master_key;
                      } else if (mac.first == "ed25519:" + this->deviceId.toStdString()) {
                          // Sign their device key with self signing key

                          auto device_id = this->deviceId.toStdString();

                          if (their_keys.device_keys.count(device_id)) {
                              nlohmann::json j = their_keys.device_keys.at(device_id);
                              j.erase("signatures");
                              j.erase("unsigned");

                              auto secret = cache::secret(
                                mtx::secret_storage::secrets::cross_signing_self_signing);
                              if (!secret)
                                  continue;
                              auto ssk = mtx::crypto::PkSigning::from_seed(*secret);

                              mtx::crypto::DeviceKeys dev = j.get<mtx::crypto::DeviceKeys>();
                              dev.signatures[utils::localUser().toStdString()]
                                            ["ed25519:" + ssk.public_key()] = ssk.sign(j.dump());

                              req.signatures[utils::localUser().toStdString()][device_id] = dev;
                          }
                      }
                  }
              } else {
                  // Sign their master key with user signing key
                  for (const auto &mac : msg.mac) {
                      if (their_keys.master_keys.keys.count(mac.first)) {
                          nlohmann::json j = their_keys.master_keys;
                          j.erase("signatures");
                          j.erase("unsigned");

                          auto secret =
                            cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing);
                          if (!secret)
                              continue;
                          auto usk = mtx::crypto::PkSigning::from_seed(*secret);

                          mtx::crypto::CrossSigningKeys master_key =
                            j.get<mtx::crypto::CrossSigningKeys>();
                          master_key.signatures[utils::localUser().toStdString()]
                                               ["ed25519:" + usk.public_key()] = usk.sign(j.dump());

                          req.signatures[toClient.to_string()][master_key.keys.at(mac.first)] =
                            master_key;
                      }
                  }
              }

              if (!req.signatures.empty()) {
                  nhlog::crypto()->debug("Signatures to send: {}", nlohmann::json(req).dump(2));
                  http::client()->keys_signatures_upload(
                    req,
                    [](const mtx::responses::KeySignaturesUpload &res, mtx::http::RequestErr err) {
                        if (err) {
                            nhlog::net()->error("failed to upload signatures: {},{}",
                                                mtx::errors::to_string(err->matrix_error.errcode),
                                                static_cast<int>(err->status_code));
                        }

                        // MSVC bug, error C3493: 'key_id' cannot be implicitly captured because no
                        // default capture mode has been specified
                        // for (const auto &[user_id, tmp] : res.errors)
                        //     for (const auto &[key_id, e] : tmp)
                        //         nhlog::net()->error("signature error for user {} and key "
                        //                             "id {}: {}, {}",
                        //                             user_id,
                        //                             key_id,
                        //                             mtx::errors::to_string(e.errcode),
                        //                             e.error);
                        for (const auto &error : res.errors) {
                            const auto &user_id = error.first;
                            for (const auto &key_error : error.second) {
                                const auto &key_id = key_error.first;
                                const auto &e      = key_error.second;

                                nhlog::net()->error("signature error for user {} and key "
                                                    "id {}: {}, {}",
                                                    user_id,
                                                    key_id,
                                                    mtx::errors::to_string(e.errcode),
                                                    e.error);
                            }
                        }
                    });
              }

              this->isMacVerified = true;
              this->acceptDevice();
          } else {
              this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch);
          }
      });

    connect(
      ChatPage::instance(),
      &ChatPage::receivedDeviceVerificationReady,
      this,
      [this](const mtx::events::msg::KeyVerificationReady &msg) {
          if (state_ == Failed || state_ == Success)
              return;

          nhlog::crypto()->info("verification: received ready {}", (void *)this);
          if (!sender) {
              if (msg.from_device != this->deviceId.toStdString()) {
                  nhlog::crypto()->debug("Accepted by {}, we are communicating with {}",
                                         msg.from_device,
                                         this->deviceId.toStdString());
                  cancelVerification(AcceptedOnOtherDevice);
              }

              return;
          }

          if (msg.transaction_id.has_value()) {
              if (msg.transaction_id.value() != this->transaction_id)
                  return;

              if (this->deviceId.isEmpty() && this->deviceIds.size() > 1) {
                  auto from = QString::fromStdString(msg.from_device);
                  if (std::find(deviceIds.begin(), deviceIds.end(), from) != deviceIds.end()) {
                      mtx::events::msg::KeyVerificationCancel req{};
                      req.code           = "m.user";
                      req.reason         = "accepted by other device";
                      req.transaction_id = this->transaction_id;
                      mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationCancel> body;

                      for (const auto &d : this->deviceIds) {
                          if (d != from)
                              body[this->toClient][d.toStdString()] = req;
                      }

                      http::client()->send_to_device(
                        http::client()->generate_txn_id(), body, [](mtx::http::RequestErr err) {
                            if (err)
                                nhlog::net()->warn(
                                  "failed to send verification to_device message: {} {}",
                                  err->matrix_error.error,
                                  static_cast<int>(err->status_code));
                        });

                      this->deviceId  = from;
                      this->deviceIds = {from};
                  }
              }
          } else if (msg.relations.references()) {
              if (msg.relations.references() != this->relation.event_id)
                  return;
              else {
                  this->deviceId = QString::fromStdString(msg.from_device);
              }
          } else {
              return;
          }
          nhlog::crypto()->info("verification: received ready sending start {}", (void *)this);
          this->startVerificationRequest();
      });

    connect(ChatPage::instance(),
            &ChatPage::receivedDeviceVerificationDone,
            this,
            [this](const mtx::events::msg::KeyVerificationDone &msg) {
                nhlog::crypto()->info("verification: received done");
                if (msg.transaction_id.has_value()) {
                    if (msg.transaction_id.value() != this->transaction_id)
                        return;
                } else if (msg.relations.references()) {
                    if (msg.relations.references() != this->relation.event_id)
                        return;
                }
                nhlog::ui()->info("Flow done on other side");
            });

    timeout->start(TIMEOUT);
}

QString
DeviceVerificationFlow::state()
{
    switch (state_) {
    case PromptStartVerification:
        return QStringLiteral("PromptStartVerification");
    case CompareEmoji:
        return QStringLiteral("CompareEmoji");
    case CompareNumber:
        return QStringLiteral("CompareNumber");
    case WaitingForKeys:
        return QStringLiteral("WaitingForKeys");
    case WaitingForOtherToAccept:
        return QStringLiteral("WaitingForOtherToAccept");
    case WaitingForMac:
        return QStringLiteral("WaitingForMac");
    case Success:
        return QStringLiteral("Success");
    case Failed:
        return QStringLiteral("Failed");
    default:
        return QString();
    }
}

void
DeviceVerificationFlow::next()
{
    if (sender) {
        switch (state_) {
        case PromptStartVerification:
            sendVerificationRequest();
            break;
        case CompareEmoji:
        case CompareNumber:
            sendVerificationMac();
            break;
        case WaitingForKeys:
        case WaitingForOtherToAccept:
        case WaitingForMac:
        case Success:
        case Failed:
            nhlog::db()->error("verification: Invalid state transition!");
            break;
        }
    } else {
        switch (state_) {
        case PromptStartVerification:
            if (canonical_json.empty())
                sendVerificationReady();
            else // legacy path without request and ready
                acceptVerificationRequest();
            break;
        case CompareEmoji:
            [[fallthrough]];
        case CompareNumber:
            sendVerificationMac();
            break;
        case WaitingForKeys:
        case WaitingForOtherToAccept:
        case WaitingForMac:
        case Success:
        case Failed:
            nhlog::db()->error("verification: Invalid state transition!");
            break;
        }
    }
}

QString
DeviceVerificationFlow::getUserId()
{
    return QString::fromStdString(this->toClient.to_string());
}

QString
DeviceVerificationFlow::getDeviceId()
{
    return this->deviceId;
}

bool
DeviceVerificationFlow::getSender()
{
    return this->sender;
}

std::vector<int>
DeviceVerificationFlow::getSasList()
{
    return this->sasList;
}

bool
DeviceVerificationFlow::isSelfVerification() const
{
    return this->toClient.to_string() == http::client()->user_id().to_string();
}

void
DeviceVerificationFlow::setEventId(const std::string &event_id_)
{
    this->relation.rel_type = mtx::common::RelationType::Reference;
    this->relation.event_id = event_id_;
    this->transaction_id    = event_id_;
}

void
DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg,
                                           std::string)
{
    if (state_ == Failed || state_ == Success)
        return;

    if (msg.transaction_id.has_value()) {
        if (msg.transaction_id.value() != this->transaction_id)
            return;
    } else if (msg.relations.references()) {
        if (msg.relations.references() != this->relation.event_id)
            return;
    } else {
        return;
    }

    if (state_ == Failed)
        return;

    if (msg.from_device != this->deviceId.toStdString()) {
        cancelVerification(AcceptedOnOtherDevice);
        return;
    }

    nhlog::crypto()->info("verification: received start with mac methods {}",
                          fmt::join(msg.message_authentication_codes, ", "));

    // TODO(Nico): Replace with contains once we use C++23
    if (std::ranges::count(msg.key_agreement_protocols, "curve25519-hkdf-sha256") &&
        std::ranges::count(msg.hashes, "sha256")) {
        if (std::ranges::count(msg.message_authentication_codes, mac_method_alg_v2)) {
            this->mac_method = mac_method_alg_v2;
        } else if (std::ranges::count(msg.message_authentication_codes, mac_method_alg_v1)) {
            this->mac_method = mac_method_alg_v1;
        } else {
            this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
            return;
        }

        if (std::ranges::count(msg.short_authentication_string,
                               mtx::events::msg::SASMethods::Emoji)) {
            this->method = mtx::events::msg::SASMethods::Emoji;
        } else if (std::ranges::count(msg.short_authentication_string,
                                      mtx::events::msg::SASMethods::Decimal)) {
            this->method = mtx::events::msg::SASMethods::Decimal;
        } else {
            this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
            return;
        }

        if (!sender)
            this->canonical_json = nlohmann::json(msg).dump();
        else {
            // resolve glare
            if (std::tuple(this->toClient.to_string(), this->deviceId.toStdString()) <
                std::tuple(utils::localUser().toStdString(), http::client()->device_id())) {
                // treat this as if the user with the smaller mxid or smaller deviceid (if the mxid
                // was the same) was the sender of "start"
                this->canonical_json = nlohmann::json(msg).dump();
                this->sender         = false;
            }

            if (msg.method != mtx::events::msg::VerificationMethods::SASv1) {
                cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
                return;
            }
        }

        // If we didn't send "start", accept the verification (otherwise wait for the other side to
        // accept
        if (state_ != PromptStartVerification && !sender)
            this->acceptVerificationRequest();
    } else {
        this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
    }
}

//! accepts a verification
void
DeviceVerificationFlow::acceptVerificationRequest()
{
    if (state_ == Failed || state_ == Success)
        return;

    if (acceptSent)
        return;

    if (mac_method.empty()) {
        nhlog::crypto()->critical("Ignoring start without mac method set!");
        return;
    } else {
        nhlog::crypto()->debug("Accepted verification using mac_method {}", mac_method);
    }

    acceptSent = true;

    mtx::events::msg::KeyVerificationAccept req;

    req.method                      = mtx::events::msg::VerificationMethods::SASv1;
    req.key_agreement_protocol      = "curve25519-hkdf-sha256";
    req.hash                        = "sha256";
    req.message_authentication_code = this->mac_method;
    if (this->method == mtx::events::msg::SASMethods::Emoji)
        req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji};
    else if (this->method == mtx::events::msg::SASMethods::Decimal)
        req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal};
    req.commitment = mtx::crypto::bin2base64_unpadded(
      mtx::crypto::sha256(this->sas->public_key() + this->canonical_json));

    send(req);
    setState(WaitingForKeys);
}
//! responds verification request
void
DeviceVerificationFlow::sendVerificationReady()
{
    mtx::events::msg::KeyVerificationReady req;

    req.from_device = http::client()->device_id();
    req.methods     = {mtx::events::msg::VerificationMethods::SASv1};

    send(req);
    setState(WaitingForKeys);
}
//! accepts a verification
void
DeviceVerificationFlow::sendVerificationDone()
{
    mtx::events::msg::KeyVerificationDone req;

    send(req);
}
//! starts the verification flow
void
DeviceVerificationFlow::startVerificationRequest()
{
    if (startSent)
        return;
    startSent = true;

    mtx::events::msg::KeyVerificationStart req;

    req.from_device                  = http::client()->device_id();
    req.method                       = mtx::events::msg::VerificationMethods::SASv1;
    req.key_agreement_protocols      = {"curve25519-hkdf-sha256"};
    req.hashes                       = {"sha256"};
    req.message_authentication_codes = {std::string(mac_method_alg_v1),
                                        std::string(mac_method_alg_v2)};
    req.short_authentication_string  = {mtx::events::msg::SASMethods::Decimal,
                                        mtx::events::msg::SASMethods::Emoji};

    if (this->type == DeviceVerificationFlow::Type::ToDevice) {
        mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationStart> body;
        req.transaction_id   = this->transaction_id;
        this->canonical_json = nlohmann::json(req).dump();
    } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
        req.relations.relations.push_back(this->relation);
        // Set synthesized to surpress the nheko relation extensions
        req.relations.synthesized = true;
        this->canonical_json      = nlohmann::json(req).dump();
    }
    send(req);
    setState(WaitingForOtherToAccept);
}
//! sends a verification request
void
DeviceVerificationFlow::sendVerificationRequest()
{
    mtx::events::msg::KeyVerificationRequest req;

    req.from_device = http::client()->device_id();
    req.methods     = {mtx::events::msg::VerificationMethods::SASv1};

    if (this->type == DeviceVerificationFlow::Type::ToDevice) {
        QDateTime currentTime = QDateTime::currentDateTimeUtc();

        req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch();

    } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
        req.to      = this->toClient.to_string();
        req.msgtype = "m.key.verification.request";
        // clang-format off
        // clang-format < 12 is buggy on this
        req.body    = "User is requesting to verify keys with you. However, your client does "
                      "not support this method, so you will need to use the legacy method of "
                      "key verification.";
        // clang-format on
    }

    send(req);
    setState(WaitingForOtherToAccept);
}
//! cancels a verification flow
void
DeviceVerificationFlow::cancelVerification(DeviceVerificationFlow::Error error_code)
{
    if (state_ == State::Success || state_ == State::Failed)
        return;

    mtx::events::msg::KeyVerificationCancel req;

    if (error_code == DeviceVerificationFlow::Error::UnknownMethod) {
        req.code   = "m.unknown_method";
        req.reason = "unknown method received";
    } else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) {
        req.code   = "m.mismatched_commitment";
        req.reason = "commitment didn't match";
    } else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) {
        req.code   = "m.mismatched_sas";
        req.reason = "sas didn't match";
    } else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) {
        req.code   = "m.key_match";
        req.reason = "keys did not match";
    } else if (error_code == DeviceVerificationFlow::Error::Timeout) {
        req.code   = "m.timeout";
        req.reason = "timed out";
    } else if (error_code == DeviceVerificationFlow::Error::User) {
        req.code   = "m.user";
        req.reason = "user cancelled the verification";
    } else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) {
        req.code   = "m.unexpected_message";
        req.reason = "received messages out of order";
    }

    this->error_ = error_code;
    this->setState(Failed);
    emit errorChanged();

    // Don't cancel if the user accepted the request elsewhere, instead just silently stop
    if (error_code != AcceptedOnOtherDevice)
        send(req);
}
//! sends the verification key
void
DeviceVerificationFlow::sendVerificationKey()
{
    if (keySent)
        return;
    keySent = true;

    mtx::events::msg::KeyVerificationKey req;

    req.key = this->sas->public_key();

    send(req);
}

//! sends the mac of the keys
void
DeviceVerificationFlow::sendVerificationMac()
{
    if (macSent)
        return;
    macSent = true;

    nhlog::crypto()->debug("Sending mac using mac_method {}", mac_method);

    std::map<std::string, std::string> key_list;
    key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519;

    // send our master key, if we trust it
    if (!this->our_trusted_master_key.empty())
        key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key;

    mtx::events::msg::KeyVerificationMac req = sas->calculate_mac(mac_method,
                                                                  http::client()->user_id(),
                                                                  http::client()->device_id(),
                                                                  this->toClient,
                                                                  this->deviceId.toStdString(),
                                                                  this->transaction_id,
                                                                  key_list);

    send(req);

    setState(WaitingForMac);
    acceptDevice();
}
//! Completes the verification flow
void
DeviceVerificationFlow::acceptDevice()
{
    if (!isMacVerified) {
        setState(WaitingForMac);
    } else if (state_ == WaitingForMac) {
        cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString());
        this->sendVerificationDone();
        setState(Success);

        // Request secrets. We should probably check somehow, if a device knowns about the
        // secrets.
        if (utils::localUser().toStdString() == this->toClient.to_string() &&
            (!cache::secret(mtx::secret_storage::secrets::cross_signing_self_signing) ||
             !cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing))) {
            olm::request_cross_signing_keys();
        }
    }
}

void
DeviceVerificationFlow::unverify()
{
    cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString());

    emit refreshProfile();
}

QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::NewInRoomVerification(QObject *parent_,
                                              TimelineModel *timelineModel_,
                                              const mtx::events::msg::KeyVerificationRequest &msg,
                                              const QString &other_user_,
                                              const QString &event_id_)
{
    QSharedPointer<DeviceVerificationFlow> flow(
      new DeviceVerificationFlow(parent_,
                                 Type::RoomMsg,
                                 timelineModel_,
                                 other_user_,
                                 {QString::fromStdString(msg.from_device)}));

    flow->setEventId(event_id_.toStdString());

    if (std::find(msg.methods.begin(),
                  msg.methods.end(),
                  mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
        flow->cancelVerification(UnknownMethod);
    }

    return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
                                                const mtx::events::msg::KeyVerificationRequest &msg,
                                                const QString &other_user_,
                                                const QString &txn_id_)
{
    QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
      parent_, Type::ToDevice, nullptr, other_user_, {QString::fromStdString(msg.from_device)}));
    flow->transaction_id = txn_id_.toStdString();

    if (std::find(msg.methods.begin(),
                  msg.methods.end(),
                  mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
        flow->cancelVerification(UnknownMethod);
    }

    return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
                                                const mtx::events::msg::KeyVerificationStart &msg,
                                                const QString &other_user_,
                                                const QString &txn_id_)
{
    QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
      parent_, Type::ToDevice, nullptr, other_user_, {QString::fromStdString(msg.from_device)}));
    flow->transaction_id = txn_id_.toStdString();

    flow->handleStartMessage(msg, "");

    return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::InitiateUserVerification(QObject *parent_,
                                                 TimelineModel *timelineModel_,
                                                 const QString &userid)
{
    QSharedPointer<DeviceVerificationFlow> flow(
      new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, {}));
    flow->sender = true;
    return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_,
                                                   const QString &userid,
                                                   const std::vector<QString> &devices)
{
    assert(!devices.empty());

    QSharedPointer<DeviceVerificationFlow> flow(
      new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, devices));

    flow->sender         = true;
    flow->transaction_id = http::client()->generate_txn_id();

    return flow;
}
template<typename T>
void
DeviceVerificationFlow::send(T msg)
{
    if (this->type == DeviceVerificationFlow::Type::ToDevice) {
        mtx::requests::ToDeviceMessages<T> body;
        msg.transaction_id = this->transaction_id;
        for (const auto &d : deviceIds)
            body[this->toClient][d.toStdString()] = msg;

        http::client()->send_to_device<T>(
          http::client()->generate_txn_id(), body, [](mtx::http::RequestErr err) {
              if (err)
                  nhlog::net()->warn("failed to send verification to_device message: {} {}",
                                     err->matrix_error.error,
                                     static_cast<int>(err->status_code));
          });
    } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
        if constexpr (!std::is_same_v<T, mtx::events::msg::KeyVerificationRequest>) {
            msg.relations.relations.push_back(this->relation);
            // Set synthesized to surpress the nheko relation extensions
            msg.relations.synthesized = true;
        }
        (model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type<T>);
    }

    nhlog::net()->debug("Sent verification step: {} in state: {}",
                        mtx::events::to_string(mtx::events::to_device_content_to_type<T>),
                        state().toStdString());
}

#include "moc_DeviceVerificationFlow.cpp"
