/*
    This file is part of TON Blockchain Library.
    TON Blockchain Library is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.
    TON Blockchain Library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    You should have received a copy of the GNU Lesser General Public License
    along with TON Blockchain Library.  If not, see .
*/
#include "StorageProvider.h"
#include "td/db/RocksDb.h"
#include "td/utils/JsonBuilder.h"
#include "auto/tl/ton_api_json.h"
#include "td/utils/port/path.h"
#include "block/block-auto.h"
#include "common/delay.h"
#include "td/actor/MultiPromise.h"
namespace ton {
td::Result ProviderParams::create(const tl_object_ptr& obj) {
  ProviderParams p;
  p.accept_new_contracts = obj->accept_new_contracts_;
  p.rate_per_mb_day = td::string_to_int256(obj->rate_per_mb_day_);
  if (p.rate_per_mb_day.is_null() || p.rate_per_mb_day->sgn() < 0) {
    return td::Status::Error("Invalid rate");
  }
  p.max_span = obj->max_span_;
  p.minimal_file_size = obj->minimal_file_size_;
  p.maximal_file_size = obj->maximal_file_size_;
  return p;
}
tl_object_ptr ProviderParams::tl() const {
  return create_tl_object(
      accept_new_contracts, rate_per_mb_day->to_dec_string(), max_span, minimal_file_size, maximal_file_size);
}
bool ProviderParams::to_builder(vm::CellBuilder& b) const {
  return b.store_long_bool(accept_new_contracts, 1) && store_coins(b, rate_per_mb_day) &&
         b.store_long_bool(max_span, 32) && b.store_long_bool(minimal_file_size, 64) &&
         b.store_long_bool(maximal_file_size, 64);
}
StorageProvider::StorageProvider(ContractAddress account_address, std::string db_root,
                                 td::actor::ActorId tonlib_client,
                                 td::actor::ActorId storage_manager,
                                 td::actor::ActorId keyring)
    : main_address_(account_address)
    , db_root_(std::move(db_root))
    , tonlib_client_(std::move(tonlib_client))
    , storage_manager_(std::move(storage_manager))
    , keyring_(std::move(keyring)) {
}
void StorageProvider::start_up() {
  LOG(INFO) << "Initing storage provider, account address: " << main_address_.to_string();
  td::mkdir(db_root_).ensure();
  db_ = std::make_unique(td::RocksDb::open(db_root_).move_as_ok());
  auto r_state = db::db_get(
      *db_, create_hash_tl_object(), true);
  r_state.ensure();
  auto state = r_state.move_as_ok();
  if (state) {
    last_processed_lt_ = state->last_processed_lt_;
    LOG(INFO) << "Loaded storage provider state";
    LOG(INFO) << "Last processed lt: " << last_processed_lt_;
  }
  class Callback : public FabricContractWrapper::Callback {
   public:
    explicit Callback(td::actor::ActorId id) : id_(std::move(id)) {
    }
    void on_transaction(tl_object_ptr transaction) override {
      td::actor::send_closure(id_, &StorageProvider::process_transaction, std::move(transaction));
    }
   private:
    td::actor::ActorId id_;
  };
  contract_wrapper_ =
      td::actor::create_actor("ContractWrapper", main_address_, tonlib_client_, keyring_,
                                                     td::make_unique(actor_id(this)), last_processed_lt_);
  auto r_config = db::db_get(
      *db_, create_hash_tl_object(), true);
  r_config.ensure();
  auto config_obj = r_config.move_as_ok();
  if (config_obj) {
    LOG(INFO) << "Loaded config from db";
    config_ = Config(config_obj);
  } else {
    LOG(INFO) << "Using default config";
    db_store_config();
  }
  LOG(INFO) << "Config: max_contracts=" << config_.max_contracts << ", max_total_size=" << config_.max_total_size;
  auto r_contract_list = db::db_get(
      *db_, create_hash_tl_object(), true);
  r_contract_list.ensure();
  auto contract_list = r_contract_list.move_as_ok();
  if (contract_list) {
    LOG(INFO) << "Loading " << contract_list->contracts_.size() << " contracts from db";
    for (auto& c : contract_list->contracts_) {
      ContractAddress address(c->wc_, c->addr_);
      if (contracts_.count(address)) {
        LOG(ERROR) << "Duplicate contract in db: " << address.to_string();
        continue;
      }
      auto r_contract = db::db_get(
          *db_, create_hash_tl_object(address.wc, address.addr),
          true);
      r_contract.ensure();
      auto db_contract = r_contract.move_as_ok();
      if (!db_contract) {
        LOG(ERROR) << "Missing contract in db: " << address.to_string();
        continue;
      }
      StorageContract& contract = contracts_[address];
      contract.torrent_hash = db_contract->torrent_hash_;
      contract.microchunk_hash = db_contract->microchunk_hash_;
      contract.created_time = db_contract->created_time_;
      contract.state = (StorageContract::State)db_contract->state_;
      contract.file_size = db_contract->file_size_;
      contract.max_span = db_contract->max_span_;
      contract.rate = td::string_to_int256(db_contract->rate_);
      contracts_total_size_ += contract.file_size;
      auto r_tree = db::db_get(
          *db_, create_hash_tl_object(address.wc, address.addr), true);
      r_tree.ensure();
      auto tree = r_tree.move_as_ok();
      if (tree) {
        contract.microchunk_tree = std::make_shared(vm::std_boc_deserialize(tree->data_).move_as_ok());
      }
      LOG(INFO) << "Loaded contract from db: " << address.to_string() << ", torrent=" << contract.torrent_hash.to_hex()
                << ", state=" << contract.state;
    }
  }
  for (auto& p : contracts_) {
    const ContractAddress& address = p.first;
    StorageContract& contract = p.second;
    switch (contract.state) {
      case StorageContract::st_downloading:
        init_new_storage_contract(address, contract);
        break;
      case StorageContract::st_downloaded:
        after_contract_downloaded(address);
        break;
      case StorageContract::st_active:
        contract.check_next_proof_at = td::Timestamp::now();
        break;
      case StorageContract::st_closing:
        check_storage_contract_deleted(address);
        break;
      default:
        LOG(FATAL) << "Invalid contract state in db";
    }
  }
  LOG(INFO) << "Loaded contracts from db";
  alarm();
}
void StorageProvider::get_params(td::Promise promise) {
  return get_provider_params(tonlib_client_, main_address_, std::move(promise));
}
void StorageProvider::get_provider_params(td::actor::ActorId client,
                                          ContractAddress address, td::Promise promise) {
  run_get_method(
      address, client, "get_storage_params", std::vector>(),
      promise.wrap([](std::vector> stack) -> td::Result {
        if (stack.size() != 5) {
          return td::Status::Error(PSTRING() << "Method returned " << stack.size() << " values, 5 expected");
        }
        TRY_RESULT_PREFIX(accept_new_contracts, entry_to_int(stack[0]), "Invalid accept_new_contracts: ");
        TRY_RESULT_PREFIX(rate_per_mb_day, entry_to_int(stack[1]), "Invalid rate_per_mb_day: ");
        TRY_RESULT_PREFIX(max_span, entry_to_int(stack[2]), "Invalid max_span: ");
        TRY_RESULT_PREFIX(minimal_file_size, entry_to_int(stack[3]), "Invalid minimal_file_size: ");
        TRY_RESULT_PREFIX(maximal_file_size, entry_to_int(stack[4]), "Invalid maximal_file_size: ");
        ProviderParams params;
        params.accept_new_contracts = accept_new_contracts;
        params.rate_per_mb_day = rate_per_mb_day;
        params.max_span = max_span;
        params.minimal_file_size = minimal_file_size;
        params.maximal_file_size = maximal_file_size;
        return params;
      }));
}
void StorageProvider::set_params(ProviderParams params, td::Promise promise) {
  vm::CellBuilder b;
  b.store_long(0x54cbf19b, 32);  // const op::update_storage_params = 0x54cbf19b;
  b.store_long(0, 64);           // query_id
  if (!params.to_builder(b)) {
    promise.set_error(td::Status::Error("Failed to store params to builder"));
    return;
  }
  LOG(INFO) << "Sending external message to update provider parameters: " << params.accept_new_contracts << ", "
            << params.max_span << ", " << params.rate_per_mb_day << ", " << params.minimal_file_size << ", "
            << params.maximal_file_size;
  td::actor::send_closure(contract_wrapper_, &FabricContractWrapper::send_internal_message, main_address_,
                          td::make_refint(100'000'000), b.as_cellslice(), std::move(promise));
}
void StorageProvider::db_store_state() {
  LOG(DEBUG) << "db_store_state last_lt=" << last_processed_lt_;
  db_->begin_transaction().ensure();
  db_->set(create_hash_tl_object().as_slice(),
           create_serialize_tl_object(last_processed_lt_))
      .ensure();
  db_->commit_transaction().ensure();
}
void StorageProvider::db_store_config() {
  LOG(DEBUG) << "db_store_config";
  db_->begin_transaction().ensure();
  db_->set(create_hash_tl_object().as_slice(),
           serialize_tl_object(config_.tl(), true))
      .ensure();
  db_->commit_transaction().ensure();
}
void StorageProvider::alarm() {
  for (auto& p : contracts_) {
    if (p.second.check_next_proof_at && p.second.check_next_proof_at.is_in_past()) {
      p.second.check_next_proof_at = td::Timestamp::never();
      check_next_proof(p.first, p.second);
    }
    alarm_timestamp().relax(p.second.check_next_proof_at);
  }
}
void StorageProvider::process_transaction(tl_object_ptr transaction) {
  std::string new_contract_address;
  for (auto& message : transaction->out_msgs_) {
    auto data = dynamic_cast(message->msg_data_.get());
    if (data == nullptr) {
      continue;
    }
    auto r_body = vm::std_boc_deserialize(data->body_);
    if (r_body.is_error()) {
      LOG(ERROR) << "Invalid message body in tonlib response: " << r_body.move_as_error();
      continue;
    }
    td::Ref body = r_body.move_as_ok();
    vm::CellSlice cs = vm::load_cell_slice(body);
    if (cs.size() >= 32) {
      long long op_code = cs.prefetch_ulong(32);
      // const op::offer_storage_contract = 0x107c49ef; -- old versions
      // const op::deploy_storage_contract = 0xe4748df1; -- new versions
      if((op_code == 0x107c49ef) || (op_code == 0xe4748df1)) {
        new_contract_address = message->destination_->account_address_;
      }
    }
  }
  if (!new_contract_address.empty()) {
    auto P = td::PromiseCreator::lambda(
        [SelfId = actor_id(this), lt = (td::uint64)transaction->transaction_id_->lt_](td::Result R) {
          if (R.is_error()) {
            LOG(ERROR) << "Error during processing new storage contract, skipping: " << R.move_as_error();
          }
        });
    on_new_storage_contract(ContractAddress::parse(new_contract_address).move_as_ok(), std::move(P));
  }
  last_processed_lt_ = transaction->transaction_id_->lt_;
  db_store_state();
}
void StorageProvider::on_new_storage_contract(ContractAddress address, td::Promise promise, int max_retries) {
  LOG(INFO) << "Processing new storage contract: " << address.to_string();
  get_storage_contract_data(
      address, tonlib_client_,
      [SelfId = actor_id(this), address, promise = std::move(promise),
       max_retries](td::Result R) mutable {
        if (R.is_error()) {
          if (max_retries > 0) {
            LOG(WARNING) << "Processing new storage contract: " << R.move_as_error() << ", retrying";
            delay_action(
                [SelfId = std::move(SelfId), promise = std::move(promise), address = std::move(address),
                 max_retries]() mutable {
                  td::actor::send_closure(SelfId, &StorageProvider::on_new_storage_contract, std::move(address),
                                          std::move(promise), max_retries - 1);
                },
                td::Timestamp::in(5.0));
          } else {
            promise.set_error(R.move_as_error());
          }
          return;
        }
        td::actor::send_closure(SelfId, &StorageProvider::on_new_storage_contract_cont, address, R.move_as_ok(),
                                std::move(promise));
      });
}
void StorageProvider::on_new_storage_contract_cont(ContractAddress address, StorageContractData data,
                                                   td::Promise promise) {
  auto it = contracts_.emplace(address, StorageContract());
  if (!it.second) {
    promise.set_error(td::Status::Error(PSTRING() << "Storage contract already registered: " << address.to_string()));
    return;
  }
  LOG(INFO) << "New storage contract " << address.to_string() << ", torrent hash: " << data.torrent_hash.to_hex();
  LOG(DEBUG) << "Stoage contract data: microchunk_hash=" << data.microchunk_hash << ", balance=" << data.balance
             << ", file_size=" << data.file_size << ", next_proof=" << data.next_proof
             << ", rate=" << data.rate_per_mb_day << ", max_span=" << data.max_span;
  StorageContract& contract = it.first->second;
  contract.torrent_hash = data.torrent_hash;
  contract.microchunk_hash = data.microchunk_hash;
  contract.state = StorageContract::st_downloading;
  contract.created_time = (td::uint32)td::Clocks::system();
  contract.file_size = data.file_size;
  contract.max_span = data.max_span;
  contract.rate = data.rate_per_mb_day;
  contracts_total_size_ += contract.file_size;
  promise.set_result(td::Unit());
  if (contracts_.size() <= config_.max_contracts && contracts_total_size_ <= config_.max_total_size) {
    db_update_storage_contract(address, true);
    init_new_storage_contract(address, contract);
  } else {
    if (contracts_.size() > config_.max_contracts) {
      LOG(WARNING) << "Cannot add new storage contract: too many contracts (limit = " << config_.max_contracts << ")";
    } else {
      LOG(WARNING) << "Cannot add new storage contract: total size exceeded (limit = "
                   << td::format::as_size(config_.max_total_size) << ")";
    }
    contract.state = StorageContract::st_closing;
    db_update_storage_contract(address, true);
    do_close_storage_contract(address);
  }
}
void StorageProvider::db_update_storage_contract(const ContractAddress& address, bool update_list) {
  LOG(DEBUG) << "db_update_storage_contract " << address.to_string() << " " << update_list;
  db_->begin_transaction().ensure();
  if (update_list) {
    std::vector> list;
    for (const auto& t : contracts_) {
      list.push_back(create_tl_object(t.first.wc, t.first.addr));
    }
    db_->set(create_hash_tl_object().as_slice(),
             create_serialize_tl_object(std::move(list)))
        .ensure();
  }
  auto key = create_hash_tl_object(address.wc, address.addr);
  auto it = contracts_.find(address);
  if (it == contracts_.end()) {
    db_->erase(key.as_slice()).ensure();
  } else {
    const StorageContract& contract = it->second;
    db_->set(key.as_slice(),
             create_serialize_tl_object(
                 contract.torrent_hash, contract.microchunk_hash, contract.created_time, (int)contract.state,
                 contract.file_size, contract.rate->to_dec_string(), contract.max_span));
  }
  db_->commit_transaction().ensure();
}
void StorageProvider::db_update_microchunk_tree(const ContractAddress& address) {
  LOG(DEBUG) << "db_update_microchunk_tree " << address.to_string();
  db_->begin_transaction().ensure();
  auto key = create_hash_tl_object(address.wc, address.addr);
  auto it = contracts_.find(address);
  if (it == contracts_.end() || it->second.microchunk_tree == nullptr) {
    db_->erase(key.as_slice()).ensure();
  } else {
    db_->set(key.as_slice(), create_serialize_tl_object(
                                 vm::std_boc_serialize(it->second.microchunk_tree->get_root()).move_as_ok()));
  }
  db_->commit_transaction().ensure();
}
void StorageProvider::init_new_storage_contract(ContractAddress address, StorageContract& contract) {
  CHECK(contract.state == StorageContract::st_downloading);
  td::actor::send_closure(storage_manager_, &StorageManager::add_torrent_by_hash, contract.torrent_hash, "", false,
                          false, [](td::Result R) {
                            // Ignore errors: error can mean that the torrent already exists, other errors will be caught later
                            if (R.is_error()) {
                              LOG(DEBUG) << "Add torrent: " << R.move_as_error();
                            } else {
                              LOG(DEBUG) << "Add torrent: OK";
                            }
                          });
  td::actor::send_closure(storage_manager_, &StorageManager::set_active_download, contract.torrent_hash, true,
                          [SelfId = actor_id(this), address](td::Result R) {
                            if (R.is_error()) {
                              LOG(ERROR) << "Failed to init storage contract: " << R.move_as_error();
                              td::actor::send_closure(SelfId, &StorageProvider::do_close_storage_contract, address);
                              return;
                            }
                            LOG(DEBUG) << "Set active download: OK";
                          });
  td::actor::send_closure(
      storage_manager_, &StorageManager::wait_for_completion, contract.torrent_hash,
      [SelfId = actor_id(this), address, hash = contract.torrent_hash, microchunk_hash = contract.microchunk_hash,
       manager = storage_manager_](td::Result R) {
        if (R.is_error()) {
          LOG(WARNING) << "Failed to download torrent " << hash.to_hex() << ": " << R.move_as_error();
          td::actor::send_closure(SelfId, &StorageProvider::do_close_storage_contract, address);
          return;
        }
        LOG(DEBUG) << "Downloaded torrent " << hash;
        td::actor::send_closure(
            manager, &StorageManager::with_torrent, hash,
            [SelfId, address, hash, microchunk_hash](td::Result R) {
              auto r_microchunk_tree = [&]() -> td::Result {
                TRY_RESULT(state, std::move(R));
                Torrent& torrent = state.torrent;
                if (!torrent.is_completed() || torrent.get_included_size() != torrent.get_info().file_size) {
                  return td::Status::Error("unknown error");
                }
                LOG(DEBUG) << "Building microchunk tree for " << hash;
                TRY_RESULT(tree, MicrochunkTree::Builder::build_for_torrent(torrent));
                if (tree.get_root_hash() != microchunk_hash) {
                  return td::Status::Error("microchunk tree hash mismatch");
                }
                return tree;
              }();
              if (r_microchunk_tree.is_error()) {
                LOG(WARNING) << "Failed to download torrent " << hash.to_hex() << ": " << R.move_as_error();
                td::actor::send_closure(SelfId, &StorageProvider::do_close_storage_contract, address);
              } else {
                td::actor::send_closure(SelfId, &StorageProvider::downloaded_torrent, address,
                                        r_microchunk_tree.move_as_ok());
              }
            });
      });
}
void StorageProvider::downloaded_torrent(ContractAddress address, MicrochunkTree microchunk_tree) {
  auto it = contracts_.find(address);
  if (it == contracts_.end()) {
    LOG(WARNING) << "Contract " << address.to_string() << " does not exist anymore";
    return;
  }
  auto& contract = it->second;
  LOG(INFO) << "Finished downloading torrent " << contract.torrent_hash.to_hex() << " for contract "
            << address.to_string();
  contract.state = StorageContract::st_downloaded;
  contract.microchunk_tree = std::make_shared(std::move(microchunk_tree));
  db_update_microchunk_tree(address);
  db_update_storage_contract(address, false);
  after_contract_downloaded(address);
}
void StorageProvider::after_contract_downloaded(ContractAddress address, td::Timestamp retry_until,
                                                td::Timestamp retry_false_until) {
  auto it = contracts_.find(address);
  if (it == contracts_.end()) {
    LOG(WARNING) << "Contract " << address.to_string() << " does not exist anymore";
    return;
  }
  auto& contract = it->second;
  td::actor::send_closure(storage_manager_, &StorageManager::set_active_upload, contract.torrent_hash, true,
                          [SelfId = actor_id(this), address](td::Result R) {
                            if (R.is_error()) {
                              LOG(ERROR) << "Set active upload: " << R.move_as_error();
                              return;
                            }
                            LOG(DEBUG) << "Set active upload: OK";
                          });
  get_storage_contract_data(address, tonlib_client_,
                            [=, SelfId = actor_id(this)](td::Result R) mutable {
                              if (R.is_error()) {
                                LOG(WARNING) << "Failed to check that contract is active: " << R.move_as_error();
                                if (retry_until && retry_until.is_in_past()) {
                                  delay_action(
                                      [=]() {
                                        td::actor::send_closure(SelfId, &StorageProvider::after_contract_downloaded,
                                                                address, retry_until, retry_false_until);
                                      },
                                      td::Timestamp::in(5.0));
                                }
                                return;
                              }
                              if (R.ok().active) {
                                td::actor::send_closure(SelfId, &StorageProvider::activated_storage_contract, address);
                              } else if (retry_false_until && retry_false_until.is_in_past()) {
                                delay_action(
                                    [=]() {
                                      td::actor::send_closure(SelfId, &StorageProvider::after_contract_downloaded,
                                                              address, retry_until, retry_false_until);
                                    },
                                    td::Timestamp::in(5.0));
                              } else {
                                td::actor::send_closure(SelfId, &StorageProvider::activate_contract_cont, address);
                              }
                            });
}
void StorageProvider::activate_contract_cont(ContractAddress address) {
  vm::CellBuilder b;
  b.store_long(0x7a361688, 32);  // const op::accept_storage_contract = 0x7a361688;
  b.store_long(0, 64);           // query_id
  LOG(DEBUG) << "Sending op::accept_storage_contract to " << address.to_string();
  td::actor::send_closure(
      contract_wrapper_, &FabricContractWrapper::send_internal_message, address, td::make_refint(100'000'000),
      b.as_cellslice(), [SelfId = actor_id(this), address](td::Result R) {
        if (R.is_error()) {
          LOG(ERROR) << "Failed to send activate message, retrying later: " << R.move_as_error();
          delay_action([=]() { td::actor::send_closure(SelfId, &StorageProvider::activate_contract_cont, address); },
                       td::Timestamp::in(10.0));
          return;
        }
        td::actor::send_closure(SelfId, &StorageProvider::after_contract_downloaded, address, td::Timestamp::in(60.0),
                                td::Timestamp::in(40.0));
      });
}
void StorageProvider::activated_storage_contract(ContractAddress address) {
  auto it = contracts_.find(address);
  if (it == contracts_.end()) {
    LOG(WARNING) << "Contract " << address.to_string() << " does not exist anymore";
    return;
  }
  LOG(INFO) << "Storage contract " << address.to_string() << " is active";
  auto& contract = it->second;
  contract.state = StorageContract::st_active;
  db_update_storage_contract(address, false);
  alarm_timestamp().relax(contract.check_next_proof_at = td::Timestamp::in(1.0));
}
void StorageProvider::do_close_storage_contract(ContractAddress address) {
  auto it = contracts_.find(address);
  if (it == contracts_.end()) {
    LOG(WARNING) << "Contract " << address.to_string() << " does not exist anymore";
    return;
  }
  LOG(INFO) << "Closing storage contract " << address.to_string();
  auto& contract = it->second;
  contract.state = StorageContract::st_closing;
  db_update_storage_contract(address, false);
  check_storage_contract_deleted(address);
}
void StorageProvider::send_close_storage_contract(ContractAddress address) {
  vm::CellBuilder b;
  b.store_long(0x79f937ea, 32);  // const op::close_contract = 0x79f937ea;
  b.store_long(0, 64);           // query_id
  LOG(DEBUG) << "Sending op::close_contract to " << address.to_string();
  td::actor::send_closure(
      contract_wrapper_, &FabricContractWrapper::send_internal_message, address, td::make_refint(100'000'000),
      b.as_cellslice(), [SelfId = actor_id(this), address](td::Result R) {
        if (R.is_error()) {
          LOG(ERROR) << "Failed to send close message, retrying later: " << R.move_as_error();
          delay_action([=]() { td::actor::send_closure(SelfId, &StorageProvider::activate_contract_cont, address); },
                       td::Timestamp::in(10.0));
          return;
        }
        td::actor::send_closure(SelfId, &StorageProvider::check_storage_contract_deleted, address,
                                td::Timestamp::in(40.0));
      });
}
void StorageProvider::check_storage_contract_deleted(ContractAddress address, td::Timestamp retry_false_until) {
  check_contract_exists(address, tonlib_client_, [=, SelfId = actor_id(this)](td::Result R) {
    if (R.is_error()) {
      delay_action(
          [=]() {
            td::actor::send_closure(SelfId, &StorageProvider::check_storage_contract_deleted, address,
                                    retry_false_until);
          },
          td::Timestamp::in(10.0));
      return;
    }
    if (!R.move_as_ok()) {
      td::actor::send_closure(SelfId, &StorageProvider::storage_contract_deleted, address);
    } else if (retry_false_until && !retry_false_until.is_in_past()) {
      delay_action(
          [=]() {
            td::actor::send_closure(SelfId, &StorageProvider::check_storage_contract_deleted, address,
                                    retry_false_until);
          },
          td::Timestamp::in(5.0));
    } else {
      td::actor::send_closure(SelfId, &StorageProvider::send_close_storage_contract, address);
    }
  });
}
void StorageProvider::storage_contract_deleted(ContractAddress address) {
  auto it = contracts_.find(address);
  if (it == contracts_.end()) {
    return;
  }
  LOG(INFO) << "Storage contract " << address.to_string() << " was deleted";
  td::Bits256 hash = it->second.torrent_hash;
  contracts_total_size_ -= it->second.file_size;
  contracts_.erase(it);
  bool delete_torrent = true;
  for (const auto& p : contracts_) {
    if (p.second.torrent_hash == hash) {
      delete_torrent = false;
      break;
    }
  }
  if (delete_torrent) {
    LOG(INFO) << "Deleting torrent " << hash.to_hex();
    td::actor::send_closure(storage_manager_, &StorageManager::remove_torrent, hash, true,
                            [](td::Result R) {});
  }
  db_update_storage_contract(address, true);
}
void StorageProvider::check_next_proof(ContractAddress address, StorageContract& contract) {
  if (contract.state != StorageContract::st_active) {
    return;
  }
  CHECK(contract.microchunk_tree != nullptr);
  get_storage_contract_data(
      address, tonlib_client_, [SelfId = actor_id(this), address](td::Result R) {
        td::actor::send_closure(SelfId, &StorageProvider::got_next_proof_info, address, std::move(R));
      });
}
void StorageProvider::got_next_proof_info(ContractAddress address, td::Result R) {
  auto it = contracts_.find(address);
  if (it == contracts_.end() || it->second.state != StorageContract::st_active) {
    return;
  }
  auto& contract = it->second;
  if (R.is_error()) {
    LOG(ERROR) << "get_next_proof_info for " << address.to_string() << ": " << R.move_as_error();
    check_contract_exists(address, tonlib_client_, [SelfId = actor_id(this), address](td::Result R) {
      td::actor::send_closure(SelfId, &StorageProvider::got_contract_exists, address, std::move(R));
    });
    return;
  }
  auto data = R.move_as_ok();
  if (data.balance->sgn() == 0) {
    LOG(INFO) << "Balance of contract " << address.to_string() << " is zero, closing";
    do_close_storage_contract(address);
    return;
  }
  td::uint32 send_at = data.last_proof_time + data.max_span / 2, now = (td::uint32)td::Clocks::system();
  if (now < send_at) {
    LOG(DEBUG) << "Will send proof in " << send_at - now << "s (last_proof_time=" << data.last_proof_time
               << ", max_span=" << data.max_span << ")";
    alarm_timestamp().relax(contract.check_next_proof_at = td::Timestamp::in(send_at - now + 2));
    return;
  }
  LOG(INFO) << "Sending proof for " << address.to_string() << ": next_proof=" << data.next_proof
            << ", max_span=" << data.max_span << ", last_proof_time=" << data.last_proof_time << " ("
            << now - data.last_proof_time << "s ago)";
  td::actor::send_closure(
      storage_manager_, &StorageManager::with_torrent, contract.torrent_hash,
      [=, SelfId = actor_id(this), tree = contract.microchunk_tree](td::Result R) {
        if (R.is_error()) {
          LOG(ERROR) << "Missing torrent for " << address.to_string();
          return;
        }
        auto state = R.move_as_ok();
        td::uint64 l = data.next_proof / MicrochunkTree::MICROCHUNK_SIZE * MicrochunkTree::MICROCHUNK_SIZE;
        td::uint64 r = l + MicrochunkTree::MICROCHUNK_SIZE;
        auto proof = tree->get_proof(l, r, state.torrent);
        td::actor::send_closure(SelfId, &StorageProvider::got_next_proof, address, std::move(proof));
      });
}
void StorageProvider::got_contract_exists(ContractAddress address, td::Result R) {
  auto it = contracts_.find(address);
  if (it == contracts_.end() || it->second.state != StorageContract::st_active) {
    return;
  }
  auto& contract = it->second;
  if (R.is_error()) {
    LOG(ERROR) << "Check contract exists for " << address.to_string() << ": " << R.move_as_error();
    alarm_timestamp().relax(contract.check_next_proof_at = td::Timestamp::in(10.0));
    return;
  }
  if (R.ok()) {
    alarm_timestamp().relax(contract.check_next_proof_at = td::Timestamp::in(10.0));
    return;
  }
  storage_contract_deleted(address);
}
void StorageProvider::got_next_proof(ContractAddress address, td::Result> R) {
  if (R.is_error()) {
    LOG(ERROR) << "Failed to build proof: " << R.move_as_error();
    return;
  }
  LOG(INFO) << "Got proof, sending";
  vm::CellBuilder b;
  b.store_long(0x419d5d4d, 32);  // const op::proof_storage = 0x419d5d4d;
  b.store_long(0, 64);           // query_id
  b.store_ref(R.move_as_ok());
  td::actor::send_closure(contract_wrapper_, &FabricContractWrapper::send_internal_message, address,
                          td::make_refint(100'000'000), b.as_cellslice(),
                          [SelfId = actor_id(this), address](td::Result R) {
                            if (R.is_error()) {
                              LOG(ERROR) << "Failed to send proof message: " << R.move_as_error();
                            } else {
                              LOG(DEBUG) << "Proof for " << address.to_string() << " was sent";
                            }
                            td::actor::send_closure(SelfId, &StorageProvider::sent_next_proof, address);
                          });
}
void StorageProvider::sent_next_proof(ContractAddress address) {
  auto it = contracts_.find(address);
  if (it == contracts_.end() || it->second.state != StorageContract::st_active) {
    return;
  }
  auto& contract = it->second;
  alarm_timestamp().relax(contract.check_next_proof_at = td::Timestamp::in(30.0));
}
void StorageProvider::get_provider_info(bool with_balances, bool with_contracts,
                                        td::Promise> promise) {
  auto result = std::make_shared();
  td::MultiPromise mp;
  auto ig = mp.init_guard();
  ig.add_promise(promise.wrap(
      [result](td::Unit) { return create_tl_object(std::move(*result)); }));
  result->address_ = main_address_.to_string();
  result->config_ = config_.tl();
  result->contracts_count_ = (int)contracts_.size();
  result->contracts_total_size_ = contracts_total_size_;
  if (with_balances) {
    get_contract_balance(main_address_, tonlib_client_, ig.get_promise().wrap([result](td::RefInt256 balance) {
      result->balance_ = balance->to_dec_string();
      return td::Unit();
    }));
  } else {
    result->balance_ = "-1";
  }
  if (with_contracts) {
    for (const auto& p : contracts_) {
      auto obj = create_tl_object();
      const StorageContract& contract = p.second;
      obj->address_ = p.first.to_string();
      obj->state_ = (int)contract.state;
      obj->torrent_ = contract.torrent_hash;
      obj->created_time_ = contract.created_time;
      obj->rate_ = contract.rate->to_dec_string();
      obj->max_span_ = contract.max_span;
      obj->file_size_ = contract.file_size;
      obj->downloaded_size_ = obj->file_size_;
      obj->client_balance_ = "-1";
      obj->contract_balance_ = "-1";
      result->contracts_.push_back(std::move(obj));
    }
    size_t i = 0;
    for (const auto& p : contracts_) {
      const StorageContract& contract = p.second;
      if (contract.state == StorageContract::st_downloading) {
        td::actor::send_closure(storage_manager_, &StorageManager::with_torrent, contract.torrent_hash,
                                [i, result, promise = ig.get_promise()](td::Result R) mutable {
                                  if (R.is_error()) {
                                    result->contracts_[i]->downloaded_size_ = 0;
                                  } else {
                                    auto state = R.move_as_ok();
                                    result->contracts_[i]->downloaded_size_ = state.torrent.get_included_ready_size();
                                  }
                                  promise.set_result(td::Unit());
                                });
      }
      if (with_balances) {
        get_contract_balance(p.first, tonlib_client_,
                             [i, result, promise = ig.get_promise()](td::Result R) mutable {
                               if (R.is_ok()) {
                                 result->contracts_[i]->contract_balance_ = R.ok()->to_dec_string();
                               }
                               promise.set_result(td::Unit());
                             });
        get_storage_contract_data(p.first, tonlib_client_,
                                  [i, result, promise = ig.get_promise()](td::Result R) mutable {
                                    auto S = [&]() -> td::Status {
                                      TRY_RESULT(data, std::move(R));
                                      result->contracts_[i]->client_balance_ = data.balance->to_dec_string();
                                      return td::Status::OK();
                                    }();
                                    promise.set_result(td::Unit());
                                  });
      }
      i += 1;
    }
  }
}
void StorageProvider::set_provider_config(Config config, td::Promise promise) {
  config_ = config;
  LOG(INFO) << "Changing provider config: max_contracts=" << config_.max_contracts
            << ", max_total_size=" << config_.max_total_size;
  db_store_config();
  promise.set_result(td::Unit());
}
void StorageProvider::withdraw(ContractAddress address, td::Promise promise) {
  auto it = contracts_.find(address);
  if (it == contracts_.end() || it->second.state != StorageContract::st_active) {
    promise.set_error(td::Status::Error("No such storage contract"));
    return;
  }
  if (it->second.state != StorageContract::st_active) {
    promise.set_error(td::Status::Error("Storage contract is not active"));
    return;
  }
  vm::CellBuilder b;
  b.store_long(0x46ed2e94, 32);  // const op::withdraw = 0x46ed2e94;
  b.store_long(0, 64);           // query_id
  LOG(INFO) << "Sending op::withdraw to storage contract " << address.to_string();
  td::actor::send_closure(contract_wrapper_, &FabricContractWrapper::send_internal_message, address,
                          td::make_refint(100'000'000), b.as_cellslice(), std::move(promise));
}
void StorageProvider::send_coins(ContractAddress dest, td::RefInt256 amount, std::string message,
                                 td::Promise promise) {
  if (amount->sgn() < 0) {
    promise.set_error(td::Status::Error("Amount is negative"));
    return;
  }
  vm::CellBuilder b;
  if (!message.empty()) {
    b.store_long(0, 32);
    if (b.remaining_bits() < message.size() * 8) {
      promise.set_error(td::Status::Error("Message is too long (max 122 bytes)"));
      return;
    }
    b.store_bytes(td::Slice(message));
  }
  LOG(INFO) << "Sending " << amount << " nanoTON to " << dest.to_string();
  td::actor::send_closure(contract_wrapper_, &FabricContractWrapper::send_internal_message, dest, amount,
                          b.finalize_novm(), std::move(promise));
}
void StorageProvider::close_storage_contract(ContractAddress address, td::Promise promise) {
  if (!contracts_.count(address)) {
    promise.set_error(td::Status::Error("No such storage contract"));
    return;
  }
  do_close_storage_contract(address);
  promise.set_result(td::Unit());
}
StorageProvider::Config::Config(const tl_object_ptr& obj)
    : max_contracts(obj->max_contracts_), max_total_size(obj->max_total_size_) {
}
tl_object_ptr StorageProvider::Config::tl() const {
  return create_tl_object(max_contracts, max_total_size);
}
}  // namespace ton