#include "server/request_handler.hpp"
#include <ctime>

using namespace binom;

namespace {

inline TcpServer::Client& operator<<(TcpServer::Client& client, binom::ByteArray data) {
  client.sendData(data.begin(), data.length());
  return client;
}

inline TcpServer::Client& operator<<(TcpServer::Client& client, binom::Variable data) {
  return client << data.serialize();
}

inline binom::Variable enc_data(binom::Variable data, binom::Variable aes_key) {
  return binom::vobj{
    {"act", binom::ui8(ServerAction::encrypted)},
    {"content", binom::BufferArray(Security::encrypt(data.serialize(), aes_key.toBufferArray().toByteArray()))}
  };
}

}

void RequestHandler::invalidActionCode() {
  std::clog << getHostStr() << " (invalid action code)\n";
  client << vobj{
    {"act", ui8(ServerAction::err)},
    {"err", ui8(ServerError::invalid_action_code)}
  };
}

//======================================== Layered router of actions

std::string RequestHandler::getHostStr() {
  uint32_t ip = client.getHost ();
  return std::to_string(int(reinterpret_cast<char*>(&ip)[0])) + '.' +
         std::to_string(int(reinterpret_cast<char*>(&ip)[1])) + '.' +
         std::to_string(int(reinterpret_cast<char*>(&ip)[2])) + '.' +
         std::to_string(int(reinterpret_cast<char*>(&ip)[3])) + ':' +
         std::to_string(client.getPort());
}

void RequestHandler::routeOpenLayer() {
  if(!data.contains("act")) return invalidActionCode();
  switch (ClientAction(data["act"].getValue().asUi8())) {
    default: return invalidActionCode();
    case ClientAction::key_exchange: return exchangeKey();
    case ClientAction::encrypted: return gotoSecuredLayer();
  }
}

void RequestHandler::routeSecuredLayer() {
  if(!data.contains("act")) return invalidActionCode();
  switch (ClientAction(data["act"].getValue().asUi8())) {
    default: return invalidActionCode();
    case ClientAction::server_info: return sendServerInfo();
    case ClientAction::registration: return registrate();
    case ClientAction::authentication: return authenticate();
    case ClientAction::uuid_authentication: return authenticateByUUID();
    case ClientAction::uuid_check: return gotoAuthenticatedLayer();
  }
}

void RequestHandler::routeAuthenticatedLayer() {
  if(!data.contains("act")) return invalidActionCode();
  switch (ClientAction(data["act"].getValue().asUi8())) {
    default: return invalidActionCode();
    case ClientAction::request: return processRequest();
  }
}

void RequestHandler::processRequest() {
  try{
    switch (RequestCode(data["request_type"].getValue().asUi8())) {
      case RequestCode::change_profile: return changeProfile();
      case RequestCode::delete_account: return deleteAccount();
      case RequestCode::create_dialog: return createDialog();
      case RequestCode::send_message: return sendMessage();
      case RequestCode::change_message: return changeMessage();
      case RequestCode::delete_message: return deleteMessage();
      case RequestCode::publish_post: return publishPost();
      case RequestCode::change_post: return changePost();
      case RequestCode::delete_post: return deletePost();
      case RequestCode::get_user_profiles: return requestUserProfiles();
      case RequestCode::get_user_public_key: return requestUserPublicKey();
      case RequestCode::get_messages: return requestMessages();
      case RequestCode::get_posts: return requestPosts();
    }
  } catch(...) {
    std::clog << getHostStr() << " (request handling error)\n";
    client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::request_handling_error)}
    };
  }
}

//======================================== Layer gates

void RequestHandler::gotoSecuredLayer() {
  try{
    if(!data.contains("content")) {
      client << vobj{
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::cryptographic_error)}
    };
    }

    data =
        Variable::deserialize(
          Security::decrypt(
            data["content"].toBufferArray().toByteArray(),
          Security::AES_t(session_data.getVariable("aes_key").toBufferArray().toByteArray())
          )
        );

    std::clog << getHostStr() << " (successful data decryption) -> Secure layer of data processing\n";
    return routeSecuredLayer();
  } catch(...) {
    client << vobj{
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::cryptographic_error)}
    };
    return;
  }
}

void RequestHandler::gotoAuthenticatedLayer() {
  try {
    if(!session_data.contains("login") ||
       !data.contains("uuid") ||
       !data.contains("content")) {
      std::clog << getHostStr() << " (invalid uuid authentication data)\n";
      client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
      return;
    }

    FileNodeVisitor users = db["user"];

    if(BufferArray login = session_data.getVariable("login").toBufferArray(); !users.contains(login)) {
      std::clog << getHostStr() << " (uuid authentication failed)\n";
      client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
      return;
    } else users(login);

    if(!users["session"].contains(data["uuid"].toBufferArray())) {
      std::clog << getHostStr() << " (uuid authentication failed)\n";
      client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
      return;
    }

    data = std::move(data.getVariable("content"));
    std::clog << getHostStr() << " (successful uuid authentication) -> Authenticated layer of data processing\n";
    return routeAuthenticatedLayer();

  } catch(...) {
    std::clog << getHostStr() << " (authentication failed)\n";
    client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
  }
}

//======================================== Open layer actions

void RequestHandler::exchangeKey() {
  Security::AES_t aes_key  = Security::getSecret(data["key"].toBufferArray().toByteArray(), server_key);
  session_data.insert("aes_key", BufferArray(ByteArray(&aes_key, sizeof (aes_key))));
  std::clog << getHostStr() << " (successful key exchange)\n";
  if(session_data.getVariable("state").getValue().asUi8() == ui8(ClientState::connected))
    session_data["state"].setVariable(ui8(ClientState::secure_connected));

  client << vobj{
    {"act", ui8(ServerAction::key_exchange)},
    {"key", BufferArray(Security::extractPublicKey(server_key))},
    {"server_info", BufferArray(Security::encrypt(db["server_data"].getVariable().serialize(), aes_key))}
  };
}

//======================================== Secured layer actions

void RequestHandler::sendServerInfo() {
  FileNodeVisitor node = db.getRoot()("server_data");
  std::clog << getHostStr() << " (request server info)\n";
  client << enc_data(
                      vobj{
                        {"act", ui8(ServerAction::server_info)},
                        {"name", node.getVariable("name")},
                        {"description", node.getVariable("description")}
                      }, session_data.getVariable("aes_key"));
}

void RequestHandler::authenticate() {
  try{

    if(!data.contains("login") ||
       !data.contains("password")) {
      std::clog << getHostStr() << " (invalid authentication data)\n";
      client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
      return;
    }

    FileNodeVisitor users = db.getRoot()("user");

    if(BufferArray login = data["login"].toBufferArray(); !users.contains(login)) {
      std::clog << getHostStr() << " (authentication failed)\n";
      client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
      return;
    } else users(login);

    if(users.getVariable("password").toBufferArray() != data["password"].toBufferArray()) {
      std::clog << getHostStr() << " (authentication failed)\n";
      client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
      return;
    }

    session_data.setVariable("login", data["login"]);
    session_data.setVariable("state", ui8(ClientState::authenticated));
    session_data.setVariable("account_ptr", users.getNodeIndex());

    BufferArray uuid(uuid::generateUUIDv4());
    while (true) {
      if(users["session"].contains(uuid)) {
        uuid = BufferArray(uuid::generateUUIDv4());
        continue;
      }
      users["session"].insert(uuid, vobj{{"last_sign_in", std::time(nullptr)}});
      break;
    }

    std::clog << getHostStr() << " (authentication successful)\n";
    client << enc_data(
                        vobj{
                          {"act", ui8(ServerAction::authentication_success)},
                          {"key", users.getVariable("key")},
                          {"uuid", uuid},
                          {"profile", users.getVariable("profile")}
                        }, session_data.getVariable("aes_key"));

  } catch(...) {
    std::clog << getHostStr() << " (authentication failed)\n";
    client << vobj {
    {"act", ui8(ServerAction::err)},
    {"err", ui8(ServerError::authentication_failed)}
  };
  }
}



void RequestHandler::registrate() {
  try{
    FileNodeVisitor users = db.getRoot()("user");

    // Check data struct
    if(!data.contains("login") ||
       !data.contains("password") ||
       !data.contains("key")) {
      std::clog << getHostStr() << " (invalid registration data)\n";
      client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
      return;
    }

    // Check user login
    if(users.contains(data["login"].toBufferArray())) {
      std::clog << getHostStr() << " (registration failed)\n";
      client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
      return;
    }

    BufferArray uuid(uuid::generateUUIDv4());
    BufferArray login = data["login"].toBufferArray();
    users.insert(
          login,
          vobj{
            {"reg_date", std::time(nullptr)},
            {"password", data["password"]},
            {"key", data["key"]},
            {"session", vobj{
                {uuid, vobj{{"last_sign_in", std::time(nullptr)}}}
              }},
            {"dialog", vobj{}},
            {"profile", vobj{
                {"nickname", data["login"].toBufferArray()},
                {"name", ""},
                {"second_name", ""},
                {"about", ""},
                {"avatar", ""},
                {"background", ""},
              }},
            {"post", vobj{}}
          }
    );
    users(login);

    session_data.setVariable("login", data["login"]);
    session_data.setVariable("state", ui8(ClientState::authenticated));
    session_data.setVariable("account_ptr", users.getNodeIndex());

    client << enc_data(
                        vobj{
                          {"act", ui8(ServerAction::authentication_success)},
                          {"uuid", uuid},
                          {"profile", users.getVariable("profile")}
                        },
                        session_data.getVariable("aes_key")
                        );

    std::clog << getHostStr() << " (registration successful)\n";

  } catch(...) {
    std::clog << getHostStr() << " (registration failed)\n";
    client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
  }
}

void RequestHandler::authenticateByUUID() {
  try{
    if(!data.contains("login") ||
       !data.contains("uuid")) {
      std::clog << getHostStr() << " (invalid uuid authentication data)\n";
      client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
      return;
    }

    FileNodeVisitor users = db.getRoot()("user");

    if(BufferArray login = data["login"].toBufferArray(); !users.contains(login)) {
      std::clog << getHostStr() << " (uuid authentication failed)\n";
      client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
      return;
    } else users(login);

    if(!users["session"].contains(data["uuid"].toBufferArray())) {
      std::clog << getHostStr() << " (uuid authentication failed)\n";
      client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
      return;
    }

    users[{"session", data["uuid"].toBufferArray(), "last_sign_in"}].setVariable(std::time(nullptr));

    session_data.setVariable("login", data["login"]);
    session_data.setVariable("state", ui8(ClientState::authenticated));
    session_data.setVariable("account_ptr", users.getNodeIndex());

    std::clog << getHostStr() << " (uuid authentication successful)\n";
    client << enc_data(vobj{
                          {"act", ui8(ServerAction::authentication_success)},
                          {"key", users.getVariable("key")},
                          {"profile", users.getVariable("profile")}
                        },
                        session_data.getVariable("aes_key"));

  } catch(...) {
    std::clog << getHostStr() << " (authentication failed)\n";
    client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::authentication_failed)}
    };
  }
}

//======================================== Authenticated layer actions

void RequestHandler::changeProfile() {
  /* {
   *   request_type: DataPorcessingCode::create_dialog
   *   interlocutor: <user-login>
   * }
  */

  FileNodeVisitor account_profile = db[session_data.getVariable("account_ptr").getValue().asUi64()];
  for(auto profile_entry : account_profile)
    if(data.contains(*profile_entry.getName()))
      profile_entry.setVariable(data[*profile_entry.getName()]);
}



void RequestHandler::deleteAccount() {

}



void RequestHandler::createDialog() {

  /* {
   *   request_type: DataPorcessingCode::create_dialog
   *   interlocutor: <user-login>
   * }
  */

  FileNodeVisitor account_dialogs = db[session_data.getVariable("account_ptr").getValue().asUi64()]["dialog"];
  if(account_dialogs.contains(data["interlocutor"].toBufferArray())) {
    return;
  }
  FileNodeVisitor interlocutor_user = db[{"user", data["interlocutor"].toBufferArray()}];
}

void RequestHandler::sendMessage() {

}

void RequestHandler::changeMessage() {

}

void RequestHandler::deleteMessage() {

  /* {
   *  act: ClientAction::process_data
   *  request_type: DataPorcessingCode::delete_message
   *  post_place: PostPlace::<user/group/server>
   * }
  */

}

void RequestHandler::changePost() {

}



void RequestHandler::publishPost() {

  /* {
   *  act: ClientAction::process_data
   *  request_type: DataPorcessingCode::publish_post
   *  content: [
   *    {type: <mime-type> data: <data>}
   *  ]
   *  answer_on: [
   *    {post_place: PostPlace::<user/group/server>, post_key: <post key>}
   *  ]
   * }
  */

  FileNodeVisitor account_posts = db[session_data.getVariable("account_ptr").getValue().asUi64()]["post"];
  FileNodeVisitor content = db["content"];

  Array post_element_list;
  for(auto post_element : data["content"].toArray())
    post_element_list.pushBack(
          BufferArray(
            insertContent(
              post_element["data"].toBufferArray().toByteArray(),
              post_element["type"].toBufferArray().toByteArray(),
              session_data.getVariable("login").toBufferArray().toByteArray()
            )
          )
        );

  BufferArray post_key = ui64arr{0xFFFFFFFFFFFFFFFF - std::time(nullptr)};
  Variable post_info = vobj{
    {"author", session_data.getVariable("login")},
    {"content", post_element_list}
  };

  account_posts.insert(post_key, post_info);
  if(data["is_public"].getValue().asBool()) db["post"].insert(post_key, post_info);
  std::clog << getHostStr() << " (post published)\n";

}

void RequestHandler::deletePost() {

}

void RequestHandler::requestUserProfiles() {

  /* {
   *  act: RequestCode::get_user_profiles
   *  from: <last key of the last data transfer> (optional)
   * }
  */

  FileNodeVisitor node = db["user"];
  Array user_profiles;

  { // Transaction begin
    auto rwg = node.getScopedRWGuard(RWSyncMap::LockType::shared_lock);
    ui64 count = 0;
    for(auto it = data.contains("from") ? node.beginFrom(data["from"].toBufferArray()) : node.begin(), end = node.end();
        it != end && count <= 20;
        (++it, ++count)) {
      FileNodeVisitor user = (*it);
      user_profiles.pushBack(vobj{{"login", *user.getName()},{"profile",user.getVariable("profile")}});
    }
  } // Transaction end

  client << enc_data(
                      vobj {
                        {"act", ui8(ServerAction::request_result)},
                        {"request_type", ui8(RequestCode::get_user_profiles)},
                        {"data", vobj{
                           {"list", user_profiles},
                           {"last_key", user_profiles[user_profiles.getMemberCount() - 1]["login"]}
                         }}
                      }, session_data["aes_key"] );

}

void RequestHandler::requestUserPublicKey() {

  /* {
   *  act: RequestCode::get_user_public_key
   *  login: <user login>
   * }
  */

  FileNodeVisitor user_object = db[{"user", data["login"].toBufferArray()}];
  if(user_object.isNull()) {
    client << vobj {
      {"act", ui8(ServerAction::err)},
      {"err", ui8(ServerError::request_handling_error)}
    };
    return;
  }
  client << enc_data(
                      vobj{
                        {"act", ui8(ServerAction::request_result)},
                        {"request_type", ui8(RequestCode::get_user_public_key)},
                        {"login", data["login"].toBufferArray()},
                        {"pub_key", user_object[{"key", "pub"}]}
                      }
                      , session_data.getVariable("aes_key") );

}

void RequestHandler::requestMessages() {

  /* {
   *  act: RequestCode::get_messages
   *  login: <user login>
   *  from: <last key of the last data transfer> (optional)
   * }
  */

}

void RequestHandler::requestPosts() {

  /* {
   *  act: RequestCode::get_posts
   *  name: <name of group> (if fer from group)
   *  login: <login of user> (if get from user)
   *  (if isn't exist "login" or "name" fields - get from server)
   *  from: <last key of the last data transfer> [optional]
   * }
  */


}





ByteArray RequestHandler::insertContent(ByteArray content, ByteArray mime_type, ByteArray owner) {
  /* Content-addressable storage entry
   * ...
   * <hash_key>: {
   *  data: <content>
   *  type: <mime-type>
   *  owners: [<owner>...]
   * }
   * ...
  */

  FileNodeVisitor content_storage = db["content"];
  BufferArray hash_key = Security::getHashOf(content, Security::HashMethod::sha3_512);

  { // Transaction begin
    auto transaction_locker = content_storage.getScopedRWGuard(RWSyncMap::LockType::shared_lock);

    if(content_storage.contains(hash_key)) {
      ui64 salt = 0;
      BufferArray content_buffer(content);
      do {
        FileNodeVisitor entry = content_storage[hash_key];

        // If finded entry with content dublication...
        if(entry.getVariable("data") == content_buffer) {
          auto owners = entry["owners"];

          // If current user isn't owner of this entry...
          if(BufferArray owner_bfr = owner; owners.find({{QProp::value, QOper::equal, owner_bfr}}).isNull())
            entry["owners"].pushBack(owner_bfr);

          return hash_key.toByteArray();
        }

        // If hash-key collision...
        hash_key = Security::getHashOf(content, ByteArray(&salt, sizeof(salt)), Security::HashMethod::sha3_512);
        if(content_storage.contains(hash_key)) {
          ++salt;
          continue;
        } else break; // If isn't collision

      } while(true);
    }
    content_storage.insert(hash_key, vobj{
                             {"data", BufferArray(content)},
                             {"type", BufferArray(mime_type)},
                             {"owners", varr{BufferArray(owner)}}
                           });
  } // Transaction end

  return hash_key.toByteArray();
}



Variable RequestHandler::getContent(ByteArray key) {return db[{"content", BufferArray(key)}];}



Array RequestHandler::assembleContentList(Array hash_key_list) {
  FileNodeVisitor content_storage = db["content"];
  for(auto& hash_key : hash_key_list) hash_key = content_storage[hash_key];
  return hash_key_list;
}
