// Copyright 2022 The Centipede Authors.
//
// Licensed 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
//
//      https://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 "./centipede/stats.h"

#include <algorithm>
#include <atomic>
#include <cinttypes>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>  // NOLINT: C++17
#include <initializer_list>
#include <iomanip>
#include <ios>
#include <iosfwd>
#include <limits>
#include <numeric>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "./centipede/environment.h"
#include "./centipede/workdir.h"
#include "./common/logging.h"
#include "./common/remote_file.h"

namespace fuzztest::internal {

namespace fs = std::filesystem;

using TraitBits = Stats::TraitBits;

// -----------------------------------------------------------------------------
//                               StatsReporter

StatsReporter::StatsReporter(const std::vector<std::atomic<Stats>> &stats_vec,
                             const std::vector<Environment> &env_vec)
    : stats_vec_{stats_vec}, env_vec_{env_vec} {
  FUZZTEST_CHECK_EQ(stats_vec.size(), env_vec.size());
  for (size_t i = 0; i < env_vec.size(); ++i) {
    const auto &env = env_vec[i];
    group_to_indices_[env.experiment_name].push_back(i);
    // NOTE: This will overwrite repeatedly for all indices of each group,
    // but the value will be the same by construction in environment.cc.
    group_to_flags_[env.experiment_name] = env.experiment_flags;
  }
}

void StatsReporter::ReportCurrStats() {
  // Collect snapshots of the current elements of `stats_vec_`: the elements
  // are `std::atomic`s; snapshotting them is required, and also provides
  // temporal consistency between the fields of each `Stats` object, even
  // as it is being modified by a different thread.
  std::vector<Stats> stats_snapshots;
  stats_snapshots.reserve(stats_vec_.size());
  for (const auto &stats : stats_vec_) {
    stats_snapshots.push_back(stats.load());
  }

  PreAnnounceFields(Stats::kFieldInfos);
  for (const Stats::FieldInfo &field_info : Stats::kFieldInfos) {
    if (!ShouldReportThisField(field_info)) continue;
    SetCurrField(field_info);
    for (const auto &[group_name, group_indices] : group_to_indices_) {
      SetCurrGroup(env_vec_[group_indices.at(0)]);
      // Get the required stat fields into a vector `stat_values`.
      std::vector<uint64_t> stat_values;
      stat_values.reserve(group_indices.size());
      for (const auto idx : group_indices) {
        stat_values.push_back(stats_snapshots.at(idx).*(field_info.field));
      }
      ReportCurrFieldSample(std::move(stat_values));
    }
  }
  ReportFlags(group_to_flags_);
  DoneFieldSamplesBatch();
}

// -----------------------------------------------------------------------------
//                               StatsLogger

bool StatsLogger::ShouldReportThisField(const Stats::FieldInfo &field) {
  // Skip timestamps and rusage stats: the former because timestamps are
  // not very useful in these logs (only in CSVs), the latter because rusage is
  // (at least currently) measured for the whole process, not per shard or
  // experiment, so reporting nearly identical numbers would be useless and
  // confusing.
  return (field.traits & TraitBits::kFuzzStat) != 0;
}

void StatsLogger::PreAnnounceFields(
    std::initializer_list<Stats::FieldInfo> fields) {
  // Nothing to do: field names are logged together with every sample's values.
}

void StatsLogger::SetCurrGroup(const Environment &master_env) {
  curr_experiment_name_ = master_env.experiment_name;
}

void StatsLogger::SetCurrField(const Stats::FieldInfo &field_info) {
  curr_field_info_ = field_info;
  os_ << curr_field_info_.description << ":\n";
}

void StatsLogger::ReportCurrFieldSample(std::vector<uint64_t> &&values) {
  if (!curr_experiment_name_.empty())
    os_ << "  " << curr_experiment_name_ << ": ";

  // Print the requested aggregate stats as well as the full sorted contents of
  // `values`.
  std::sort(values.begin(), values.end());
  const uint64_t min = values.front();
  const uint64_t max = values.back();
  const uint64_t sum = std::accumulate(values.begin(), values.end(), 0.);
  const double avg = !values.empty() ? (1.0 * sum / values.size()) : 0;

  os_ << std::fixed << std::setprecision(1);
  if (curr_field_info_.traits & TraitBits::kMin) os_ << "min:\t" << min << "\t";
  if (curr_field_info_.traits & TraitBits::kMax) os_ << "max:\t" << max << "\t";
  if (curr_field_info_.traits & TraitBits::kAvg) os_ << "avg:\t" << avg << "\t";
  if (curr_field_info_.traits & TraitBits::kSum) os_ << "sum:\t" << sum << "\t";

  os_ << "--";
  for (const auto value : values) {
    os_ << "\t" << value;
  }
  os_ << "\n";
}

void StatsLogger::ReportFlags(const GroupToFlags &group_to_flags) {
  std::stringstream fos;
  for (const auto &[group_name, group_flags] : group_to_flags) {
    if (!group_name.empty() || !group_flags.empty()) {
      fos << "  " << group_name << ": " << group_flags << "\n";
    }
  }
  if (fos.tellp() != std::streampos{0}) os_ << "Flags:\n" << fos.rdbuf();
}

void StatsLogger::DoneFieldSamplesBatch() {
  FUZZTEST_LOG(INFO) << "Current stats:\n"
                     << absl::StripAsciiWhitespace(os_.str());
  // Reset the stream for the next round of logging.
  os_.str("");
}

// -----------------------------------------------------------------------------
//                           StatsCsvFileAppender

StatsCsvFileAppender::~StatsCsvFileAppender() {
  if (files_ == nullptr) return;
  for (const auto &[group_name, file] : *files_) {
    FUZZTEST_CHECK_OK(RemoteFileClose(file.file));
  }
}

void StatsCsvFileAppender::PreAnnounceFields(
    std::initializer_list<Stats::FieldInfo> fields) {
  if (!csv_header_.empty()) return;

  for (const auto &field : fields) {
    if (field.traits & TraitBits::kMin)
      absl::StrAppend(&csv_header_, field.name, "_Min,");
    if (field.traits & TraitBits::kMax)
      absl::StrAppend(&csv_header_, field.name, "_Max,");
    if (field.traits & TraitBits::kAvg)
      absl::StrAppend(&csv_header_, field.name, "_Avg,");
    if (field.traits & TraitBits::kSum)
      absl::StrAppend(&csv_header_, field.name, "_Sum,");
  }
  absl::StrAppend(&csv_header_, "\n");
}

void StatsCsvFileAppender::SetCurrGroup(const Environment &master_env) {
  FUZZTEST_CHECK(files_ != nullptr);
  BufferedRemoteFile &file = (*files_)[master_env.experiment_name];
  if (file.file == nullptr) {
    const std::string filename =
        WorkDir{master_env}.FuzzingStatsPath(master_env.experiment_name);
    // If a non-empty file already exists and has the same CVS header, then
    // keep appending new CSV lines to the file. If the file exists, but has a
    // different CSV header (ostensibly because it was created by a different
    // version of Centipede), then make a backup copy of the file and start a
    // a new one from scratch.
    bool append = false;
    if (RemotePathExists(filename)) {
      std::string contents;
      FUZZTEST_CHECK_OK(RemoteFileGetContents(filename, contents));
      // NOTE: `csv_header_` ends with '\n', so the match is exact.
      if (absl::StartsWith(contents, csv_header_)) {
        append = true;
      } else {
        append = false;
        FUZZTEST_CHECK_OK(
            RemoteFileSetContents(GetBackupFilename(filename), contents));
      }
    }
    file.file = *RemoteFileOpen(filename, append ? "a" : "w");
    FUZZTEST_CHECK(file.file != nullptr) << VV(filename);
    if (!append) {
      FUZZTEST_CHECK_OK(RemoteFileAppend(file.file, csv_header_));
      FUZZTEST_CHECK_OK(RemoteFileFlush(file.file));
    }
  }
  // This is OK even though hash maps provide no pointer stability because the
  // field is always updated immediately after the map is modified.
  curr_file_ = &file;
}

void StatsCsvFileAppender::SetCurrField(const Stats::FieldInfo &field_info) {
  curr_field_info_ = field_info;
}

void StatsCsvFileAppender::ReportCurrFieldSample(
    std::vector<uint64_t> &&values) {
  uint64_t min = std::numeric_limits<uint64_t>::max();
  uint64_t max = std::numeric_limits<uint64_t>::min();
  uint64_t sum = 0;
  for (const auto value : values) {
    min = std::min(min, value);
    max = std::max(max, value);
    sum += value;
  }
  double avg = !values.empty() ? (1.0 * sum / values.size()) : 0;

  FUZZTEST_CHECK(curr_file_ != nullptr);
  std::string &values_str = curr_file_->buffer;
  if (curr_field_info_.traits & TraitBits::kMin)
    absl::StrAppendFormat(&values_str, "%" PRIu64 ",", min);
  if (curr_field_info_.traits & TraitBits::kMax)
    absl::StrAppendFormat(&values_str, "%" PRIu64 ",", max);
  if (curr_field_info_.traits & TraitBits::kAvg)
    absl::StrAppendFormat(&values_str, "%.1lf,", avg);
  if (curr_field_info_.traits & TraitBits::kSum)
    absl::StrAppendFormat(&values_str, "%" PRIu64 ",", sum);
}

void StatsCsvFileAppender::ReportFlags(const GroupToFlags &group_to_flags) {
  // Do nothing: can't write to CSV, as it has no concept of comments.
  // TODO(ussuri): Consider writing to a sidecar file.
}

void StatsCsvFileAppender::DoneFieldSamplesBatch() {
  FUZZTEST_CHECK(files_ != nullptr);
  for (auto &[group_name, file] : *files_) {
    FUZZTEST_CHECK_OK(
        RemoteFileAppend(file.file, absl::StrCat(file.buffer, "\n")));
    FUZZTEST_CHECK_OK(RemoteFileFlush(file.file));
    file.buffer.clear();
  }
}

std::string StatsCsvFileAppender::GetBackupFilename(
    const std::string &filename) const {
  fs::path path{filename};
  const auto timestamp = absl::ToUnixSeconds(absl::Now());
  const auto new_extension =
      absl::StrCat(path.extension().string(), ".", timestamp);
  path.replace_extension(new_extension);
  return path.string();
}

// -----------------------------------------------------------------------------

void PrintRewardValues(absl::Span<const std::atomic<Stats>> stats_vec,
                       std::ostream &os) {
  size_t n = stats_vec.size();
  FUZZTEST_CHECK_GT(n, 0);
  std::vector<size_t> num_covered_pcs(n);
  for (size_t i = 0; i < n; ++i) {
    num_covered_pcs[i] = stats_vec[i].load().num_covered_pcs;
  }
  std::sort(num_covered_pcs.begin(), num_covered_pcs.end());
  os << "REWARD_MAX " << num_covered_pcs.back() << "\n";
  os << "REWARD_SECOND_MAX " << num_covered_pcs[n == 1 ? 1 : n - 2] << "\n";
  os << "REWARD_MIN " << num_covered_pcs.front() << "\n";
  os << "REWARD_MEDIAN " << num_covered_pcs[n / 2] << "\n";
  os << "REWARD_AVERAGE "
     << (std::accumulate(num_covered_pcs.begin(), num_covered_pcs.end(), 0.) /
         n)
     << "\n";
}

}  // namespace fuzztest::internal
