// 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/enterprise/data_controls/dlp_reporting_manager.h"

#include <memory>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "build/build_config.h"
#include "components/enterprise/common/proto/synced/dlp_policy_event.pb.h"
#include "components/enterprise/data_controls/core/browser/dlp_histogram_helper.h"
#include "components/reporting/client/report_queue.h"
#include "components/reporting/client/report_queue_configuration.h"
#include "components/reporting/client/report_queue_factory.h"
#include "components/reporting/proto/synced/record_constants.pb.h"
#include "components/reporting/util/status.h"
#include "content/public/common/content_constants.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

namespace data_controls {
// TODO(1187477, marcgrimme): revisit if this should be refactored.
DlpPolicyEvent_Mode RuleLevel2DlpEventMode(
    Rule::Level level) {
  switch (level) {
    case Rule::Level::kBlock:
      return DlpPolicyEvent_Mode_BLOCK;
    case Rule::Level::kWarn:
      return DlpPolicyEvent_Mode_WARN;
    case Rule::Level::kReport:
      return DlpPolicyEvent_Mode_REPORT;
    case Rule::Level::kNotSet:
    case Rule::Level::kAllow:
      return DlpPolicyEvent_Mode_UNDEFINED_MODE;
  }
}

// TODO(1187477, marcgrimme): revisit if this should be refactored.
DlpPolicyEvent_Restriction RuleRestriction2DlpEventRestriction(
    Rule::Restriction restriction) {
  switch (restriction) {
    case Rule::Restriction::kPrinting:
      return DlpPolicyEvent_Restriction_PRINTING;
    case Rule::Restriction::kScreenshot:
      return DlpPolicyEvent_Restriction_SCREENSHOT;
    case Rule::Restriction::kScreenShare:
      return DlpPolicyEvent_Restriction_SCREENCAST;
    case Rule::Restriction::kPrivacyScreen:
      return DlpPolicyEvent_Restriction_EPRIVACY;
    case Rule::Restriction::kClipboard:
      return DlpPolicyEvent_Restriction_CLIPBOARD;
    case Rule::Restriction::kFiles:
      return DlpPolicyEvent_Restriction_FILES;
    case Rule::Restriction::kUnknownRestriction:
    case Rule::Restriction::kFileDownload:
      return DlpPolicyEvent_Restriction_UNDEFINED_RESTRICTION;
  }
}

// TODO(1187477, marcgrimme): revisit if this should be refactored.
Rule::Restriction DlpEventRestriction2RuleRestriction(
    DlpPolicyEvent_Restriction restriction) {
  switch (restriction) {
    case DlpPolicyEvent_Restriction_PRINTING:
      return Rule::Restriction::kPrinting;
    case DlpPolicyEvent_Restriction_SCREENSHOT:
      return Rule::Restriction::kScreenshot;
    case DlpPolicyEvent_Restriction_SCREENCAST:
      return Rule::Restriction::kScreenShare;
    case DlpPolicyEvent_Restriction_EPRIVACY:
      return Rule::Restriction::kPrivacyScreen;
    case DlpPolicyEvent_Restriction_CLIPBOARD:
      return Rule::Restriction::kClipboard;
    case DlpPolicyEvent_Restriction_FILES:
      return Rule::Restriction::kFiles;
    case DlpPolicyEvent_Restriction_UNDEFINED_RESTRICTION:
      return Rule::Restriction::kUnknownRestriction;
    case DlpPolicyEvent_Restriction_DlpPolicyEvent_Restriction_INT_MIN_SENTINEL_DO_NOT_USE_:
    case DlpPolicyEvent_Restriction_DlpPolicyEvent_Restriction_INT_MAX_SENTINEL_DO_NOT_USE_:
      NOTREACHED();
  }
}

DlpPolicyEvent_UserType GetCurrentUserType() {
#if BUILDFLAG(IS_CHROMEOS)
  // Could be not initialized in tests.
  if (!user_manager::UserManager::IsInitialized() ||
      !user_manager::UserManager::Get()->GetPrimaryUser()) {
    return DlpPolicyEvent_UserType_UNDEFINED_USER_TYPE;
  }
  const user_manager::User* const user =
      user_manager::UserManager::Get()->GetPrimaryUser();
  DCHECK(user);
  switch (user->GetType()) {
    case user_manager::UserType::kRegular:
      return DlpPolicyEvent_UserType_REGULAR;
    case user_manager::UserType::kPublicAccount:
      return DlpPolicyEvent_UserType_MANAGED_GUEST;
    case user_manager::UserType::kKioskChromeApp:
    case user_manager::UserType::kKioskWebApp:
    case user_manager::UserType::kKioskIWA:
    case user_manager::UserType::kKioskArcvmApp:
      return DlpPolicyEvent_UserType_KIOSK;
    case user_manager::UserType::kGuest:
    case user_manager::UserType::kChild:
      return DlpPolicyEvent_UserType_UNDEFINED_USER_TYPE;
  }
#else
  // TODO(b/303640183): Revisit what this should return for non-CrOS platforms.
  return DlpPolicyEvent_UserType_UNDEFINED_USER_TYPE;
#endif  // BUILDFLAG(IS_CHROMEOS)
}

// static
std::unique_ptr<DlpPolicyEventBuilder> DlpPolicyEventBuilder::Event(
    const std::string& src_url,
    const std::string& rule_name,
    const std::string& rule_id,
    Rule::Restriction restriction,
    Rule::Level level) {
  std::unique_ptr<DlpPolicyEventBuilder> event_builder(
      new DlpPolicyEventBuilder());
  event_builder->SetSourceUrl(src_url);
  event_builder->SetRestriction(restriction);
  event_builder->event.set_mode(RuleLevel2DlpEventMode(level));

  if (!rule_name.empty()) {
    event_builder->event.set_triggered_rule_name(rule_name);
  }
  if (!rule_id.empty()) {
    event_builder->event.set_triggered_rule_id(rule_id);
  }
  return event_builder;
}

// static
std::unique_ptr<DlpPolicyEventBuilder>
DlpPolicyEventBuilder::WarningProceededEvent(const std::string& src_url,
                                             const std::string& rule_name,
                                             const std::string& rule_id,
                                             Rule::Restriction restriction) {
  std::unique_ptr<DlpPolicyEventBuilder> event_builder(
      new DlpPolicyEventBuilder());
  event_builder->SetSourceUrl(src_url);
  event_builder->SetRestriction(restriction);
  event_builder->event.set_mode(DlpPolicyEvent_Mode_WARN_PROCEED);
  return event_builder;
}

DlpPolicyEventBuilder::DlpPolicyEventBuilder() {
  int64_t timestamp_micro =
      (base::Time::Now() - base::Time::UnixEpoch()).InMicroseconds();
  event.set_timestamp_micro(timestamp_micro);
  event.set_user_type(GetCurrentUserType());
}

void DlpPolicyEventBuilder::SetDestinationUrl(const std::string& dst_url) {
  DlpPolicyEventDestination* event_destination = new DlpPolicyEventDestination;
  event_destination->set_url(dst_url.substr(0, content::kMaxURLDisplayChars));
  event.set_allocated_destination(event_destination);
}

#if BUILDFLAG(IS_CHROMEOS)
void DlpPolicyEventBuilder::SetDestinationComponent(
    Component dst_component) {
  DlpPolicyEventDestination* event_destination = new DlpPolicyEventDestination;
  switch (dst_component) {
    case (Component::kArc):
      event_destination->set_component(DlpPolicyEventDestination_Component_ARC);
      break;
    case (Component::kCrostini):
      event_destination->set_component(
          DlpPolicyEventDestination_Component_CROSTINI);
      break;
    case (Component::kPluginVm):
      event_destination->set_component(
          DlpPolicyEventDestination_Component_PLUGIN_VM);
      break;
    case (Component::kUsb):
      event_destination->set_component(DlpPolicyEventDestination_Component_USB);
      break;
    case (Component::kDrive):
      event_destination->set_component(
          DlpPolicyEventDestination_Component_DRIVE);
      break;
    case (Component::kOneDrive):
      event_destination->set_component(
          DlpPolicyEventDestination_Component_ONEDRIVE);
      break;
    case (Component::kUnknownComponent):
      event_destination->set_component(
          DlpPolicyEventDestination_Component_UNDEFINED_COMPONENT);
      break;
  }
  event.set_allocated_destination(event_destination);
}
#endif  // BUILDFLAG(IS_CHROMEOS)

void DlpPolicyEventBuilder::SetContentName(const std::string& content_name) {
  event.set_content_name(content_name);
}

DlpPolicyEvent DlpPolicyEventBuilder::Create() {
  return event;
}

void DlpPolicyEventBuilder::SetSourceUrl(const std::string& src_url) {
  DlpPolicyEventSource* event_source = new DlpPolicyEventSource;
  event_source->set_url(src_url.substr(0, content::kMaxURLDisplayChars));
  event.set_allocated_source(event_source);
}

void DlpPolicyEventBuilder::SetRestriction(
    Rule::Restriction restriction) {
  event.set_restriction(
      RuleRestriction2DlpEventRestriction(restriction));
}

DlpPolicyEvent CreateDlpPolicyEvent(const std::string& src_url,
                                    Rule::Restriction restriction,
                                    const std::string& rule_name,
                                    const std::string& rule_id,
                                    Rule::Level level) {
  auto event_builder = DlpPolicyEventBuilder::Event(src_url, rule_name, rule_id,
                                                    restriction, level);
  return event_builder->Create();
}

DlpPolicyEvent CreateDlpPolicyEvent(const std::string& src_url,
                                    const std::string& dst_url,
                                    Rule::Restriction restriction,
                                    const std::string& rule_name,
                                    const std::string& rule_id,
                                    Rule::Level level) {
  auto event_builder = DlpPolicyEventBuilder::Event(src_url, rule_name, rule_id,
                                                    restriction, level);
  event_builder->SetDestinationUrl(dst_url);
  return event_builder->Create();
}

#if BUILDFLAG(IS_CHROMEOS)
DlpPolicyEvent CreateDlpPolicyEvent(const std::string& src_url,
                                    Component dst_component,
                                    Rule::Restriction restriction,
                                    const std::string& rule_name,
                                    const std::string& rule_id,
                                    Rule::Level level) {
  auto event_builder = DlpPolicyEventBuilder::Event(src_url, rule_name, rule_id,
                                                    restriction, level);
  event_builder->SetDestinationComponent(dst_component);

  return event_builder->Create();
}
#endif  // BUILDFLAG(IS_CHROMEOS)

DlpReportingManager::DlpReportingManager()
    : report_queue_(
          ::reporting::ReportQueueFactory::CreateSpeculativeReportQueue([]() {
            ::reporting::SourceInfo source_info;
            source_info.set_source(::reporting::SourceInfo::ASH);
            return ::reporting::ReportQueueConfiguration::Create(
                       {.event_type = ::reporting::EventType::kUser,
                        .destination = ::reporting::Destination::DLP_EVENTS})
                .SetSourceInfo(std::move(source_info));
          }())) {}

DlpReportingManager::~DlpReportingManager() = default;

void DlpReportingManager::SetReportQueueForTest(
    std::unique_ptr<::reporting::ReportQueue, base::OnTaskRunnerDeleter>
        report_queue) {
  report_queue_.reset();
  report_queue_ = std::move(report_queue);
}

void DlpReportingManager::ReportEvent(const std::string& src_url,
                                      Rule::Restriction restriction,
                                      Rule::Level level,
                                      const std::string& rule_name,
                                      const std::string& rule_id) {
  auto event =
      CreateDlpPolicyEvent(src_url, restriction, rule_name, rule_id, level);
  ReportEvent(std::move(event));
}

void DlpReportingManager::ReportEvent(const std::string& src_url,
                                      const std::string& dst_url,
                                      Rule::Restriction restriction,
                                      Rule::Level level,
                                      const std::string& rule_name,
                                      const std::string& rule_id) {
  auto event = CreateDlpPolicyEvent(src_url, dst_url, restriction, rule_name,
                                    rule_id, level);
  ReportEvent(std::move(event));
}

#if BUILDFLAG(IS_CHROMEOS)
void DlpReportingManager::ReportEvent(const std::string& src_url,
                                      const Component dst_component,
                                      Rule::Restriction restriction,
                                      Rule::Level level,
                                      const std::string& rule_name,
                                      const std::string& rule_id) {
  auto event = CreateDlpPolicyEvent(src_url, dst_component, restriction,
                                    rule_name, rule_id, level);
  ReportEvent(std::move(event));
}
#endif  // BUILDFLAG(IS_CHROMEOS)

void DlpReportingManager::OnEventEnqueued(reporting::Status status) {
  if (!status.ok()) {
    VLOG(1) << "Could not enqueue event to DLP reporting queue because of "
            << status;
  }
  events_reported_++;
  base::UmaHistogramEnumeration(GetDlpHistogramPrefix() +
                                    dlp::kReportedEventStatus,
                                status.code(),
                                reporting::error::Code::MAX_VALUE);
}

void DlpReportingManager::ReportEvent(DlpPolicyEvent event) {
  // TODO(1187506, marcgrimme) Refactor to handle gracefully with user
  // interaction when queue is not ready.
  DlpBooleanHistogram(
      dlp::kErrorsReportQueueNotReady, !report_queue_.get());
  if (!report_queue_.get()) {
    DLOG(WARNING) << "Report queue could not be initialized. DLP reporting "
                     "functionality will be disabled.";
    return;
  }

  for (auto& observer : observers_) {
    observer.OnReportEvent(event);
  }

  reporting::ReportQueue::EnqueueCallback callback = base::BindOnce(
      &DlpReportingManager::OnEventEnqueued, weak_factory_.GetWeakPtr());

  switch (event.mode()) {
    case DlpPolicyEvent_Mode_BLOCK:
      base::UmaHistogramEnumeration(
          GetDlpHistogramPrefix() +
              dlp::kReportedBlockLevelRestriction,
          DlpEventRestriction2RuleRestriction(event.restriction()));
      break;
    case DlpPolicyEvent_Mode_REPORT:
      base::UmaHistogramEnumeration(
          GetDlpHistogramPrefix() +
              dlp::kReportedReportLevelRestriction,
          DlpEventRestriction2RuleRestriction(event.restriction()));
      break;
    case DlpPolicyEvent_Mode_WARN:
      base::UmaHistogramEnumeration(
          GetDlpHistogramPrefix() +
              dlp::kReportedWarnLevelRestriction,
          DlpEventRestriction2RuleRestriction(event.restriction()));
      break;
    case DlpPolicyEvent_Mode_WARN_PROCEED:
      base::UmaHistogramEnumeration(
          GetDlpHistogramPrefix() +
              dlp::kReportedWarnProceedLevelRestriction,
          DlpEventRestriction2RuleRestriction(event.restriction()));
      break;
    case DlpPolicyEvent_Mode_UNDEFINED_MODE:
    case DlpPolicyEvent_Mode_DlpPolicyEvent_Mode_INT_MIN_SENTINEL_DO_NOT_USE_:
    case DlpPolicyEvent_Mode_DlpPolicyEvent_Mode_INT_MAX_SENTINEL_DO_NOT_USE_:
      NOTREACHED();
  }
  report_queue_->Enqueue(std::make_unique<DlpPolicyEvent>(std::move(event)),
                         reporting::Priority::SLOW_BATCH, std::move(callback));
  VLOG(1) << "DLP event sent to reporting infrastructure.";
}

void DlpReportingManager::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void DlpReportingManager::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

}  // namespace data_controls
