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

#include "content/browser/devtools/devtools_background_services_context_impl.h"

#include <algorithm>

#include "base/guid.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/common/content_client.h"
#include "url/origin.h"

namespace content {

namespace {

std::string CreateEntryKeyPrefix(devtools::proto::BackgroundService service) {
  DCHECK_NE(service, devtools::proto::BackgroundService::UNKNOWN);
  return "devtools_background_services_" + base::NumberToString(service) + "_";
}

std::string CreateEntryKey(devtools::proto::BackgroundService service) {
  return CreateEntryKeyPrefix(service) + base::GenerateGUID();
}

void DidLogServiceEvent(blink::ServiceWorkerStatusCode status) {
  DCHECK_CURRENTLY_ON(ServiceWorkerContext::GetCoreThreadId());
  UMA_HISTOGRAM_ENUMERATION("DevTools.BackgroundService.LogEvent", status);
}

void DidClearServiceEvents(blink::ServiceWorkerStatusCode status) {
  DCHECK_CURRENTLY_ON(ServiceWorkerContext::GetCoreThreadId());
  UMA_HISTOGRAM_ENUMERATION("DevTools.BackgroundService.ClearEvents", status);
}

constexpr devtools::proto::BackgroundService ServiceToProtoEnum(
    DevToolsBackgroundService service) {
  switch (service) {
    case DevToolsBackgroundService::kBackgroundFetch:
      return devtools::proto::BACKGROUND_FETCH;
    case DevToolsBackgroundService::kBackgroundSync:
      return devtools::proto::BACKGROUND_SYNC;
    case DevToolsBackgroundService::kPushMessaging:
      return devtools::proto::PUSH_MESSAGING;
    case DevToolsBackgroundService::kNotifications:
      return devtools::proto::NOTIFICATIONS;
    case DevToolsBackgroundService::kPaymentHandler:
      return devtools::proto::PAYMENT_HANDLER;
    case DevToolsBackgroundService::kPeriodicBackgroundSync:
      return devtools::proto::PERIODIC_BACKGROUND_SYNC;
  }
}

}  // namespace

DevToolsBackgroundServicesContextImpl::DevToolsBackgroundServicesContextImpl(
    BrowserContext* browser_context,
    scoped_refptr<ServiceWorkerContextWrapper> service_worker_context)
    : browser_context_(browser_context),
      service_worker_context_(std::move(service_worker_context)) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  auto expiration_times =
      GetContentClient()->browser()->GetDevToolsBackgroundServiceExpirations(
          browser_context_);

  for (const auto& expiration_time : expiration_times) {
    DCHECK(devtools::proto::BackgroundService_IsValid(expiration_time.first));
    expiration_times_[expiration_time.first] = expiration_time.second;

    auto service =
        static_cast<devtools::proto::BackgroundService>(expiration_time.first);
    // If the recording permission for |service| has expired, set it to null.
    if (IsRecordingExpired(service))
      expiration_times_[expiration_time.first] = base::Time();
  }
}

DevToolsBackgroundServicesContextImpl::
    ~DevToolsBackgroundServicesContextImpl() = default;

void DevToolsBackgroundServicesContextImpl::AddObserver(
    EventObserver* observer) {
  observers_.AddObserver(observer);
}

void DevToolsBackgroundServicesContextImpl::RemoveObserver(
    const EventObserver* observer) {
  observers_.RemoveObserver(observer);
}

void DevToolsBackgroundServicesContextImpl::StartRecording(
    devtools::proto::BackgroundService service) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // TODO(rayankans): Make the time delay finch configurable.
  base::Time expiration_time = base::Time::Now() + base::TimeDelta::FromDays(3);
  expiration_times_[service] = expiration_time;

  GetContentClient()->browser()->UpdateDevToolsBackgroundServiceExpiration(
      browser_context_, service, expiration_time);

  for (EventObserver& observer : observers_)
    observer.OnRecordingStateChanged(/* should_record= */ true, service);
}

void DevToolsBackgroundServicesContextImpl::StopRecording(
    devtools::proto::BackgroundService service) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  expiration_times_[service] = base::Time();
  GetContentClient()->browser()->UpdateDevToolsBackgroundServiceExpiration(
      browser_context_, service, base::Time());

  for (EventObserver& observer : observers_)
    observer.OnRecordingStateChanged(/* should_record= */ false, service);
}

bool DevToolsBackgroundServicesContextImpl::IsRecording(
    DevToolsBackgroundService service) {
  return IsRecording(ServiceToProtoEnum(service));
}

bool DevToolsBackgroundServicesContextImpl::IsRecording(
    devtools::proto::BackgroundService service) {
  // Returns whether |service| has been enabled. When the expiration time has
  // been met it will be lazily updated to be null.
  return !expiration_times_[service].is_null();
}

bool DevToolsBackgroundServicesContextImpl::IsRecordingExpired(
    devtools::proto::BackgroundService service) {
  // Copy the expiration time to avoid data races.
  const base::Time expiration_time = expiration_times_[service];
  return !expiration_time.is_null() && expiration_time < base::Time::Now();
}

void DevToolsBackgroundServicesContextImpl::GetLoggedBackgroundServiceEvents(
    devtools::proto::BackgroundService service,
    GetLoggedBackgroundServiceEventsCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  RunOrPostTaskOnThread(
      FROM_HERE, ServiceWorkerContext::GetCoreThreadId(),
      base::BindOnce(&DevToolsBackgroundServicesContextImpl::
                         GetLoggedBackgroundServiceEventsOnCoreThread,
                     weak_ptr_factory_core_.GetWeakPtr(), service,
                     std::move(callback)));
}

void DevToolsBackgroundServicesContextImpl::
    GetLoggedBackgroundServiceEventsOnCoreThread(
        devtools::proto::BackgroundService service,
        GetLoggedBackgroundServiceEventsCallback callback) {
  DCHECK_CURRENTLY_ON(ServiceWorkerContext::GetCoreThreadId());

  service_worker_context_->GetUserDataForAllRegistrationsByKeyPrefix(
      CreateEntryKeyPrefix(service),
      base::BindOnce(&DevToolsBackgroundServicesContextImpl::DidGetUserData,
                     weak_ptr_factory_core_.GetWeakPtr(), std::move(callback)));
}

void DevToolsBackgroundServicesContextImpl::DidGetUserData(
    GetLoggedBackgroundServiceEventsCallback callback,
    const std::vector<std::pair<int64_t, std::string>>& user_data,
    blink::ServiceWorkerStatusCode status) {
  DCHECK_CURRENTLY_ON(ServiceWorkerContext::GetCoreThreadId());

  UMA_HISTOGRAM_ENUMERATION("DevTools.BackgroundService.GetEvents", status);

  std::vector<devtools::proto::BackgroundServiceEvent> events;

  if (status != blink::ServiceWorkerStatusCode::kOk) {
    std::move(callback).Run(events);
    return;
  }

  events.reserve(user_data.size());
  for (const auto& data : user_data) {
    devtools::proto::BackgroundServiceEvent event;
    if (!event.ParseFromString(data.second)) {
      // TODO(rayankans): Log errors to UMA.
      std::move(callback).Run({});
      return;
    }
    DCHECK_EQ(data.first, event.service_worker_registration_id());
    events.push_back(std::move(event));
  }

  std::sort(events.begin(), events.end(),
            [](const auto& state1, const auto& state2) {
              return state1.timestamp() < state2.timestamp();
            });

  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), std::move(events)));
}

void DevToolsBackgroundServicesContextImpl::ClearLoggedBackgroundServiceEvents(
    devtools::proto::BackgroundService service) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  RunOrPostTaskOnThread(
      FROM_HERE, ServiceWorkerContext::GetCoreThreadId(),
      base::BindOnce(&DevToolsBackgroundServicesContextImpl::
                         ClearLoggedBackgroundServiceEventsOnCoreThread,
                     weak_ptr_factory_core_.GetWeakPtr(), service));
}

void DevToolsBackgroundServicesContextImpl::
    ClearLoggedBackgroundServiceEventsOnCoreThread(
        devtools::proto::BackgroundService service) {
  DCHECK_CURRENTLY_ON(ServiceWorkerContext::GetCoreThreadId());

  service_worker_context_->ClearUserDataForAllRegistrationsByKeyPrefix(
      CreateEntryKeyPrefix(service), base::BindOnce(&DidClearServiceEvents));
}

void DevToolsBackgroundServicesContextImpl::LogBackgroundServiceEvent(
    uint64_t service_worker_registration_id,
    const url::Origin& origin,
    DevToolsBackgroundService service,
    const std::string& event_name,
    const std::string& instance_id,
    const std::map<std::string, std::string>& event_metadata) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  RunOrPostTaskOnThread(
      FROM_HERE, ServiceWorkerContext::GetCoreThreadId(),
      base::BindOnce(&DevToolsBackgroundServicesContextImpl::
                         LogBackgroundServiceEventOnCoreThread,
                     weak_ptr_factory_core_.GetWeakPtr(),
                     service_worker_registration_id, origin, service,
                     event_name, instance_id, event_metadata));
}

void DevToolsBackgroundServicesContextImpl::
    LogBackgroundServiceEventOnCoreThread(
        uint64_t service_worker_registration_id,
        const url::Origin& origin,
        DevToolsBackgroundService service,
        const std::string& event_name,
        const std::string& instance_id,
        const std::map<std::string, std::string>& event_metadata) {
  DCHECK_CURRENTLY_ON(ServiceWorkerContext::GetCoreThreadId());

  if (!IsRecording(service))
    return;

  if (IsRecordingExpired(ServiceToProtoEnum(service))) {
    // We should stop recording because of the expiration time. We should
    // also inform the observers that we stopped recording.
    RunOrPostTaskOnThread(
        FROM_HERE, BrowserThread::UI,
        base::BindOnce(
            &DevToolsBackgroundServicesContextImpl::OnRecordingTimeExpired,
            weak_ptr_factory_ui_.GetWeakPtr(), ServiceToProtoEnum(service)));
    return;
  }

  devtools::proto::BackgroundServiceEvent event;
  event.set_timestamp(
      base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds());
  event.set_origin(origin.GetURL().spec());
  event.set_service_worker_registration_id(service_worker_registration_id);
  event.set_background_service(ServiceToProtoEnum(service));
  event.set_event_name(event_name);
  event.set_instance_id(instance_id);
  event.mutable_event_metadata()->insert(event_metadata.begin(),
                                         event_metadata.end());

  service_worker_context_->StoreRegistrationUserData(
      service_worker_registration_id, origin,
      {{CreateEntryKey(event.background_service()), event.SerializeAsString()}},
      base::BindOnce(&DidLogServiceEvent));

  RunOrPostTaskOnThread(
      FROM_HERE, BrowserThread::UI,
      base::BindOnce(
          &DevToolsBackgroundServicesContextImpl::NotifyEventObservers,
          weak_ptr_factory_ui_.GetWeakPtr(), std::move(event)));
}

void DevToolsBackgroundServicesContextImpl::NotifyEventObservers(
    const devtools::proto::BackgroundServiceEvent& event) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  for (EventObserver& observer : observers_)
    observer.OnEventReceived(event);
}

void DevToolsBackgroundServicesContextImpl::OnRecordingTimeExpired(
    devtools::proto::BackgroundService service) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // This could have been stopped by the user in the meanwhile, or we
  // received duplicate time expiry events.
  if (IsRecordingExpired(service))
    StopRecording(service);
}

}  // namespace content
