//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020
//
// 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)
//
#pragma once

#include "telegram-bot-api/ClientParameters.h"

#include "td/actor/actor.h"
#include "td/actor/PromiseFuture.h"

#include "td/net/HttpFile.h"

#include "td/utils/buffer.h"
#include "td/utils/common.h"
#include "td/utils/JsonBuilder.h"
#include "td/utils/List.h"
#include "td/utils/port/IPAddress.h"
#include "td/utils/Slice.h"
#include "td/utils/StringBuilder.h"

#include <algorithm>
#include <memory>
#include <unordered_map>
#include <utility>

namespace telegram_bot_api {

class BotStatActor;

class Query : public td::ListNode {
 public:
  enum class State : td::int8 { Query, OK, Error };

  td::Slice token() const {
    return token_;
  }
  bool is_user() const {
    return is_user_;
  }
  bool is_test_dc() const {
    return is_test_dc_;
  }
  td::Slice method() const {
    return method_;
  }
  bool has_arg(td::Slice key) const {
    auto it = std::find_if(args_.begin(), args_.end(),
                           [&key](const std::pair<td::MutableSlice, td::MutableSlice> &s) { return s.first == key; });
    return it != args_.end();
  }
  td::MutableSlice arg(td::Slice key) const {
    auto it = std::find_if(args_.begin(), args_.end(),
                           [&key](const std::pair<td::MutableSlice, td::MutableSlice> &s) { return s.first == key; });
    return it == args_.end() ? td::MutableSlice() : it->second;
  }
  const td::vector<std::pair<td::MutableSlice, td::MutableSlice>> &args() const {
    return args_;
  }
  td::Slice get_header(td::Slice key) const {
    auto it = std::find_if(headers_.begin(), headers_.end(),
                           [&key](const std::pair<td::MutableSlice, td::MutableSlice> &s) { return s.first == key; });
    return it == headers_.end() ? td::Slice() : it->second;
  }
  const td::HttpFile *file(td::Slice key) const {
    auto it = std::find_if(files_.begin(), files_.end(), [&key](const td::HttpFile &f) { return f.field_name == key; });
    return it == files_.end() ? nullptr : &*it;
  }
  const td::vector<td::HttpFile> &files() const {
    return files_;
  }

  const td::IPAddress &peer_address() const {
    return peer_address_;
  }

  // for stats
  td::int32 file_count() const {
    return static_cast<td::int32>(files_.size());
  }

  td::int64 query_size() const;

  td::int64 files_size() const;

  td::int64 files_max_size() const;

  td::BufferSlice &answer() {
    return answer_;
  }

  int http_status_code() const {
    return http_status_code_;
  }

  int retry_after() const {
    return retry_after_;
  }

  void set_ok(td::BufferSlice result);

  void set_error(int http_status_code, td::BufferSlice result);

  void set_retry_after_error(int retry_after);

  bool is_ready() const {
    return state_ != State::Query;
  }

  bool is_error() const {
    return state_ == State::Error;
  }

  bool is_ok() const {
    return state_ == State::OK;
  }

  bool is_internal() const {
    return is_internal_;
  }

  void set_internal(bool is_internal) {
    is_internal_ = is_internal;
  }

  Query(td::vector<td::BufferSlice> &&container, td::Slice token, bool is_user, bool is_test_dc, td::MutableSlice method,
        td::vector<std::pair<td::MutableSlice, td::MutableSlice>> &&args,
        td::vector<std::pair<td::MutableSlice, td::MutableSlice>> &&headers, td::vector<td::HttpFile> &&files,
        std::shared_ptr<SharedData> shared_data, const td::IPAddress &peer_address);
  Query(const Query &) = delete;
  Query &operator=(const Query &) = delete;
  Query(Query &&) = delete;
  Query &operator=(Query &&) = delete;
  ~Query() {
    if (shared_data_) {
      shared_data_->query_count_--;
    }
  }

  double start_timestamp() const {
    return start_timestamp_;
  }

  void set_stat_actor(td::ActorId<BotStatActor> stat_actor) {
    stat_actor_ = stat_actor;
  }
  void send_response_stat();

 private:
  State state_;
  std::shared_ptr<SharedData> shared_data_;
  double start_timestamp_;
  td::IPAddress peer_address_;
  td::ActorId<BotStatActor> stat_actor_;

  // request
  td::vector<td::BufferSlice> container_;
  td::Slice token_;
  bool is_user_;
  bool is_test_dc_;
  td::MutableSlice method_;
  td::vector<std::pair<td::MutableSlice, td::MutableSlice>> args_;
  td::vector<std::pair<td::MutableSlice, td::MutableSlice>> headers_;
  td::vector<td::HttpFile> files_;
  bool is_internal_ = false;

  // response
  td::BufferSlice answer_;
  int http_status_code_ = 0;
  int retry_after_ = 0;
};

td::StringBuilder &operator<<(td::StringBuilder &sb, const Query &query);

// fix for outdated C++14 libraries
// https://stackoverflow.com/questions/26947704/implicit-conversion-failure-from-initializer-list
extern std::unordered_map<td::string, std::unique_ptr<td::VirtuallyJsonable>> empty_parameters;

class JsonParameters : public td::Jsonable {
 public:
  explicit JsonParameters(const std::unordered_map<td::string, std::unique_ptr<td::VirtuallyJsonable>> &parameters)
      : parameters_(parameters) {
  }
  void store(td::JsonValueScope *scope) const {
    auto object = scope->enter_object();
    for (auto &parameter : parameters_) {
      CHECK(parameter.second != nullptr);
      object(parameter.first, *parameter.second);
    }
  }

 private:
  const std::unordered_map<td::string, std::unique_ptr<td::VirtuallyJsonable>> &parameters_;
};

template <class T>
class JsonQueryOk : public td::Jsonable {
 public:
  JsonQueryOk(const T &result, td::Slice description) : result_(result), description_(description) {
  }
  void store(td::JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("ok", td::JsonTrue());
    object("result", result_);
    if (!description_.empty()) {
      object("description", description_);
    }
  }

 private:
  const T &result_;
  td::Slice description_;
};

class JsonQueryError : public td::Jsonable {
 public:
  JsonQueryError(
      int error_code, td::Slice description,
      const std::unordered_map<td::string, std::unique_ptr<td::VirtuallyJsonable>> &parameters = empty_parameters)
      : error_code_(error_code), description_(description), parameters_(parameters) {
  }
  void store(td::JsonValueScope *scope) const {
    auto object = scope->enter_object();
    object("ok", td::JsonFalse());
    object("error_code", error_code_);
    object("description", description_);
    if (!parameters_.empty()) {
      object("parameters", JsonParameters(parameters_));
    }
  }

 private:
  int error_code_;
  td::Slice description_;
  const std::unordered_map<td::string, std::unique_ptr<td::VirtuallyJsonable>> &parameters_;
};

class PromiseDeleter {
 public:
  explicit PromiseDeleter(td::PromiseActor<td::unique_ptr<Query>> &&promise) : promise_(std::move(promise)) {
  }
  PromiseDeleter() = default;
  PromiseDeleter(const PromiseDeleter &) = delete;
  PromiseDeleter &operator=(const PromiseDeleter &) = delete;
  PromiseDeleter(PromiseDeleter &&) = default;
  PromiseDeleter &operator=(PromiseDeleter &&) = default;
  void operator()(Query *raw_ptr) {
    td::unique_ptr<Query> query(raw_ptr);  // now I cannot forget to delete this pointer
    if (!promise_.empty_promise()) {
      if (!query->is_ready()) {
        query->set_retry_after_error(5);
      }

      promise_.set_value(std::move(query));
    }
  }
  ~PromiseDeleter() {
    CHECK(promise_.empty());
  }

 private:
  td::PromiseActor<td::unique_ptr<Query>> promise_;
};
using PromisedQueryPtr = std::unique_ptr<Query, PromiseDeleter>;

template <class Jsonable>
void answer_query(const Jsonable &result, PromisedQueryPtr query, td::Slice description = td::Slice()) {
  query->set_ok(td::json_encode<td::BufferSlice>(JsonQueryOk<Jsonable>(result, description)));
  query.reset();  // send query into promise explicitly
}

inline void fail_query(
    int http_status_code, td::Slice description, PromisedQueryPtr query,
    const std::unordered_map<td::string, std::unique_ptr<td::VirtuallyJsonable>> &parameters = empty_parameters) {
  query->set_error(http_status_code,
                   td::json_encode<td::BufferSlice>(JsonQueryError(http_status_code, description, parameters)));
  query.reset();  // send query into promise explicitly
}

}  // namespace telegram_bot_api
