/** @file

  Implementation of Host Proxy routing

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */
#include "iocore/eventsystem/EventProcessor.h"
#include "tsutil/ts_bw_format.h"
#include "proxy/HostStatus.h"
#include "iocore/eventsystem/Tasks.h"

#include "mgmt/rpc/jsonrpc/JsonRPC.h"
#include "shared/rpc/RPCRequests.h"

#include <fstream>
#include <utility>

namespace
{
const std::string STATUS_LIST_KEY{"statusList"};
const std::string ERROR_LIST_KEY{"errorList"};
const std::string HOST_NAME_KEY{"hostname"};
const std::string STATUS_KEY{"status"};

struct HostCmdInfo {
  TSHostStatus             type{TSHostStatus::TS_HOST_STATUS_INIT};
  unsigned int             reasonType{0};
  std::vector<std::string> hosts;
  int                      time{0};
};

DbgCtl dbg_ctl_host_statuses{"host_statuses"};

} // namespace

swoc::Rv<YAML::Node> server_get_status(std::string_view const id, YAML::Node const &params);
swoc::Rv<YAML::Node> server_set_status(std::string_view const id, YAML::Node const &params);

HostStatRec::HostStatRec()
  : status(TS_HOST_STATUS_UP),
    reasons(0),
    active_marked_down(0),
    local_marked_down(0),
    manual_marked_down(0),
    self_detect_marked_down(0),
    active_down_time(0),
    local_down_time(0),
    manual_down_time(0){};

HostStatRec::HostStatRec(std::string str)
{
  std::vector<std::string> v1;
  std::stringstream        ss1(str);

  reasons = 0;

  // parse the csv strings from the stat record value string.
  while (ss1.good()) {
    char b1[64];
    ss1.getline(b1, 64, ',');
    v1.push_back(b1);
  }

  // v1 contains 5 strings.
  ink_assert(v1.size() == 5);

  // set the status and reasons fields.
  for (unsigned int i = 0; i < v1.size(); i++) {
    if (i == 0) { // set the status field
      if (v1.at(i).compare("HOST_STATUS_UP") == 0) {
        status = TS_HOST_STATUS_UP;
      } else if (v1.at(i).compare("HOST_STATUS_DOWN") == 0) {
        status = TS_HOST_STATUS_DOWN;
      }
    } else { // parse and set remaining reason fields.
      std::vector<std::string> v2;
      v2.clear();
      std::stringstream ss2(v1.at(i));
      while (ss2.good()) {
        char b2[64];
        ss2.getline(b2, 64, ':');
        v2.push_back(b2);
      }
      // v2 contains 4 strings.
      ink_assert(v2.size() == 3 || v2.size() == 4);

      if (v2.at(0).compare("ACTIVE") == 0) {
        if (v2.at(1).compare("DOWN") == 0) {
          reasons |= Reason::ACTIVE;
        } else if (reasons & Reason::ACTIVE) {
          reasons ^= Reason::ACTIVE;
        }
        active_marked_down = atoi(v2.at(2).c_str());
        active_down_time   = atoi(v2.at(3).c_str());
      }
      if (v2.at(0).compare("LOCAL") == 0) {
        if (v2.at(1).compare("DOWN") == 0) {
          reasons |= Reason::LOCAL;
        } else if (reasons & Reason::LOCAL) {
          reasons ^= Reason::LOCAL;
        }
        local_marked_down = atoi(v2.at(2).c_str());
        local_down_time   = atoi(v2.at(3).c_str());
      }
      if (v2.at(0).compare("MANUAL") == 0) {
        if (v2.at(1).compare("DOWN") == 0) {
          reasons |= Reason::MANUAL;
        } else if (reasons & Reason::MANUAL) {
          reasons ^= Reason::MANUAL;
        }
        manual_marked_down = atoi(v2.at(2).c_str());
        manual_down_time   = atoi(v2.at(3).c_str());
      }
      if (v2.at(0).compare("SELF_DETECT") == 0) {
        if (v2.at(1).compare("DOWN") == 0) {
          reasons |= Reason::SELF_DETECT;
        } else if (reasons & Reason::SELF_DETECT) {
          reasons ^= Reason::SELF_DETECT;
        }
        self_detect_marked_down = atoi(v2.at(2).c_str());
      }
    }
  }
}

HostStatus::HostStatus()
{
  ink_rwlock_init(&host_status_rwlock);

  // register JSON-RPC methods.
  rpc::add_method_handler("admin_host_set_status", &server_set_status, &rpc::core_ats_rpc_service_provider_handle,
                          {{rpc::RESTRICTED_API}});
  rpc::add_method_handler("admin_host_get_status", &server_get_status, &rpc::core_ats_rpc_service_provider_handle,
                          {{rpc::NON_RESTRICTED_API}});
}

HostStatus::~HostStatus()
{
  for (auto &&it : hosts_statuses) {
    ats_free(it.second);
  }
  // release the read and writer locks.
  ink_rwlock_destroy(&host_status_rwlock);
}

// loads host status persistent store file
void
HostStatus::loadFromPersistentStore()
{
  YAML::Node  records;
  std::string fileStore = getHostStatusPersistentFilePath();
  if (access(fileStore.c_str(), R_OK) == 0) {
    try {
      records               = YAML::LoadFile(fileStore.c_str());
      YAML::Node statusList = records["statuses"];
      for (YAML::const_iterator it = statusList.begin(); it != statusList.end(); ++it) {
        const YAML::Node &host     = *it;
        std::string       hostName = host[HOST_NAME_KEY].as<std::string>();
        std::string       status   = host[STATUS_KEY].as<std::string>();
        HostStatRec       h(std::move(status));
        loadRecord(hostName, h);
      }
    } catch (std::exception const &ex) {
      Warning("Error loading and decoding %s : %s", fileStore.c_str(), ex.what());
    }
  }
}

// loads in host status record.
void
HostStatus::loadRecord(std::string_view name, HostStatRec &h)
{
  HostStatRec *host_stat = nullptr;
  Dbg(dbg_ctl_host_statuses, "loading host status record for %.*s", int(name.size()), name.data());
  ink_rwlock_wrlock(&host_status_rwlock);
  {
    auto it = hosts_statuses.find(std::string(name));
    if (it == hosts_statuses.end()) {
      host_stat  = static_cast<HostStatRec *>(ats_malloc(sizeof(HostStatRec)));
      *host_stat = h;
      hosts_statuses.emplace(name, host_stat);
    }
  }
  ink_rwlock_unlock(&host_status_rwlock);
}

void
HostStatus::setHostStatus(const std::string_view name, TSHostStatus status, const unsigned int down_time, const unsigned int reason)
{
  std::string stat_name;

  // update / insert status.
  // using the hash table pointer to store the TSHostStatus value.
  HostStatRec *host_stat = nullptr;
  ink_rwlock_wrlock(&host_status_rwlock);
  {
    if (auto it = hosts_statuses.find(std::string(name)); it != hosts_statuses.end()) {
      host_stat = it->second;
    } else {
      host_stat = static_cast<HostStatRec *>(ats_malloc(sizeof(HostStatRec)));
      bzero(static_cast<void *>(host_stat), sizeof(HostStatRec));
      hosts_statuses.emplace(name, host_stat);
    }
    if (reason & Reason::ACTIVE) {
      Dbg(dbg_ctl_host_statuses, "for host %.*s set status: %s, Reason:ACTIVE", int(name.size()), name.data(),
          HostStatusNames[status]);
      if (status == TSHostStatus::TS_HOST_STATUS_DOWN) {
        host_stat->active_marked_down  = time(0);
        host_stat->active_down_time    = down_time;
        host_stat->reasons            |= Reason::ACTIVE;
      } else {
        host_stat->active_marked_down = 0;
        host_stat->active_down_time   = 0;
        if (host_stat->reasons & Reason::ACTIVE) {
          host_stat->reasons ^= Reason::ACTIVE;
        }
      }
    }
    if (reason & Reason::LOCAL) {
      Dbg(dbg_ctl_host_statuses, "for host %.*s set status: %s, Reason:LOCAL", int(name.size()), name.data(),
          HostStatusNames[status]);
      if (status == TSHostStatus::TS_HOST_STATUS_DOWN) {
        host_stat->local_marked_down  = time(0);
        host_stat->local_down_time    = down_time;
        host_stat->reasons           |= Reason::LOCAL;
      } else {
        host_stat->local_marked_down = 0;
        host_stat->local_down_time   = 0;
        if (host_stat->reasons & Reason::LOCAL) {
          host_stat->reasons ^= Reason::LOCAL;
        }
      }
    }
    if (reason & Reason::MANUAL) {
      Dbg(dbg_ctl_host_statuses, "for host %.*s set status: %s, Reason:MANUAL", int(name.size()), name.data(),
          HostStatusNames[status]);
      if (status == TSHostStatus::TS_HOST_STATUS_DOWN) {
        host_stat->manual_marked_down  = time(0);
        host_stat->manual_down_time    = down_time;
        host_stat->reasons            |= Reason::MANUAL;
      } else {
        host_stat->manual_marked_down = 0;
        host_stat->manual_down_time   = 0;
        if (host_stat->reasons & Reason::MANUAL) {
          host_stat->reasons ^= Reason::MANUAL;
        }
      }
    }
    if (reason & Reason::SELF_DETECT) {
      Dbg(dbg_ctl_host_statuses, "for host %.*s set status: %s, Reason:SELF_DETECT", int(name.size()), name.data(),
          HostStatusNames[status]);
      if (status == TSHostStatus::TS_HOST_STATUS_DOWN) {
        host_stat->self_detect_marked_down  = time(0);
        host_stat->reasons                 |= Reason::SELF_DETECT;
      } else {
        host_stat->self_detect_marked_down = 0;
        if (host_stat->reasons & Reason::SELF_DETECT) {
          host_stat->reasons ^= Reason::SELF_DETECT;
        }
      }
    }
    if (status == TSHostStatus::TS_HOST_STATUS_UP) {
      if (host_stat->reasons == 0) {
        host_stat->status = TSHostStatus::TS_HOST_STATUS_UP;
      }
      Dbg(dbg_ctl_host_statuses, "reasons: %d, status: %s", host_stat->reasons, HostStatusNames[host_stat->status]);
    } else {
      host_stat->status = status;
      Dbg(dbg_ctl_host_statuses, "reasons: %d, status: %s", host_stat->reasons, HostStatusNames[host_stat->status]);
    }
  }
  ink_rwlock_unlock(&host_status_rwlock);

  // log it.
  if (status == TSHostStatus::TS_HOST_STATUS_DOWN) {
    Note("Host %.*s has been marked down, down_time: %d - %s.", int(name.size()), name.data(), down_time,
         down_time == 0 ? "indefinitely." : "seconds.");
  } else {
    Note("Host %.*s has been marked up.", int(name.size()), name.data());
  }
}

// retrieve all host statuses.
void
HostStatus::getAllHostStatuses(std::vector<HostStatuses> &hosts)
{
  if (hosts_statuses.empty()) {
    return;
  }

  ink_rwlock_rdlock(&host_status_rwlock);
  {
    for (std::pair<std::string, HostStatRec *> hsts : hosts_statuses) {
      std::stringstream ss;
      HostStatuses      h;
      h.hostname = hsts.first;
      ss << *hsts.second;
      h.status = ss.str();
      hosts.push_back(h);
    }
  }
  ink_rwlock_unlock(&host_status_rwlock);
}

// retrieve the named host status.
HostStatRec *
HostStatus::getHostStatus(const std::string_view name)
{
  HostStatRec *_status = nullptr;
  time_t       now     = time(0);
  bool         lookup  = false;

  // if host_statuses is empty, just return
  // a nullptr as there is no need to lock
  // and search.  A return of nullptr indicates
  // to the caller that the host is available,
  // HOST_STATUS_UP.
  if (hosts_statuses.empty()) {
    return _status;
  }

  // the hash table value pointer has the TSHostStatus value.
  ink_rwlock_rdlock(&host_status_rwlock);
  {
    auto it = hosts_statuses.find(std::string(name));
    lookup  = it != hosts_statuses.end();
    if (lookup) {
      _status = it->second;
    }
  }
  ink_rwlock_unlock(&host_status_rwlock);

  // if the host was marked down and it's down_time has elapsed, mark it up.
  if (lookup && _status->status == TSHostStatus::TS_HOST_STATUS_DOWN) {
    unsigned int reasons = _status->reasons;
    if ((_status->reasons & Reason::ACTIVE) && _status->active_down_time > 0) {
      if ((_status->active_down_time + _status->active_marked_down) < now) {
        Dbg(dbg_ctl_host_statuses, "name: %.*s, now: %ld, down_time: %d, marked_down: %ld, reason: %s", int(name.size()),
            name.data(), now, _status->active_down_time, _status->active_marked_down, Reason::ACTIVE_REASON);
        setHostStatus(name, TSHostStatus::TS_HOST_STATUS_UP, 0, Reason::ACTIVE);
        reasons ^= Reason::ACTIVE;
      }
    }
    if ((_status->reasons & Reason::LOCAL) && _status->local_down_time > 0) {
      if ((_status->local_down_time + _status->local_marked_down) < now) {
        Dbg(dbg_ctl_host_statuses, "name: %.*s, now: %ld, down_time: %d, marked_down: %ld, reason: %s", int(name.size()),
            name.data(), now, _status->local_down_time, _status->local_marked_down, Reason::LOCAL_REASON);
        setHostStatus(name, TSHostStatus::TS_HOST_STATUS_UP, 0, Reason::LOCAL);
        reasons ^= Reason::LOCAL;
      }
    }
    if ((_status->reasons & Reason::MANUAL) && _status->manual_down_time > 0) {
      if ((_status->manual_down_time + _status->manual_marked_down) < now) {
        Dbg(dbg_ctl_host_statuses, "name: %.*s, now: %ld, down_time: %d, marked_down: %ld, reason: %s", int(name.size()),
            name.data(), now, _status->manual_down_time, _status->manual_marked_down, Reason::MANUAL_REASON);
        setHostStatus(name, TSHostStatus::TS_HOST_STATUS_UP, 0, Reason::MANUAL);
        reasons ^= Reason::MANUAL;
      }
    }
    _status->reasons = reasons;
  }

  return _status;
}

namespace YAML
{
template <> struct convert<HostCmdInfo> {
  static bool
  decode(const Node &node, HostCmdInfo &rhs)
  {
    if (auto n = node["operation"]) {
      auto const &str = n.as<std::string>();
      if (str == "up") {
        rhs.type = TSHostStatus::TS_HOST_STATUS_UP;
      } else if (str == "down") {
        rhs.type = TSHostStatus::TS_HOST_STATUS_DOWN;
      } else {
        // unknown.
        return false;
      }
    } else {
      return false;
    }

    if (auto n = node["host"]; n.IsSequence() && n.size()) {
      for (auto &&it : n) {
        rhs.hosts.push_back(it.as<std::string>());
      }
    } else {
      return false;
    }

    if (auto n = node["reason"]) {
      auto reasonStr = n.as<std::string>();
      rhs.reasonType = Reason::getReason(reasonStr.c_str());
    } // manual by default.

    if (auto n = node["time"]) {
      rhs.time = std::stoi(n.as<std::string>());
      if (rhs.time < 0) {
        return false;
      }
    } else {
      return false;
    }

    return true;
  }
};
} // namespace YAML

// JSON-RPC method to retrieve host status information.
swoc::Rv<YAML::Node>
server_get_status(std::string_view /* id ATS_UNUSED */, YAML::Node const &params)
{
  namespace err = rpc::handlers::errors;
  swoc::Rv<YAML::Node> resp;
  YAML::Node           statusList{YAML::NodeType::Sequence}, errorList{YAML::NodeType::Sequence};

  try {
    if (!params.IsNull() && params.size() > 0) { // returns host statuses for just the ones asked for.
      for (YAML::const_iterator it = params.begin(); it != params.end(); ++it) {
        YAML::Node   host{YAML::NodeType::Map};
        auto         name     = it->as<std::string>();
        HostStatRec *host_rec = nullptr;
        HostStatus  &hs       = HostStatus::instance();
        host_rec              = hs.getHostStatus(name);
        if (host_rec == nullptr) {
          Dbg(dbg_ctl_host_statuses, "no record for %s was found", name.c_str());
          errorList.push_back("no record for " + name + " was found");
          continue;
        } else {
          std::stringstream s;
          s << *host_rec;
          host[HOST_NAME_KEY] = name;
          host[STATUS_KEY]    = s.str();
          statusList.push_back(host);
          Dbg(dbg_ctl_host_statuses, "hostname: %s, status: %s", name.c_str(), s.str().c_str());
        }
      }
    } else { // return all host statuses.
      std::vector<HostStatuses> hostInfo;
      HostStatus               &hs = HostStatus::instance();
      hs.getAllHostStatuses(hostInfo);
      for (auto &h : hostInfo) {
        YAML::Node host{YAML::NodeType::Map};
        host[HOST_NAME_KEY] = h.hostname;
        host[STATUS_KEY]    = h.status;
        statusList.push_back(host);
      }
    }
  } catch (std::exception const &ex) {
    Dbg(dbg_ctl_host_statuses, "Got an error decoding the parameters: %s", ex.what());
    errorList.push_back("Error decoding parameters : " + std::string(ex.what()));
  }

  resp.result()[STATUS_LIST_KEY] = statusList;
  resp.result()[ERROR_LIST_KEY]  = errorList;

  return resp;
}

// JSON-RPC method to mark up or down a host.
swoc::Rv<YAML::Node>
server_set_status(std::string_view id, YAML::Node const &params)
{
  Dbg(dbg_ctl_host_statuses, "id=%s", id.data());
  namespace err = rpc::handlers::errors;
  swoc::Rv<YAML::Node> resp;

  try {
    if (!params.IsNull()) {
      auto cmdInfo = params.as<HostCmdInfo>();

      for (auto const &name : cmdInfo.hosts) {
        HostStatus &hs       = HostStatus::instance();
        std::string statName = stat_prefix + name;
        Dbg(dbg_ctl_host_statuses, "marking server %s : %s", name.c_str(),
            (cmdInfo.type == TSHostStatus::TS_HOST_STATUS_UP ? "up" : "down"));
        hs.setHostStatus(name.c_str(), cmdInfo.type, cmdInfo.time, cmdInfo.reasonType);
      }
    } else {
      resp.errata().assign(std::error_code{rpc::handlers::errors::Codes::SERVER}).note("Invalid input parameters, null");
    }

    // schedule a write to the persistent store.
    Dbg(dbg_ctl_host_statuses, "updating persistent store");
    eventProcessor.schedule_imm(HostStatusSync::new_instance(), ET_TASK);
  } catch (std::exception const &ex) {
    Dbg(dbg_ctl_host_statuses, "Got an error HostCmdInfo decoding: %s", ex.what());
    resp.errata()
      .assign(std::error_code{rpc::handlers::errors::Codes::SERVER})
      .note("Error found during host status set: {}", ex.what());
  }
  return resp;
}

// method to write host status records to the persistent store.
void
HostStatusSync::sync_task()
{
  YAML::Node records{YAML::NodeType::Map};

  YAML::Node                statusList{YAML::NodeType::Sequence};
  std::vector<HostStatuses> statuses;
  HostStatus               &hs = HostStatus::instance();
  hs.getAllHostStatuses(statuses);

  for (auto &&h : statuses) {
    YAML::Node host{YAML::NodeType::Map};
    host[HOST_NAME_KEY] = h.hostname;
    host[STATUS_KEY]    = h.status;
    statusList.push_back(host);
  }
  records["statuses"] = statusList;

  std::ofstream fout;
  fout.open(hostRecordsFile.c_str(), std::ofstream::out | std::ofstream::trunc);
  if (fout) {
    fout << records;
    fout << '\n';
    fout.close();
  } else {
    Warning("failed to open %s for writing", hostRecordsFile.c_str());
  }
}
