//
// 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/ForumTopicManager.h"

#include "td/telegram/AccessRights.h"
#include "td/telegram/AuthManager.h"
#include "td/telegram/ChannelId.h"
#include "td/telegram/ChatManager.h"
#include "td/telegram/CustomEmojiId.h"
#include "td/telegram/DialogManager.h"
#include "td/telegram/DraftMessageManager.h"
#include "td/telegram/ForumTopic.h"
#include "td/telegram/ForumTopic.hpp"
#include "td/telegram/ForumTopicIcon.h"
#include "td/telegram/ForumTopicInfo.hpp"
#include "td/telegram/Global.h"
#include "td/telegram/LinkManager.h"
#include "td/telegram/logevent/LogEvent.h"
#include "td/telegram/MessageQueryManager.h"
#include "td/telegram/MessagesManager.h"
#include "td/telegram/MessageThreadDb.h"
#include "td/telegram/MessageTopic.h"
#include "td/telegram/misc.h"
#include "td/telegram/NotificationManager.h"
#include "td/telegram/NotificationSettingsManager.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/UserManager.h"

#include "td/utils/algorithm.h"
#include "td/utils/buffer.h"
#include "td/utils/logging.h"
#include "td/utils/Random.h"
#include "td/utils/Slice.h"
#include "td/utils/tl_helpers.h"

namespace td {

class CreateForumTopicQuery final : public Td::ResultHandler {
  Promise<td_api::object_ptr<td_api::forumTopicInfo>> promise_;
  DialogId dialog_id_;
  DialogId creator_dialog_id_;
  int64 random_id_;

 public:
  explicit CreateForumTopicQuery(Promise<td_api::object_ptr<td_api::forumTopicInfo>> &&promise)
      : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, const string &title, bool title_missing, int32 icon_color,
            CustomEmojiId icon_custom_emoji_id, DialogId as_dialog_id) {
    dialog_id_ = dialog_id;
    creator_dialog_id_ = td_->dialog_manager_->get_my_dialog_id();

    int32 flags = 0;
    if (icon_color != -1) {
      flags |= telegram_api::messages_createForumTopic::ICON_COLOR_MASK;
    }
    if (icon_custom_emoji_id.is_valid()) {
      flags |= telegram_api::messages_createForumTopic::ICON_EMOJI_ID_MASK;
    }
    telegram_api::object_ptr<telegram_api::InputPeer> as_input_peer;
    if (as_dialog_id.is_valid()) {
      as_input_peer = td_->dialog_manager_->get_input_peer(as_dialog_id, AccessRights::Write);
      if (as_input_peer != nullptr) {
        flags |= telegram_api::messages_createForumTopic::SEND_AS_MASK;
        creator_dialog_id_ = as_dialog_id;
      }
    }

    do {
      random_id_ = Random::secure_int64();
    } while (random_id_ == 0);

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Write);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }
    send_query(G()->net_query_creator().create(
        telegram_api::messages_createForumTopic(flags, title_missing, std::move(input_peer), title, icon_color,
                                                icon_custom_emoji_id.get(), random_id_, std::move(as_input_peer)),
        {{dialog_id}}));
  }

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

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for CreateForumTopicQuery: " << to_string(ptr);
    auto message = UpdatesManager::get_message_by_random_id(ptr.get(), dialog_id_, random_id_);
    if (message == nullptr || message->get_id() != telegram_api::messageService::ID) {
      LOG(ERROR) << "Receive invalid result for CreateForumTopicQuery: " << to_string(ptr);
      return promise_.set_error(400, "Invalid result received");
    }
    auto service_message = static_cast<const telegram_api::messageService *>(message);
    if (service_message->action_->get_id() != telegram_api::messageActionTopicCreate::ID) {
      LOG(ERROR) << "Receive invalid result for CreateForumTopicQuery: " << to_string(ptr);
      return promise_.set_error(400, "Invalid result received");
    }

    auto action = static_cast<const telegram_api::messageActionTopicCreate *>(service_message->action_.get());
    auto forum_topic_id = ForumTopicId(service_message->id_);
    if (service_message->reply_to_ != nullptr &&
        service_message->reply_to_->get_id() == telegram_api::messageReplyHeader::ID) {
      auto reply_header = static_cast<const telegram_api::messageReplyHeader *>(service_message->reply_to_.get());
      if (reply_header->forum_topic_ && reply_header->reply_to_top_id_ > 0) {
        forum_topic_id = ForumTopicId(reply_header->reply_to_top_id_);
      }
    }
    auto forum_topic_info = td::make_unique<ForumTopicInfo>(
        dialog_id_, forum_topic_id, action->title_, ForumTopicIcon(action->icon_color_, action->icon_emoji_id_),
        service_message->date_, creator_dialog_id_, true, false, false, action->title_missing_);
    td_->updates_manager_->on_get_updates(
        std::move(ptr), PromiseCreator::lambda([dialog_id = dialog_id_, forum_topic_info = std::move(forum_topic_info),
                                                promise = std::move(promise_)](Unit result) mutable {
          send_closure(G()->forum_topic_manager(), &ForumTopicManager::on_forum_topic_created, dialog_id,
                       std::move(forum_topic_info), std::move(promise));
        }));
  }

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

class EditForumTopicQuery final : public Td::ResultHandler {
  Promise<Unit> promise_;
  DialogId dialog_id_;
  ForumTopicId forum_topic_id_;

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

  void send(DialogId dialog_id, ForumTopicId forum_topic_id, bool edit_title, const string &title,
            bool edit_custom_emoji_id, CustomEmojiId icon_custom_emoji_id) {
    dialog_id_ = dialog_id;
    forum_topic_id_ = forum_topic_id;

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Write);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

    int32 flags = 0;
    if (edit_title) {
      flags |= telegram_api::messages_editForumTopic::TITLE_MASK;
    }
    if (edit_custom_emoji_id) {
      flags |= telegram_api::messages_editForumTopic::ICON_EMOJI_ID_MASK;
    }
    send_query(G()->net_query_creator().create(
        telegram_api::messages_editForumTopic(flags, std::move(input_peer), forum_topic_id.get(), title,
                                              icon_custom_emoji_id.get(), false, false),
        {{dialog_id}}));
  }

  void send(ChannelId channel_id, ForumTopicId forum_topic_id, bool is_closed) {
    dialog_id_ = DialogId(channel_id);
    forum_topic_id_ = forum_topic_id;

    auto input_peer = td_->dialog_manager_->get_input_peer(DialogId(channel_id), AccessRights::Write);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

    int32 flags = telegram_api::messages_editForumTopic::CLOSED_MASK;
    send_query(G()->net_query_creator().create(
        telegram_api::messages_editForumTopic(flags, std::move(input_peer), forum_topic_id.get(), string(), 0,
                                              is_closed, false),
        {{channel_id}}));
  }

  void send(ChannelId channel_id, bool is_hidden) {
    dialog_id_ = DialogId(channel_id);
    forum_topic_id_ = ForumTopicId::general();

    auto input_peer = td_->dialog_manager_->get_input_peer(DialogId(channel_id), AccessRights::Write);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

    int32 flags = telegram_api::messages_editForumTopic::HIDDEN_MASK;
    send_query(G()->net_query_creator().create(
        telegram_api::messages_editForumTopic(flags, std::move(input_peer), forum_topic_id_.get(), string(), 0, false,
                                              is_hidden),
        {{channel_id}}));
  }

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

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

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

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

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

  void send(DialogId dialog_id, ForumTopicId forum_topic_id, bool is_pinned) {
    dialog_id_ = dialog_id;

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Write);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

    send_query(G()->net_query_creator().create(
        telegram_api::messages_updatePinnedForumTopic(std::move(input_peer), forum_topic_id.get(), is_pinned),
        {{dialog_id}}));
  }

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

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

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

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

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

  void send(DialogId dialog_id, const vector<ForumTopicId> &forum_topic_ids) {
    dialog_id_ = dialog_id;

    auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Write);
    if (input_peer == nullptr) {
      return on_error(Status::Error(400, "Can't access the chat"));
    }

    send_query(G()->net_query_creator().create(
        telegram_api::messages_reorderPinnedForumTopics(0, true, std::move(input_peer),
                                                        ForumTopicId::get_top_msg_ids(forum_topic_ids)),
        {{dialog_id}}));
  }

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

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

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

class GetForumTopicQuery final : public Td::ResultHandler {
  Promise<td_api::object_ptr<td_api::forumTopic>> promise_;
  DialogId dialog_id_;
  ForumTopicId forum_topic_id_;

 public:
  explicit GetForumTopicQuery(Promise<td_api::object_ptr<td_api::forumTopic>> &&promise)
      : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, ForumTopicId forum_topic_id) {
    dialog_id_ = dialog_id;
    forum_topic_id_ = forum_topic_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"));
    }

    send_query(G()->net_query_creator().create(
        telegram_api::messages_getForumTopicsByID(std::move(input_peer), {forum_topic_id.get()}), {{dialog_id}}));
  }

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

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for GetForumTopicQuery: " << to_string(ptr);

    td_->user_manager_->on_get_users(std::move(ptr->users_), "GetForumTopicQuery");
    td_->chat_manager_->on_get_chats(std::move(ptr->chats_), "GetForumTopicQuery");

    if (ptr->topics_.size() != 1u) {
      return promise_.set_value(nullptr);
    }

    MessagesInfo messages_info;
    messages_info.messages = std::move(ptr->messages_);
    messages_info.total_count = ptr->count_;
    messages_info.is_channel_messages = dialog_id_.get_type() == DialogType::Channel;

    td_->messages_manager_->get_channel_difference_if_needed(
        dialog_id_, std::move(messages_info),
        PromiseCreator::lambda([actor_id = td_->forum_topic_manager_actor_.get(), dialog_id = dialog_id_,
                                forum_topic_id = forum_topic_id_, topic = std::move(ptr->topics_[0]),
                                promise = std::move(promise_)](Result<MessagesInfo> &&result) mutable {
          if (result.is_error()) {
            promise.set_error(result.move_as_error());
          } else {
            auto info = result.move_as_ok();
            send_closure(actor_id, &ForumTopicManager::on_get_forum_topic, dialog_id, forum_topic_id, std::move(info),
                         std::move(topic), std::move(promise));
          }
        }),
        "GetForumTopicQuery");
  }

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

class GetForumTopicsQuery final : public Td::ResultHandler {
  Promise<td_api::object_ptr<td_api::forumTopics>> promise_;
  DialogId dialog_id_;

 public:
  explicit GetForumTopicsQuery(Promise<td_api::object_ptr<td_api::forumTopics>> &&promise)
      : promise_(std::move(promise)) {
  }

  void send(DialogId dialog_id, const string &query, int32 offset_date, MessageId offset_message_id,
            ForumTopicId offset_forum_topic_id, int32 limit) {
    dialog_id_ = 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"));
    }

    int32 flags = 0;
    if (!query.empty()) {
      flags |= telegram_api::messages_getForumTopics::Q_MASK;
    }
    send_query(G()->net_query_creator().create(
        telegram_api::messages_getForumTopics(flags, std::move(input_peer), query, offset_date,
                                              offset_message_id.get_server_message_id().get(),
                                              offset_forum_topic_id.get(), limit),
        {{dialog_id}}));
  }

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

    auto ptr = result_ptr.move_as_ok();
    LOG(INFO) << "Receive result for GetForumTopicsQuery: " << to_string(ptr);

    td_->user_manager_->on_get_users(std::move(ptr->users_), "GetForumTopicsQuery");
    td_->chat_manager_->on_get_chats(std::move(ptr->chats_), "GetForumTopicsQuery");

    MessagesInfo messages_info;
    messages_info.messages = std::move(ptr->messages_);
    messages_info.total_count = ptr->count_;
    messages_info.is_channel_messages = dialog_id_.get_type() == DialogType::Channel;

    // ignore ptr->pts_
    td_->messages_manager_->get_channel_difference_if_needed(
        dialog_id_, std::move(messages_info),
        PromiseCreator::lambda([actor_id = td_->forum_topic_manager_actor_.get(), dialog_id = dialog_id_,
                                order_by_creation_date = ptr->order_by_create_date_, topics = std::move(ptr->topics_),
                                promise = std::move(promise_)](Result<MessagesInfo> &&result) mutable {
          if (result.is_error()) {
            promise.set_error(result.move_as_error());
          } else {
            auto info = result.move_as_ok();
            send_closure(actor_id, &ForumTopicManager::on_get_forum_topics, dialog_id, order_by_creation_date,
                         std::move(info), std::move(topics), std::move(promise));
          }
        }),
        "GetForumTopicsQuery");
  }

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

class ReadForumTopicQuery final : public Td::ResultHandler {
  DialogId dialog_id_;

 public:
  void send(DialogId dialog_id, ForumTopicId forum_topic_id, MessageId max_message_id) {
    dialog_id_ = 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"));
    }
    send_query(G()->net_query_creator().create(
        telegram_api::messages_readDiscussion(std::move(input_peer), forum_topic_id.get(),
                                              max_message_id.get_server_message_id().get()),
        {{dialog_id}}));
  }

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

  void on_error(Status status) final {
    td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "ReadForumTopicQuery");
  }
};

template <class StorerT>
void ForumTopicManager::Topic::store(StorerT &storer) const {
  CHECK(info_ != nullptr);
  using td::store;

  store(MAGIC, storer);

  bool has_topic = topic_ != nullptr;
  BEGIN_STORE_FLAGS();
  STORE_FLAG(has_topic);
  END_STORE_FLAGS();
  store(info_, storer);
  if (has_topic) {
    store(topic_, storer);
  }
}

template <class ParserT>
void ForumTopicManager::Topic::parse(ParserT &parser) {
  CHECK(info_ != nullptr);
  using td::parse;

  int32 magic;
  parse(magic, parser);
  if (magic != MAGIC) {
    return parser.set_error("Invalid magic");
  }

  bool has_topic;
  BEGIN_PARSE_FLAGS();
  PARSE_FLAG(has_topic);
  END_PARSE_FLAGS();
  parse(info_, parser);
  if (has_topic) {
    parse(topic_, parser);
  }
}

ForumTopicManager::ForumTopicManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) {
}

ForumTopicManager::~ForumTopicManager() {
  Scheduler::instance()->destroy_on_scheduler(G()->get_gc_scheduler_id(), dialog_topics_);
}

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

void ForumTopicManager::create_forum_topic(DialogId dialog_id, string &&title, bool title_missing,
                                           td_api::object_ptr<td_api::forumTopicIcon> &&icon,
                                           Promise<td_api::object_ptr<td_api::forumTopicInfo>> &&promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id, true));
  if (dialog_id.get_type() == DialogType::Channel) {
    auto channel_id = dialog_id.get_channel_id();

    if (!td_->chat_manager_->get_channel_permissions(channel_id).can_create_topics()) {
      return promise.set_error(400, "Not enough rights to create a topic");
    }
  }

  auto new_title = clean_name(std::move(title), MAX_FORUM_TOPIC_TITLE_LENGTH);
  if (new_title.empty()) {
    return promise.set_error(400, "Name must be non-empty");
  }

  int32 icon_color = -1;
  CustomEmojiId icon_custom_emoji_id;
  if (icon != nullptr) {
    icon_color = icon->color_;
    if (icon_color < 0 || icon_color > 0xFFFFFF) {
      return promise.set_error(400, "Invalid icon color specified");
    }
    icon_custom_emoji_id = CustomEmojiId(icon->custom_emoji_id_);
  }

  DialogId as_dialog_id = td_->messages_manager_->get_dialog_default_send_message_as_dialog_id(dialog_id);

  td_->create_handler<CreateForumTopicQuery>(std::move(promise))
      ->send(dialog_id, new_title, title_missing, icon_color, icon_custom_emoji_id, as_dialog_id);
}

void ForumTopicManager::on_forum_topic_created(DialogId dialog_id, unique_ptr<ForumTopicInfo> &&forum_topic_info,
                                               Promise<td_api::object_ptr<td_api::forumTopicInfo>> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());

  CHECK(forum_topic_info != nullptr);
  ForumTopicId forum_topic_id = forum_topic_info->get_forum_topic_id();
  auto topic = add_topic(dialog_id, forum_topic_id);
  if (topic == nullptr) {
    return promise.set_value(forum_topic_info->get_forum_topic_info_object(td_));
  }
  if (topic->info_ == nullptr) {
    set_topic_info(dialog_id, topic, std::move(forum_topic_info));
  }
  save_topic_to_database(dialog_id, topic);
  promise.set_value(topic->info_->get_forum_topic_info_object(td_));
}

void ForumTopicManager::edit_forum_topic(DialogId dialog_id, ForumTopicId forum_topic_id, string &&title,
                                         bool edit_icon_custom_emoji, CustomEmojiId icon_custom_emoji_id,
                                         Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id, true));
  TRY_STATUS_PROMISE(promise, can_be_forum_topic_id(forum_topic_id));
  if (dialog_id.get_type() == DialogType::Channel) {
    auto channel_id = dialog_id.get_channel_id();

    if (!td_->chat_manager_->get_channel_permissions(channel_id).can_edit_topics()) {
      auto topic_info = get_topic_info(dialog_id, forum_topic_id);
      if (topic_info != nullptr && !topic_info->is_outgoing()) {
        return promise.set_error(400, "Not enough rights to edit the topic");
      }
    }
  }

  bool edit_title = !title.empty();
  auto new_title = clean_name(std::move(title), MAX_FORUM_TOPIC_TITLE_LENGTH);
  if (edit_title && new_title.empty()) {
    return promise.set_error(400, "Name must be non-empty");
  }
  if (!edit_title && !edit_icon_custom_emoji) {
    return promise.set_value(Unit());
  }

  td_->create_handler<EditForumTopicQuery>(std::move(promise))
      ->send(dialog_id, forum_topic_id, edit_title, new_title, edit_icon_custom_emoji, icon_custom_emoji_id);
}

void ForumTopicManager::read_forum_topic_messages(DialogId dialog_id, ForumTopicId forum_topic_id,
                                                  MessageId last_read_inbox_message_id) {
  CHECK(!td_->auth_manager_->is_bot());
  auto topic = get_topic(dialog_id, forum_topic_id);
  if (topic == nullptr || topic->topic_ == nullptr) {
    return;
  }

  bool need_update = false;
  if (topic->topic_->update_last_read_inbox_message_id(last_read_inbox_message_id, -1)) {
    need_update = true;
    auto max_message_id = last_read_inbox_message_id.get_prev_server_message_id();
    LOG(INFO) << "Send read topic history request in topic of " << forum_topic_id << " in " << dialog_id << " up to "
              << max_message_id;
    td_->create_handler<ReadForumTopicQuery>()->send(dialog_id, forum_topic_id, max_message_id);
  }
  if (need_update) {
    on_forum_topic_changed(dialog_id, topic);
  }
}

void ForumTopicManager::on_update_forum_topic_unread(DialogId dialog_id, ForumTopicId forum_topic_id,
                                                     MessageId last_message_id, MessageId last_read_inbox_message_id,
                                                     MessageId last_read_outbox_message_id, int32 unread_count) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto topic = get_topic(dialog_id, forum_topic_id);
  if (topic == nullptr || topic->topic_ == nullptr) {
    return;
  }

  bool need_update = false;
  if (topic->topic_->update_last_read_outbox_message_id(last_read_outbox_message_id)) {
    need_update = true;
  }
  if (topic->topic_->update_last_read_inbox_message_id(last_read_inbox_message_id, unread_count)) {
    need_update = true;
  }
  if (need_update) {
    on_forum_topic_changed(dialog_id, topic);
  }
}

DialogNotificationSettings *ForumTopicManager::get_forum_topic_notification_settings(DialogId dialog_id,
                                                                                     ForumTopicId forum_topic_id) {
  auto topic = get_topic(dialog_id, forum_topic_id);
  if (topic == nullptr || topic->topic_ == nullptr) {
    return nullptr;
  }
  return topic->topic_->get_notification_settings();
}

const DialogNotificationSettings *ForumTopicManager::get_forum_topic_notification_settings(
    DialogId dialog_id, ForumTopicId forum_topic_id) const {
  auto topic = get_topic(dialog_id, forum_topic_id);
  if (topic == nullptr || topic->topic_ == nullptr) {
    return nullptr;
  }
  return topic->topic_->get_notification_settings();
}

void ForumTopicManager::on_update_forum_topic_notify_settings(
    DialogId dialog_id, ForumTopicId forum_topic_id,
    tl_object_ptr<telegram_api::peerNotifySettings> &&peer_notify_settings, const char *source) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }

  VLOG(notifications) << "Receive notification settings for topic of " << forum_topic_id << " in " << dialog_id
                      << " from " << source << ": " << to_string(peer_notify_settings);

  DialogNotificationSettings *current_settings = get_forum_topic_notification_settings(dialog_id, forum_topic_id);
  if (current_settings == nullptr) {
    return;
  }

  auto notification_settings = get_dialog_notification_settings(std::move(peer_notify_settings), current_settings);
  if (!notification_settings.is_synchronized) {
    return;
  }

  update_forum_topic_notification_settings(dialog_id, forum_topic_id, current_settings,
                                           std::move(notification_settings));
}

void ForumTopicManager::on_update_forum_topic_draft_message(DialogId dialog_id, ForumTopicId forum_topic_id,
                                                            unique_ptr<DraftMessage> &&draft_message) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  auto topic = get_topic(dialog_id, forum_topic_id);
  if (topic == nullptr || topic->topic_ == nullptr) {
    LOG(DEBUG) << "Ignore update about unknown " << forum_topic_id << " in " << dialog_id;
    return;
  }
  auto old_file_ids = get_topic_file_ids(topic);
  if (topic->topic_->set_draft_message(std::move(draft_message), true)) {
    on_forum_topic_changed(dialog_id, topic);
    td_->draft_message_manager_->change_draft_message_files(dialog_id, MessageTopic::forum(dialog_id, forum_topic_id),
                                                            old_file_ids, get_topic_file_ids(topic), true);
  }
}

void ForumTopicManager::clear_forum_topic_draft_by_sent_message(DialogId dialog_id, ForumTopicId forum_topic_id,
                                                                bool message_clear_draft,
                                                                MessageContentType message_content_type) {
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  LOG(INFO) << "Clear draft in " << forum_topic_id << " of " << dialog_id << " by sent message";
  auto topic = get_topic(dialog_id, forum_topic_id);
  if (topic == nullptr || topic->topic_ == nullptr) {
    return;
  }

  if (!message_clear_draft) {
    const auto *draft_message = topic->topic_->get_draft_message().get();
    if (draft_message == nullptr || !draft_message->need_clear_local(message_content_type)) {
      return;
    }
  }
  auto old_file_ids = get_topic_file_ids(topic);
  if (topic->topic_->set_draft_message(nullptr, false)) {
    on_forum_topic_changed(dialog_id, topic);
    td_->draft_message_manager_->change_draft_message_files(dialog_id, MessageTopic::forum(dialog_id, forum_topic_id),
                                                            old_file_ids, get_topic_file_ids(topic), false);
  }
}

void ForumTopicManager::on_update_forum_topic_is_pinned(DialogId dialog_id, ForumTopicId forum_topic_id,
                                                        bool is_pinned) {
  if (!td_->dialog_manager_->have_dialog_force(dialog_id, "on_update_forum_topic_is_pinned")) {
    return;
  }
  if (!can_be_forum(dialog_id)) {
    LOG(ERROR) << "Receive pinned topics in " << dialog_id;
    return;
  }

  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto topic = get_topic(dialog_id, forum_topic_id);
  if (topic == nullptr || topic->topic_ == nullptr) {
    return;
  }
  if (topic->topic_->set_is_pinned(is_pinned)) {
    on_forum_topic_changed(dialog_id, topic);
  }
}

void ForumTopicManager::on_update_pinned_forum_topics(DialogId dialog_id, vector<ForumTopicId> forum_topic_ids) {
  if (!td_->dialog_manager_->have_dialog_force(dialog_id, "on_update_pinned_forum_topics")) {
    return;
  }
  if (!can_be_forum(dialog_id)) {
    LOG(ERROR) << "Receive pinned topics in " << dialog_id;
    return;
  }

  if (td_->auth_manager_->is_bot()) {
    return;
  }

  auto dialog_topics = get_dialog_topics(dialog_id);
  if (dialog_topics == nullptr) {
    return;
  }

  dialog_topics->topics_.foreach([&](const ForumTopicId &forum_topic_id, unique_ptr<Topic> &topic) {
    if (topic->topic_ == nullptr) {
      return;
    }
    if (topic->topic_->set_is_pinned(contains(forum_topic_ids, forum_topic_id))) {
      on_forum_topic_changed(dialog_id, topic.get());
    }
  });
}

Status ForumTopicManager::set_forum_topic_notification_settings(
    DialogId dialog_id, ForumTopicId forum_topic_id,
    td_api::object_ptr<td_api::chatNotificationSettings> &&notification_settings) {
  CHECK(!td_->auth_manager_->is_bot());
  TRY_STATUS(is_forum(dialog_id, true));
  TRY_STATUS(can_be_forum_topic_id(forum_topic_id));
  auto current_settings = get_forum_topic_notification_settings(dialog_id, forum_topic_id);
  if (current_settings == nullptr) {
    return Status::Error(400, "Unknown forum topic identifier specified");
  }

  TRY_RESULT(new_settings, get_dialog_notification_settings(std::move(notification_settings), current_settings));
  if (update_forum_topic_notification_settings(dialog_id, forum_topic_id, current_settings, std::move(new_settings))) {
    // TODO log event
    td_->notification_settings_manager_->update_dialog_notify_settings(dialog_id, forum_topic_id, *current_settings,
                                                                       Promise<Unit>());
  }
  return Status::OK();
}

bool ForumTopicManager::update_forum_topic_notification_settings(DialogId dialog_id, ForumTopicId forum_topic_id,
                                                                 DialogNotificationSettings *current_settings,
                                                                 DialogNotificationSettings &&new_settings) {
  if (td_->auth_manager_->is_bot()) {
    // just in case
    return false;
  }

  auto need_update = need_update_dialog_notification_settings(current_settings, new_settings);
  if (need_update.are_changed) {
    // TODO update unmute timeouts, remove notifications
    *current_settings = std::move(new_settings);

    auto topic = get_topic(dialog_id, forum_topic_id);
    on_forum_topic_changed(dialog_id, topic);
  }
  return need_update.need_update_server;
}

Status ForumTopicManager::set_forum_topic_draft_message(DialogId dialog_id, ForumTopicId forum_topic_id,
                                                        unique_ptr<DraftMessage> &&draft_message) {
  TRY_STATUS(is_forum(dialog_id, true));
  TRY_STATUS(can_be_forum_topic_id(forum_topic_id));
  auto topic = get_topic(dialog_id, forum_topic_id);
  if (topic == nullptr || topic->topic_ == nullptr) {
    td_->draft_message_manager_->save_draft_message(dialog_id, MessageTopic::forum(dialog_id, forum_topic_id),
                                                    std::move(draft_message), Promise<Unit>());
  } else if (topic->topic_->set_draft_message(std::move(draft_message), false)) {
    td_->draft_message_manager_->save_draft_message(dialog_id, MessageTopic::forum(dialog_id, forum_topic_id),
                                                    topic->topic_->get_draft_message(), Promise<Unit>());
    on_forum_topic_changed(dialog_id, topic);
  }
  return Status::OK();
}

void ForumTopicManager::get_forum_topic(DialogId dialog_id, ForumTopicId forum_topic_id,
                                        Promise<td_api::object_ptr<td_api::forumTopic>> &&promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id, true));
  TRY_STATUS_PROMISE(promise, can_be_forum_topic_id(forum_topic_id));

  if (td_->auth_manager_->is_bot()) {
    auto topic = get_topic(dialog_id, forum_topic_id);
    if (topic != nullptr && topic->topic_ != nullptr && topic->receive_date_ > G()->unix_time() - 60) {
      CHECK(topic->info_ != nullptr);
      auto forum_topic = topic->topic_->get_forum_topic_object(td_, dialog_id, *topic->info_);
      if (forum_topic != nullptr) {
        return promise.set_value(std::move(forum_topic));
      }
    }
  }

  td_->create_handler<GetForumTopicQuery>(std::move(promise))->send(dialog_id, forum_topic_id);
}

void ForumTopicManager::reload_forum_topic(DialogId dialog_id, ForumTopicId forum_topic_id, Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id, true));
  TRY_STATUS_PROMISE(promise, can_be_forum_topic_id(forum_topic_id));

  auto query_promise = PromiseCreator::lambda(
      [promise = std::move(promise)](Result<td_api::object_ptr<td_api::forumTopic>> r_forum_topic) mutable {
        if (r_forum_topic.is_error()) {
          promise.set_error(r_forum_topic.move_as_error());
        } else {
          promise.set_value(Unit());
        }
      });
  td_->create_handler<GetForumTopicQuery>(std::move(query_promise))->send(dialog_id, forum_topic_id);
}

void ForumTopicManager::on_get_forum_topic(DialogId dialog_id, ForumTopicId expected_forum_topic_id,
                                           MessagesInfo &&info,
                                           telegram_api::object_ptr<telegram_api::ForumTopic> &&topic,
                                           Promise<td_api::object_ptr<td_api::forumTopic>> &&promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id, true));
  td_->messages_manager_->on_get_messages(dialog_id, std::move(info.messages), false, Promise<Unit>(),
                                          "on_get_forum_topic");

  auto forum_topic_id = on_get_forum_topic_impl(dialog_id, std::move(topic));
  if (!forum_topic_id.is_valid()) {
    return promise.set_value(nullptr);
  }
  if (forum_topic_id != expected_forum_topic_id) {
    return promise.set_error(500, "Wrong forum topic received");
  }
  promise.set_value(get_forum_topic_object(dialog_id, forum_topic_id));
}

void ForumTopicManager::get_forum_topic_link(DialogId dialog_id, ForumTopicId forum_topic_id,
                                             Promise<td_api::object_ptr<td_api::messageLink>> &&promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id));
  TRY_STATUS_PROMISE(promise, can_be_forum_topic_id(forum_topic_id));
  auto channel_id = dialog_id.get_channel_id();

  SliceBuilder sb;
  sb << LinkManager::get_t_me_url();

  bool is_public = false;
  auto dialog_username = td_->chat_manager_->get_channel_first_username(channel_id);
  if (!dialog_username.empty()) {
    sb << dialog_username;
    is_public = true;
  } else {
    sb << "c/" << channel_id.get();
  }
  sb << '/' << forum_topic_id.get();

  promise.set_value(td_api::make_object<td_api::messageLink>(sb.as_cslice().str(), is_public));
}

void ForumTopicManager::get_forum_topics(DialogId dialog_id, string query, int32 offset_date,
                                         MessageId offset_message_id, ForumTopicId offset_forum_topic_id, int32 limit,
                                         Promise<td_api::object_ptr<td_api::forumTopics>> promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id, true));

  if (offset_date < 0) {
    return promise.set_error(400, "Invalid offset date specified");
  }
  if (offset_message_id != MessageId() && !offset_message_id.is_server()) {
    return promise.set_error(400, "Invalid offset message identifier specified");
  }
  if (offset_forum_topic_id != ForumTopicId()) {
    TRY_STATUS_PROMISE(promise, can_be_forum_topic_id(offset_forum_topic_id));
  }
  if (limit <= 0) {
    return promise.set_error(400, "Invalid limit specified");
  }
  td_->create_handler<GetForumTopicsQuery>(std::move(promise))
      ->send(dialog_id, query, offset_date, offset_message_id, offset_forum_topic_id, limit);
}

void ForumTopicManager::on_get_forum_topics(DialogId dialog_id, bool order_by_creation_date, MessagesInfo &&info,
                                            vector<telegram_api::object_ptr<telegram_api::ForumTopic>> &&topics,
                                            Promise<td_api::object_ptr<td_api::forumTopics>> &&promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id, true));
  td_->messages_manager_->on_get_messages(dialog_id, std::move(info.messages), false, Promise<Unit>(),
                                          "on_get_forum_topics");
  vector<td_api::object_ptr<td_api::forumTopic>> forum_topics;
  int32 next_offset_date = 0;
  MessageId next_offset_message_id;
  ForumTopicId next_offset_forum_topic_id;
  for (auto &topic : topics) {
    auto forum_topic_id = on_get_forum_topic_impl(dialog_id, std::move(topic));
    if (!forum_topic_id.is_valid()) {
      continue;
    }
    auto forum_topic_object = get_forum_topic_object(dialog_id, forum_topic_id);
    CHECK(forum_topic_object != nullptr);
    if (order_by_creation_date || forum_topic_object->last_message_ == nullptr) {
      next_offset_date = forum_topic_object->info_->creation_date_;
    } else {
      next_offset_date = forum_topic_object->last_message_->date_;
    }
    next_offset_message_id =
        forum_topic_object->last_message_ != nullptr ? MessageId(forum_topic_object->last_message_->id_) : MessageId();
    next_offset_forum_topic_id = forum_topic_id;
    forum_topics.push_back(std::move(forum_topic_object));
  }

  promise.set_value(td_api::make_object<td_api::forumTopics>(info.total_count, std::move(forum_topics),
                                                             next_offset_date, next_offset_message_id.get(),
                                                             next_offset_forum_topic_id.get()));
}

void ForumTopicManager::toggle_forum_topic_is_closed(DialogId dialog_id, ForumTopicId forum_topic_id, bool is_closed,
                                                     Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id));
  TRY_STATUS_PROMISE(promise, can_be_forum_topic_id(forum_topic_id));
  auto channel_id = dialog_id.get_channel_id();

  if (!td_->chat_manager_->get_channel_permissions(channel_id).can_edit_topics()) {
    auto topic_info = get_topic_info(dialog_id, forum_topic_id);
    if (topic_info != nullptr && !topic_info->is_outgoing()) {
      return promise.set_error(400, "Not enough rights to close or open the topic");
    }
  }

  td_->create_handler<EditForumTopicQuery>(std::move(promise))->send(channel_id, forum_topic_id, is_closed);
}

void ForumTopicManager::toggle_forum_topic_is_hidden(DialogId dialog_id, bool is_hidden, Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id));
  auto channel_id = dialog_id.get_channel_id();

  if (!td_->chat_manager_->get_channel_permissions(channel_id).can_edit_topics()) {
    return promise.set_error(400, "Not enough rights to close or open the topic");
  }

  td_->create_handler<EditForumTopicQuery>(std::move(promise))->send(channel_id, is_hidden);
}

void ForumTopicManager::toggle_forum_topic_is_pinned(DialogId dialog_id, ForumTopicId forum_topic_id, bool is_pinned,
                                                     Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id, true));
  TRY_STATUS_PROMISE(promise, can_be_forum_topic_id(forum_topic_id));

  if (dialog_id.get_type() == DialogType::Channel) {
    auto channel_id = dialog_id.get_channel_id();
    if (!td_->chat_manager_->get_channel_permissions(channel_id).can_pin_topics()) {
      return promise.set_error(400, "Not enough rights to pin or unpin the topic");
    }
  }

  td_->create_handler<UpdatePinnedForumTopicQuery>(std::move(promise))->send(dialog_id, forum_topic_id, is_pinned);
}

void ForumTopicManager::set_pinned_forum_topics(DialogId dialog_id, vector<ForumTopicId> forum_topic_ids,
                                                Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id, true));
  for (auto forum_topic_id : forum_topic_ids) {
    TRY_STATUS_PROMISE(promise, can_be_forum_topic_id(forum_topic_id));
  }

  if (dialog_id.get_type() == DialogType::Channel) {
    auto channel_id = dialog_id.get_channel_id();
    if (!td_->chat_manager_->get_channel_permissions(channel_id).can_pin_topics()) {
      return promise.set_error(400, "Not enough rights to reorder forum topics");
    }
  }

  td_->create_handler<ReorderPinnedForumTopicsQuery>(std::move(promise))->send(dialog_id, forum_topic_ids);
}

void ForumTopicManager::delete_forum_topic(DialogId dialog_id, ForumTopicId forum_topic_id, Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, is_forum(dialog_id, true));
  TRY_STATUS_PROMISE(promise, can_be_forum_topic_id(forum_topic_id));

  if (dialog_id.get_type() == DialogType::Channel) {
    auto channel_id = dialog_id.get_channel_id();
    if (!td_->chat_manager_->get_channel_permissions(channel_id).can_delete_messages()) {
      auto topic_info = get_topic_info(dialog_id, forum_topic_id);
      if (topic_info != nullptr && !topic_info->is_outgoing()) {
        return promise.set_error(400, "Not enough rights to delete the topic");
      }
    }
  }

  auto delete_promise = PromiseCreator::lambda([actor_id = actor_id(this), dialog_id, forum_topic_id,
                                                promise = std::move(promise)](Result<Unit> result) mutable {
    if (result.is_error()) {
      return promise.set_error(result.move_as_error());
    }
    send_closure(actor_id, &ForumTopicManager::on_delete_forum_topic, dialog_id, forum_topic_id, std::move(promise));
  });
  td_->message_query_manager_->delete_topic_history(dialog_id, forum_topic_id, std::move(delete_promise));
}

void ForumTopicManager::on_delete_forum_topic(DialogId dialog_id, ForumTopicId forum_topic_id,
                                              Promise<Unit> &&promise) {
  TRY_STATUS_PROMISE(promise, G()->close_status());
  auto *dialog_topics = dialog_topics_.get_pointer(dialog_id);
  if (dialog_topics != nullptr) {
    CHECK(forum_topic_id.is_valid());
    dialog_topics->topics_.erase(forum_topic_id);
    dialog_topics->deleted_topic_ids_.insert(forum_topic_id);
  }
  delete_topic_from_database(dialog_id, forum_topic_id, std::move(promise));
}

void ForumTopicManager::delete_all_dialog_topics(DialogId dialog_id) {
  dialog_topics_.erase(dialog_id);

  auto message_thread_db = G()->td_db()->get_message_thread_db_async();
  if (message_thread_db == nullptr) {
    return;
  }

  LOG(INFO) << "Delete all topics in " << dialog_id << " from database";
  message_thread_db->delete_all_dialog_message_threads(dialog_id, Auto());
}

void ForumTopicManager::on_forum_topic_edited(DialogId dialog_id, ForumTopicId forum_topic_id,
                                              const ForumTopicEditedData &edited_data) {
  auto topic = get_topic(dialog_id, forum_topic_id);
  if (topic == nullptr || topic->info_ == nullptr) {
    return;
  }
  if (topic->info_->apply_edited_data(edited_data)) {
    send_update_forum_topic_info(topic->info_.get());
    topic->need_save_to_database_ = true;
  }
  save_topic_to_database(dialog_id, topic);
}

bool ForumTopicManager::on_get_forum_topic_error(DialogId dialog_id, ForumTopicId forum_topic_id, const Status &status,
                                                 const char *source) {
  return td_->dialog_manager_->on_get_dialog_error(dialog_id, status, source);
}

void ForumTopicManager::on_get_forum_topic_info(DialogId dialog_id, const ForumTopicInfo &topic_info,
                                                const char *source) {
  if (!can_be_forum(dialog_id)) {
    LOG(ERROR) << "Receive forum topics in " << dialog_id << " from " << source;
    return;
  }

  auto *dialog_topics = add_dialog_topics(dialog_id);
  CHECK(dialog_topics != nullptr);
  auto forum_topic_info = td::make_unique<ForumTopicInfo>(topic_info);
  ForumTopicId forum_topic_id = forum_topic_info->get_forum_topic_id();
  CHECK(can_be_forum_topic_id(forum_topic_id).is_ok());
  auto topic = add_topic(dialog_topics, forum_topic_id);
  if (topic == nullptr) {
    return;
  }
  set_topic_info(dialog_id, topic, std::move(forum_topic_info));
  save_topic_to_database(dialog_id, topic);
}

void ForumTopicManager::on_get_forum_topic_infos(DialogId dialog_id,
                                                 vector<tl_object_ptr<telegram_api::ForumTopic>> &&forum_topics,
                                                 const char *source) {
  if (forum_topics.empty()) {
    return;
  }
  if (dialog_id == DialogId()) {
    for (auto &forum_topic : forum_topics) {
      auto forum_topic_info = td::make_unique<ForumTopicInfo>(td_, forum_topic, DialogId());
      ForumTopicId forum_topic_id = forum_topic_info->get_forum_topic_id();
      if (can_be_forum_topic_id(forum_topic_id).is_error()) {
        continue;
      }
      dialog_id = forum_topic_info->get_dialog_id();
      if (!can_be_forum(dialog_id)) {
        LOG(ERROR) << "Receive forum topics in " << dialog_id << " from " << source;
        return;
      }
      auto *dialog_topics = add_dialog_topics(dialog_id);
      CHECK(dialog_topics != nullptr);
      auto topic = add_topic(dialog_topics, forum_topic_id);
      if (topic != nullptr) {
        set_topic_info(dialog_id, topic, std::move(forum_topic_info));
        save_topic_to_database(dialog_id, topic);
      }
    }
    return;
  }
  if (!can_be_forum(dialog_id)) {
    LOG(ERROR) << "Receive forum topics in " << dialog_id << " from " << source;
    return;
  }

  auto *dialog_topics = add_dialog_topics(dialog_id);
  CHECK(dialog_topics != nullptr);
  for (auto &forum_topic : forum_topics) {
    auto forum_topic_info = td::make_unique<ForumTopicInfo>(td_, forum_topic, dialog_id);
    ForumTopicId forum_topic_id = forum_topic_info->get_forum_topic_id();
    if (can_be_forum_topic_id(forum_topic_id).is_error()) {
      continue;
    }
    auto topic = add_topic(dialog_topics, forum_topic_id);
    if (topic != nullptr) {
      set_topic_info(dialog_id, topic, std::move(forum_topic_info));
      save_topic_to_database(dialog_id, topic);
    }
  }
}

ForumTopicId ForumTopicManager::on_get_forum_topic_impl(DialogId dialog_id,
                                                        tl_object_ptr<telegram_api::ForumTopic> &&forum_topic) {
  CHECK(forum_topic != nullptr);
  switch (forum_topic->get_id()) {
    case telegram_api::forumTopicDeleted::ID: {
      auto forum_topic_id = ForumTopicId(static_cast<const telegram_api::forumTopicDeleted *>(forum_topic.get())->id_);
      if (!forum_topic_id.is_valid()) {
        LOG(ERROR) << "Receive " << to_string(forum_topic);
        return ForumTopicId();
      }
      on_delete_forum_topic(dialog_id, forum_topic_id, Promise<Unit>());
      return ForumTopicId();
    }
    case telegram_api::forumTopic::ID: {
      auto forum_topic_info = td::make_unique<ForumTopicInfo>(td_, forum_topic, dialog_id);
      auto forum_topic_id = forum_topic_info->get_forum_topic_id();
      Topic *topic = add_topic(dialog_id, forum_topic_id);
      if (topic == nullptr) {
        return ForumTopicId();
      }
      auto current_notification_settings =
          topic->topic_ == nullptr ? nullptr : topic->topic_->get_notification_settings();
      auto forum_topic_full = td::make_unique<ForumTopic>(td_, std::move(forum_topic), current_notification_settings);
      if (forum_topic_full->is_short() && !td_->auth_manager_->is_bot()) {
        LOG(ERROR) << "Receive short forum topic";
        return ForumTopicId();
      }
      if (topic->topic_ == nullptr || true) {
        auto old_file_ids = get_topic_file_ids(topic);
        topic->topic_ = std::move(forum_topic_full);
        topic->need_save_to_database_ = true;  // TODO temporary
        td_->draft_message_manager_->change_draft_message_files(
            dialog_id, MessageTopic::forum(dialog_id, forum_topic_id), old_file_ids, get_topic_file_ids(topic), true);
      }
      topic->receive_date_ = G()->unix_time();
      set_topic_info(dialog_id, topic, std::move(forum_topic_info));
      send_update_forum_topic(dialog_id, topic);
      save_topic_to_database(dialog_id, topic);
      return forum_topic_id;
    }
    default:
      UNREACHABLE();
      return ForumTopicId();
  }
}

vector<FileId> ForumTopicManager::get_topic_file_ids(const Topic *topic) const {
  if (topic == nullptr || topic->topic_ == nullptr) {
    return {};
  }
  return get_draft_message_file_ids(td_, topic->topic_->get_draft_message());
}

int32 ForumTopicManager::get_forum_topic_id_object(DialogId dialog_id, ForumTopicId forum_topic_id) {
  if (forum_topic_id == ForumTopicId()) {
    return 0;
  }

  // TODO load topic from database or get from the server

  return forum_topic_id.get();
}

td_api::object_ptr<td_api::forumTopic> ForumTopicManager::get_forum_topic_object(DialogId dialog_id,
                                                                                 ForumTopicId forum_topic_id) const {
  auto topic = get_topic(dialog_id, forum_topic_id);
  if (topic == nullptr || topic->topic_ == nullptr) {
    return nullptr;
  }
  CHECK(topic->info_ != nullptr);
  return topic->topic_->get_forum_topic_object(td_, dialog_id, *topic->info_);
}

Status ForumTopicManager::is_forum(DialogId dialog_id, bool allow_bots) {
  if (!td_->dialog_manager_->have_dialog_force(dialog_id, "ForumTopicManager::is_forum")) {
    return Status::Error(400, "Chat not found");
  }
  switch (dialog_id.get_type()) {
    case DialogType::User: {
      auto bot_user_id = td_->auth_manager_->is_bot() ? td_->user_manager_->get_my_id() : dialog_id.get_user_id();
      if (td_->user_manager_->is_user_forum_bot(bot_user_id)) {
        if (!allow_bots) {
          return Status::Error(400, "The chat is not a supergroup forum");
        }
        return Status::OK();
      }
      break;
    }
    case DialogType::Channel:
      if (td_->chat_manager_->is_forum_channel(dialog_id.get_channel_id())) {
        return Status::OK();
      }
      break;
    case DialogType::Chat:
    case DialogType::SecretChat:
    case DialogType::None:
    default:
      break;
  }
  return Status::Error(400, "The chat is not a forum");
}

bool ForumTopicManager::can_be_forum(DialogId dialog_id) const {
  switch (dialog_id.get_type()) {
    case DialogType::User:
      return td_->auth_manager_->is_bot() || td_->user_manager_->is_user_bot(dialog_id.get_user_id());
    case DialogType::Channel:
      return !td_->chat_manager_->is_broadcast_channel(dialog_id.get_channel_id()) &&
             !td_->chat_manager_->is_monoforum_channel(dialog_id.get_channel_id());
    case DialogType::Chat:
    case DialogType::SecretChat:
    case DialogType::None:
    default:
      break;
  }
  return false;
}

Status ForumTopicManager::can_be_forum_topic_id(ForumTopicId forum_topic_id) {
  if (!forum_topic_id.is_valid()) {
    return Status::Error(400, "Invalid forum topic identifier specified");
  }
  return Status::OK();
}

bool ForumTopicManager::can_send_message_to_forum_topic(DialogId dialog_id, ForumTopicId forum_topic_id) const {
  auto *topic_info = get_topic_info(dialog_id, forum_topic_id);
  if (topic_info == nullptr) {
    return true;  // allow sending to unknown topic
  }
  if (topic_info->is_closed() && !topic_info->is_outgoing() && dialog_id.get_type() == DialogType::Channel &&
      !td_->chat_manager_->get_channel_status(dialog_id.get_channel_id()).can_edit_topics()) {
    return false;  // don't allow sending to closed topic
  }
  return true;
}

ForumTopicManager::DialogTopics *ForumTopicManager::add_dialog_topics(DialogId dialog_id) {
  auto *dialog_topics = dialog_topics_.get_pointer(dialog_id);
  if (dialog_topics == nullptr) {
    auto new_dialog_topics = make_unique<DialogTopics>();
    dialog_topics = new_dialog_topics.get();
    dialog_topics_.set(dialog_id, std::move(new_dialog_topics));
  }
  return dialog_topics;
}

ForumTopicManager::DialogTopics *ForumTopicManager::get_dialog_topics(DialogId dialog_id) {
  return dialog_topics_.get_pointer(dialog_id);
}

ForumTopicManager::Topic *ForumTopicManager::add_topic(DialogTopics *dialog_topics, ForumTopicId forum_topic_id) {
  auto topic = dialog_topics->topics_.get_pointer(forum_topic_id);
  if (topic == nullptr) {
    if (dialog_topics->deleted_topic_ids_.count(forum_topic_id) > 0) {
      return nullptr;
    }
    auto new_topic = make_unique<Topic>();
    topic = new_topic.get();
    CHECK(forum_topic_id.is_valid());
    dialog_topics->topics_.set(forum_topic_id, std::move(new_topic));
  }
  return topic;
}

ForumTopicManager::Topic *ForumTopicManager::get_topic(DialogTopics *dialog_topics, ForumTopicId forum_topic_id) {
  return dialog_topics->topics_.get_pointer(forum_topic_id);
}

ForumTopicManager::Topic *ForumTopicManager::add_topic(DialogId dialog_id, ForumTopicId forum_topic_id) {
  return add_topic(add_dialog_topics(dialog_id), forum_topic_id);
}

ForumTopicManager::Topic *ForumTopicManager::get_topic(DialogId dialog_id, ForumTopicId forum_topic_id) {
  auto *dialog_topics = dialog_topics_.get_pointer(dialog_id);
  if (dialog_topics == nullptr) {
    return nullptr;
  }
  return dialog_topics->topics_.get_pointer(forum_topic_id);
}

const ForumTopicManager::Topic *ForumTopicManager::get_topic(DialogId dialog_id, ForumTopicId forum_topic_id) const {
  auto *dialog_topics = dialog_topics_.get_pointer(dialog_id);
  if (dialog_topics == nullptr) {
    return nullptr;
  }
  return dialog_topics->topics_.get_pointer(forum_topic_id);
}

ForumTopicInfo *ForumTopicManager::get_topic_info(DialogId dialog_id, ForumTopicId forum_topic_id) {
  auto *topic = get_topic(dialog_id, forum_topic_id);
  if (topic == nullptr) {
    return nullptr;
  }
  return topic->info_.get();
}

const ForumTopicInfo *ForumTopicManager::get_topic_info(DialogId dialog_id, ForumTopicId forum_topic_id) const {
  auto *topic = get_topic(dialog_id, forum_topic_id);
  if (topic == nullptr) {
    return nullptr;
  }
  return topic->info_.get();
}

void ForumTopicManager::set_topic_info(DialogId dialog_id, Topic *topic, unique_ptr<ForumTopicInfo> forum_topic_info) {
  if (topic->info_ == nullptr || *topic->info_ != *forum_topic_info) {
    topic->info_ = std::move(forum_topic_info);
    send_update_forum_topic_info(topic->info_.get());
    topic->need_save_to_database_ = true;
  }
}

td_api::object_ptr<td_api::updateForumTopicInfo> ForumTopicManager::get_update_forum_topic_info_object(
    const ForumTopicInfo *topic_info) const {
  return td_api::make_object<td_api::updateForumTopicInfo>(topic_info->get_forum_topic_info_object(td_));
}

void ForumTopicManager::send_update_forum_topic_info(const ForumTopicInfo *topic_info) const {
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  send_closure(G()->td(), &Td::send_update, get_update_forum_topic_info_object(topic_info));
}

td_api::object_ptr<td_api::updateForumTopic> ForumTopicManager::get_update_forum_topic_object(
    DialogId dialog_id, const Topic *topic) const {
  CHECK(topic != nullptr);
  return topic->topic_->get_update_forum_topic_object(td_, dialog_id, topic->info_->get_forum_topic_id());
}

void ForumTopicManager::send_update_forum_topic(DialogId dialog_id, const Topic *topic) const {
  if (td_->auth_manager_->is_bot()) {
    return;
  }
  send_closure(G()->td(), &Td::send_update, get_update_forum_topic_object(dialog_id, topic));
}

void ForumTopicManager::on_forum_topic_changed(DialogId dialog_id, Topic *topic) {
  CHECK(topic != nullptr);
  send_update_forum_topic(dialog_id, topic);
  topic->need_save_to_database_ = true;
  save_topic_to_database(dialog_id, topic);
}

void ForumTopicManager::save_topic_to_database(DialogId dialog_id, const Topic *topic) {
  CHECK(topic != nullptr);
  if (topic->info_ == nullptr || !topic->need_save_to_database_) {
    return;
  }
  topic->need_save_to_database_ = false;

  auto message_thread_db = G()->td_db()->get_message_thread_db_async();
  if (message_thread_db == nullptr) {
    return;
  }

  auto forum_topic_id = topic->info_->get_forum_topic_id();
  LOG(INFO) << "Save topic of " << forum_topic_id << " in " << dialog_id << " to database";
  message_thread_db->add_message_thread(dialog_id, forum_topic_id, 0, log_event_store(*topic), Auto());
}

void ForumTopicManager::delete_topic_from_database(DialogId dialog_id, ForumTopicId forum_topic_id,
                                                   Promise<Unit> &&promise) {
  auto message_thread_db = G()->td_db()->get_message_thread_db_async();
  if (message_thread_db == nullptr) {
    return promise.set_value(Unit());
  }

  LOG(INFO) << "Delete topic of " << forum_topic_id << " in " << dialog_id << " from database";
  message_thread_db->delete_message_thread(dialog_id, forum_topic_id, std::move(promise));
}

void ForumTopicManager::on_topic_message_count_changed(DialogId dialog_id, ForumTopicId forum_topic_id, int diff) {
  if (!can_be_forum(dialog_id) || can_be_forum_topic_id(forum_topic_id).is_error()) {
    LOG(ERROR) << "Change by " << diff << " number of loaded messages in thread of " << forum_topic_id << " in "
               << dialog_id;
    return;
  }

  LOG(INFO) << "Change by " << diff << " number of loaded messages in thread of " << forum_topic_id << " in "
            << dialog_id;
  auto *dialog_topics = add_dialog_topics(dialog_id);
  auto topic = add_topic(dialog_topics, forum_topic_id);
  if (topic == nullptr) {
    return;
  }
  topic->message_count_ += diff;
  CHECK(topic->message_count_ >= 0);
  if (topic->message_count_ == 0 && !td_->auth_manager_->is_bot()) {
    // TODO keep topics in the topic list
    dialog_topics->topics_.erase(forum_topic_id);
  }
}

void ForumTopicManager::on_topic_mention_count_changed(DialogId dialog_id, ForumTopicId forum_topic_id, int32 count,
                                                       bool is_relative) {
  LOG(INFO) << "Change " << (is_relative ? "by" : "to") << ' ' << count << " number of mentions in thread of "
            << forum_topic_id << " in " << dialog_id;
  auto dialog_topics = get_dialog_topics(dialog_id);
  if (dialog_topics == nullptr) {
    return;
  }
  auto topic = get_topic(dialog_topics, forum_topic_id);
  if (topic == nullptr || topic->topic_ == nullptr) {
    return;
  }
  if (topic->topic_->update_unread_mention_count(count, is_relative)) {
    on_forum_topic_changed(dialog_id, topic);
  }
}

void ForumTopicManager::on_topic_reaction_count_changed(DialogId dialog_id, ForumTopicId forum_topic_id, int32 count,
                                                        bool is_relative) {
  LOG(INFO) << "Change " << (is_relative ? "by" : "to") << ' ' << count << " number of reactions in thread of "
            << forum_topic_id << " in " << dialog_id;
  auto dialog_topics = get_dialog_topics(dialog_id);
  if (dialog_topics == nullptr) {
    return;
  }
  auto topic = get_topic(dialog_topics, forum_topic_id);
  if (topic == nullptr || topic->topic_ == nullptr) {
    return;
  }
  if (topic->topic_->update_unread_reaction_count(count, is_relative)) {
    on_forum_topic_changed(dialog_id, topic);
  }
}

void ForumTopicManager::on_topic_poll_vote_count_changed(DialogId dialog_id, ForumTopicId forum_topic_id, int32 count,
                                                         bool is_relative) {
  LOG(INFO) << "Change " << (is_relative ? "by" : "to") << ' ' << count << " number of poll votes in thread of "
            << forum_topic_id << " in " << dialog_id;
  auto dialog_topics = get_dialog_topics(dialog_id);
  if (dialog_topics == nullptr) {
    LOG(INFO) << "Topic list of " << dialog_id << " not found";
    return;
  }
  auto topic = get_topic(dialog_topics, forum_topic_id);
  if (topic == nullptr || topic->topic_ == nullptr) {
    LOG(INFO) << "Topic " << forum_topic_id << " not found";
    return;
  }
  if (topic->topic_->update_unread_poll_vote_count(count, is_relative)) {
    on_forum_topic_changed(dialog_id, topic);
  }
}

void ForumTopicManager::repair_topic_unread_mention_count(DialogId dialog_id, ForumTopicId forum_topic_id) {
  // no need to repair mention count in private chats with bots
  if (is_forum(dialog_id, false).is_error() || can_be_forum_topic_id(forum_topic_id).is_error()) {
    return;
  }

  td_->create_handler<GetForumTopicQuery>(Promise<td_api::object_ptr<td_api::forumTopic>>())
      ->send(dialog_id, forum_topic_id);
}

void ForumTopicManager::repair_topic_unread_reaction_count(DialogId dialog_id, ForumTopicId forum_topic_id) {
  if (is_forum(dialog_id, true).is_error() || can_be_forum_topic_id(forum_topic_id).is_error()) {
    return;
  }

  td_->create_handler<GetForumTopicQuery>(Promise<td_api::object_ptr<td_api::forumTopic>>())
      ->send(dialog_id, forum_topic_id);
}

void ForumTopicManager::repair_topic_unread_poll_vote_count(DialogId dialog_id, ForumTopicId forum_topic_id) {
  // no need to repair unread vote count in private chats with bots
  if (is_forum(dialog_id, false).is_error() || can_be_forum_topic_id(forum_topic_id).is_error()) {
    return;
  }

  td_->create_handler<GetForumTopicQuery>(Promise<td_api::object_ptr<td_api::forumTopic>>())
      ->send(dialog_id, forum_topic_id);
}

}  // namespace td
