//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2026
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "td/telegram/PollManager.h"

#include "td/telegram/AccessRights.h"
#include "td/telegram/AuthManager.h"
#include "td/telegram/ChainId.h"
#include "td/telegram/ChatManager.h"
#include "td/telegram/Dependencies.h"
#include "td/telegram/DialogId.h"
#include "td/telegram/DialogManager.h"
#include "td/telegram/ForumTopicManager.h"
#include "td/telegram/Global.h"
#include "td/telegram/logevent/LogEvent.h"
#include "td/telegram/MessageContent.h"
#include "td/telegram/MessageContentDupType.h"
#include "td/telegram/MessageCopyOptions.h"
#include "td/telegram/MessageId.h"
#include "td/telegram/MessageQueryManager.h"
#include "td/telegram/MessageSender.h"
#include "td/telegram/MessagesManager.h"
#include "td/telegram/OnlineManager.h"
#include "td/telegram/OptionManager.h"
#include "td/telegram/PollId.hpp"
#include "td/telegram/PollManager.hpp"
#include "td/telegram/StateManager.h"
#include "td/telegram/Td.h"
#include "td/telegram/TdDb.h"
#include "td/telegram/telegram_api.h"
#include "td/telegram/UpdatesManager.h"
#include "td/telegram/UserId.h"
#include "td/telegram/UserManager.h"
#include "td/telegram/WebPagesManager.h"

#include "td/db/binlog/BinlogEvent.h"
#include "td/db/binlog/BinlogHelper.h"
#include "td/db/SqliteKeyValue.h"
#include "td/db/SqliteKeyValueAsync.h"

#include "td/utils/algorithm.h"
#include "td/utils/base64.h"
#include "td/utils/buffer.h"
#include "td/utils/crypto.h"
#include "td/utils/format.h"
#include "td/utils/logging.h"
#include "td/utils/misc.h"
#include "td/utils/Random.h"
#include "td/utils/Slice.h"
#include "td/utils/SliceBuilder.h"
#include "td/utils/Status.h"
#include "td/utils/tl_helpers.h"
#include "td/utils/utf8.h"

#include <algorithm>
#include <limits>
#include <utility>

namespace td {

class GetPollResultsQuery final : public Td::ResultHandler {
  Promise<tl_object_ptr<telegram_api::Updates>> promise_;
  PollId poll_id_;
  DialogId dialog_id_;
  MessageId message_id_;

 public:
  explicit GetPollResultsQuery(Promise<tl_object_ptr<telegram_api::Updates>> &&promise) : promise_(std::move(promise)) {
  }

  void send(PollId poll_id, MessageFullId message_full_id, int64 hash) {
    poll_id_ = poll_id;
    dialog_id_ = message_full_id.get_dialog_id();
    message_id_ = message_full_id.get_message_id();
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id_, AccessRights::Read);
    if (input_peer == nullptr) {
      LOG(INFO) << "Can't reget poll, because have no read access to " << dialog_id_;
      return promise_.set_value(nullptr);
    }

    auto message_id = message_id_.get_server_message_id().get();
    send_query(G()->net_query_creator().create(
        telegram_api::messages_getPollResults(std::move(input_peer), message_id, hash)));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_getPollResults>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    promise_.set_value(result_ptr.move_as_ok());
  }

  void on_error(Status status) final {
    if (!td_->messages_manager_->on_get_message_error(dialog_id_, message_id_, status, "GetPollResultsQuery")) {
      LOG(ERROR) << "Receive " << status << ", while trying to get results of " << poll_id_;
    }
    promise_.set_error(std::move(status));
  }
};

class GetPollVotersQuery final : public Td::ResultHandler {
  Promise<tl_object_ptr<telegram_api::messages_votesList>> promise_;
  PollId poll_id_;
  DialogId dialog_id_;

 public:
  explicit GetPollVotersQuery(Promise<tl_object_ptr<telegram_api::messages_votesList>> &&promise)
      : promise_(std::move(promise)) {
  }

  void send(PollId poll_id, MessageFullId message_full_id, BufferSlice &&option, const string &offset, int32 limit) {
    poll_id_ = poll_id;
    dialog_id_ = message_full_id.get_dialog_id();
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id_, AccessRights::Read);
    if (input_peer == nullptr) {
      LOG(INFO) << "Can't get poll, because have no read access to " << dialog_id_;
      return promise_.set_error(400, "Chat is not accessible");
    }

    CHECK(!option.empty());
    int32 flags = telegram_api::messages_getPollVotes::OPTION_MASK;
    if (!offset.empty()) {
      flags |= telegram_api::messages_getPollVotes::OFFSET_MASK;
    }

    auto message_id = message_full_id.get_message_id().get_server_message_id().get();
    send_query(G()->net_query_creator().create(telegram_api::messages_getPollVotes(
        flags, std::move(input_peer), message_id, std::move(option), offset, limit)));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_getPollVotes>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    promise_.set_value(result_ptr.move_as_ok());
  }

  void on_error(Status status) final {
    if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "GetPollVotersQuery") &&
        status.message() != "MESSAGE_ID_INVALID") {
      LOG(ERROR) << "Receive " << status << ", while trying to get voters of " << poll_id_;
    }
    promise_.set_error(std::move(status));
  }
};

class AddPollAnswerQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit AddPollAnswerQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(MessageFullId message_full_id, const FormattedText &text) {
    dialog_id_ = message_full_id.get_dialog_id();
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id_, AccessRights::Read);
    CHECK(input_peer != nullptr);
    auto message_id = message_full_id.get_message_id().get_server_message_id().get();
    send_query(G()->net_query_creator().create(
        telegram_api::messages_addPollAnswer(
            std::move(input_peer), message_id,
            telegram_api::make_object<telegram_api::inputPollAnswer>(
                0, get_input_text_with_entities(nullptr, text, "AddPollAnswerQuery"), nullptr)),
        {{dialog_id_}}));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_addPollAnswer>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto result = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for AddPollAnswerQuery: " << to_string(result);
    td_->updates_manager_->on_get_updates(std::move(result), std::move(promise_));
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "AddPollAnswerQuery");
    promise_.set_error(std::move(status));
  }
};

class DeletePollAnswerQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit DeletePollAnswerQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(MessageFullId message_full_id, const string &option_id) {
    dialog_id_ = message_full_id.get_dialog_id();
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id_, AccessRights::Read);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }
    auto message_id = message_full_id.get_message_id().get_server_message_id().get();
    send_query(G()->net_query_creator().create(
        telegram_api::messages_deletePollAnswer(std::move(input_peer), message_id, BufferSlice(option_id)),
        {{dialog_id_}}));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_deletePollAnswer>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto result = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for DeletePollAnswerQuery: " << to_string(result);
    td_->updates_manager_->on_get_updates(std::move(result), std::move(promise_));
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "DeletePollAnswerQuery");
    promise_.set_error(std::move(status));
  }
};

class SendVoteQuery final : public Td::ResultHandler {
  Promise<tl_object_ptr<telegram_api::Updates>> promise_;
  DialogId dialog_id_;

 public:
  explicit SendVoteQuery(Promise<tl_object_ptr<telegram_api::Updates>> &&promise) : promise_(std::move(promise)) {
  }

  void send(MessageFullId message_full_id, vector<BufferSlice> &&options, PollId poll_id, uint64 generation,
            NetQueryRef *query_ref) {
    dialog_id_ = message_full_id.get_dialog_id();
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id_, AccessRights::Read);
    if (input_peer == nullptr) {
      LOG(INFO) << "Can't set poll answer, because have no read access to " << dialog_id_;
      return on_error(Status::Error(400, "Can't access the chat"));
    }

    auto message_id = message_full_id.get_message_id().get_server_message_id().get();
    auto query = G()->net_query_creator().create(
        telegram_api::messages_sendVote(std::move(input_peer), message_id, std::move(options)),
        {{poll_id}, {dialog_id_}});
    *query_ref = query.get_weak();
    send_query(std::move(query));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_sendVote>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto result = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for SendVoteQuery: " << to_string(result);
    promise_.set_value(std::move(result));
  }

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "SendVoteQuery");
    promise_.set_error(std::move(status));
  }
};

class StopPollQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;

 public:
  explicit StopPollQuery(Promise<Unit> &&promise) : promise_(std::move(promise)) {
  }

  void send(MessageFullId message_full_id, unique_ptr<ReplyMarkup> &&reply_markup, PollId poll_id) {
    dialog_id_ = message_full_id.get_dialog_id();
    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id_, AccessRights::Edit);
    if (input_peer == nullptr) {
      LOG(INFO) << "Can't close poll, because have no edit access to " << dialog_id_;
      return on_error(Status::Error(400, "Can't access the chat"));
    }

    int32 flags = telegram_api::messages_editMessage::MEDIA_MASK;
    auto input_reply_markup = get_input_reply_markup(td_->user_manager_.get(), reply_markup);
    if (input_reply_markup != nullptr) {
      flags |= telegram_api::messages_editMessage::REPLY_MARKUP_MASK;
    }

    auto message_id = message_full_id.get_message_id().get_server_message_id().get();
    auto poll = telegram_api::make_object<telegram_api::poll>(
        poll_id.get(), 0, true, false, false, false, false, false, false, false, true, false,
        telegram_api::make_object<telegram_api::textWithEntities>(string(), Auto()), Auto(), 0, 0, vector<string>(), 0);
    auto input_media = telegram_api::make_object<telegram_api::inputMediaPoll>(0, std::move(poll), vector<int32>(),
                                                                               nullptr, string(), Auto(), nullptr);
    send_query(G()->net_query_creator().create(
        telegram_api::messages_editMessage(flags, false, false, std::move(input_peer), message_id, string(),
                                           std::move(input_media), std::move(input_reply_markup),
                                           vector<tl_object_ptr<telegram_api::MessageEntity>>(), 0, 0, 0, nullptr),
        {{poll_id}, {dialog_id_}}));
  }

  void on_result(BufferSlice packet) final {
    auto result_ptr = fetch_result<telegram_api::messages_editMessage>(packet);
    if (result_ptr.is_error()) {
      return on_error(result_ptr.move_as_error());
    }

    auto result = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for StopPollQuery: " << to_string(result);
    td_->updates_manager_->on_get_updates(std::move(result), std::move(promise_));
  }

  void on_error(Status status) final {
    if (!td_->auth_manager_->is_bot() && status.message() == "MESSAGE_NOT_MODIFIED") {
      return promise_.set_value(Unit());
    }
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "StopPollQuery");
    promise_.set_error(std::move(status));
  }
};

PollManager::PollManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) {
  update_poll_timeout_.set_callback(on_update_poll_timeout_callback);
  update_poll_timeout_.set_callback_data(static_cast<void *>(this));

  close_poll_timeout_.set_callback(on_close_poll_timeout_callback);
  close_poll_timeout_.set_callback_data(static_cast<void *>(this));

  unload_poll_timeout_.set_callback(on_unload_poll_timeout_callback);
  unload_poll_timeout_.set_callback_data(static_cast<void *>(this));
}

void PollManager::start_up() {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  class StateCallback final : public StateManager::Callback {
   public:
    explicit StateCallback(ActorId<PollManager> parent) : parent_(std::move(parent)) {
    }
    bool on_online(bool is_online) final {
      if (is_online) {
        send_closure(parent_, &PollManager::on_online);
      }
      return parent_.is_alive();
    }

   private:
    ActorId<PollManager> parent_;
  };
  send_closure(G()->state_manager(), &StateManager::add_callback, make_unique<StateCallback>(actor_id(this)));
}

void PollManager::tear_down() {
  parent_.reset();
}

PollManager::~PollManager() {
  Scheduler::instance()->destroy_on_scheduler(G()->get_gc_scheduler_id(), polls_, server_poll_messages_,
                                              other_poll_messages_, reply_poll_counts_, poll_voters_,
                                              loaded_from_database_polls_);
}

void PollManager::on_update_poll_timeout_callback(void *poll_manager_ptr, int64 poll_id_int) {
  if (G()->close_flag()) {
    return;
  }

  auto poll_manager = static_cast<PollManager *>(poll_manager_ptr);
  send_closure_later(poll_manager->actor_id(poll_manager), &PollManager::on_update_poll_timeout, PollId(poll_id_int));
}

void PollManager::on_close_poll_timeout_callback(void *poll_manager_ptr, int64 poll_id_int) {
  if (G()->close_flag()) {
    return;
  }

  auto poll_manager = static_cast<PollManager *>(poll_manager_ptr);
  send_closure_later(poll_manager->actor_id(poll_manager), &PollManager::on_close_poll_timeout, PollId(poll_id_int));
}

void PollManager::on_unload_poll_timeout_callback(void *poll_manager_ptr, int64 poll_id_int) {
  if (G()->close_flag()) {
    return;
  }

  auto poll_manager = static_cast<PollManager *>(poll_manager_ptr);
  send_closure_later(poll_manager->actor_id(poll_manager), &PollManager::on_unload_poll_timeout, PollId(poll_id_int));
}

bool PollManager::is_local_poll_id(PollId poll_id) {
  return poll_id.get() < 0 && poll_id.get() > std::numeric_limits<int32>::min();
}

Status PollManager::check_quiz_correct_option_ids(const vector<int32> &correct_option_ids, size_t option_count,
                                                  bool allow_empty) {
  if (!allow_empty && correct_option_ids.empty()) {
    return Status::Error(400, "Correct quiz option list must be non-empty");
  }
  for (size_t i = 0; i + 1 < correct_option_ids.size(); i++) {
    if (correct_option_ids[i] >= correct_option_ids[i + 1]) {
      return Status::Error(400, "Correct quiz option list must be increasing");
    }
  }
  for (auto correct_option_id : correct_option_ids) {
    if (correct_option_id < 0 || correct_option_id >= static_cast<int32>(option_count)) {
      return Status::Error("Wrong quiz correct_option_id");
    }
  }
  return Status::OK();
}

const PollManager::Poll *PollManager::get_poll(PollId poll_id) const {
  return polls_.get_pointer(poll_id);
}

const PollManager::Poll *PollManager::get_poll(PollId poll_id) {
  auto p = polls_.get_pointer(poll_id);
  if (p != nullptr) {
    schedule_poll_unload(poll_id);
  }
  return p;
}

PollManager::Poll *PollManager::get_poll_editable(PollId poll_id) {
  auto p = polls_.get_pointer(poll_id);
  if (p != nullptr) {
    schedule_poll_unload(poll_id);
  }
  return p;
}

bool PollManager::have_poll(PollId poll_id) const {
  return get_poll(poll_id) != nullptr;
}

void PollManager::notify_on_poll_update(PollId poll_id) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  if (server_poll_messages_.count(poll_id) > 0) {
    server_poll_messages_[poll_id].foreach([&](const MessageFullId &message_full_id) {
      td_->messages_manager_->on_external_update_message_content(message_full_id, "notify_on_poll_update 1");
    });
  }

  if (other_poll_messages_.count(poll_id) > 0) {
    other_poll_messages_[poll_id].foreach([&](const MessageFullId &message_full_id) {
      td_->messages_manager_->on_external_update_message_content(message_full_id, "notify_on_poll_update 2");
    });
  }
}

void PollManager::notify_on_poll_has_unread_votes_update(PollId poll_id, bool has_unread_votes) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  if (server_poll_messages_.count(poll_id) > 0) {
    server_poll_messages_[poll_id].foreach([&](const MessageFullId &message_full_id) {
      td_->messages_manager_->on_update_poll_has_unread_votes(message_full_id, has_unread_votes);
    });
  }
}

string PollManager::get_poll_database_key(PollId poll_id) {
  return PSTRING() << "poll" << poll_id.get();
}

void PollManager::save_poll(const Poll *poll, PollId poll_id) {
  CHECK(!is_local_poll_id(poll_id));
  poll->was_saved_ = true;

  if (!G()->use_message_database()) {
    return;
  }

  LOG(INFO) << "Save " << poll_id << " to database";
  CHECK(poll != nullptr);
  G()->td_db()->get_sqlite_pmc()->set(get_poll_database_key(poll_id), log_event_store(*poll).as_slice().str(), Auto());
}

void PollManager::on_load_poll_from_database(PollId poll_id, string value) {
  CHECK(poll_id.is_valid());
  loaded_from_database_polls_.insert(poll_id);

  LOG(INFO) << "Successfully loaded " << poll_id << " of size " << value.size() << " from database";
  //  G()->td_db()->get_sqlite_pmc()->erase(get_poll_database_key(poll_id), Auto());
  //  return;

  CHECK(!have_poll(poll_id));
  if (!value.empty()) {
    auto poll = make_unique<Poll>();
    if (log_event_parse(*poll, value).is_error()) {
      return;
    }
    for (const auto &min_channel : poll->option_min_channels_) {
      LOG(INFO) << "Add min option " << min_channel.first;
      td_->chat_manager_->add_min_channel(min_channel.first, min_channel.second);
    }
    for (const auto &min_channel : poll->recent_option_voter_min_channels_) {
      LOG(INFO) << "Add min recent option voter " << min_channel.first;
      td_->chat_manager_->add_min_channel(min_channel.first, min_channel.second);
    }
    for (const auto &min_channel : poll->recent_voter_min_channels_) {
      LOG(INFO) << "Add min voted " << min_channel.first;
      td_->chat_manager_->add_min_channel(min_channel.first, min_channel.second);
    }

    auto my_user_id = td_->user_manager_->get_my_id();
    bool is_bot = td_->auth_manager_->is_bot();
    Dependencies dependencies;
    for (auto dialog_id : poll->recent_voter_dialog_ids_) {
      dependencies.add_message_sender_dependencies(dialog_id);
    }
    for (const auto &option : poll->options_) {
      option.add_dependencies(dependencies, my_user_id, is_bot);
    }
    if (poll->explanation_media_ != nullptr) {
      add_message_content_dependencies(dependencies, poll->explanation_media_.get(), my_user_id, is_bot);
    }
    if (!dependencies.resolve_force(td_, "on_load_poll_from_database")) {
      return;
    }
    if (!poll->is_closed_ && poll->close_date_ != 0) {
      if (poll->close_date_ <= G()->server_time()) {
        poll->is_closed_ = true;
      } else {
        CHECK(!is_local_poll_id(poll_id));
        if (!G()->close_flag()) {
          close_poll_timeout_.set_timeout_in(poll_id.get(), poll->close_date_ - G()->server_time() + 1e-3);
        }
      }
    }
    td_->web_pages_manager_->register_poll_web_pages(poll_id, get_poll_web_page_ids(poll.get()));
    polls_[poll_id] = std::move(poll);
  }
}

bool PollManager::have_poll_force(PollId poll_id) {
  return get_poll_force(poll_id) != nullptr;
}

PollManager::Poll *PollManager::get_poll_force(PollId poll_id) {
  auto poll = get_poll_editable(poll_id);
  if (poll != nullptr) {
    return poll;
  }
  if (!G()->use_message_database()) {
    return nullptr;
  }
  if (!poll_id.is_valid() || loaded_from_database_polls_.count(poll_id)) {
    return nullptr;
  }

  LOG(INFO) << "Trying to load " << poll_id << " from database";
  on_load_poll_from_database(poll_id, G()->td_db()->get_sqlite_sync_pmc()->get(get_poll_database_key(poll_id)));
  return get_poll_editable(poll_id);
}

vector<int32> PollManager::get_vote_percentage(const vector<int32> &voter_counts, int32 total_voter_count) {
  int32 sum = 0;
  for (auto voter_count : voter_counts) {
    CHECK(0 <= voter_count);
    CHECK(voter_count <= std::numeric_limits<int32>::max() - sum);
    sum += voter_count;
  }
  if (total_voter_count > sum) {
    if (sum != 0) {
      LOG(ERROR) << "Have total_voter_count = " << total_voter_count << ", but votes sum = " << sum << ": "
                 << voter_counts;
    }
    total_voter_count = sum;
  }

  vector<int32> result(voter_counts.size(), 0);
  if (total_voter_count == 0) {
    return result;
  }
  if (total_voter_count != sum) {
    // just round to the nearest
    for (size_t i = 0; i < result.size(); i++) {
      result[i] =
          static_cast<int32>((static_cast<int64>(voter_counts[i]) * 200 + total_voter_count) / total_voter_count / 2);
    }
    return result;
  }

  // make sure that options with equal votes have equal percent and total sum is less than 100%
  int32 percent_sum = 0;
  vector<int32> gap(voter_counts.size(), 0);
  for (size_t i = 0; i < result.size(); i++) {
    auto multiplied_voter_count = static_cast<int64>(voter_counts[i]) * 100;
    result[i] = static_cast<int32>(multiplied_voter_count / total_voter_count);
    CHECK(0 <= result[i] && result[i] <= 100);
    gap[i] = static_cast<int32>(static_cast<int64>(result[i] + 1) * total_voter_count - multiplied_voter_count);
    CHECK(0 <= gap[i] && gap[i] <= total_voter_count);
    percent_sum += result[i];
  }
  CHECK(0 <= percent_sum && percent_sum <= 100);
  if (percent_sum == 100) {
    return result;
  }

  // now we need to choose up to (100 - percent_sum) options with a minimum total gap, such that
  // any two options with the same voter_count are chosen or not chosen simultaneously
  struct Option {
    int32 pos = -1;
    int32 count = 0;
  };
  FlatHashMap<int32, Option> options;
  for (size_t i = 0; i < result.size(); i++) {
    auto &option = options[voter_counts[i] + 1];
    if (option.pos == -1) {
      option.pos = narrow_cast<int32>(i);
    }
    option.count++;
  }
  vector<Option> sorted_options;
  for (const auto &it : options) {
    const auto &option = it.second;
    auto pos = option.pos;
    if (gap[pos] > total_voter_count / 2) {
      // do not round to wrong direction
      continue;
    }
    if (total_voter_count % 2 == 0 && gap[pos] == total_voter_count / 2 && result[pos] >= 50) {
      // round halves to the 50%
      continue;
    }
    sorted_options.push_back(option);
  }
  std::sort(sorted_options.begin(), sorted_options.end(), [&](const Option &lhs, const Option &rhs) {
    if (gap[lhs.pos] != gap[rhs.pos]) {
      // prefer options with smallest gap
      return gap[lhs.pos] < gap[rhs.pos];
    }
    if (lhs.count != rhs.count) {
      // prefer more popular options
      return lhs.count > rhs.count;
    }
    return lhs.pos < rhs.pos;  // prefer the first encountered option
  });

  // dynamic programming or brute force can give perfect result, but for now we use simple gready approach
  int32 left_percent = 100 - percent_sum;
  for (auto option : sorted_options) {
    if (option.count <= left_percent) {
      left_percent -= option.count;

      auto pos = option.pos;
      for (size_t i = 0; i < result.size(); i++) {
        if (voter_counts[i] == voter_counts[pos]) {
          result[i]++;
        }
      }
      if (left_percent == 0) {
        break;
      }
    }
  }
  return result;
}

bool PollManager::is_poll_vote_subscribers_only_limited(const Poll *poll, DialogId initial_dialog_id,
                                                        int32 initial_date) const {
  if (!poll->subscribers_only_ || initial_dialog_id.get_type() != DialogType::Channel) {
    return false;
  }
  auto channel_id = initial_dialog_id.get_channel_id();
  if (!td_->chat_manager_->get_channel_status(channel_id).is_member()) {
    return true;
  }
  int32 membership_delay = G()->is_test_dc() ? 300 : 86400;
  return td_->chat_manager_->get_channel_date(channel_id) >=
         (initial_date > 0 ? initial_date : G()->unix_time()) - membership_delay;
}

td_api::object_ptr<td_api::PollVoteRestrictionReason> PollManager::get_poll_vote_restriction_reason_object(
    PollId poll_id, const Poll *poll, DialogId dialog_id, MessageId message_id, DialogId initial_dialog_id,
    int32 initial_date, bool is_real_message_content) const {
  if (td_->auth_manager_->is_bot()) {
    return nullptr;
  }
  if (poll->is_closed_) {
    return td_api::make_object<td_api::pollVoteRestrictionReasonClosed>();
  }
  if (message_id.is_valid() || message_id.is_scheduled()) {
    if (message_id.is_local()) {
      return td_api::make_object<td_api::pollVoteRestrictionReasonClosed>();
    }
    if (message_id.is_yet_unsent()) {
      return td_api::make_object<td_api::pollVoteRestrictionReasonYetUnsent>();
    }
    if (message_id.is_scheduled()) {
      return td_api::make_object<td_api::pollVoteRestrictionReasonScheduled>();
    }
  }
  if (!is_real_message_content) {
    return td_api::make_object<td_api::pollVoteRestrictionReasonOther>();
  }
  CHECK(!is_local_poll_id(poll_id));
  if (!poll->country_codes_.empty()) {
    auto country_code = td_->option_manager_->get_option_string("phone_country_iso2");
    if (!td::contains(poll->country_codes_, country_code)) {
      return td_api::make_object<td_api::pollVoteRestrictionReasonCountryRestricted>(country_code);
    }
  }
  if (is_poll_vote_subscribers_only_limited(poll, initial_dialog_id, initial_date)) {
    return td_api::make_object<td_api::pollVoteRestrictionReasonMembershipRequired>(
        td_->dialog_manager_->get_chat_id_object(initial_dialog_id, "pollVoteRestrictionReasonMembershipRequired"));
  }
  return nullptr;
}

td_api::object_ptr<td_api::poll> PollManager::get_poll_object(PollId poll_id, DialogId dialog_id, MessageId message_id,
                                                              DialogId initial_dialog_id, int32 initial_date,
                                                              bool is_real_message_content) const {
  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);
  return get_poll_object(poll_id, poll, dialog_id, message_id, initial_dialog_id, initial_date,
                         is_real_message_content);
}

td_api::object_ptr<td_api::poll> PollManager::get_poll_object(PollId poll_id, const Poll *poll, DialogId dialog_id,
                                                              MessageId message_id, DialogId initial_dialog_id,
                                                              int32 initial_date, bool is_real_message_content) const {
  auto poll_options = transform(
      poll->options_, [td = td_](const PollOption &poll_option) { return poll_option.get_poll_option_object(td); });
  int32 voter_count_diff = 0;
  auto it = pending_answers_.find(poll_id);
  if (it != pending_answers_.end() && !(it->second.is_finished_ && poll->was_saved_)) {
    const auto &chosen_options = it->second.options_;
    LOG(INFO) << "Have pending chosen options " << chosen_options << " in " << poll_id;
    for (auto &poll_option : poll_options) {
      poll_option->is_being_chosen_ = td::contains(chosen_options, poll_option->id_);
      if (poll_option->is_chosen_) {
        voter_count_diff = -1;
        poll_option->voter_count_--;
        poll_option->is_chosen_ = false;
      }
    }
  }
  for (const auto &const_poll_option : poll_options) {
    if (!check_utf8(const_poll_option->id_)) {
      for (auto &poll_option : poll_options) {
        poll_option->id_ = base64_encode(poll_option->id_);
      }
      break;
    }
  }

  auto total_voter_count = poll->total_voter_count_ + voter_count_diff;
  auto can_get_voters = can_get_poll_voters(poll_id, poll, initial_dialog_id, initial_date) && is_real_message_content;
  auto can_see_results = can_get_voters || td_->auth_manager_->is_bot();
  if (!can_see_results) {
    // hide the voter counts
    for (auto &poll_option : poll_options) {
      poll_option->voter_count_ = 0;
      poll_option->recent_voter_ids_.clear();
    }
  } else {
    // calculate vote percentage and fix total_voter_count
    auto voter_counts = transform(poll_options, [](auto &poll_option) { return poll_option->voter_count_; });
    auto voter_count_sum = 0;
    for (auto voter_count : voter_counts) {
      if (total_voter_count < voter_count) {
        LOG(ERROR) << "Fix total voter count from " << poll->total_voter_count_ << " + " << voter_count_diff << " to "
                   << voter_count << " in " << poll_id;
        total_voter_count = voter_count;
      }
      voter_count_sum += voter_count;
    }
    if (voter_count_sum < total_voter_count && voter_count_sum != 0) {
      LOG(ERROR) << "Fix total voter count from " << poll->total_voter_count_ << " + " << voter_count_diff << " to "
                 << voter_count_sum << " in " << poll_id;
      total_voter_count = voter_count_sum;
    }

    auto vote_percentage = get_vote_percentage(voter_counts, total_voter_count);
    CHECK(poll_options.size() == vote_percentage.size());
    for (size_t i = 0; i < poll_options.size(); i++) {
      poll_options[i]->vote_percentage_ = vote_percentage[i];
    }
  }
  td_api::object_ptr<td_api::PollType> poll_type;
  if (poll->is_quiz_) {
    if (is_local_poll_id(poll_id)) {
      poll_type = td_api::make_object<td_api::pollTypeQuiz>(
          vector<int32>(), get_formatted_text_object(nullptr, FormattedText(), true, -1), nullptr);
    } else {
      auto correct_option_ids = poll->correct_option_ids_;
      poll_type = td_api::make_object<td_api::pollTypeQuiz>(
          std::move(correct_option_ids), get_formatted_text_object(nullptr, poll->explanation_, true, -1),
          get_poll_media_object(poll->explanation_media_.get(), td_));
    }
  } else {
    poll_type = td_api::make_object<td_api::pollTypeRegular>();
  }

  auto open_period = poll->open_period_;
  auto close_date = poll->close_date_;
  if (open_period != 0 && close_date == 0) {
    close_date = G()->unix_time() + open_period;
  }
  if (open_period == 0 && close_date != 0) {
    auto now = G()->unix_time();
    if (close_date < now + 5) {
      close_date = 0;
    } else {
      open_period = close_date - now;
    }
  }
  if (poll->is_closed_) {
    open_period = 0;
    close_date = 0;
  }

  auto recent_voters = get_min_message_senders_object(td_, poll->recent_voter_dialog_ids_, "get_poll_object");
  vector<int32> option_order;
  if (poll->shuffle_answers_ && !poll->is_creator_ && !td_->auth_manager_->is_bot()) {
    auto my_user_id = td_->user_manager_->get_my_id().get();
    vector<std::pair<uint32, int32>> sort_pairs;
    for (int32 i = 0; i < static_cast<int32>(poll_options.size()); i++) {
      sort_pairs.emplace_back(crc32(PSLICE() << my_user_id << poll_options[i]->id_ << poll_id.get()), i);
    }
    std::sort(sort_pairs.begin(), sort_pairs.end());
    for (auto &sort_pair : sort_pairs) {
      option_order.push_back(sort_pair.second);
    }
  }

  return td_api::make_object<td_api::poll>(
      poll_id.get(), get_formatted_text_object(nullptr, poll->question_, true, -1), std::move(poll_options),
      total_voter_count, std::move(recent_voters),
      can_get_voters && !poll->is_anonymous_ && message_id.is_server() && !is_local_poll_id(poll_id), can_see_results,
      poll->is_anonymous_, poll->allow_multiple_answers_, !poll->has_revoting_disabled_, poll->subscribers_only_,
      vector<string>(poll->country_codes_), std::move(option_order), std::move(poll_type), open_period, close_date,
      poll->is_closed_,
      get_poll_vote_restriction_reason_object(poll_id, poll, dialog_id, message_id, initial_dialog_id, initial_date,
                                              is_real_message_content));
}

PollId PollManager::create_poll(FormattedText &&question, vector<PollOption> &&options, bool is_anonymous,
                                bool allow_multiple_answers, bool has_open_answers, bool has_revoting_disabled,
                                bool subscribers_only, vector<string> &&country_codes, bool shuffle_answers,
                                bool hide_results_until_close, bool is_quiz, vector<int32> correct_option_ids,
                                FormattedText &&explanation, unique_ptr<MessageContent> &&explanation_media,
                                int32 open_period, int32 close_date, bool is_closed) {
  if (is_quiz && has_open_answers) {
    LOG(ERROR) << "Receive quiz with open answers";
    has_open_answers = false;
  }
  keep_only_custom_emoji(question);
  auto poll = make_unique<Poll>();
  poll->question_ = std::move(question);
  poll->options_ = std::move(options);
  poll->is_anonymous_ = is_anonymous;
  poll->allow_multiple_answers_ = allow_multiple_answers;
  poll->has_open_answers_ = has_open_answers;
  poll->has_revoting_disabled_ = has_revoting_disabled;
  poll->subscribers_only_ = subscribers_only;
  poll->country_codes_ = std::move(country_codes);
  poll->shuffle_answers_ = shuffle_answers;
  poll->hide_results_until_close_ = hide_results_until_close;
  poll->is_quiz_ = is_quiz;
  poll->correct_option_ids_ = std::move(correct_option_ids);
  poll->explanation_ = std::move(explanation);
  poll->explanation_media_ = std::move(explanation_media);
  poll->open_period_ = open_period;
  poll->close_date_ = close_date;
  poll->is_closed_ = is_closed;
  poll->is_creator_ = true;
  poll->can_view_stats_ = false;

  PollId poll_id(--current_local_poll_id_);
  CHECK(is_local_poll_id(poll_id));
  polls_.set(poll_id, std::move(poll));
  return poll_id;
}

void PollManager::register_poll(PollId poll_id, MessageFullId message_full_id, const char *source) {
  CHECK(have_poll(poll_id));
  if (message_full_id.get_message_id().is_scheduled() || !message_full_id.get_message_id().is_server()) {
    other_poll_messages_[poll_id].insert(message_full_id);
    if (!G()->close_flag()) {
      unload_poll_timeout_.cancel_timeout(poll_id.get());
    }
    return;
  }
  LOG(INFO) << "Register " << poll_id << " from " << message_full_id << " from " << source;
  server_poll_messages_[poll_id].insert(message_full_id);
  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);
  if (!td_->auth_manager_->is_bot() && !is_local_poll_id(poll_id) &&
      !(poll->is_closed_ && poll->is_updated_after_close_) && !G()->close_flag()) {
    update_poll_timeout_.add_timeout_in(poll_id.get(), 0);
  }
  if (!G()->close_flag()) {
    unload_poll_timeout_.cancel_timeout(poll_id.get());
  }
}

void PollManager::unregister_poll(PollId poll_id, MessageFullId message_full_id, const char *source) {
  CHECK(have_poll(poll_id));
  if (message_full_id.get_message_id().is_scheduled() || !message_full_id.get_message_id().is_server()) {
    auto &message_ids = other_poll_messages_[poll_id];
    auto is_deleted = message_ids.erase(message_full_id) > 0;
    LOG_CHECK(is_deleted) << source << ' ' << poll_id << ' ' << message_full_id;
    if (is_local_poll_id(poll_id)) {
      CHECK(message_ids.empty());
      forget_local_poll(poll_id);
    }
    if (message_ids.empty()) {
      other_poll_messages_.erase(poll_id);

      schedule_poll_unload(poll_id);
    }
    return;
  }
  LOG(INFO) << "Unregister " << poll_id << " from " << message_full_id << " from " << source;
  auto &message_ids = server_poll_messages_[poll_id];
  auto is_deleted = message_ids.erase(message_full_id) > 0;
  LOG_CHECK(is_deleted) << source << ' ' << poll_id << ' ' << message_full_id;
  if (is_local_poll_id(poll_id)) {
    CHECK(message_ids.empty());
    forget_local_poll(poll_id);
  }
  if (message_ids.empty()) {
    server_poll_messages_.erase(poll_id);
    if (!G()->close_flag()) {
      update_poll_timeout_.cancel_timeout(poll_id.get(), "unregister_poll");
    }

    schedule_poll_unload(poll_id);
  }
}

void PollManager::register_reply_poll(PollId poll_id) {
  CHECK(have_poll(poll_id));
  LOG(INFO) << "Register replied " << poll_id;
  reply_poll_counts_[poll_id]++;
  if (!G()->close_flag()) {
    unload_poll_timeout_.cancel_timeout(poll_id.get());
  }
}

void PollManager::unregister_reply_poll(PollId poll_id) {
  CHECK(have_poll(poll_id));
  LOG(INFO) << "Unregister replied " << poll_id;
  auto &count = reply_poll_counts_[poll_id];
  CHECK(count > 0);
  count--;
  if (is_local_poll_id(poll_id)) {
    CHECK(count == 0);
    forget_local_poll(poll_id);
  }
  if (count == 0) {
    reply_poll_counts_.erase(poll_id);
    schedule_poll_unload(poll_id);
  }
}

bool PollManager::can_unload_poll(PollId poll_id) {
  if (G()->close_flag()) {
    return false;
  }
  if (is_local_poll_id(poll_id) || server_poll_messages_.count(poll_id) != 0 ||
      other_poll_messages_.count(poll_id) != 0 || reply_poll_counts_.count(poll_id) != 0 ||
      pending_answers_.count(poll_id) != 0 || being_closed_polls_.count(poll_id) != 0) {
    return false;
  }

  auto it = poll_voters_.find(poll_id);
  if (it != poll_voters_.end() && !it->second.empty()) {
    for (auto &voters : it->second) {
      if (!voters.pending_queries_.empty()) {
        return false;
      }
    }
  }

  return true;
}

void PollManager::schedule_poll_unload(PollId poll_id) {
  if (can_unload_poll(poll_id)) {
    unload_poll_timeout_.set_timeout_in(poll_id.get(), UNLOAD_POLL_DELAY);
  }
}

bool PollManager::get_poll_is_closed(PollId poll_id) const {
  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);
  return poll->is_closed_;
}

bool PollManager::get_poll_is_anonymous(PollId poll_id) const {
  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);
  return poll->is_anonymous_;
}

bool PollManager::get_poll_can_add_option(PollId poll_id) const {
  if (is_local_poll_id(poll_id)) {
    return false;
  }
  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);
  return poll->has_open_answers_ && !poll->is_closed_ &&
         static_cast<int64>(poll->options_.size()) < td_->option_manager_->get_option_integer("poll_answer_count_max");
}

bool PollManager::get_poll_can_view_stats(PollId poll_id) const {
  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);
  return poll->can_view_stats_;
}

bool PollManager::get_poll_has_unread_votes(PollId poll_id) const {
  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);
  return poll->has_unread_votes_;
}

void PollManager::remove_poll_has_unread_votes(PollId poll_id) {
  auto poll = get_poll_editable(poll_id);
  CHECK(poll != nullptr);
  if (!poll->has_unread_votes_) {
    return;
  }
  poll->has_unread_votes_ = false;
  save_poll(poll, poll_id);
  // no need to notify about update of the poll
  // notify_on_poll_has_unread_votes_update(poll_id, false);
}

bool PollManager::can_delete_poll_option(const Poll *poll, const PollOption *option, MessageId message_id,
                                         bool is_forward, bool is_outgoing) {
  CHECK(poll != nullptr);
  CHECK(option != nullptr);
  if (poll->is_closed_ || !poll->has_open_answers_) {
    return false;
  }
  if (!message_id.is_server() || is_forward) {
    return false;
  }
  auto added_by_dialog_id = option->get_added_by_dialog_id();
  if (!added_by_dialog_id.is_valid()) {
    return false;
  }
  if (is_outgoing) {
    return true;
  }
  // TODO option->is_creator
  if (added_by_dialog_id == td_->dialog_manager_->get_my_dialog_id() &&
      G()->unix_time() <
          option->get_added_date() + td_->option_manager_->get_option_integer("poll_answer_delete_period")) {
    return true;
  }
  return false;
}

void PollManager::get_poll_option_properties(PollId poll_id, const string &option_id, DialogId dialog_id,
                                             MessageId message_id, bool can_be_replied,
                                             bool can_be_replied_in_another_chat, bool can_get_link, bool is_forward,
                                             bool is_outgoing,
                                             Promise<td_api::object_ptr<td_api::pollOptionProperties>> &&promise) {
  const auto *poll = get_poll(poll_id);
  CHECK(poll != nullptr);
  const PollOption *option = nullptr;
  for (const auto &poll_option : poll->options_) {
    if (poll_option.get_data() == option_id) {
      option = &poll_option;
    }
  }
  if (option == nullptr) {
    if (option_id.empty()) {
      return promise.set_value(td_api::make_object<td_api::pollOptionProperties>(false, false, false, false));
    }
    return promise.set_error(400, "Poll option not found");
  }
  bool can_be_deleted = can_delete_poll_option(poll, option, message_id, is_forward, is_outgoing);
  if (dialog_id.get_type() == DialogType::SecretChat || !message_id.is_server()) {
    can_be_replied = false;
    can_be_replied_in_another_chat = false;
    can_get_link = false;
  }
  promise.set_value(td_api::make_object<td_api::pollOptionProperties>(can_be_deleted, can_be_replied,
                                                                      can_be_replied_in_another_chat, can_get_link));
}

string PollManager::get_poll_search_text(PollId poll_id) const {
  const auto *poll = get_poll(poll_id);
  CHECK(poll != nullptr);

  string result = poll->question_.text;
  for (const auto &option : poll->options_) {
    result += ' ';
    result += option.text_.text;
  }
  return result;
}

vector<FileId> PollManager::get_poll_file_ids(PollId poll_id) const {
  const auto *poll = get_poll(poll_id);
  CHECK(poll != nullptr);

  vector<FileId> result;
  if (poll->explanation_media_ != nullptr) {
    result = get_message_content_file_ids(poll->explanation_media_.get(), td_);
  }
  for (const auto &poll_option : poll->options_) {
    poll_option.append_file_ids(td_, result);
  }
  return result;
}

void PollManager::add_poll_option(MessageFullId message_full_id, td_api::object_ptr<td_api::inputPollOption> &&option,
                                  Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, td_->messages_manager_->get_message_poll_id(message_full_id, false));
  TRY_RESULT_PROMISE(promise, poll_option,
                     PollOption::get_poll_option(td_, message_full_id.get_dialog_id(), std::move(option)));
  td_->create_handler<AddPollAnswerQuery>(std::move(promise))->send(message_full_id, poll_option.text_);
}

void PollManager::delete_poll_option(MessageFullId message_full_id, const string &option_id, Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, td_->messages_manager_->get_message_poll_id(message_full_id, false));
  td_->create_handler<DeletePollAnswerQuery>(std::move(promise))->send(message_full_id, option_id);
}

void PollManager::set_poll_answer(MessageFullId message_full_id, vector<int32> &&option_ids, Promise<Unit> &&promise) {
  TRY_RESULT_PROMISE(promise, poll_id, td_->messages_manager_->get_message_poll_id(message_full_id, false));
  td::unique(option_ids);

  if (is_local_poll_id(poll_id)) {
    return promise.set_error(400, "Poll can't be answered");
  }

  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);
  if (poll->is_closed_) {
    return promise.set_error(400, "Can't answer closed poll");
  }
  if (!poll->allow_multiple_answers_ && option_ids.size() > 1) {
    return promise.set_error(400, "Can't choose more than 1 option in the poll");
  }
  if (poll->has_revoting_disabled_ && option_ids.empty()) {
    return promise.set_error(400, "Can't retract vote in the poll");
  }
  if (poll->has_revoting_disabled_ && pending_answers_.count(poll_id) != 0) {
    return promise.set_error(400, "Can't revote in the poll");
  }

  FlatHashMap<uint64, int> affected_option_ids;
  vector<string> options;
  for (auto &option_id : option_ids) {
    auto index = static_cast<size_t>(option_id);
    if (index >= poll->options_.size()) {
      return promise.set_error(400, "Invalid option identifier specified");
    }
    options.push_back(poll->options_[index].get_data());

    affected_option_ids[index + 1]++;
  }
  for (size_t option_index = 0; option_index < poll->options_.size(); option_index++) {
    if (poll->options_[option_index].is_chosen_) {
      if (poll->has_revoting_disabled_) {
        return promise.set_error(400, "Can't revote in a quiz");
      }
      affected_option_ids[option_index + 1]++;
    }
  }
  for (const auto &it : affected_option_ids) {
    if (it.second == 1) {
      invalidate_poll_option_voters(poll, poll_id, static_cast<size_t>(it.first - 1));
    }
  }

  do_set_poll_answer(poll_id, message_full_id, std::move(options), 0, std::move(promise));
}

class PollManager::SetPollAnswerLogEvent {
 public:
  PollId poll_id_;
  MessageFullId message_full_id_;
  vector<string> options_;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(poll_id_, storer);
    td::store(message_full_id_, storer);
    td::store(options_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(poll_id_, parser);
    td::parse(message_full_id_, parser);
    td::parse(options_, parser);
  }
};

void PollManager::do_set_poll_answer(PollId poll_id, MessageFullId message_full_id, vector<string> &&options,
                                     uint64 log_event_id, Promise<Unit> &&promise) {
  LOG(INFO) << "Set answer in " << poll_id << " from " << message_full_id;
  if (!poll_id.is_valid() || !message_full_id.get_dialog_id().is_valid() ||
      !message_full_id.get_message_id().is_valid()) {
    CHECK(log_event_id != 0);
    LOG(ERROR) << "Invalid SetPollAnswer log event";
    binlog_erase(G()->td_db()->get_binlog(), log_event_id);
    return;
  }
  if (!G()->close_flag()) {
    unload_poll_timeout_.cancel_timeout(poll_id.get());
  }

  auto &pending_answer = pending_answers_[poll_id];
  if (!pending_answer.promises_.empty() && pending_answer.options_ == options) {
    pending_answer.promises_.push_back(std::move(promise));
    return;
  }

  if (pending_answer.log_event_id_ != 0 && log_event_id != 0) {
    LOG(ERROR) << "Duplicate SetPollAnswer log event: " << pending_answer.log_event_id_ << " and " << log_event_id;
    binlog_erase(G()->td_db()->get_binlog(), log_event_id);
    return;
  }
  if (log_event_id == 0 && G()->use_message_database()) {
    SetPollAnswerLogEvent log_event;
    log_event.poll_id_ = poll_id;
    log_event.message_full_id_ = message_full_id;
    log_event.options_ = options;
    auto storer = get_log_event_storer(log_event);
    if (pending_answer.generation_ == 0 || pending_answer.is_finished_) {
      CHECK(pending_answer.log_event_id_ == 0);
      log_event_id = binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::SetPollAnswer, storer);
      LOG(INFO) << "Add set poll answer log event " << log_event_id;
      CHECK(log_event_id != 0);
    } else {
      CHECK(pending_answer.log_event_id_ != 0);
      log_event_id = pending_answer.log_event_id_;
      auto new_log_event_id =
          binlog_rewrite(G()->td_db()->get_binlog(), log_event_id, LogEvent::HandlerType::SetPollAnswer, storer);
      LOG(INFO) << "Rewrite set poll answer log event " << log_event_id << " with " << new_log_event_id;
    }
  }

  if (!pending_answer.promises_.empty()) {
    CHECK(!pending_answer.query_ref_.empty());
    cancel_query(pending_answer.query_ref_);
    pending_answer.query_ref_ = NetQueryRef();

    auto promises = std::move(pending_answer.promises_);
    pending_answer.promises_.clear();
    for (auto &old_promise : promises) {
      old_promise.set_value(Unit());
    }
  }

  vector<BufferSlice> sent_options;
  for (auto &option : options) {
    sent_options.emplace_back(option);
  }

  auto generation = ++current_generation_;

  pending_answer.options_ = std::move(options);
  pending_answer.promises_.push_back(std::move(promise));
  pending_answer.generation_ = generation;
  pending_answer.log_event_id_ = log_event_id;
  pending_answer.is_finished_ = false;

  notify_on_poll_update(poll_id);

  auto query_promise = PromiseCreator::lambda(
      [actor_id = actor_id(this), poll_id, generation](Result<tl_object_ptr<telegram_api::Updates>> &&result) {
        send_closure(actor_id, &PollManager::on_set_poll_answer, poll_id, generation, std::move(result));
      });
  td_->create_handler<SendVoteQuery>(std::move(query_promise))
      ->send(message_full_id, std::move(sent_options), poll_id, generation, &pending_answer.query_ref_);
}

void PollManager::on_set_poll_answer(PollId poll_id, uint64 generation,
                                     Result<tl_object_ptr<telegram_api::Updates>> &&result) {
  if (G()->close_flag() && result.is_error()) {
    // request will be re-sent after restart
    return;
  }
  auto it = pending_answers_.find(poll_id);
  if (it == pending_answers_.end()) {
    // can happen if this is an answer with mismatched generation and server has ignored invoke-after
    return;
  }

  auto &pending_answer = it->second;
  CHECK(!pending_answer.promises_.empty());
  if (pending_answer.generation_ != generation) {
    return;
  }

  if (pending_answer.log_event_id_ != 0) {
    LOG(INFO) << "Delete set poll answer log event " << pending_answer.log_event_id_;
    binlog_erase(G()->td_db()->get_binlog(), pending_answer.log_event_id_);
    pending_answer.log_event_id_ = 0;
  }

  pending_answer.is_finished_ = true;

  auto poll = get_poll(poll_id);
  if (poll != nullptr) {
    poll->was_saved_ = false;
  }
  if (result.is_ok()) {
    td_->updates_manager_->on_get_updates(
        result.move_as_ok(),
        PromiseCreator::lambda([actor_id = actor_id(this), poll_id, generation](Result<Unit> &&result) mutable {
          send_closure(actor_id, &PollManager::on_set_poll_answer_finished, poll_id, Unit(), generation);
        }));
  } else {
    on_set_poll_answer_finished(poll_id, result.move_as_error(), generation);
  }
}

void PollManager::on_set_poll_answer_finished(PollId poll_id, Result<Unit> &&result, uint64 generation) {
  auto it = pending_answers_.find(poll_id);
  if (it == pending_answers_.end()) {
    return;
  }

  auto &pending_answer = it->second;
  CHECK(!pending_answer.promises_.empty());
  if (pending_answer.generation_ != generation) {
    return;
  }
  CHECK(pending_answer.is_finished_);

  auto promises = std::move(pending_answer.promises_);
  pending_answers_.erase(it);

  if (!G()->close_flag()) {
    auto poll = get_poll(poll_id);
    if (poll != nullptr && !poll->was_saved_) {
      // no updates were sent during updates processing, so send them
      // poll wasn't changed, so there is no reason to actually save it
      if (!(poll->is_closed_ && poll->is_updated_after_close_)) {
        LOG(INFO) << "Schedule updating of " << poll_id << " soon";
        update_poll_timeout_.set_timeout_in(poll_id.get(), 0.0);
      }

      notify_on_poll_update(poll_id);
      poll->was_saved_ = true;
    }
  }

  LOG(INFO) << "Finish to set answer for " << poll_id;

  if (result.is_ok()) {
    set_promises(promises);
  } else {
    fail_promises(promises, result.move_as_error());
  }
}

void PollManager::invalidate_poll_voters(const Poll *poll, PollId poll_id) {
  if (poll->is_anonymous_) {
    return;
  }

  auto it = poll_voters_.find(poll_id);
  if (it == poll_voters_.end()) {
    return;
  }

  for (auto &voters : it->second) {
    voters.was_invalidated_ = true;
  }
}

void PollManager::invalidate_poll_option_voters(const Poll *poll, PollId poll_id, size_t option_index) {
  if (poll->is_anonymous_) {
    return;
  }

  auto it = poll_voters_.find(poll_id);
  if (it == poll_voters_.end()) {
    return;
  }

  auto &poll_voters = it->second;
  CHECK(poll_voters.size() == poll->options_.size());
  CHECK(option_index < poll_voters.size());
  poll_voters[option_index].was_invalidated_ = true;
}

PollManager::PollOptionVoters &PollManager::get_poll_option_voters(const Poll *poll, PollId poll_id, int32 option_id) {
  auto &poll_voters = poll_voters_[poll_id];
  if (poll_voters.empty()) {
    poll_voters.resize(poll->options_.size());
  }
  auto index = narrow_cast<size_t>(option_id);
  CHECK(index < poll_voters.size());
  return poll_voters[index];
}

td_api::object_ptr<td_api::pollVoters> PollManager::get_poll_voters_object(
    int32 total_count, const vector<std::pair<DialogId, int32>> &voters) const {
  auto result = td_api::make_object<td_api::pollVoters>();
  result->total_count_ = total_count;
  for (auto &voter : voters) {
    auto message_sender = get_min_message_sender_object(td_, voter.first, "get_poll_voters_object");
    if (message_sender != nullptr) {
      result->voters_.push_back(td_api::make_object<td_api::pollVoter>(std::move(message_sender), voter.second));
    }
  }
  return result;
}

bool PollManager::can_get_poll_voters(PollId poll_id, const Poll *poll, DialogId initial_dialog_id,
                                      int32 initial_date) const {
  CHECK(poll != nullptr);
  if (td_->auth_manager_->is_bot()) {
    return false;
  }
  if (poll->is_closed_ || poll->is_creator_) {
    return true;
  }
  if (!poll->hide_results_until_close_) {
    auto it = pending_answers_.find(poll_id);
    if (it == pending_answers_.end() || (it->second.is_finished_ && poll->was_saved_)) {
      for (const auto &poll_option : poll->options_) {
        if (poll_option.is_chosen_) {
          return true;
        }
      }
    }
  }
  if (!poll->country_codes_.empty() &&
      !td::contains(poll->country_codes_, td_->option_manager_->get_option_string("phone_country_iso2"))) {
    return true;
  }
  if (is_poll_vote_subscribers_only_limited(poll, initial_dialog_id, initial_date)) {
    return true;
  }
  return false;
}

void PollManager::get_poll_voters(MessageFullId message_full_id, int32 option_id, int32 offset, int32 limit,
                                  Promise<td_api::object_ptr<td_api::pollVoters>> &&promise) {
  TRY_RESULT_PROMISE(promise, poll_id, td_->messages_manager_->get_message_poll_id(message_full_id, false));
  if (offset < 0) {
    return promise.set_error(400, "Invalid offset specified");
  }
  if (limit <= 0) {
    return promise.set_error(400, "Parameter limit must be positive");
  }
  if (limit > MAX_GET_POLL_VOTERS) {
    limit = MAX_GET_POLL_VOTERS;
  }

  auto poll = get_poll(poll_id);
  if ((!poll->subscribers_only_ && !can_get_poll_voters(poll_id, poll, DialogId(), 0)) || poll->is_anonymous_ ||
      is_local_poll_id(poll_id)) {
    return promise.set_error(400, "Poll results can't be received");
  }
  if (option_id < 0 || static_cast<size_t>(option_id) >= poll->options_.size()) {
    return promise.set_error(400, "Invalid option ID specified");
  }

  auto &voters = get_poll_option_voters(poll, poll_id, option_id);
  if (voters.pending_queries_.empty() && voters.was_invalidated_ && offset == 0) {
    voters.voters_.clear();
    voters.next_offset_.clear();
    voters.was_invalidated_ = false;
  }

  auto cur_offset = narrow_cast<int32>(voters.voters_.size());

  if (offset > cur_offset) {
    return promise.set_error(400, "Too big offset specified; voters can be received only consequently");
  }
  if (offset < cur_offset) {
    vector<std::pair<DialogId, int32>> result;
    for (int32 i = offset; i != cur_offset && i - offset < limit; i++) {
      result.push_back(voters.voters_[i]);
    }
    return promise.set_value(
        get_poll_voters_object(max(poll->options_[option_id].voter_count_, cur_offset), std::move(result)));
  }

  if (poll->options_[option_id].voter_count_ == 0 || (voters.next_offset_.empty() && cur_offset > 0)) {
    return promise.set_value(get_poll_voters_object(0, Auto()));
  }

  voters.pending_queries_.push_back(std::move(promise));
  if (voters.pending_queries_.size() > 1) {
    return;
  }

  unload_poll_timeout_.cancel_timeout(poll_id.get());

  auto query_promise =
      PromiseCreator::lambda([actor_id = actor_id(this), poll_id, option_id, offset = voters.next_offset_,
                              limit](Result<tl_object_ptr<telegram_api::messages_votesList>> &&result) mutable {
        send_closure(actor_id, &PollManager::on_get_poll_voters, poll_id, option_id, std::move(offset), limit,
                     std::move(result));
      });
  td_->create_handler<GetPollVotersQuery>(std::move(query_promise))
      ->send(poll_id, message_full_id, BufferSlice(poll->options_[option_id].get_data()), voters.next_offset_,
             max(limit, 10));
}

void PollManager::on_get_poll_voters(PollId poll_id, int32 option_id, string offset, int32 limit,
                                     Result<tl_object_ptr<telegram_api::messages_votesList>> &&result) {
  G()->ignore_result_if_closing(result);

  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);
  if (option_id < 0 || static_cast<size_t>(option_id) >= poll->options_.size()) {
    LOG(ERROR) << "Can't process voters for option " << option_id << " in " << poll_id << ", because it has only "
               << poll->options_.size() << " options";
    return;
  }
  if (poll->is_anonymous_) {
    // just in case
    result = Status::Error(400, "Poll is anonymous");
  }

  auto &voters = get_poll_option_voters(poll, poll_id, option_id);
  if (voters.next_offset_ != offset) {
    LOG(ERROR) << "Expected results for option " << option_id << " in " << poll_id << " with offset "
               << voters.next_offset_ << ", but received with " << offset;
    return;
  }
  auto promises = std::move(voters.pending_queries_);
  if (promises.empty()) {
    LOG(ERROR) << "Have no waiting promises for option " << option_id << " in " << poll_id;
    return;
  }
  if (result.is_error()) {
    fail_promises(promises, result.move_as_error());
    return;
  }

  auto vote_list = result.move_as_ok();
  td_->user_manager_->on_get_users(std::move(vote_list->users_), "on_get_poll_voters");
  td_->chat_manager_->on_get_chats(std::move(vote_list->chats_), "on_get_poll_voters");

  voters.next_offset_ = std::move(vote_list->next_offset_);
  if (poll->options_[option_id].voter_count_ != vote_list->count_) {
    ++current_generation_;
    update_poll_timeout_.set_timeout_in(poll_id.get(), 0.0);
  }

  vector<std::pair<DialogId, int32>> voter_ids;
  for (auto &peer_vote : vote_list->votes_) {
    DialogId dialog_id;
    int32 date = 0;
    switch (peer_vote->get_id()) {
      case telegram_api::messagePeerVote::ID: {
        auto voter = telegram_api::move_object_as<telegram_api::messagePeerVote>(peer_vote);
        if (voter->option_ != poll->options_[option_id].get_data()) {
          continue;
        }

        dialog_id = DialogId(voter->peer_);
        date = voter->date_;
        break;
      }
      case telegram_api::messagePeerVoteInputOption::ID: {
        auto voter = telegram_api::move_object_as<telegram_api::messagePeerVoteInputOption>(peer_vote);
        dialog_id = DialogId(voter->peer_);
        date = voter->date_;
        break;
      }
      case telegram_api::messagePeerVoteMultiple::ID: {
        auto voter = telegram_api::move_object_as<telegram_api::messagePeerVoteMultiple>(peer_vote);
        if (!td::contains(voter->options_, poll->options_[option_id].get_data())) {
          continue;
        }

        dialog_id = DialogId(voter->peer_);
        date = voter->date_;
        break;
      }
      default:
        UNREACHABLE();
    }
    if (dialog_id.is_valid()) {
      voter_ids.emplace_back(dialog_id, date);
    } else {
      LOG(ERROR) << "Receive " << dialog_id << " as voter in " << poll_id;
    }
  }
  append(voters.voters_, voter_ids);
  if (static_cast<int32>(voter_ids.size()) > limit) {
    voter_ids.resize(limit);
  }
  auto known_voter_count = narrow_cast<int32>(voters.voters_.size());
  if (voters.next_offset_.empty() && known_voter_count != vote_list->count_) {
    // invalidate_poll_option_voters(poll, poll_id, option_id);
    voters.was_invalidated_ = true;
  }

  for (auto &promise : promises) {
    promise.set_value(get_poll_voters_object(max(vote_list->count_, known_voter_count),
                                             vector<std::pair<DialogId, int32>>(voter_ids)));
  }
}

void PollManager::stop_poll(MessageFullId message_full_id, td_api::object_ptr<td_api::ReplyMarkup> &&reply_markup,
                            Promise<Unit> &&promise) {
  TRY_RESULT_PROMISE(promise, poll_id, td_->messages_manager_->get_message_poll_id(message_full_id, true));
  if (get_poll_is_closed(poll_id)) {
    return promise.set_error(400, "Poll has already been closed");
  }
  TRY_RESULT_PROMISE(promise, new_reply_markup,
                     get_inline_reply_markup(std::move(reply_markup), td_->auth_manager_->is_bot(),
                                             td_->messages_manager_->has_message_sender_user_id(message_full_id)));
  if (is_local_poll_id(poll_id)) {
    LOG(ERROR) << "Receive local " << poll_id << " from " << message_full_id << " in stop_poll";
    stop_local_poll(poll_id);
    promise.set_value(Unit());
    return;
  }
  auto poll = get_poll_editable(poll_id);
  CHECK(poll != nullptr);
  if (poll->is_closed_) {
    promise.set_value(Unit());
    return;
  }

  ++current_generation_;

  poll->is_closed_ = true;
  save_poll(poll, poll_id);
  notify_on_poll_update(poll_id);

  do_stop_poll(poll_id, message_full_id, std::move(new_reply_markup), 0, std::move(promise));
}

class PollManager::StopPollLogEvent {
 public:
  PollId poll_id_;
  MessageFullId message_full_id_;

  template <class StorerT>
  void store(StorerT &storer) const {
    td::store(poll_id_, storer);
    td::store(message_full_id_, storer);
  }

  template <class ParserT>
  void parse(ParserT &parser) {
    td::parse(poll_id_, parser);
    td::parse(message_full_id_, parser);
  }
};

void PollManager::do_stop_poll(PollId poll_id, MessageFullId message_full_id, unique_ptr<ReplyMarkup> &&reply_markup,
                               uint64 log_event_id, Promise<Unit> &&promise) {
  LOG(INFO) << "Stop " << poll_id << " from " << message_full_id;
  CHECK(poll_id.is_valid());

  if (log_event_id == 0 && G()->use_message_database() && reply_markup == nullptr) {
    StopPollLogEvent log_event{poll_id, message_full_id};
    log_event_id =
        binlog_add(G()->td_db()->get_binlog(), LogEvent::HandlerType::StopPoll, get_log_event_storer(log_event));
  }

  unload_poll_timeout_.cancel_timeout(poll_id.get());

  bool is_inserted = being_closed_polls_.insert(poll_id).second;
  CHECK(is_inserted);
  auto new_promise = PromiseCreator::lambda([actor_id = actor_id(this), poll_id, message_full_id, log_event_id,
                                             promise = std::move(promise)](Result<Unit> result) mutable {
    send_closure(actor_id, &PollManager::on_stop_poll_finished, poll_id, message_full_id, log_event_id,
                 std::move(result), std::move(promise));
  });

  td_->create_handler<StopPollQuery>(std::move(new_promise))->send(message_full_id, std::move(reply_markup), poll_id);
}

void PollManager::on_stop_poll_finished(PollId poll_id, MessageFullId message_full_id, uint64 log_event_id,
                                        Result<Unit> &&result, Promise<Unit> &&promise) {
  being_closed_polls_.erase(poll_id);

  if (log_event_id != 0 && !G()->close_flag()) {
    binlog_erase(G()->td_db()->get_binlog(), log_event_id);
  }

  if (td_->auth_manager_->is_bot()) {
    if ((server_poll_messages_.count(poll_id) > 0 && server_poll_messages_[poll_id].count(message_full_id) > 0) ||
        (other_poll_messages_.count(poll_id) > 0 && other_poll_messages_[poll_id].count(message_full_id) > 0)) {
      td_->messages_manager_->on_external_update_message_content(message_full_id, "on_stop_poll_finished");
    }
  }

  promise.set_result(std::move(result));
}

void PollManager::stop_local_poll(PollId poll_id) {
  CHECK(is_local_poll_id(poll_id));
  auto poll = get_poll_editable(poll_id);
  CHECK(poll != nullptr);
  if (poll->is_closed_) {
    return;
  }

  poll->is_closed_ = true;
  notify_on_poll_update(poll_id);
}

void PollManager::delete_pending_web_page(PollId poll_id, WebPageId web_page_id) {
  auto poll = get_poll_editable(poll_id);
  CHECK(poll != nullptr);
  for (auto &option : poll->options_) {
    if (option.get_web_page_id() == web_page_id) {
      option.remove_web_page();
    }
  }
  td_->web_pages_manager_->register_poll_web_pages(poll_id, get_poll_web_page_ids(poll));

  // no need to notify about poll update, because the web pages were pending
  // notify_on_poll_update(poll_id);
  save_poll(poll, poll_id);
}

double PollManager::get_polling_timeout() const {
  double result = td_->online_manager_->is_online() ? 60 : 30 * 60;
  return result * Random::fast(70, 100) * 0.01;
}

void PollManager::on_update_poll_timeout(PollId poll_id) {
  if (G()->close_flag()) {
    return;
  }
  CHECK(!td_->auth_manager_->is_bot());
  CHECK(!is_local_poll_id(poll_id));

  auto poll = get_poll(poll_id);
  if (poll == nullptr || (poll->is_closed_ && poll->is_updated_after_close_)) {
    return;
  }
  if (pending_answers_.count(poll_id) > 0) {
    LOG(INFO) << "Skip fetching results of " << poll_id << ", because it is being voted now";
    return;
  }

  if (server_poll_messages_.count(poll_id) == 0) {
    return;
  }

  auto message_full_id = server_poll_messages_[poll_id].get_random();
  LOG(INFO) << "Fetching results of " << poll_id << " from " << message_full_id;
  auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), poll_id, generation = current_generation_](
                                                  Result<tl_object_ptr<telegram_api::Updates>> &&result) {
    send_closure(actor_id, &PollManager::on_get_poll_results, poll_id, generation, std::move(result));
  });
  td_->create_handler<GetPollResultsQuery>(std::move(query_promise))->send(poll_id, message_full_id, poll->hash_);
}

void PollManager::on_close_poll_timeout(PollId poll_id) {
  if (G()->close_flag()) {
    return;
  }
  CHECK(!is_local_poll_id(poll_id));

  auto poll = get_poll_editable(poll_id);
  if (poll == nullptr || poll->is_closed_ || poll->close_date_ == 0) {
    return;
  }

  LOG(INFO) << "Trying to close " << poll_id << " by timer";
  if (poll->close_date_ <= G()->server_time()) {
    poll->is_closed_ = true;
    save_poll(poll, poll_id);
    notify_on_poll_update(poll_id);

    // don't send updatePoll for bots, because there is no way to guarantee it

    if (!td_->auth_manager_->is_bot()) {
      update_poll_timeout_.set_timeout_in(poll_id.get(), 1.0);
    }
  } else {
    close_poll_timeout_.set_timeout_in(poll_id.get(), poll->close_date_ - G()->server_time() + 1e-3);
  }
}

void PollManager::on_unload_poll_timeout(PollId poll_id) {
  if (G()->close_flag()) {
    return;
  }
  if (is_local_poll_id(poll_id)) {
    LOG(INFO) << "Forget " << poll_id;

    auto is_deleted = polls_.erase(poll_id) > 0;
    CHECK(is_deleted);

    CHECK(poll_voters_.count(poll_id) == 0);
    CHECK(loaded_from_database_polls_.count(poll_id) == 0);
    return;
  }

  if (!can_unload_poll(poll_id)) {
    return;
  }
  if (!have_poll(poll_id)) {
    return;
  }

  LOG(INFO) << "Unload " << poll_id;

  update_poll_timeout_.cancel_timeout(poll_id.get(), "on_unload_poll_timeout");
  close_poll_timeout_.cancel_timeout(poll_id.get());

  auto is_deleted = polls_.erase(poll_id) > 0;
  CHECK(is_deleted);

  poll_voters_.erase(poll_id);
  loaded_from_database_polls_.erase(poll_id);
  unload_poll_timeout_.cancel_timeout(poll_id.get());
  td_->web_pages_manager_->register_poll_web_pages(poll_id, {});
}

void PollManager::forget_local_poll(PollId poll_id) {
  if (G()->close_flag()) {
    return;
  }
  CHECK(is_local_poll_id(poll_id));
  unload_poll_timeout_.set_timeout_in(poll_id.get(), UNLOAD_POLL_DELAY);
}

void PollManager::on_get_poll_results(PollId poll_id, uint64 generation,
                                      Result<tl_object_ptr<telegram_api::Updates>> result) {
  G()->ignore_result_if_closing(result);

  auto poll = get_poll(poll_id);
  if (poll == nullptr) {
    return;
  }
  if (result.is_error()) {
    if (!(poll->is_closed_ && poll->is_updated_after_close_) && !G()->close_flag() && !td_->auth_manager_->is_bot()) {
      auto timeout = get_polling_timeout();
      LOG(INFO) << "Schedule updating of " << poll_id << " in " << timeout;
      update_poll_timeout_.add_timeout_in(poll_id.get(), timeout);
    }
    return;
  }
  if (result.ok() == nullptr) {
    return;
  }
  if (generation != current_generation_) {
    LOG(INFO) << "Receive possibly outdated result of " << poll_id << ", reget it";
    if (!(poll->is_closed_ && poll->is_updated_after_close_) && !G()->close_flag() && !td_->auth_manager_->is_bot()) {
      update_poll_timeout_.set_timeout_in(poll_id.get(), 0.0);
    }
    return;
  }

  td_->updates_manager_->on_get_updates(result.move_as_ok(), Promise<Unit>());
}

void PollManager::on_online() {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  server_poll_messages_.foreach([&](const PollId &poll_id, WaitFreeHashSet<MessageFullId, MessageFullIdHash> &) {
    if (update_poll_timeout_.has_timeout(poll_id.get())) {
      auto timeout = Random::fast(3, 30);
      LOG(INFO) << "Schedule updating of " << poll_id << " in " << timeout;
      update_poll_timeout_.set_timeout_in(poll_id.get(), timeout);
    }
  });
}

vector<unique_ptr<MessageContent>> PollManager::get_individual_message_contents(
    PollId poll_id, const MessageContent *attached_media) const {
  return transform(get_individual_message_content_refs(poll_id, attached_media),
                   [td = td_](const MessageContent *content) {
                     if (content == nullptr) {
                       return create_empty_text_message_content();
                     }
                     return dup_message_content(td, DialogId(), content, MessageContentDupType::ServerCopy,
                                                MessageCopyOptions(true, false));
                   });
}

vector<MessageContent *> PollManager::get_individual_message_content_refs(PollId poll_id,
                                                                          MessageContent *attached_media) {
  auto poll = get_poll_editable(poll_id);
  CHECK(poll != nullptr);

  vector<MessageContent *> message_contents;
  message_contents.push_back(attached_media);
  message_contents.push_back(poll->explanation_media_.get());
  for (auto &option : poll->options_) {
    message_contents.push_back(option.media_.get());
  }
  return message_contents;
}

vector<const MessageContent *> PollManager::get_individual_message_content_refs(
    PollId poll_id, const MessageContent *attached_media) const {
  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);

  vector<const MessageContent *> message_contents;
  message_contents.push_back(attached_media);
  message_contents.push_back(poll->explanation_media_.get());
  for (const auto &option : poll->options_) {
    message_contents.push_back(option.media_.get());
  }
  return message_contents;
}

unique_ptr<MessageContent> &PollManager::get_individual_message_content(PollId poll_id,
                                                                        unique_ptr<MessageContent> &attached_media,
                                                                        int32 media_pos) {
  auto poll = get_poll_editable(poll_id);
  CHECK(poll != nullptr);
  CHECK(is_local_poll_id(poll_id));
  if (media_pos == 0) {
    return attached_media;
  }
  if (media_pos == 1) {
    return poll->explanation_media_;
  }
  auto pos = static_cast<size_t>(media_pos - 2);
  CHECK(pos < poll->options_.size());
  return poll->options_[pos].media_;
}

PollId PollManager::dup_poll(DialogId dialog_id, PollId poll_id) {
  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);

  auto question = poll->question_;
  remove_unallowed_entities(td_, question, dialog_id);
  auto options =
      transform(poll->options_, [td = td_, dialog_id](const auto &option) { return option.dup_option(td, dialog_id); });
  auto explanation = poll->explanation_;
  remove_unallowed_entities(td_, explanation, dialog_id);
  unique_ptr<MessageContent> explanation_media;
  if (poll->explanation_media_ != nullptr) {
    explanation_media = dup_message_content(td_, dialog_id, poll->explanation_media_.get(), MessageContentDupType::Copy,
                                            MessageCopyOptions(true, false));
  }
  bool is_broadcast = td_->dialog_manager_->is_broadcast_channel(dialog_id);
  return create_poll(std::move(question), std::move(options), poll->is_anonymous_, poll->allow_multiple_answers_,
                     poll->has_open_answers_, poll->has_revoting_disabled_,
                     is_broadcast ? poll->subscribers_only_ : false,
                     is_broadcast ? vector<string>(poll->country_codes_) : vector<string>(), poll->shuffle_answers_,
                     poll->hide_results_until_close_, poll->is_quiz_, poll->correct_option_ids_, std::move(explanation),
                     std::move(explanation_media), poll->open_period_,
                     poll->open_period_ == 0 ? 0 : G()->unix_time() + poll->open_period_, false);
}

bool PollManager::has_input_media(PollId poll_id) const {
  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);
  if (poll->is_quiz_ && poll->correct_option_ids_.empty()) {
    return false;
  }
  return true;
}

telegram_api::object_ptr<telegram_api::InputMedia> PollManager::get_input_media(
    PollId poll_id, vector<telegram_api::object_ptr<telegram_api::InputMedia>> &&input_media) const {
  auto poll = get_poll(poll_id);
  CHECK(poll != nullptr);
  CHECK(input_media.size() == 2 + poll->options_.size());

  int32 poll_flags = 0;
  if (poll->open_period_ != 0) {
    poll_flags |= telegram_api::poll::CLOSE_PERIOD_MASK;
  }
  if (poll->close_date_ != 0) {
    poll_flags |= telegram_api::poll::CLOSE_DATE_MASK;
  }
  auto country_codes = poll->country_codes_;
  if (!country_codes.empty()) {
    poll_flags |= telegram_api::poll::COUNTRIES_ISO2_MASK;
  }

  int32 flags = 0;
  vector<int32> correct_answers;
  if (poll->is_quiz_) {
    flags |= telegram_api::inputMediaPoll::CORRECT_ANSWERS_MASK;
    CHECK(!poll->correct_option_ids_.empty());
    for (auto correct_option_id : poll->correct_option_ids_) {
      CHECK(static_cast<size_t>(correct_option_id) < poll->options_.size());
      correct_answers.push_back(correct_option_id);
    }

    if (!poll->explanation_.text.empty()) {
      flags |= telegram_api::inputMediaPoll::SOLUTION_MASK;
    }
    if (input_media[1] != nullptr) {
      flags |= telegram_api::inputMediaPoll::SOLUTION_MEDIA_MASK;
    }
  }
  if (input_media[0] != nullptr) {
    flags |= telegram_api::inputMediaPoll::ATTACHED_MEDIA_MASK;
  }
  vector<telegram_api::object_ptr<telegram_api::PollAnswer>> answers;
  for (size_t i = 0; i < poll->options_.size(); i++) {
    answers.push_back(poll->options_[i].get_input_poll_answer(std::move(input_media[2 + i])));
  }
  return telegram_api::make_object<telegram_api::inputMediaPoll>(
      flags,
      telegram_api::make_object<telegram_api::poll>(
          0, poll_flags, poll->is_closed_, !poll->is_anonymous_, poll->allow_multiple_answers_, poll->is_quiz_,
          poll->has_open_answers_, poll->has_revoting_disabled_, poll->shuffle_answers_,
          poll->hide_results_until_close_, true, poll->subscribers_only_,
          get_input_text_with_entities(nullptr, poll->question_, "get_input_media_poll"), std::move(answers),
          poll->open_period_, poll->close_date_, std::move(country_codes), 0),
      std::move(correct_answers), std::move(input_media[0]), poll->explanation_.text,
      get_input_message_entities(td_->user_manager_.get(), poll->explanation_.entities, "get_input_media_poll"),
      std::move(input_media[1]));
}

PollId PollManager::on_get_poll(PollId poll_id, tl_object_ptr<telegram_api::poll> &&poll_server,
                                tl_object_ptr<telegram_api::pollResults> &&poll_results, const char *source) {
  bool is_bot = td_->auth_manager_->is_bot();
  bool need_update_poll = poll_id.is_valid() && is_bot;
  if (!poll_id.is_valid() && poll_server != nullptr) {
    poll_id = PollId(poll_server->id_);
  }
  if (!poll_id.is_valid() || is_local_poll_id(poll_id)) {
    LOG(ERROR) << "Receive " << poll_id << " from " << source << ": " << oneline(to_string(poll_server)) << " "
               << oneline(to_string(poll_results));
    return PollId();
  }
  if (poll_server != nullptr && poll_server->id_ != poll_id.get()) {
    LOG(ERROR) << "Receive poll " << poll_server->id_ << " instead of " << poll_id << " from " << source;
    return PollId();
  }
  auto max_poll_options = clamp(td_->option_manager_->get_option_integer("poll_answer_count_max"),
                                static_cast<int64>(12), static_cast<int64>(1000));
  if (poll_server != nullptr &&
      (poll_server->answers_.empty() || static_cast<int64>(poll_server->answers_.size()) > 10 * max_poll_options)) {
    LOG(ERROR) << "Receive " << poll_id << " from " << source
               << " with wrong number of answers: " << to_string(poll_server);
    return PollId();
  }
  if (poll_server != nullptr) {
    FlatHashSet<Slice, SliceHash> option_data;
    for (auto &answer_ptr : poll_server->answers_) {
      if (answer_ptr->get_id() != telegram_api::pollAnswer::ID) {
        LOG(ERROR) << "Receive " << poll_id << " from " << source << " with invalid answer: " << to_string(poll_server);
        return PollId();
      }
      const auto *answer = static_cast<const telegram_api::pollAnswer *>(answer_ptr.get());
      if (answer->option_.empty()) {
        LOG(ERROR) << "Receive " << poll_id << " from " << source
                   << " with an empty option data: " << to_string(poll_server);
        return PollId();
      }
      option_data.insert(answer->option_.as_slice());
    }
    if (option_data.size() != poll_server->answers_.size()) {
      LOG(ERROR) << "Receive " << poll_id << " from " << source
                 << " with duplicate options: " << to_string(poll_server);
      return PollId();
    }
  }

  auto poll = get_poll_force(poll_id);
  bool is_changed = false;
  bool need_save_to_database = false;
  if (poll == nullptr) {
    if (poll_server == nullptr) {
      LOG(INFO) << "Ignore " << poll_id << ", because have no data about it";
      return PollId();
    }

    auto p = make_unique<Poll>();
    poll = p.get();
    polls_.set(poll_id, std::move(p));
  } else if (poll_results != nullptr && poll_results->min_ && pending_answers_.count(poll_id) != 0) {
    LOG(INFO) << "Ignore being answered min-" << poll_id;
    return poll_id;
  }

  CHECK(poll != nullptr);

  bool poll_server_is_closed = false;
  if (poll_server != nullptr) {
    vector<std::pair<ChannelId, MinChannel>> option_min_channels;
    auto options = PollOption::get_poll_options(td_, std::move(poll_server->answers_), option_min_channels);
    if (poll->options_ != options) {
      vector<string> correct_option_datas;
      for (auto correct_option_id : poll->correct_option_ids_) {
        CHECK(0 <= correct_option_id && correct_option_id < static_cast<int32>(poll->options_.size()));
        correct_option_datas.push_back(poll->options_[correct_option_id].get_data());
      }
      poll->options_ = std::move(options);
      poll->option_min_channels_ = std::move(option_min_channels);
      if (!correct_option_datas.empty()) {  // repair correct_option_ids just in case
        poll->correct_option_ids_.clear();
        for (size_t i = 0; i < poll->options_.size(); i++) {
          if (td::contains(correct_option_datas, poll->options_[i].get_data())) {
            poll->correct_option_ids_.push_back(static_cast<int32>(i));
          }
        }
      }
      auto it = poll_voters_.find(poll_id);
      if (it != poll_voters_.end()) {
        for (auto &voters : it->second) {
          fail_promises(voters.pending_queries_, Status::Error(500, "The poll was changed"));
        }
        poll_voters_.erase(it);
      }
      td_->web_pages_manager_->register_poll_web_pages(poll_id, get_poll_web_page_ids(poll));
      is_changed = true;
    }
    auto question = get_formatted_text(nullptr, std::move(poll_server->question_), true, true, "on_get_poll");
    keep_only_custom_emoji(question);
    if (poll->question_ != question) {
      poll->question_ = std::move(question);
      is_changed = true;
    }
    poll_server_is_closed = poll_server->closed_;
    if (poll_server_is_closed && !poll->is_closed_) {
      poll->is_closed_ = poll_server_is_closed;
      is_changed = true;
    }
    if (poll_server_is_closed && !poll->is_updated_after_close_) {
      poll->is_updated_after_close_ = true;
      is_changed = true;
    }
    int32 open_period = poll_server->close_period_;
    int32 close_date = poll_server->close_date_;
    if (close_date == 0 || open_period == 0) {
      close_date = 0;
      open_period = 0;
    }
    if (open_period != poll->open_period_) {
      poll->open_period_ = open_period;
      if (!poll->is_closed_) {
        is_changed = true;
      } else {
        need_save_to_database = true;
      }
    }
    if (close_date != poll->close_date_) {
      poll->close_date_ = close_date;
      if (!poll->is_closed_) {
        is_changed = true;
        if (close_date != 0) {
          if (close_date <= G()->server_time()) {
            poll->is_closed_ = true;
          } else if (!G()->close_flag()) {
            close_poll_timeout_.set_timeout_in(poll_id.get(), close_date - G()->server_time() + 1e-3);
          }
        } else if (!G()->close_flag()) {
          close_poll_timeout_.cancel_timeout(poll_id.get());
        }
      } else {
        need_save_to_database = true;
      }
    }
    bool is_anonymous = !poll_server->public_voters_;
    if (is_anonymous != poll->is_anonymous_) {
      poll->is_anonymous_ = is_anonymous;
      is_changed = true;
    }
    bool allow_multiple_answers = poll_server->multiple_choice_;
    bool has_open_answers = poll_server->open_answers_;
    bool has_revoting_disabled = poll_server->revoting_disabled_;
    bool subscribers_only = poll_server->subscribers_only_;
    bool shuffle_answers = poll_server->shuffle_answers_;
    bool hide_results_until_close = poll_server->hide_results_until_close_;
    bool is_quiz = poll_server->quiz_;
    auto country_codes = std::move(poll_server->countries_iso2_);
    if (is_quiz && has_open_answers) {
      LOG(ERROR) << "Receive quiz with open answers";
      has_open_answers = false;
    }
    if (allow_multiple_answers != poll->allow_multiple_answers_) {
      poll->allow_multiple_answers_ = allow_multiple_answers;
      is_changed = true;
    }
    if (has_open_answers != poll->has_open_answers_) {
      poll->has_open_answers_ = has_open_answers;
      is_changed = true;
    }
    if (has_revoting_disabled != poll->has_revoting_disabled_) {
      poll->has_revoting_disabled_ = has_revoting_disabled;
      is_changed = true;
    }
    if (shuffle_answers != poll->shuffle_answers_) {
      poll->shuffle_answers_ = shuffle_answers;
      is_changed = true;
    }
    if (subscribers_only != poll->subscribers_only_) {
      poll->subscribers_only_ = subscribers_only;
      is_changed = true;
    }
    if (country_codes != poll->country_codes_) {
      poll->country_codes_ = std::move(country_codes);
      is_changed = true;
    }
    if (hide_results_until_close != poll->hide_results_until_close_) {
      poll->hide_results_until_close_ = hide_results_until_close;
      is_changed = true;
    }
    if (is_quiz != poll->is_quiz_) {
      poll->is_quiz_ = is_quiz;
      is_changed = true;
    }
    if (poll_server->creator_ && !poll->is_creator_) {
      poll->is_creator_ = true;
      is_changed = true;
    }
    if (poll_server->hash_ != poll->hash_) {
      poll->hash_ = poll_server->hash_;
      is_changed = true;
    }
  }

  CHECK(poll_results != nullptr);
  bool is_min = poll_results->min_;
  bool has_total_voters = (poll_results->flags_ & telegram_api::pollResults::TOTAL_VOTERS_MASK) != 0;
  if (has_total_voters && poll_results->total_voters_ != poll->total_voter_count_) {
    poll->total_voter_count_ = poll_results->total_voters_;
    if (poll->total_voter_count_ < 0) {
      LOG(ERROR) << "Receive " << poll->total_voter_count_ << " voters in " << poll_id << " from " << source;
      poll->total_voter_count_ = 0;
    }
    is_changed = true;
  }
  vector<std::pair<ChannelId, MinChannel>> recent_option_voter_min_channels;
  bool need_update_recent_option_voter_min_channels = false;
  vector<int32> correct_option_ids;
  for (auto &poll_result : poll_results->results_) {
    Slice data = poll_result->option_.as_slice();
    for (size_t option_index = 0; option_index < poll->options_.size(); option_index++) {
      auto &option = poll->options_[option_index];
      if (option.get_data() != data) {
        continue;
      }
      if (!is_min) {
        bool is_chosen = poll_result->chosen_;
        if (is_chosen != option.is_chosen_) {
          option.is_chosen_ = is_chosen;
          is_changed = true;
        }
      }
      if (poll_result->correct_) {
        correct_option_ids.push_back(static_cast<int32>(option_index));
      }

      if (poll_result->voters_ < 0) {
        LOG(ERROR) << "Receive " << poll_result->voters_ << " voters for an option in " << poll_id << " from "
                   << source;
        poll_result->voters_ = 0;
      }
      if (option.is_chosen_ && poll_result->voters_ == 0 && !poll->hide_results_until_close_) {
        LOG(ERROR) << "Receive 0 voters for the chosen option " << option_index << " in " << poll_id << " from "
                   << source;
        poll_result->voters_ = 1;
      }
      if (poll_result->voters_ > poll->total_voter_count_) {
        LOG(ERROR) << "Have only " << poll->total_voter_count_ << " poll voters, but there are " << poll_result->voters_
                   << " voters for an option in " << poll_id << " from " << source;
        poll->total_voter_count_ = poll_result->voters_;
      }
      auto max_voter_count = std::numeric_limits<int32>::max() / narrow_cast<int32>(poll->options_.size()) - 2;
      if (poll_result->voters_ > max_voter_count) {
        LOG(ERROR) << "Have too many " << poll_result->voters_ << " poll voters for an option in " << poll_id
                   << " from " << source;
        poll_result->voters_ = max_voter_count;
      }
      if (poll_result->voters_ != option.voter_count_) {
        invalidate_poll_option_voters(poll, poll_id, option_index);
        option.voter_count_ = poll_result->voters_;
        is_changed = true;
      }
      // creator sees poll results, while others don't, therefore the recent voters must not be dropped
      // if countries, are limited, then results will be available for everyone, but hidden in get_poll_object
      if (!(is_min && poll->is_creator_ && poll->hide_results_until_close_) && !is_bot) {
        vector<DialogId> recent_option_voter_dialog_ids;
        for (const auto &recent_voter : poll_result->recent_voters_) {
          DialogId dialog_id(recent_voter);
          if (td::contains(recent_option_voter_dialog_ids, dialog_id)) {
            LOG(ERROR) << "Receive duplicate " << dialog_id << " as recent option voter in " << poll_id << " from "
                       << source;
            continue;
          }
          if (check_min_message_sender(td_, dialog_id, recent_option_voter_min_channels)) {
            recent_option_voter_dialog_ids.push_back(dialog_id);
          }
        }
        if (recent_option_voter_dialog_ids.size() > static_cast<size_t>(option.voter_count_)) {
          LOG(ERROR) << "Receive option with " << option.voter_count_ << " votes and "
                     << recent_option_voter_dialog_ids.size() << " recent voters";
          recent_option_voter_dialog_ids.resize(static_cast<size_t>(option.voter_count_));
        }
        if (recent_option_voter_dialog_ids != option.recent_voter_dialog_ids_) {
          option.recent_voter_dialog_ids_ = std::move(recent_option_voter_dialog_ids);
          is_changed = true;
          need_update_recent_option_voter_min_channels = true;
        }
      }
    }
  }
  if (need_update_recent_option_voter_min_channels) {
    poll->recent_option_voter_min_channels_ = std::move(recent_option_voter_min_channels);
  }
  if (!poll_results->results_.empty() && has_total_voters) {
    int32 max_total_voter_count = 0;
    for (const auto &option : poll->options_) {
      max_total_voter_count += option.voter_count_;
    }
    if (poll->total_voter_count_ > max_total_voter_count && max_total_voter_count != 0) {
      LOG(ERROR) << "Have only " << max_total_voter_count << " total poll voters, but there are "
                 << poll->total_voter_count_ << " voters in " << poll_id << " from " << source;
      poll->total_voter_count_ = max_total_voter_count;
    }
  }
  if (!is_min && !td_->auth_manager_->is_bot() && poll_results->has_unread_votes_ != poll->has_unread_votes_) {
    if (!poll->is_creator_) {
      LOG(ERROR) << "Have unread votes for a non-created " << poll_id;
    } else {
      bool has_penqing_query = false;
      if (server_poll_messages_.count(poll_id) > 0) {
        server_poll_messages_[poll_id].foreach([&](const MessageFullId &message_full_id) {
          if (td_->message_query_manager_->has_message_pending_read_poll_votes(message_full_id)) {
            has_penqing_query = true;
          }
        });
      }
      if (!has_penqing_query) {
        poll->has_unread_votes_ = poll_results->has_unread_votes_;
        need_save_to_database = true;
        notify_on_poll_has_unread_votes_update(poll_id, poll->has_unread_votes_);
      }
    }
  }
  if (!is_min && poll_results->can_view_stats_ != poll->can_view_stats_) {
    poll->can_view_stats_ = poll_results->can_view_stats_;
    is_changed = true;
  }

  auto explanation = get_formatted_text(td_->user_manager_.get(), std::move(poll_results->solution_),
                                        std::move(poll_results->solution_entities_), true, false, source);
  if (poll->is_quiz_) {
    if (!is_min || poll_server_is_closed) {
      if (poll->correct_option_ids_ != correct_option_ids) {
        if (correct_option_ids.empty() && !poll->correct_option_ids_.empty()) {
          LOG(ERROR) << "Can't change correct options of " << poll_id << " from " << poll->correct_option_ids_ << " to "
                     << correct_option_ids << " from " << source;
        } else {
          poll->correct_option_ids_ = std::move(correct_option_ids);
          is_changed = true;
        }
      }
      if (poll->explanation_ != explanation) {
        if (explanation.text.empty() && !poll->explanation_.text.empty()) {
          LOG(ERROR) << "Can't change known " << poll_id << " explanation to empty from " << source;
        } else {
          poll->explanation_ = std::move(explanation);
          is_changed = true;
        }
      }
      unique_ptr<MessageContent> explanation_media;
      if (poll_results->solution_media_ != nullptr) {
        explanation_media =
            get_message_content(td_, FormattedText(), nullptr, std::move(poll_results->solution_media_), DialogId(), 0,
                                false, UserId(), nullptr, nullptr, "pollResults solution");
        if (!is_allowed_poll_content(explanation_media->get_type())) {
          LOG(ERROR) << "Receive " << explanation_media->get_type() << " in a poll explanation";
          explanation_media = nullptr;
        }
      }
      bool is_content_changed = false;
      bool need_update = false;
      compare_message_contents(td_, poll->explanation_media_.get(), explanation_media.get(), is_content_changed,
                               need_update);
      if (is_content_changed || need_update) {
        poll->explanation_media_ = std::move(explanation_media);
        if (is_content_changed) {
          need_save_to_database = true;
        }
        if (need_update) {
          is_changed = true;
        }
      }
    }
  } else {
    if (!correct_option_ids.empty()) {
      LOG(ERROR) << "Receive correct option " << correct_option_ids << " in non-quiz " << poll_id << " from " << source;
    }
    if (!explanation.text.empty() || poll_results->solution_media_ != nullptr) {
      LOG(ERROR) << "Receive explanation " << explanation << " in non-quiz " << poll_id << " from " << source;
    }
  }

  vector<DialogId> recent_voter_dialog_ids;
  vector<std::pair<ChannelId, MinChannel>> recent_voter_min_channels;
  if (!is_bot) {
    for (auto &peer_id : poll_results->recent_voters_) {
      DialogId dialog_id(peer_id);
      if (td::contains(recent_voter_dialog_ids, dialog_id)) {
        LOG(ERROR) << "Receive duplicate " << dialog_id << " as recent voter in " << poll_id << " from " << source;
        continue;
      }
      if (check_min_message_sender(td_, dialog_id, recent_voter_min_channels)) {
        recent_voter_dialog_ids.push_back(dialog_id);
      }
    }
  }
  if (poll->is_anonymous_ && !recent_voter_dialog_ids.empty()) {
    LOG(ERROR) << "Receive anonymous " << poll_id << " with recent voters " << recent_voter_dialog_ids << " from "
               << source;
    recent_voter_dialog_ids.clear();
    recent_voter_min_channels.clear();
  }
  if (recent_voter_dialog_ids != poll->recent_voter_dialog_ids_) {
    poll->recent_voter_dialog_ids_ = std::move(recent_voter_dialog_ids);
    poll->recent_voter_min_channels_ = std::move(recent_voter_min_channels);
    invalidate_poll_voters(poll, poll_id);
    is_changed = true;
  }

  if (!is_bot && !poll->is_closed_ && !G()->close_flag()) {
    auto timeout = get_polling_timeout();
    LOG(INFO) << "Schedule updating of " << poll_id << " in " << timeout;
    update_poll_timeout_.set_timeout_in(poll_id.get(), timeout);
  }
  if (is_changed || need_save_to_database) {
    save_poll(poll, poll_id);
  }
  if (is_changed) {
    notify_on_poll_update(poll_id);
  }
  if (need_update_poll && (is_changed || (poll->is_closed_ && being_closed_polls_.erase(poll_id) != 0))) {
    CHECK(td_->auth_manager_->is_bot());
    send_closure(G()->td(), &Td::send_update,
                 td_api::make_object<td_api::updatePoll>(
                     get_poll_object(poll_id, poll, DialogId(), MessageId(), DialogId(), 0, false)));

    schedule_poll_unload(poll_id);
  }
  return poll_id;
}

void PollManager::on_update_poll(PollId poll_id, telegram_api::object_ptr<telegram_api::poll> &&poll_server,
                                 telegram_api::object_ptr<telegram_api::pollResults> &&poll_results,
                                 MessageFullId message_full_id, ForumTopicId forum_topic_id, const char *source) {
  if (message_full_id.get_message_id().is_valid() &&
      !td_->messages_manager_->have_message_force(message_full_id, "on_update_poll")) {
    auto dialog_id = message_full_id.get_dialog_id();
    if (td_->dialog_manager_->have_input_peer(dialog_id, false, AccessRights::Read) &&
        td_->messages_manager_->have_dialog(dialog_id)) {
      // the counters of unread poll votes could have been changed
      if (forum_topic_id.is_valid()) {
        td_->forum_topic_manager_->repair_topic_unread_poll_vote_count(dialog_id, forum_topic_id);
      }
      td_->messages_manager_->repair_dialog_unread_poll_vote_count(dialog_id, "on_update_poll");
    }
  }
  on_get_poll(poll_id, std::move(poll_server), std::move(poll_results), source);
}

void PollManager::on_get_poll_vote(PollId poll_id, DialogId dialog_id, vector<BufferSlice> &&options,
                                   vector<int32> positions) {
  if (!poll_id.is_valid()) {
    LOG(ERROR) << "Receive updateMessagePollVote about invalid " << poll_id;
    return;
  }
  if (!dialog_id.is_valid()) {
    LOG(ERROR) << "Receive updateMessagePollVote from invalid " << dialog_id;
    return;
  }
  if (options.size() != positions.size()) {
    LOG(ERROR) << "Receive updateMessagePollVote from " << dialog_id << " in " << poll_id
               << " with mismatch option size";
    return;
  }
  CHECK(td_->auth_manager_->is_bot());

  vector<string> option_ids;
  for (auto &option : options) {
    auto option_id = option.as_slice().str();
    if (!check_utf8(option_id)) {
      LOG(INFO) << "Receive updateMessagePollVote with unexpected option \"" << format::escaped(option_id) << '"';
      return;
    }
    if (option_id.empty()) {
      LOG(INFO) << "Receive updateMessagePollVote with empty option";
      return;
    }
    option_ids.push_back(std::move(option_id));
  }
  for (auto &position : positions) {
    if (position < 0 || position > 100) {
      LOG(ERROR) << "Receive updateMessagePollVote with unexpected position " << position;
      return;
    }
  }

  send_closure(G()->td(), &Td::send_update,
               td_api::make_object<td_api::updatePollAnswer>(
                   poll_id.get(), get_message_sender_object(td_, dialog_id, "on_get_poll_vote"), std::move(option_ids),
                   std::move(positions)));
}

vector<WebPageId> PollManager::get_poll_web_page_ids(const Poll *poll) {
  CHECK(poll != nullptr);
  vector<WebPageId> web_page_ids;
  for (const auto &option : poll->options_) {
    auto web_page_id = option.get_web_page_id();
    if (web_page_id.is_valid() && !td::contains(web_page_ids, web_page_id)) {
      web_page_ids.push_back(web_page_id);
    }
  }
  return web_page_ids;
}

void PollManager::on_binlog_events(vector<BinlogEvent> &&events) {
  if (G()->close_flag()) {
    return;
  }
  bool have_old_message_database = G()->use_message_database() && !G()->td_db()->was_dialog_db_created();
  for (auto &event : events) {
    switch (event.type_) {
      case LogEvent::HandlerType::SetPollAnswer: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        SetPollAnswerLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.message_full_id_.get_dialog_id();

        Dependencies dependencies;
        dependencies.add_dialog_dependencies(dialog_id);  // do not load the dialog itself
        dependencies.resolve_force(td_, "SetPollAnswerLogEvent");

        do_set_poll_answer(log_event.poll_id_, log_event.message_full_id_, std::move(log_event.options_), event.id_,
                           Auto());
        break;
      }
      case LogEvent::HandlerType::StopPoll: {
        if (!have_old_message_database) {
          binlog_erase(G()->td_db()->get_binlog(), event.id_);
          break;
        }

        StopPollLogEvent log_event;
        log_event_parse(log_event, event.get_data()).ensure();

        auto dialog_id = log_event.message_full_id_.get_dialog_id();

        Dependencies dependencies;
        dependencies.add_dialog_dependencies(dialog_id);  // do not load the dialog itself
        dependencies.resolve_force(td_, "StopPollLogEvent");

        do_stop_poll(log_event.poll_id_, log_event.message_full_id_, nullptr, event.id_, Auto());
        break;
      }
      default:
        LOG(FATAL) << "Unsupported log event type " << event.type_;
    }
  }
}

}  // namespace td
