// Copyright 2023 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/ui/safety_hub/menu_notification.h"

#include <memory>
#include <string>

#include "base/json/values_util.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/ui/safety_hub/extensions_result.h"
#include "chrome/browser/ui/safety_hub/notification_permission_review_service.h"
#include "chrome/browser/ui/safety_hub/password_status_check_result.h"
#include "chrome/browser/ui/safety_hub/safe_browsing_result.h"
#include "chrome/browser/ui/safety_hub/safety_hub_constants.h"
#include "chrome/browser/ui/safety_hub/safety_hub_result.h"

SafetyHubMenuNotification::SafetyHubMenuNotification(
    safety_hub::SafetyHubModuleType type)
    : first_impression_time_(std::nullopt),
      last_impression_time_(std::nullopt),
      show_only_after_(std::nullopt),
      module_type_(type) {}

SafetyHubMenuNotification::~SafetyHubMenuNotification() = default;

SafetyHubMenuNotification::SafetyHubMenuNotification(
    const base::DictValue& dict,
    safety_hub::SafetyHubModuleType type)
    : module_type_(type) {
  is_currently_active_ =
      dict.FindBool(safety_hub::kSafetyHubMenuNotificationActiveKey).value();
  impression_count_ =
      dict.FindInt(safety_hub::kSafetyHubMenuNotificationImpressionCountKey)
          .value_or(0);
  all_time_notification_count_ =
      dict.FindInt(safety_hub::kSafetyHubMenuNotificationAllTimeCountKey)
          .value_or(0);
  first_impression_time_ = base::ValueToTime(
      dict.Find(safety_hub::kSafetyHubMenuNotificationFirstImpressionKey));
  last_impression_time_ = base::ValueToTime(
      dict.Find(safety_hub::kSafetyHubMenuNotificationLastImpressionKey));
  show_only_after_ = base::ValueToTime(
      dict.Find(safety_hub::kSafetyHubMenuNotificationShowAfterTimeKey));
  if (dict.contains(safety_hub::kSafetyHubMenuNotificationResultKey)) {
    prev_stored_result_ =
        dict.FindDict(safety_hub::kSafetyHubMenuNotificationResultKey)->Clone();
  }
}

base::DictValue SafetyHubMenuNotification::ToDictValue() const {
  base::DictValue result;
  result.Set(safety_hub::kSafetyHubMenuNotificationActiveKey,
             is_currently_active_);
  if (impression_count_ != 0) {
    result.Set(safety_hub::kSafetyHubMenuNotificationImpressionCountKey,
               impression_count_);
  }
  if (first_impression_time_.has_value()) {
    result.Set(safety_hub::kSafetyHubMenuNotificationFirstImpressionKey,
               base::TimeToValue(first_impression_time_.value()));
  }
  if (last_impression_time_.has_value()) {
    result.Set(safety_hub::kSafetyHubMenuNotificationLastImpressionKey,
               base::TimeToValue(last_impression_time_.value()));
  }
  if (all_time_notification_count_ != 0) {
    result.Set(safety_hub::kSafetyHubMenuNotificationAllTimeCountKey,
               all_time_notification_count_);
  }
  if (show_only_after_.has_value()) {
    result.Set(safety_hub::kSafetyHubMenuNotificationShowAfterTimeKey,
               base::TimeToValue(show_only_after_.value()));
  }
  if (current_result_ != nullptr) {
    result.Set(safety_hub::kSafetyHubMenuNotificationResultKey,
               current_result_->ToDictValue());
  }
  return result;
}

void SafetyHubMenuNotification::Show() {
  ++impression_count_;
  if (!first_impression_time_.has_value()) {
    is_currently_active_ = true;
    should_be_shown_after_interval_ = false;
    first_impression_time_ = base::Time::Now();
  }
  last_impression_time_ = base::Time::Now();
}

void SafetyHubMenuNotification::Dismiss() {
  is_currently_active_ = false;
  impression_count_ = 0;
  first_impression_time_ = std::nullopt;
  ++all_time_notification_count_;
}

// The maximum all time impressions for a notification are passed on each call
// instead of determined in the constructor to make these easier
// Finch-configurable as the constructor will only called once initially for
// each Safety Hub module, after which it will always be read from disk.
bool SafetyHubMenuNotification::ShouldBeShown(
    base::TimeDelta interval,
    int max_all_time_impressions) const {
  // The type of notification has already been shown the maximum number of times
  // in the total lifetime.
  if (max_all_time_impressions > 0 &&
      all_time_notification_count_ >= max_all_time_impressions) {
    return false;
  }

  // There is no associated result, or the result does not meet the bar for menu
  // notifications.
  if (!current_result_ || !current_result_->IsTriggerForMenuNotification()) {
    return false;
  }

  // Do not show notification if it is too soon.
  if (show_only_after_.has_value() &&
      base::Time::Now() < show_only_after_.value()) {
    return false;
  }

  // Notifications that have never been shown can be shown as long as the result
  // is a trigger.
  if (!HasAnyNotificationBeenShown()) {
    return true;
  }

  // For active notifications, the notification should be shown if it is either
  // not shown enough times, or not sufficiently long enough.
  if (is_currently_active_) {
    return (!IsShownEnough());
  }

  // For notifications that are inactive, showing the notification is determined
  // by whether the interval has passed.
  return should_be_shown_after_interval_ && HasIntervalPassed(interval);
}

bool SafetyHubMenuNotification::IsCurrentlyActive() const {
  return is_currently_active_;
}

bool SafetyHubMenuNotification::IsShownEnough() const {
  // The notification has never been shown before.
  if (!first_impression_time_.has_value() ||
      !last_impression_time_.has_value()) {
    return false;
  }

  bool isShownEnoughDays =
      (base::Time::Now() - first_impression_time_.value()) >=
      kSafetyHubMenuNotificationMinNotificationDuration;

  bool isShownEnoughTimes =
      impression_count_ >= kSafetyHubMenuNotificationMinImpressionCount;

  return isShownEnoughDays && isShownEnoughTimes;
}

bool SafetyHubMenuNotification::HasIntervalPassed(
    base::TimeDelta interval) const {
  // Notification has never been shown, so no interval in this case.
  if (!HasAnyNotificationBeenShown()) {
    return true;
  }
  return base::Time::Now() - last_impression_time_.value() >= interval;
}

bool SafetyHubMenuNotification::HasAnyNotificationBeenShown() const {
  return last_impression_time_.has_value();
}

void SafetyHubMenuNotification::UpdateResult(
    std::unique_ptr<SafetyHubResult> new_result) {
  // Use the latest available result. This is either the current result when a
  // new result was received, or, if it is unavailble, the result that was
  // stored on the disk.
  base::DictValue previous_result_dict = current_result_
                                             ? current_result_->ToDictValue()
                                             : prev_stored_result_.Clone();
  // For notifications that are not currently active yet, and have a previous
  // result, we have to determine whether the notification should be shown after
  // the interval by comparing the old (stored) data with the new data to check
  // whether it warrants a new notification.
  if (!is_currently_active_ && !previous_result_dict.empty() &&
      new_result->WarrantsNewMenuNotification(previous_result_dict)) {
    should_be_shown_after_interval_ = true;
  }
  current_result_ = std::move(new_result);
}

std::u16string SafetyHubMenuNotification::GetNotificationString() const {
  CHECK(current_result_);
  return current_result_->GetNotificationString();
}

int SafetyHubMenuNotification::GetNotificationCommandId() const {
  CHECK(current_result_);
  return current_result_->GetNotificationCommandId();
}

SafetyHubResult* SafetyHubMenuNotification::GetResultForTesting() const {
  return current_result_.get();
}

void SafetyHubMenuNotification::SetOnlyShowAfter(base::Time time) {
  show_only_after_ = time;
}

void SafetyHubMenuNotification::ResetAllTimeNotificationCount() {
  all_time_notification_count_ = 0;
}

safety_hub::SafetyHubModuleType SafetyHubMenuNotification::GetModuleType()
    const {
  return module_type_;
}
