// Copyright 2017 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/feature_engagement/internal/feature_config_condition_validator.h"

#include <algorithm>
#include <optional>
#include <string>
#include <vector>

#include "base/check.h"
#include "base/feature_list.h"
#include "base/notreached.h"
#include "components/feature_engagement/internal/availability_model.h"
#include "components/feature_engagement/internal/display_lock_controller.h"
#include "components/feature_engagement/internal/event_model_reader.h"
#include "components/feature_engagement/internal/proto/feature_event.pb.h"
#include "components/feature_engagement/internal/time_provider.h"
#include "components/feature_engagement/public/configuration.h"
#include "components/feature_engagement/public/feature_list.h"

namespace feature_engagement {

FeatureConfigConditionValidator::FeatureConfigConditionValidator() = default;

FeatureConfigConditionValidator::~FeatureConfigConditionValidator() = default;

ConditionValidator::Result FeatureConfigConditionValidator::MeetsConditions(
    const base::Feature& feature,
    const FeatureConfig& config,
    const std::vector<GroupConfig>& group_configs,
    const EventModelReader& event_model_reader,
    const AvailabilityModel& availability_model,
    const DisplayLockController& display_lock_controller,
    const Configuration* configuration,
    const TimeProvider& time_provider) const {
  uint32_t current_day = time_provider.GetCurrentDay();
  ConditionValidator::Result result(true);
  result.event_model_ready_ok = event_model_reader.IsReady();
  result.currently_showing_ok = !IsBlocked(feature, config, configuration);
  result.feature_enabled_ok = base::FeatureList::IsEnabled(feature);
  result.config_ok = config.valid;
  result.used_ok =
      EventConfigMeetsConditions(config.used, event_model_reader, current_day);
  result.trigger_ok = EventConfigMeetsConditions(
      config.trigger, event_model_reader, current_day);

  for (const auto& event_config : config.event_configs) {
    result.preconditions_ok &= EventConfigMeetsConditions(
        event_config, event_model_reader, current_day);
  }

  result.session_rate_ok =
      SessionRateMeetsConditions(config.session_rate, feature);

  result.availability_model_ready_ok = availability_model.IsReady();

  result.availability_ok = AvailabilityMeetsConditions(
      feature, config.availability, availability_model, current_day);

  result.display_lock_ok = !display_lock_controller.IsDisplayLocked();

  result.snooze_expiration_ok =
      !event_model_reader.IsSnoozeDismissed(config.trigger.name) &&
      (event_model_reader.GetLastSnoozeTimestamp(config.trigger.name) <
       time_provider.Now() - base::Days(config.snooze_params.snooze_interval));

  result.priority_notification_ok =
      !pending_priority_notification_.has_value() ||
      pending_priority_notification_.value() == feature.name;

  result.should_show_snooze = result.snooze_expiration_ok &&
                              event_model_reader.GetSnoozeCount(
                                  config.trigger.name, config.trigger.window,
                                  current_day) < config.snooze_params.max_limit;

  // Add on group additions
  for (const auto& group_config : group_configs) {
    bool valid = group_config.valid;
    result.config_ok &= valid;
    result.groups_ok &= valid;

    bool trigger_ok = EventConfigMeetsConditions(
        group_config.trigger, event_model_reader, current_day);
    result.trigger_ok &= trigger_ok;
    result.groups_ok &= trigger_ok;

    for (const auto& event_config : group_config.event_configs) {
      bool precondition_ok = EventConfigMeetsConditions(
          event_config, event_model_reader, current_day);
      result.preconditions_ok &= precondition_ok;
      result.groups_ok &= precondition_ok;
    }

    bool session_rate_ok =
        SessionRateMeetsConditions(group_config.session_rate, feature);
    result.session_rate_ok &= session_rate_ok;
    result.groups_ok &= session_rate_ok;
  }

  return result;
}

void FeatureConfigConditionValidator::NotifyIsShowing(
    const base::Feature& feature,
    const FeatureConfig& config,
    const std::vector<std::string>& all_feature_names) {
  DCHECK(base::FeatureList::IsEnabled(feature));

  currently_showing_features_.insert(feature.name);

  switch (config.session_rate_impact.type) {
    case SessionRateImpact::Type::ALL:
      for (const std::string& feature_name : all_feature_names) {
        ++times_shown_for_feature_[feature_name];
      }
      break;
    case SessionRateImpact::Type::NONE:
      // Intentionally ignore, since no features should be impacted.
      break;
    case SessionRateImpact::Type::EXPLICIT:
      DCHECK(config.session_rate_impact.affected_features.has_value());
      for (const std::string& feature_name :
           config.session_rate_impact.affected_features.value()) {
        DCHECK(std::ranges::contains(all_feature_names, feature_name));
        ++times_shown_for_feature_[feature_name];
      }
      break;
    default:
      // All cases should be covered.
      NOTREACHED();
  }
}

void FeatureConfigConditionValidator::NotifyDismissed(
    const base::Feature& feature) {
  currently_showing_features_.erase(feature.name);
}

bool FeatureConfigConditionValidator::EventConfigMeetsConditions(
    const EventConfig& event_config,
    const EventModelReader& event_model_reader,
    uint32_t current_day) const {
  uint32_t event_count = event_model_reader.GetEventCount(
      event_config.name, current_day, event_config.window);
  return event_config.comparator.MeetsCriteria(event_count);
}

void FeatureConfigConditionValidator::SetPriorityNotification(
    const std::optional<std::string>& feature) {
  DCHECK(!pending_priority_notification_.has_value() || !feature.has_value());
  pending_priority_notification_ = feature;
}

std::optional<std::string>
FeatureConfigConditionValidator::GetPendingPriorityNotification() {
  return pending_priority_notification_;
}

void FeatureConfigConditionValidator::ResetSession() {
  times_shown_for_feature_.clear();
}

bool FeatureConfigConditionValidator::AvailabilityMeetsConditions(
    const base::Feature& feature,
    Comparator comparator,
    const AvailabilityModel& availability_model,
    uint32_t current_day) const {
  if (comparator.type == ANY) {
    return true;
  }

  std::optional<uint32_t> availability_day =
      availability_model.GetAvailability(feature);
  if (!availability_day.has_value()) {
    return false;
  }

  uint32_t days_available = current_day - availability_day.value();

  // Ensure that availability days never wrap around.
  if (availability_day.value() > current_day) {
    days_available = 0u;
  }

  return comparator.MeetsCriteria(days_available);
}

bool FeatureConfigConditionValidator::SessionRateMeetsConditions(
    const Comparator session_rate,
    const base::Feature& feature) const {
  const auto it = times_shown_for_feature_.find(feature.name);
  if (it == times_shown_for_feature_.end()) {
    return session_rate.MeetsCriteria(0u);
  }
  return session_rate.MeetsCriteria(it->second);
}

bool FeatureConfigConditionValidator::IsBlocked(
    const base::Feature& feature,
    const FeatureConfig& config,
    const Configuration* configuration) const {
  // A feature is always blocked from triggering if it is already showing,
  // regardless of its BlockedBy or Blocking configuration.
  if (currently_showing_features_.contains(feature.name)) {
    return true;
  }

  switch (config.blocked_by.type) {
    case BlockedBy::Type::NONE:
      return false;

    case BlockedBy::Type::ALL: {
      bool is_blocked = false;
      for (const std::string& currently_showing_feature :
           currently_showing_features_) {
        auto currently_showing_feature_config =
            configuration->GetFeatureConfigByName(currently_showing_feature);
        if (currently_showing_feature_config.blocking.type ==
            Blocking::Type::NONE) {
          continue;
        }
        is_blocked = true;
      }
      return is_blocked;
    }
    case BlockedBy::Type::EXPLICIT:
      for (const std::string& feature_name :
           *config.blocked_by.affected_features) {
        if (currently_showing_features_.contains(feature_name)) {
          return true;
        }
      }
      return false;
    default:
      // All cases should be covered.
      NOTREACHED();
  }
}

}  // namespace feature_engagement
