/* 
    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 "block-parse.h"
#include "block.h"
#include "td/actor/core/Actor.h"
#include "td/db/utils/BlobView.h"
#include 
#include "td/utils/OptionParser.h"
#include "td/utils/Time.h"
#include "td/utils/base64.h"
#include "td/utils/filesystem.h"
#include "td/utils/logging.h"
#include "vm/cells/MerkleProof.h"
#include "vm/db/StaticBagOfCellsDb.h"
#include 
#include 
const size_t KEY_LEN = 3 + 8 + 256;
void print_help() {
  std::cerr << "mintless-proof-generator - generates proofs for mintless jettons. Usage:\n\n";
  std::cerr << "mintless-proof-generator generate  \n";
  std::cerr << "  Generate a full tree for , save boc to .\n";
  std::cerr << "  Input format: each line is    .\n\n";
  std::cerr << "mintless-proof-generator make_proof   .\n";
  std::cerr << "  Generate a proof for address  from tree , save boc to file .\n\n";
  std::cerr << "mintless-proof-generator parse  \n";
  std::cerr << "  Read a tree from  and output it as text to .\n";
  std::cerr << "  Output format: same as input for 'generate'.\n\n";
  std::cerr << "mintless-proof-generator make_all_proofs   [--threads ]\n";
  std::cerr << "  Read a tree from  and output proofs for all accounts to .\n";
  std::cerr << "  Output format: ,\n";
  std::cerr << "  Default : 1\n";
  exit(2);
}
void log_mem_stat() {
  auto r_stat = td::mem_stat();
  if (r_stat.is_error()) {
    LOG(WARNING) << "Memory: " << r_stat.move_as_error();
    return;
  }
  auto stat = r_stat.move_as_ok();
  LOG(WARNING) << "Memory: "
               << "res=" << stat.resident_size_ << " (peak=" << stat.resident_size_peak_
               << ") virt=" << stat.virtual_size_ << " (peak=" << stat.virtual_size_peak_ << ")";
}
td::BitArray address_to_key(const block::StdAddress &address) {
  // addr_std$10 anycast:(Maybe Anycast) workchain_id:int8 address:bits256 = MsgAddressInt;
  vm::CellBuilder cb;
  cb.store_long(0b100, 3);
  cb.store_long(address.workchain, 8);
  cb.store_bits(address.addr.as_bitslice());
  return cb.data_bits();
}
block::StdAddress key_to_address(const td::BitArray &key) {
  block::StdAddress addr;
  td::ConstBitPtr ptr = key.bits();
  LOG_CHECK(ptr.get_uint(3) == 0b100) << "Invalid address";
  ptr.advance(3);
  addr.workchain = (ton::WorkchainId)ptr.get_int(8);
  ptr.advance(8);
  addr.addr = ptr;
  return addr;
}
struct Entry {
  block::StdAddress address;
  td::RefInt256 amount;
  td::uint64 start_from = 0, expired_at = 0;
  td::BitArray get_key() const {
    return address_to_key(address);
  }
  td::Ref get_value() const {
    // _ amount:Coins start_from:uint48 expired_at:uint48 = AirdropItem;
    vm::CellBuilder cb;
    bool ok = block::tlb::t_Grams.store_integer_value(cb, *amount) && cb.store_ulong_rchk_bool(start_from, 48) &&
              cb.store_ulong_rchk_bool(expired_at, 48);
    LOG_CHECK(ok) << "Failed to serialize AirdropItem";
    return cb.as_cellslice_ref();
  }
  static Entry parse(const td::BitArray &key, vm::CellSlice value) {
    Entry e;
    e.address = key_to_address(key);
    bool ok = block::tlb::t_Grams.as_integer_skip_to(value, e.amount) && value.fetch_uint_to(48, e.start_from) &&
              value.fetch_uint_to(48, e.expired_at) && value.empty_ext();
    LOG_CHECK(ok) << "Failed to parse AirdropItem";
    return e;
  }
};
bool read_entry(std::istream &f, Entry &entry) {
  std::string line;
  while (std::getline(f, line)) {
    std::vector v = td::full_split(line, ' ');
    if (v.empty()) {
      continue;
    }
    auto S = [&]() -> td::Status {
      if (v.size() != 4) {
        return td::Status::Error("Invalid line in input");
      }
      TRY_RESULT_PREFIX_ASSIGN(entry.address, block::StdAddress::parse(v[0]), "Invalid address in input: ");
      entry.amount = td::string_to_int256(v[1]);
      if (entry.amount.is_null() || !entry.amount->is_valid() || entry.amount->sgn() < 0) {
        return td::Status::Error(PSTRING() << "Invalid amount in input: " << v[1]);
      }
      TRY_RESULT_PREFIX_ASSIGN(entry.start_from, td::to_integer_safe(v[2]),
                               "Invalid start_from in input: ");
      TRY_RESULT_PREFIX_ASSIGN(entry.expired_at, td::to_integer_safe(v[3]),
                               "Invalid expired_at in input: ");
      return td::Status::OK();
    }();
    S.ensure();
    return true;
  }
  return false;
}
td::Status run_generate(std::string in_filename, std::string out_filename) {
  LOG(INFO) << "Generating tree from " << in_filename;
  std::ifstream in_file{in_filename};
  LOG_CHECK(in_file.is_open()) << "Cannot open file " << in_filename;
  Entry entry;
  vm::Dictionary dict{KEY_LEN};
  td::uint64 count = 0;
  td::Timestamp log_at = td::Timestamp::in(5.0);
  while (read_entry(in_file, entry)) {
    ++count;
    bool ok = dict.set(entry.get_key(), entry.get_value(), vm::DictionaryBase::SetMode::Add);
    LOG_CHECK(ok) << "Failed to add entry " << entry.address.rserialize() << " (line #" << count << ")";
    if (log_at.is_in_past()) {
      LOG(INFO) << "Added " << count << " entries";
      log_at = td::Timestamp::in(5.0);
    }
  }
  LOG_CHECK(in_file.eof()) << "Failed to read file " << in_filename;
  in_file.close();
  LOG_CHECK(count != 0) << "Input is empty";
  td::Ref root = dict.get_root_cell();
  LOG(INFO) << "Total: " << count << " entries, root hash: " << root->get_hash().to_hex();
  vm::BagOfCells boc;
  boc.add_root(root);
  TRY_STATUS(boc.import_cells());
  LOG(INFO) << "Writing to " << out_filename;
  TRY_RESULT(fd, td::FileFd::open(out_filename, td::FileFd::Write | td::FileFd::Truncate | td::FileFd::Create));
  TRY_STATUS(boc.serialize_to_file(fd, 31));
  TRY_STATUS(fd.sync());
  fd.close();
  log_mem_stat();
  return td::Status::OK();
}
td::Status run_make_proof(std::string in_filename, std::string s_address, std::string out_filename) {
  LOG(INFO) << "Generating proof for " << s_address << ", input file is " << in_filename;
  TRY_RESULT(address, block::StdAddress::parse(s_address));
  TRY_RESULT(blob_view, td::FileBlobView::create(in_filename));
  TRY_RESULT(boc, vm::StaticBagOfCellsDbLazy::create(std::move(blob_view)));
  TRY_RESULT(root, boc->get_root_cell(0));
  vm::MerkleProofBuilder mpb{root};
  vm::Dictionary dict{mpb.root(), KEY_LEN};
  auto key = address_to_key(address);
  td::Ref value = dict.lookup(key);
  LOG_CHECK(value.not_null()) << "No entry for address " << s_address;
  Entry e = Entry::parse(key, *value);
  LOG(INFO) << "Entry: address=" << e.address.workchain << ":" << e.address.addr.to_hex()
            << " amount=" << e.amount->to_dec_string() << " start_from=" << e.start_from
            << " expire_at=" << e.expired_at;
  TRY_RESULT(proof, mpb.extract_proof_boc());
  LOG(INFO) << "Writing proof to " << out_filename << " (" << td::format::as_size(proof.size()) << ")";
  TRY_STATUS(td::write_file(out_filename, proof));
  log_mem_stat();
  return td::Status::OK();
}
td::Status run_parse(std::string in_filename, std::string out_filename) {
  LOG(INFO) << "Parsing " << in_filename;
  std::ofstream out_file{out_filename};
  LOG_CHECK(out_file.is_open()) << "Cannot open file " << out_filename;
  TRY_RESULT(blob_view, td::FileBlobView::create(in_filename));
  TRY_RESULT(boc, vm::StaticBagOfCellsDbLazy::create(std::move(blob_view)));
  TRY_RESULT(root, boc->get_root_cell(0));
  LOG(INFO) << "Root hash = " << root->get_hash().to_hex();
  vm::Dictionary dict{root, KEY_LEN};
  td::Timestamp log_at = td::Timestamp::in(5.0);
  td::uint64 count = 0;
  bool ok = dict.check_for_each([&](td::Ref value, td::ConstBitPtr key, int key_len) {
    CHECK(key_len == KEY_LEN);
    Entry e = Entry::parse(key, *value);
    out_file << e.address.workchain << ":" << e.address.addr.to_hex() << " " << e.amount->to_dec_string() << " "
             << e.start_from << " " << e.expired_at << "\n";
    LOG_CHECK(!out_file.fail()) << "Failed to write to " << out_filename;
    ++count;
    if (log_at.is_in_past()) {
      LOG(INFO) << "Parsed " << count << " entries";
      log_at = td::Timestamp::in(5.0);
    }
    return true;
  });
  LOG_CHECK(ok) << "Failed to parse dictionary";
  out_file.close();
  LOG_CHECK(!out_file.fail()) << "Failed to write to " << out_filename;
  LOG(INFO) << "Written " << count << " entries to " << out_filename;
  log_mem_stat();
  return td::Status::OK();
}
class MakeAllProofsActor : public td::actor::core::Actor {
 public:
  MakeAllProofsActor(std::string in_filename, std::string out_filename, int max_workers)
      : in_filename_(in_filename), out_filename_(out_filename), max_workers_(max_workers) {
  }
  void start_up() override {
    auto S = [&]() -> td::Status {
      out_file_.open(out_filename_);
      LOG_CHECK(out_file_.is_open()) << "Cannot open file " << out_filename_;
      LOG(INFO) << "Reading " << in_filename_;
      TRY_RESULT(blob_view, td::FileBlobView::create(in_filename_));
      TRY_RESULT(boc, vm::StaticBagOfCellsDbLazy::create(std::move(blob_view)));
      TRY_RESULT(root, boc->get_root_cell(0));
      LOG(INFO) << "Root hash = " << root->get_hash().to_hex();
      dict_ = vm::Dictionary{root, KEY_LEN};
      return td::Status::OK();
    }();
    S.ensure();
    run();
    alarm_timestamp() = td::Timestamp::in(5.0);
  }
  void alarm() override {
    alarm_timestamp() = td::Timestamp::in(5.0);
    LOG(INFO) << "Processed " << written_count_ << " entries";
  }
  void run() {
    for (auto it = pending_results_.begin(); it != pending_results_.end() && !it->second.empty();) {
      out_file_ << it->second << "\n";
      LOG_CHECK(!out_file_.fail()) << "Failed to write to " << out_filename_;
      it = pending_results_.erase(it);
      ++written_count_;
    }
    while (active_workers_ < max_workers_ && !eof_) {
      td::Ref value = dict_.lookup_nearest_key(current_key_, true, current_idx_ == 0);
      if (value.is_null()) {
        eof_ = true;
        break;
      }
      run_worker(current_key_, current_idx_);
      ++current_idx_;
      ++active_workers_;
    }
    if (eof_ && active_workers_ == 0) {
      out_file_.close();
      LOG_CHECK(!out_file_.fail()) << "Failed to write to " << out_filename_;
      LOG(INFO) << "Written " << written_count_ << " entries to " << out_filename_;
      stop();
      td::actor::SchedulerContext::get()->stop();
    }
  }
  void run_worker(td::BitArray key, td::uint64 idx) {
    pending_results_[idx] = "";
    ton::delay_action(
        [SelfId = actor_id(this), key, idx, root = dict_.get_root_cell()]() {
          vm::MerkleProofBuilder mpb{root};
          CHECK(vm::Dictionary(mpb.root(), KEY_LEN).lookup(key).not_null());
          auto r_proof = mpb.extract_proof_boc();
          r_proof.ensure();
          block::StdAddress addr = key_to_address(key);
          std::string result = PSTRING() << addr.workchain << ":" << addr.addr.to_hex() << ","
                                         << td::base64_encode(r_proof.move_as_ok());
          td::actor::send_closure(SelfId, &MakeAllProofsActor::on_result, idx, std::move(result));
        },
        td::Timestamp::now());
  }
  void on_result(td::uint64 idx, std::string result) {
    pending_results_[idx] = std::move(result);
    --active_workers_;
    run();
  }
 private:
  std::string in_filename_, out_filename_;
  int max_workers_;
  std::ofstream out_file_;
  vm::Dictionary dict_{KEY_LEN};
  td::BitArray current_key_ = td::BitArray::zero();
  td::uint64 current_idx_ = 0;
  bool eof_ = false;
  int active_workers_ = 0;
  std::map pending_results_;
  td::uint64 written_count_ = 0;
};
td::Status run_make_all_proofs(std::string in_filename, std::string out_filename, int threads) {
  td::actor::Scheduler scheduler({(size_t)threads});
  scheduler.run_in_context(
      [&] { td::actor::create_actor("proofs", in_filename, out_filename, threads).release(); });
  while (scheduler.run(1)) {
  }
  log_mem_stat();
  return td::Status::OK();
}
int main(int argc, char *argv[]) {
  SET_VERBOSITY_LEVEL(verbosity_INFO);
  td::set_log_fatal_error_callback([](td::CSlice) { exit(2); });
  if (argc <= 1) {
    print_help();
    return 2;
  }
  std::string command = argv[1];
  try {
    if (command == "generate") {
      if (argc != 4) {
        print_help();
      }
      run_generate(argv[2], argv[3]).ensure();
      return 0;
    }
    if (command == "make_proof") {
      if (argc != 5) {
        print_help();
      }
      run_make_proof(argv[2], argv[3], argv[4]).ensure();
      return 0;
    }
    if (command == "parse") {
      if (argc != 4) {
        print_help();
      }
      run_parse(argv[2], argv[3]).ensure();
      return 0;
    }
    if (command == "make_all_proofs") {
      std::vector args;
      int threads = 1;
      for (int i = 2; i < argc; ++i) {
        if (!strcmp(argv[i], "--threads")) {
          ++i;
          auto r = td::to_integer_safe(td::as_slice(argv[i]));
          LOG_CHECK(r.is_ok() && r.ok() >= 1 && r.ok() <= 127) << " should be in [1..127]";
          threads = r.move_as_ok();
        } else {
          args.push_back(argv[i]);
        }
      }
      if (args.size() != 2) {
        print_help();
      }
      run_make_all_proofs(args[0], args[1], threads).ensure();
      return 0;
    }
  } catch (vm::VmError &e) {
    LOG(FATAL) << "VM error: " << e.get_msg();
  } catch (vm::VmVirtError &e) {
    LOG(FATAL) << "VM error: " << e.get_msg();
  }
  LOG(FATAL) << "Unknown command '" << command << "'";
}