/*
    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 "smc-util.h"
#include "td/utils/filesystem.h"
#include "keys/encryptor.h"
#include "smartcont/provider-code.h"
namespace ton {
static void smc_forget(td::actor::ActorId client, td::int64 id) {
  auto query = create_tl_object(id);
  td::actor::send_closure(client, &tonlib::TonlibClientWrapper::send_request, std::move(query),
                          [](td::Result> R) mutable {
                            if (R.is_error()) {
                              LOG(WARNING) << "smc_forget failed: " << R.move_as_error();
                            }
                          });
}
void run_get_method(ContractAddress address, td::actor::ActorId client, std::string method,
                    std::vector> args,
                    td::Promise>> promise) {
  LOG(DEBUG) << "Running get method " << method << " on " << address.to_string();
  auto query =
      create_tl_object(create_tl_object(address.to_string()));
  td::actor::send_closure(
      client, &tonlib::TonlibClientWrapper::send_request, std::move(query),
      [client, method = std::move(method), args = std::move(args),
       promise = std::move(promise)](td::Result> R) mutable {
        TRY_RESULT_PROMISE(promise, obj, std::move(R));
        auto query = create_tl_object(
            obj->id_, create_tl_object(std::move(method)), std::move(args));
        td::actor::send_closure(
            client, &tonlib::TonlibClientWrapper::send_request, std::move(query),
            [client, id = obj->id_,
             promise = std::move(promise)](td::Result> R) mutable {
              smc_forget(client, id);
              TRY_RESULT_PROMISE(promise, obj, std::move(R));
              if (obj->exit_code_ != 0 && obj->exit_code_ != 1) {
                promise.set_error(
                    td::Status::Error(PSTRING() << "Method execution finished with code " << obj->exit_code_));
                return;
              }
              promise.set_result(std::move(obj->stack_));
            });
      });
}
void check_contract_exists(ContractAddress address, td::actor::ActorId client,
                           td::Promise promise) {
  auto query =
      create_tl_object(create_tl_object(address.to_string()));
  td::actor::send_closure(
      client, &tonlib::TonlibClientWrapper::send_request, std::move(query),
      [client, promise = std::move(promise)](td::Result> R) mutable {
        TRY_RESULT_PROMISE(promise, obj, std::move(R));
        auto query = create_tl_object(obj->id_);
        td::actor::send_closure(
            client, &tonlib::TonlibClientWrapper::send_request, std::move(query),
            [client, id = obj->id_,
             promise = std::move(promise)](td::Result> R) mutable {
              smc_forget(client, id);
              TRY_RESULT_PROMISE(promise, r, std::move(R));
              promise.set_result(!r->bytes_.empty());
            });
      });
}
void get_contract_balance(ContractAddress address, td::actor::ActorId client,
                          td::Promise promise) {
  auto query =
      create_tl_object(create_tl_object(address.to_string()));
  td::actor::send_closure(
      client, &tonlib::TonlibClientWrapper::send_request, std::move(query),
      promise.wrap([](tonlib_api::object_ptr r) -> td::Result {
        return td::make_refint(r->balance_);
      }));
}
FabricContractWrapper::FabricContractWrapper(ContractAddress address,
                                             td::actor::ActorId client,
                                             td::actor::ActorId keyring,
                                             td::unique_ptr callback, td::uint64 last_processed_lt)
    : address_(address)
    , client_(std::move(client))
    , keyring_(std::move(keyring))
    , callback_(std::move(callback))
    , last_processed_lt_(last_processed_lt) {
}
void FabricContractWrapper::start_up() {
  alarm();
}
void FabricContractWrapper::alarm() {
  if (process_transactions_at_ && process_transactions_at_.is_in_past()) {
    process_transactions_at_ = td::Timestamp::never();
    load_transactions();
  }
  alarm_timestamp().relax(process_transactions_at_);
  if (send_message_at_ && send_message_at_.is_in_past()) {
    send_message_at_ = td::Timestamp::never();
    do_send_external_message();
  }
  alarm_timestamp().relax(send_message_at_);
}
void FabricContractWrapper::load_transactions() {
  LOG(DEBUG) << "Loading transactions for " << address_.to_string() << ", last_lt=" << last_processed_lt_;
  auto query =
      create_tl_object(create_tl_object(address_.to_string()));
  td::actor::send_closure(
      client_, &tonlib::TonlibClientWrapper::send_request, std::move(query),
      [SelfId = actor_id(this)](td::Result> R) mutable {
        if (R.is_error()) {
          td::actor::send_closure(SelfId, &FabricContractWrapper::loaded_last_transactions, R.move_as_error());
          return;
        }
        auto obj = R.move_as_ok();
        td::actor::send_closure(SelfId, &FabricContractWrapper::load_last_transactions,
                                std::vector>(),
                                std::move(obj->last_transaction_id_), (td::uint32)obj->sync_utime_);
      });
}
void FabricContractWrapper::load_last_transactions(std::vector> transactions,
                                                   tl_object_ptr next_id,
                                                   td::uint32 utime) {
  if ((td::uint64)next_id->lt_ <= last_processed_lt_) {
    loaded_last_transactions(std::make_pair(std::move(transactions), utime));
    return;
  }
  auto query = create_tl_object(
      nullptr, create_tl_object(address_.to_string()), std::move(next_id), 10, false);
  td::actor::send_closure(
      client_, &tonlib::TonlibClientWrapper::send_request, std::move(query),
      [transactions = std::move(transactions), last_processed_lt = last_processed_lt_, SelfId = actor_id(this),
       utime](td::Result> R) mutable {
        if (R.is_error()) {
          td::actor::send_closure(SelfId, &FabricContractWrapper::loaded_last_transactions, R.move_as_error());
          return;
        }
        auto obj = R.move_as_ok();
        for (auto& transaction : obj->transactions_) {
          if ((td::uint64)transaction->transaction_id_->lt_ <= last_processed_lt ||
              (double)transaction->utime_ < td::Clocks::system() - 86400 || transactions.size() >= 1000) {
            LOG(DEBUG) << "Stopping loading transactions (too many or too old)";
            td::actor::send_closure(SelfId, &FabricContractWrapper::loaded_last_transactions,
                                    std::make_pair(std::move(transactions), utime));
            return;
          }
          LOG(DEBUG) << "Adding trtansaction, lt=" << transaction->transaction_id_->lt_;
          transactions.push_back(std::move(transaction));
        }
        td::actor::send_closure(SelfId, &FabricContractWrapper::load_last_transactions, std::move(transactions),
                                std::move(obj->previous_transaction_id_), utime);
      });
}
void FabricContractWrapper::loaded_last_transactions(
    td::Result>, td::uint32>> R) {
  if (R.is_error()) {
    LOG(ERROR) << "Error during loading last transactions: " << R.move_as_error();
    alarm_timestamp().relax(process_transactions_at_ = td::Timestamp::in(30.0));
    return;
  }
  auto r = R.move_as_ok();
  auto transactions = std::move(r.first);
  td::uint32 utime = r.second;
  LOG(DEBUG) << "Finished loading " << transactions.size() << " transactions. sync_utime=" << utime;
  std::reverse(transactions.begin(), transactions.end());
  for (tl_object_ptr& transaction : transactions) {
    LOG(DEBUG) << "Processing transaction tl=" << transaction->transaction_id_->lt_;
    last_processed_lt_ = transaction->transaction_id_->lt_;
    // transaction->in_msg_->source_->account_address_.empty() - message is external
    if (current_ext_message_ && current_ext_message_.value().sent &&
        transaction->in_msg_->source_->account_address_.empty()) {
      auto msg_data = dynamic_cast(transaction->in_msg_->msg_data_.get());
      if (msg_data == nullptr) {
        continue;
      }
      auto r_body = vm::std_boc_deserialize(msg_data->body_);
      if (r_body.is_error()) {
        LOG(WARNING) << "Invalid response from tonlib: " << r_body.move_as_error();
        continue;
      }
      td::Ref body = r_body.move_as_ok();
      vm::CellSlice cs(vm::NoVm(), body);
      if (cs.size() < 512 + 96) {
        continue;
      }
      cs.skip_first(512 + 64);
      auto seqno = (td::uint32)cs.fetch_ulong(32);
      if (seqno != current_ext_message_.value().seqno) {
        continue;
      }
      if (current_ext_message_.value().ext_msg_body_hash != body->get_hash().bits()) {
        do_send_external_message_finish(td::Status::Error("Another external message with the same seqno was accepted"));
        continue;
      }
      do_send_external_message_finish(&transaction->out_msgs_);
    }
  }
  for (tl_object_ptr& transaction : transactions) {
    callback_->on_transaction(std::move(transaction));
  }
  if (current_ext_message_ && current_ext_message_.value().sent && current_ext_message_.value().timeout < utime) {
    do_send_external_message_finish(td::Status::Error("Timeout"));
  }
  alarm_timestamp().relax(process_transactions_at_ = td::Timestamp::in(10.0));
}
void FabricContractWrapper::run_get_method(
    std::string method, std::vector> args,
    td::Promise>> promise) {
  ton::run_get_method(address_, client_, std::move(method), std::move(args), std::move(promise));
}
void FabricContractWrapper::send_internal_message(ContractAddress dest, td::RefInt256 coins, vm::CellSlice body,
                                                  td::Promise promise) {
  td::Bits256 body_hash = vm::CellBuilder().append_cellslice(body).finalize_novm()->get_hash().bits();
  LOG(DEBUG) << "send_internal_message " << address_.to_string() << " -> " << dest.to_string() << ", " << coins
             << " nanoTON, body=" << body_hash.to_hex();
  CHECK(coins->sgn() >= 0);
  pending_messages_.push(PendingMessage{dest, std::move(coins), std::move(body), body_hash, std::move(promise)});
  if (!send_message_at_ && !current_ext_message_) {
    alarm_timestamp().relax(send_message_at_ = td::Timestamp::in(1.0));
  }
}
void FabricContractWrapper::do_send_external_message() {
  CHECK(!current_ext_message_);
  LOG(DEBUG) << "do_send_external message: " << pending_messages_.size() << " messages in queue";
  if (pending_messages_.empty()) {
    return;
  }
  current_ext_message_ = CurrentExtMessage();
  while (current_ext_message_.value().int_msgs.size() < 4 && !pending_messages_.empty()) {
    PendingMessage msg = std::move(pending_messages_.front());
    current_ext_message_.value().int_msgs.push_back(std::move(msg));
    pending_messages_.pop();
  }
  run_get_method(
      "get_wallet_params", {},
      [SelfId = actor_id(this)](td::Result>> R) {
        td::uint32 seqno = 0;
        td::uint32 subwallet_id = 0;
        td::Bits256 public_key = td::Bits256::zero();
        auto S = [&]() -> td::Status {
          TRY_RESULT(stack, std::move(R));
          if (stack.size() != 3) {
            return td::Status::Error(PSTRING() << "Method returned " << stack.size() << " values, 3 expected");
          }
          TRY_RESULT_PREFIX_ASSIGN(seqno, entry_to_int(stack[0]), "Invalid seqno: ");
          TRY_RESULT_PREFIX_ASSIGN(subwallet_id, entry_to_int(stack[1]), "Invalid subwallet_id: ");
          TRY_RESULT_PREFIX_ASSIGN(public_key, entry_to_bits256(stack[2]), "Invalid public_key: ");
          return td::Status::OK();
        }();
        if (S.is_error()) {
          td::actor::send_closure(SelfId, &FabricContractWrapper::do_send_external_message_finish,
                                  S.move_as_error_prefix("Failed to get wallet params: "));
          return;
        }
        td::actor::send_closure(SelfId, &FabricContractWrapper::do_send_external_message_cont, seqno, subwallet_id,
                                public_key);
      });
}
void FabricContractWrapper::do_send_external_message_cont(td::uint32 seqno, td::uint32 subwallet_id,
                                                          td::Bits256 public_key) {
  LOG(DEBUG) << "Got wallet params: seqno=" << seqno << ", subwallet_id=" << subwallet_id
             << ", key=" << public_key.to_hex();
  CHECK(current_ext_message_);
  current_ext_message_.value().seqno = seqno;
  current_ext_message_.value().timeout = (td::uint32)td::Clocks::system() + 45;
  vm::CellBuilder b;
  b.store_long(subwallet_id, 32);                          // subwallet id.
  b.store_long(current_ext_message_.value().timeout, 32);  // valid until
  b.store_long(seqno, 32);                                 // seqno
  for (const PendingMessage& msg : current_ext_message_.value().int_msgs) {
    vm::CellBuilder b2;
    b2.store_long(3 << 2, 6);                      // 0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt
    b2.append_cellslice(msg.dest.to_cellslice());  // dest:MsgAddressInt
    store_coins(b2, msg.value);                    // grams:Grams
    b2.store_zeroes(1 + 4 + 4 + 64 + 32 + 1);      // extre, ihr_fee, fwd_fee, created_lt, created_at, init
    // body:(Either X ^X)
    if (b2.remaining_bits() >= 1 + msg.body.size() && b2.remaining_refs() >= msg.body.size_refs()) {
      b2.store_zeroes(1);
      b2.append_cellslice(msg.body);
    } else {
      b2.store_ones(1);
      b2.store_ref(vm::CellBuilder().append_cellslice(msg.body).finalize_novm());
    }
    b.store_long(3, 8);               // mode
    b.store_ref(b2.finalize_novm());  // message
  }
  td::Ref to_sign = b.finalize_novm();
  td::BufferSlice hash(to_sign->get_hash().as_slice());
  LOG(DEBUG) << "Signing external message";
  td::actor::send_closure(
      keyring_, &keyring::Keyring::sign_message, PublicKey(pubkeys::Ed25519(public_key)).compute_short_id(),
      std::move(hash), [SelfId = actor_id(this), data = std::move(to_sign)](td::Result R) mutable {
        if (R.is_error()) {
          td::actor::send_closure(SelfId, &FabricContractWrapper::do_send_external_message_finish,
                                  R.move_as_error_prefix("Failed to sign message: "));
          return;
        }
        auto signature = R.move_as_ok();
        CHECK(signature.size() == 64);
        vm::CellBuilder b;
        b.store_bytes(signature);
        b.append_cellslice(vm::load_cell_slice(data));
        td::actor::send_closure(SelfId, &FabricContractWrapper::do_send_external_message_cont2, b.finalize_novm());
      });
}
void FabricContractWrapper::do_send_external_message_cont2(td::Ref ext_msg_body) {
  CHECK(current_ext_message_);
  LOG(DEBUG) << "Signed external message, sending: seqno=" << current_ext_message_.value().seqno;
  current_ext_message_.value().sent = true;
  current_ext_message_.value().ext_msg_body_hash = ext_msg_body->get_hash().bits();
  auto body = vm::std_boc_serialize(ext_msg_body).move_as_ok().as_slice().str();
  auto query = create_tl_object(
      create_tl_object(address_.to_string()), "", std::move(body));
  td::actor::send_closure(client_, &tonlib::TonlibClientWrapper::send_request,
                          std::move(query), [SelfId = actor_id(this)](td::Result> R) {
                            if (R.is_error()) {
                              td::actor::send_closure(SelfId, &FabricContractWrapper::do_send_external_message_finish,
                                                      R.move_as_error_prefix("Failed to send message: "));
                            } else {
                              LOG(DEBUG) << "External message was sent to liteserver";
                            }
                          });
}
void FabricContractWrapper::do_send_external_message_finish(
    td::Result>*> R) {
  CHECK(current_ext_message_);
  if (R.is_error()) {
    LOG(DEBUG) << "Failed to send external message seqno=" << current_ext_message_.value().seqno << ": " << R.error();
    for (auto& msg : current_ext_message_.value().int_msgs) {
      msg.promise.set_error(R.error().clone());
    }
  } else {
    LOG(DEBUG) << "External message seqno=" << current_ext_message_.value().seqno << " was sent";
    const auto& out_msgs = *R.ok();
    auto& msgs = current_ext_message_.value().int_msgs;
    for (const auto& out_msg : out_msgs) {
      ContractAddress dest = ContractAddress::parse(out_msg->destination_->account_address_).move_as_ok();
      td::RefInt256 value = td::make_refint((td::uint64)out_msg->value_);
      td::Bits256 body_hash;
      body_hash.as_slice().copy_from(out_msg->body_hash_);
      bool found = false;
      for (size_t i = 0; i < msgs.size(); ++i) {
        if (msgs[i].dest == dest && msgs[i].value->cmp(*value) == 0 && msgs[i].body_hash == body_hash) {
          LOG(DEBUG) << "Internal message was sent dest=" << dest.to_string() << ", value=" << value
                     << ", body_hash=" << body_hash.to_hex();
          msgs[i].promise.set_result(td::Unit());
          msgs.erase(msgs.begin() + i);
          found = true;
          break;
        }
      }
      if (!found) {
        LOG(DEBUG) << "Unexpected internal message was sent: dest=" << dest.to_string() << " value=" << value
                   << " body_hash=" << body_hash;
      }
    }
    for (auto& msg : msgs) {
      LOG(DEBUG) << "Internal message WAS NOT SENT dest=" << msg.dest.to_string() << ", value=" << msg.value
                 << ", body_hash=" << msg.body_hash.to_hex();
      msg.promise.set_result(td::Status::Error("External message was accepted, but internal message was not sent"));
    }
  }
  current_ext_message_ = {};
  if (!pending_messages_.empty()) {
    do_send_external_message();
  }
}
bool store_coins(vm::CellBuilder& b, const td::RefInt256& x) {
  unsigned len = (((unsigned)x->bit_size(false) + 7) >> 3);
  if (len >= 16) {
    return false;
  }
  return b.store_long_bool(len, 4) && b.store_int256_bool(*x, len * 8, false);
}
bool store_coins(vm::CellBuilder& b, td::uint64 x) {
  return store_coins(b, td::make_refint(x));
}
td::Result generate_fabric_contract(td::actor::ActorId keyring) {
  auto private_key = PrivateKey{privkeys::Ed25519::random()};
  td::Bits256 public_key = private_key.compute_public_key().ed25519_value().raw();
  td::Slice code_boc(STORAGE_PROVIDER_CODE, sizeof(STORAGE_PROVIDER_CODE));
  TRY_RESULT(code, vm::std_boc_deserialize(code_boc));
  LOG(DEBUG) << "Generating storage provider state init. code_hash=" << code->get_hash().to_hex()
             << " public_key=" << public_key.to_hex();
  vm::CellBuilder b;
  b.store_long(0, 32);                   // seqno
  b.store_long(0, 32);                   // subwallet_id
  b.store_bytes(public_key.as_slice());  // public_key
  b.store_long(0, 1);                    // accept_new_contracts (false by default)
  store_coins(b, 1'000'000);             // rate_per_mb_day
  b.store_long(86400, 32);               // max_span
  b.store_long(1 << 20, 64);             // min_file_size
  b.store_long(1 << 30, 64);             // max_file_size
  td::Ref data = b.finalize_novm();
  // _ split_depth:(Maybe (## 5)) special:(Maybe TickTock)
  //   code:(Maybe ^Cell) data:(Maybe ^Cell)
  //   library:(HashmapE 256 SimpleLib) = StateInit;
  td::Ref state_init =
      vm::CellBuilder().store_long(0b00110, 5).store_ref(std::move(code)).store_ref(std::move(data)).finalize_novm();
  ContractAddress address{basechainId, state_init->get_hash().bits()};
  // Message body
  b = vm::CellBuilder();
  b.store_long(0, 32);                                                 // subwallet_id
  b.store_long((td::uint32)td::Clocks::system() + 3600 * 24 * 7, 32);  // valid_until
  b.store_long(0, 32);                                                 // seqno
  td::Ref to_sign = b.finalize_novm();
  TRY_RESULT(decryptor, private_key.create_decryptor());
  TRY_RESULT(signature, decryptor->sign(to_sign->get_hash().as_slice()));
  CHECK(signature.size() == 64);
  td::Ref msg_body =
      vm::CellBuilder().store_bytes(signature).append_cellslice(vm::CellSlice(vm::NoVm(), to_sign)).finalize_novm();
  td::actor::send_closure(keyring, &keyring::Keyring::add_key, private_key, false,
                          [](td::Result R) { R.ensure(); });
  return FabricContractInit{address, state_init, msg_body};
}
td::Ref create_new_contract_message_body(td::Ref info, td::Bits256 microchunk_hash,
                                                   td::uint64 query_id, td::RefInt256 rate, td::uint32 max_span) {
  // new_storage_contract#00000001 query_id:uint64 info:(^ TorrentInfo) microchunk_hash:uint256
  //     expected_rate:Coins expected_max_span:uint32 = NewStorageContract;
  vm::CellBuilder b;
  b.store_long(0x107c49ef, 32);  // const op::offer_storage_contract = 0x107c49ef;
  b.store_long(query_id, 64);
  b.store_ref(std::move(info));
  b.store_bytes(microchunk_hash.as_slice());
  store_coins(b, rate);
  b.store_long(max_span, 32);
  return b.finalize_novm();
}
void get_storage_contract_data(ContractAddress address, td::actor::ActorId client,
                               td::Promise promise) {
  run_get_method(
      address, client, "get_storage_contract_data", {},
      promise.wrap([](std::vector> stack) -> td::Result {
        if (stack.size() < 11) {
          return td::Status::Error("Too few entries");
        }
        // active, balance, provider, merkle_hash, file_size, next_proof, rate_per_mb_day, max_span, last_proof_time,
        // client, torrent_hash
        TRY_RESULT(active, entry_to_int(stack[0]));
        TRY_RESULT(balance, entry_to_int(stack[1]));
        TRY_RESULT(microchunk_hash, entry_to_bits256(stack[3]));
        TRY_RESULT(file_size, entry_to_int(stack[4]));
        TRY_RESULT(next_proof, entry_to_int(stack[5]));
        TRY_RESULT(rate_per_mb_day, entry_to_int(stack[6]));
        TRY_RESULT(max_span, entry_to_int(stack[7]));
        TRY_RESULT(last_proof_time, entry_to_int(stack[8]));
        TRY_RESULT(torrent_hash, entry_to_bits256(stack[10]));
        return StorageContractData{(bool)active,    balance,  microchunk_hash, file_size,   next_proof,
                                   rate_per_mb_day, max_span, last_proof_time, torrent_hash};
      }));
}
}  // namespace ton