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

#include "chrome/browser/sessions/session_service_log.h"

#include <string>
#include <utility>

#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "chrome/browser/profiles/profile.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"

namespace {

// The value is a list.
constexpr char kEventPrefKey[] = "sessions.event_log";
constexpr char kEventTypeKey[] = "type";
constexpr char kEventTimeKey[] = "time";
constexpr char kStartEventDidLastSessionCrashKey[] = "crashed";
constexpr char kRestoreEventWindowCountKey[] = "window_count";
constexpr char kRestoreEventTabCountKey[] = "tab_count";
constexpr char kRestoreEventErroredReadingKey[] = "errored_reading";
constexpr char kRestoreInitiatedEventRestoreBrowserKey[] = "restore_browser";
constexpr char kRestoreInitiatedEventSynchronousKey[] = "synchronous";
constexpr char kExitEventWindowCountKey[] = "window_count";
constexpr char kExitEventTabCountKey[] = "tab_count";
constexpr char kExitEventIsFirstSessionServiceKey[] = "first_session_service";
constexpr char kExitEventDidScheduleCommandKey[] = "did_schedule_command";
constexpr char kWriteErrorEventErrorCountKey[] = "error_count";
constexpr char kWriteErrorEventUnrecoverableErrorCountKey[] =
    "unrecoverable_error_count";

// This value is a balance between keeping too much in prefs, and the
// ability to see the last few restarts.
constexpr size_t kMaxEventCount = 20;

base::DictValue SerializeEvent(const SessionServiceEvent& event) {
  base::DictValue serialized_event;
  serialized_event.Set(kEventTypeKey, static_cast<int>(event.type));
  serialized_event.Set(
      kEventTimeKey,
      base::NumberToString(event.time.since_origin().InMicroseconds()));
  switch (event.type) {
    case SessionServiceEventLogType::kStart:
      serialized_event.Set(kStartEventDidLastSessionCrashKey,
                           event.data.start.did_last_session_crash);
      break;
    case SessionServiceEventLogType::kRestore:
      serialized_event.Set(kRestoreEventWindowCountKey,
                           event.data.restore.window_count);
      serialized_event.Set(kRestoreEventTabCountKey,
                           event.data.restore.tab_count);
      serialized_event.Set(kRestoreEventErroredReadingKey,
                           event.data.restore.encountered_error_reading);
      break;
    case SessionServiceEventLogType::kExit:
      serialized_event.Set(kExitEventWindowCountKey,
                           event.data.exit.window_count);
      serialized_event.Set(kExitEventTabCountKey, event.data.exit.tab_count);
      serialized_event.Set(kExitEventIsFirstSessionServiceKey,
                           event.data.exit.is_first_session_service);
      serialized_event.Set(kExitEventDidScheduleCommandKey,
                           event.data.exit.did_schedule_command);
      break;
    case SessionServiceEventLogType::kWriteError:
      serialized_event.Set(kWriteErrorEventErrorCountKey,
                           event.data.write_error.error_count);
      serialized_event.Set(kWriteErrorEventUnrecoverableErrorCountKey,
                           event.data.write_error.unrecoverable_error_count);
      break;
    case SessionServiceEventLogType::kRestoreCanceled:
      break;
    case SessionServiceEventLogType::kRestoreInitiated:
      serialized_event.Set(kRestoreInitiatedEventRestoreBrowserKey,
                           event.data.restore_initiated.restore_browser);
      serialized_event.Set(kRestoreInitiatedEventSynchronousKey,
                           event.data.restore_initiated.synchronous);
      break;
  }
  return serialized_event;
}

bool DeserializeEvent(const base::DictValue& serialized_event,
                      SessionServiceEvent& event) {
  auto type = serialized_event.FindInt(kEventTypeKey);
  if (!type)
    return false;
  if (*type < static_cast<int>(SessionServiceEventLogType::kMinValue) ||
      *type > static_cast<int>(SessionServiceEventLogType::kMaxValue)) {
    return false;
  }
  event.type = static_cast<SessionServiceEventLogType>(*type);

  const std::string* time_value = serialized_event.FindString(kEventTimeKey);
  if (!time_value)
    return false;
  int64_t time_int;
  if (!base::StringToInt64(*time_value, &time_int))
    return false;
  event.time = base::Time() + base::Microseconds(time_int);

  switch (event.type) {
    case SessionServiceEventLogType::kStart: {
      auto crash_value =
          serialized_event.FindBool(kStartEventDidLastSessionCrashKey);
      if (!crash_value)
        return false;
      event.data.start.did_last_session_crash = *crash_value;
      break;
    }
    case SessionServiceEventLogType::kRestore: {
      auto window_count = serialized_event.FindInt(kRestoreEventWindowCountKey);
      if (!window_count)
        return false;
      event.data.restore.window_count = *window_count;

      auto tab_count = serialized_event.FindInt(kRestoreEventTabCountKey);
      if (!tab_count)
        return false;
      event.data.restore.tab_count = *tab_count;

      auto error_reading =
          serialized_event.FindBool(kRestoreEventErroredReadingKey);
      if (!error_reading)
        return false;
      event.data.restore.encountered_error_reading = *error_reading;
      break;
    }
    case SessionServiceEventLogType::kExit: {
      auto window_count = serialized_event.FindInt(kExitEventWindowCountKey);
      if (!window_count)
        return false;
      event.data.exit.window_count = *window_count;

      auto tab_count = serialized_event.FindInt(kExitEventTabCountKey);
      if (!tab_count)
        return false;
      event.data.exit.tab_count = *tab_count;

      // The remaining values were added later on. Don't error if not found.
      auto is_first_session_service =
          serialized_event.FindBool(kExitEventIsFirstSessionServiceKey);
      event.data.exit.is_first_session_service =
          !is_first_session_service || *is_first_session_service;

      auto did_schedule_command =
          serialized_event.FindBool(kExitEventDidScheduleCommandKey);
      event.data.exit.did_schedule_command =
          !did_schedule_command || *did_schedule_command;
      break;
    }
    case SessionServiceEventLogType::kWriteError: {
      auto error_count =
          serialized_event.FindInt(kWriteErrorEventErrorCountKey);
      if (!error_count)
        return false;
      event.data.write_error.error_count = *error_count;
      event.data.write_error.unrecoverable_error_count = 0;
      // `kWriteErrorEventErrorCountKey` was added after initial code landed,
      // so don't fail if it isn't present.
      auto unrecoverable_error_count =
          serialized_event.FindInt(kWriteErrorEventUnrecoverableErrorCountKey);
      if (unrecoverable_error_count) {
        event.data.write_error.unrecoverable_error_count =
            *unrecoverable_error_count;
      }
      break;
    }
    case SessionServiceEventLogType::kRestoreCanceled:
      break;
    case SessionServiceEventLogType::kRestoreInitiated: {
      auto synchronous =
          serialized_event.FindBool(kRestoreInitiatedEventSynchronousKey);
      event.data.restore_initiated.synchronous = synchronous && *synchronous;
      auto restore_browser =
          serialized_event.FindBool(kRestoreInitiatedEventRestoreBrowserKey);
      event.data.restore_initiated.restore_browser =
          restore_browser && *restore_browser;
      break;
    }
  }
  return true;
}

void SaveEventsToPrefs(Profile* profile,
                       const std::list<SessionServiceEvent>& events) {
  base::Value serialized_events(base::Value::Type::LIST);
  for (const SessionServiceEvent& event : events)
    serialized_events.GetList().Append(SerializeEvent(event));
  profile->GetPrefs()->Set(kEventPrefKey, serialized_events);
}

}  // namespace

std::list<SessionServiceEvent> GetSessionServiceEvents(Profile* profile) {
  const base::ListValue& serialized_events =
      profile->GetPrefs()->GetList(kEventPrefKey);
  std::list<SessionServiceEvent> events;
  for (const auto& serialized_event : serialized_events) {
    SessionServiceEvent event;
    if (!serialized_event.is_dict())
      continue;
    if (DeserializeEvent(serialized_event.GetDict(), event))
      events.push_back(std::move(event));
  }
  return events;
}

void LogSessionServiceStartEvent(Profile* profile, bool after_crash) {
  SessionServiceEvent event;
  event.type = SessionServiceEventLogType::kStart;
  event.time = base::Time::Now();
  event.data.start.did_last_session_crash = after_crash;
  LogSessionServiceEvent(profile, event);
}

void LogSessionServiceExitEvent(Profile* profile,
                                int window_count,
                                int tab_count,
                                bool is_first_session_service,
                                bool did_schedule_command) {
  SessionServiceEvent event;
  event.type = SessionServiceEventLogType::kExit;
  event.time = base::Time::Now();
  event.data.exit.window_count = window_count;
  event.data.exit.tab_count = tab_count;
  event.data.exit.is_first_session_service = is_first_session_service;
  event.data.exit.did_schedule_command = did_schedule_command;
  LogSessionServiceEvent(profile, event);
}

void LogSessionServiceRestoreInitiatedEvent(Profile* profile,
                                            bool synchronous,
                                            bool restore_browser) {
  SessionServiceEvent event;
  event.type = SessionServiceEventLogType::kRestoreInitiated;
  event.time = base::Time::Now();
  event.data.restore_initiated.synchronous = synchronous;
  event.data.restore_initiated.restore_browser = restore_browser;
  LogSessionServiceEvent(profile, event);
}

void LogSessionServiceRestoreEvent(Profile* profile,
                                   int window_count,
                                   int tab_count,
                                   bool encountered_error_reading) {
  SessionServiceEvent event;
  event.type = SessionServiceEventLogType::kRestore;
  event.time = base::Time::Now();
  event.data.restore.window_count = window_count;
  event.data.restore.tab_count = tab_count;
  event.data.restore.encountered_error_reading = encountered_error_reading;
  LogSessionServiceEvent(profile, event);
}

void LogSessionServiceRestoreCanceledEvent(Profile* profile) {
  SessionServiceEvent event;
  event.type = SessionServiceEventLogType::kRestoreCanceled;
  event.time = base::Time::Now();
  LogSessionServiceEvent(profile, event);
}

void LogSessionServiceWriteErrorEvent(Profile* profile,
                                      bool unrecoverable_write_error) {
  SessionServiceEvent event;
  event.type = SessionServiceEventLogType::kWriteError;
  event.time = base::Time::Now();
  event.data.write_error.error_count = 1;
  event.data.write_error.unrecoverable_error_count =
      unrecoverable_write_error ? 1 : 0;
  LogSessionServiceEvent(profile, event);
}

void RemoveLastSessionServiceEventOfType(Profile* profile,
                                         SessionServiceEventLogType type) {
  std::list<SessionServiceEvent> events = GetSessionServiceEvents(profile);
  for (auto iter = events.rbegin(); iter != events.rend(); ++iter) {
    if (iter->type == type) {
      events.erase(std::next(iter).base());
      SaveEventsToPrefs(profile, events);
      return;
    }
  }
}

void RegisterSessionServiceLogProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterListPref(kEventPrefKey);
}

void LogSessionServiceEvent(Profile* profile,
                            const SessionServiceEvent& event) {
  std::list<SessionServiceEvent> events = GetSessionServiceEvents(profile);
  if (event.type == SessionServiceEventLogType::kWriteError &&
      !events.empty() &&
      events.back().type == SessionServiceEventLogType::kWriteError) {
    events.back().data.write_error.error_count += 1;
    events.back().data.write_error.unrecoverable_error_count +=
        event.data.write_error.unrecoverable_error_count;
  } else {
    events.push_back(event);
    if (events.size() >= kMaxEventCount)
      events.erase(events.begin());
  }
  SaveEventsToPrefs(profile, events);
}
