// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/metrics/metrics_service_observer.h"

#include "base/base64.h"
#include "base/callback_list.h"
#include "base/files/file_util.h"
#include "base/json/json_string_value_serializer.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/metrics/metrics_logs_event_manager.h"

namespace metrics {
namespace {

MetricsServiceObserver::Log::Event CreateEventStruct(
    MetricsLogsEventManager::LogEvent event,
    base::StringPiece message) {
  MetricsServiceObserver::Log::Event event_struct;
  event_struct.event = event;
  event_struct.timestampMs =
      base::Time::Now().InMillisecondsFSinceUnixEpochIgnoringNull();
  if (!message.empty()) {
    event_struct.message = std::string(message);
  }
  return event_struct;
}

std::string LogTypeToString(MetricsLog::LogType log_type) {
  switch (log_type) {
    case MetricsLog::LogType::INDEPENDENT_LOG:
      return "Independent";
    case MetricsLog::LogType::INITIAL_STABILITY_LOG:
      return "Stability";
    case MetricsLog::LogType::ONGOING_LOG:
      return "Ongoing";
  }
  NOTREACHED();
}

std::string EventToString(MetricsLogsEventManager::LogEvent event) {
  switch (event) {
    case MetricsLogsEventManager::LogEvent::kLogStaged:
      return "Staged";
    case MetricsLogsEventManager::LogEvent::kLogDiscarded:
      return "Discarded";
    case MetricsLogsEventManager::LogEvent::kLogTrimmed:
      return "Trimmed";
    case MetricsLogsEventManager::LogEvent::kLogUploading:
      return "Uploading";
    case MetricsLogsEventManager::LogEvent::kLogUploaded:
      return "Uploaded";
    case MetricsLogsEventManager::LogEvent::kLogCreated:
      return "Created";
  }
  NOTREACHED();
}

std::string CreateReasonToString(
    metrics::MetricsLogsEventManager::CreateReason reason) {
  switch (reason) {
    case MetricsLogsEventManager::CreateReason::kUnknown:
      return std::string();
    case MetricsLogsEventManager::CreateReason::kPeriodic:
      return "Reason: Periodic log creation";
    case MetricsLogsEventManager::CreateReason::kServiceShutdown:
      return "Reason: Shutting down";
    case MetricsLogsEventManager::CreateReason::kLoadFromPreviousSession:
      return "Reason: Loaded from previous session";
    case MetricsLogsEventManager::CreateReason::kBackgrounded:
      return "Reason: Browser backgrounded";
    case MetricsLogsEventManager::CreateReason::kForegrounded:
      return "Reason: Browser foregrounded";
    case MetricsLogsEventManager::CreateReason::kAlternateOngoingLogStoreSet:
      return "Reason: Alternate ongoing log store set";
    case MetricsLogsEventManager::CreateReason::kAlternateOngoingLogStoreUnset:
      return "Reason: Alternate ongoing log store unset";
    case MetricsLogsEventManager::CreateReason::kStability:
      return "Reason: Stability metrics from previous session";
    case MetricsLogsEventManager::CreateReason::kIndependent:
      // TODO(crbug/1363747): Give more insight here (e.g. "independent log
      // generated from pma file").
      return "Reason: Independent log";
  }
}

}  // namespace

MetricsServiceObserver::MetricsServiceObserver(MetricsServiceType service_type)
    : service_type_(service_type) {}
MetricsServiceObserver::~MetricsServiceObserver() = default;
MetricsServiceObserver::Log::Log() = default;
MetricsServiceObserver::Log::Log(const Log&) = default;
MetricsServiceObserver::Log& MetricsServiceObserver::Log::operator=(
    const Log&) = default;
MetricsServiceObserver::Log::~Log() = default;
MetricsServiceObserver::Log::Event::Event() = default;
MetricsServiceObserver::Log::Event::Event(const Event&) = default;
MetricsServiceObserver::Log::Event&
MetricsServiceObserver::Log::Event::operator=(const Event&) = default;
MetricsServiceObserver::Log::Event::~Event() = default;

void MetricsServiceObserver::OnLogCreated(
    base::StringPiece log_hash,
    base::StringPiece log_data,
    base::StringPiece log_timestamp,
    metrics::MetricsLogsEventManager::CreateReason reason) {
  DCHECK(!GetLogFromHash(log_hash));

  // Insert a new log into |logs_| with the given |log_hash| to indicate that
  // this observer is now aware and keeping track of this log.
  std::unique_ptr<Log> log = std::make_unique<Log>();
  log->hash = std::string(log_hash);
  log->timestamp = std::string(log_timestamp);
  log->data = std::string(log_data);
  if (uma_log_type_.has_value()) {
    DCHECK_EQ(service_type_, MetricsServiceType::UMA);
    log->type = uma_log_type_;
  }

  // Immediately create a |kLogCreated| log event, along with the reason why the
  // log was created.
  log->events.push_back(
      CreateEventStruct(MetricsLogsEventManager::LogEvent::kLogCreated,
                        CreateReasonToString(reason)));

  indexed_logs_.emplace(log->hash, log.get());
  logs_.push_back(std::move(log));

  // Call all registered callbacks.
  notified_callbacks_.Notify();
}

void MetricsServiceObserver::OnLogEvent(MetricsLogsEventManager::LogEvent event,
                                        base::StringPiece log_hash,
                                        base::StringPiece message) {
  Log* log = GetLogFromHash(log_hash);

  // If this observer is not aware of any logs with the given |log_hash|, do
  // nothing. This may happen if this observer started observing after a log
  // was already created.
  if (!log)
    return;

  log->events.push_back(CreateEventStruct(event, message));

  // Call all registered callbacks.
  notified_callbacks_.Notify();
}

void MetricsServiceObserver::OnLogType(
    std::optional<MetricsLog::LogType> log_type) {
  uma_log_type_ = log_type;
}

bool MetricsServiceObserver::ExportLogsAsJson(bool include_log_proto_data,
                                              std::string* json_output) {
  base::Value::List logs_list;
  // Create and append to |logs_list| a base::Value for each log in |logs_|.
  for (const std::unique_ptr<Log>& log : logs_) {
    base::Value::Dict log_dict;

    if (log->type.has_value()) {
      DCHECK_EQ(service_type_, MetricsServiceType::UMA);
      log_dict.Set("type", LogTypeToString(log->type.value()));
    }
    log_dict.Set("hash", base::HexEncode(log->hash));
    log_dict.Set("timestamp", log->timestamp);

    if (include_log_proto_data) {
      log_dict.Set("data", base::Base64Encode(log->data));
    }

    log_dict.Set("size", static_cast<int>(log->data.length()));

    base::Value::List log_events_list;
    for (const Log::Event& event : log->events) {
      base::Value::Dict log_event_dict;
      log_event_dict.Set("event", EventToString(event.event));
      log_event_dict.Set("timestampMs", event.timestampMs);
      if (event.message.has_value())
        log_event_dict.Set("message", event.message.value());
      log_events_list.Append(std::move(log_event_dict));
    }
    log_dict.Set("events", std::move(log_events_list));

    logs_list.Append(std::move(log_dict));
  }

  // Create a last |dict| that contains all the logs and |service_type_|,
  // convert it to a JSON string, and write it to |json_output|.
  base::Value::Dict dict;
  dict.Set("logType", service_type_ == MetricsServiceType::UMA ? "UMA" : "UKM");
  dict.Set("logs", std::move(logs_list));

  JSONStringValueSerializer serializer(json_output);
  return serializer.Serialize(dict);
}

void MetricsServiceObserver::ExportLogsToFile(const base::FilePath& path) {
  std::string logs_data;
  bool success = ExportLogsAsJson(/*include_log_proto_data=*/true, &logs_data);
  DCHECK(success);
  if (!base::WriteFile(path, logs_data)) {
    LOG(ERROR) << "Failed to export logs to " << path << ": " << logs_data;
  }
}

base::CallbackListSubscription MetricsServiceObserver::AddNotifiedCallback(
    base::RepeatingClosure callback) {
  return notified_callbacks_.Add(callback);
}

MetricsServiceObserver::Log* MetricsServiceObserver::GetLogFromHash(
    base::StringPiece log_hash) {
  auto it = indexed_logs_.find(log_hash);
  return it != indexed_logs_.end() ? it->second : nullptr;
}

}  // namespace metrics
