// 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/desktop_to_mobile_promos/promos_utils.h"

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

#include "base/feature_list.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/desktop_to_mobile_promos/promos_pref_names.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "components/desktop_to_mobile_promos/pref_names.h"
#include "components/desktop_to_mobile_promos/promos_types.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/search/ntp_features.h"
#include "components/segmentation_platform/embedder/default_model/device_switcher_model.h"
#include "components/sync/base/data_type.h"
#include "components/sync/service/sync_service.h"

namespace promos_utils {

using desktop_to_mobile_promos::PromoType;

// Max impression count per user, per promo for the iOS desktop promos on
// desktop.
constexpr int kiOSDesktopPromoMaxImpressionCount = 3;

// Total impression count per user in their lifetime, for all iOS desktop
// promos.
constexpr int kiOSDesktopPromoTotalImpressionCount = 10;

// Total amount of opt-outs across any Desktop to iOS promo to block impressions
// of other instances of Desktop to iOS promos, per user.
constexpr int kiOSDesktopPromoTotalOptOuts = 2;

// Minimum time threshold between impressions for a given user to see the iOS
// desktop promo on desktop.
constexpr base::TimeDelta kiOSDesktopPromoCooldownTime = base::Days(90);

// IOSDesktopPromoHistogramType returns the promo histogram type for the given
// promo type. New promos should add themselves to this check.
std::string IOSDesktopPromoHistogramType(PromoType promo_type) {
  switch (promo_type) {
    case PromoType::kPassword:
      return "PasswordPromo";
    case PromoType::kAddress:
      return "AddressPromo";
    case PromoType::kPayment:
      return "PaymentPromo";
    case PromoType::kEnhancedBrowsing:
      return "EnhancedBrowsingPromo";
    case PromoType::kLens:
      return "LensPromo";
    case PromoType::kTabGroups:
      return "TabGroupsPromo";
    case PromoType::kPriceTracking:
      return "PriceTrackingPromo";
  }
}

base::Time GetMostRecentiOSDesktopNtpPromoTimestamp(PrefService* pref_service) {
  const base::ListValue& timestamps = pref_service->GetList(
      promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps);
  base::ListValue::const_iterator most_recent_timestamp_iter =
      std::max_element(timestamps.begin(), timestamps.end());
  if (most_recent_timestamp_iter == timestamps.end()) {
    return base::Time();
  }
  return base::ValueToTime(*most_recent_timestamp_iter).value_or(base::Time());
}

// VerifyIOSDesktopPromoTotalImpressions ensures that each individual user sees
// no more than a maximum of these promos total in their lifetime. New promos
// should add themselves to this check. If `skip_ntp_promo` is true, then this
// call will not include ntp promo data in its checks. This allows calling code
// from the NTP promo to add special logic.
bool VerifyIOSDesktopPromoTotalImpressions(Profile* profile,
                                           bool skip_ntp_promo = false) {
  int total_desktop_promo_impressions =
      profile->GetPrefs()->GetInteger(
          promos_prefs::kDesktopToiOSPasswordPromoImpressionsCounter) +
      profile->GetPrefs()->GetInteger(
          promos_prefs::kDesktopToiOSAddressPromoImpressionsCounter) +
      profile->GetPrefs()->GetInteger(
          promos_prefs::kDesktopToiOSPaymentPromoImpressionsCounter) +
      profile->GetPrefs()->GetInteger(
          promos_prefs::kDesktopToiOSEnhancedBrowsingPromoImpressionsCounter) +
      profile->GetPrefs()->GetInteger(
          promos_prefs::kDesktopToiOSLensPromoImpressionsCounter) +
      profile->GetPrefs()->GetInteger(
          promos_prefs::kDesktopToiOSTabGroupsPromoImpressionsCounter) +
      profile->GetPrefs()->GetInteger(
          promos_prefs::kDesktopToiOSPriceTrackingPromoImpressionsCounter);

  if (!skip_ntp_promo) {
    // The Desktop NTP promo shows 10 times in quick succession, but that only
    // counts as 1 impression for Desktop to iOS promos in general.
    const base::ListValue& ntp_promo_appearances = profile->GetPrefs()->GetList(
        promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps);
    total_desktop_promo_impressions += (ntp_promo_appearances.empty()) ? 0 : 1;
  }

  return total_desktop_promo_impressions < kiOSDesktopPromoTotalImpressionCount;
}

// VerifyIOSDesktopPromoTotalOptOuts verifies that a user hasn't opted-out of
// seeing more than the allowed amount of opt-outs for all iOS Desktop promos.
// New promos should add themselves to this check.
bool VerifyIOSDesktopPromoTotalOptOuts(Profile* profile) {
  std::vector<bool> promo_opt_outs = {
      profile->GetPrefs()->GetBoolean(
          promos_prefs::kDesktopToiOSPasswordPromoOptOut),
      profile->GetPrefs()->GetBoolean(
          promos_prefs::kDesktopToiOSAddressPromoOptOut),
      profile->GetPrefs()->GetBoolean(
          promos_prefs::kDesktopToiOSPaymentPromoOptOut),
      profile->GetPrefs()->GetBoolean(
          promos_prefs::kDesktopToiOSEnhancedBrowsingPromoOptOut),
      profile->GetPrefs()->GetBoolean(
          promos_prefs::kDesktopToiOSLensPromoOptOut),
      profile->GetPrefs()->GetBoolean(
          promos_prefs::kDesktopToiOSTabGroupsPromoOptOut),
      profile->GetPrefs()->GetBoolean(
          promos_prefs::kDesktopToiOSPriceTrackingPromoOptOut)};

  int total_desktop_promo_opt_outs_counter =
      std::count(promo_opt_outs.begin(), promo_opt_outs.end(), true);

  return total_desktop_promo_opt_outs_counter < kiOSDesktopPromoTotalOptOuts;
}

// VerifyMostRecentPromoTimestamp ensures that each individual user sees a
// Desktop to iOS promo a maximum of once per cooldown period. New promos should
// add themselves to this check. If `skip_ntp_promo` is true, then this call
// will not include ntp promo data in its checks. This allows calling code from
// the NTP promo to add special logic.
bool VerifyMostRecentPromoTimestamp(Profile* profile,
                                    bool skip_ntp_promo = false) {
  std::vector<base::Time> promos_timestamps = {
      profile->GetPrefs()->GetTime(
          promos_prefs::kDesktopToiOSPasswordPromoLastImpressionTimestamp),
      profile->GetPrefs()->GetTime(
          promos_prefs::kDesktopToiOSAddressPromoLastImpressionTimestamp),
      profile->GetPrefs()->GetTime(
          promos_prefs::kDesktopToiOSPaymentPromoLastImpressionTimestamp),
      profile->GetPrefs()->GetTime(
          promos_prefs::
              kDesktopToiOSEnhancedBrowsingPromoLastImpressionTimestamp),
      profile->GetPrefs()->GetTime(
          promos_prefs::kDesktopToiOSLensPromoLastImpressionTimestamp),
      profile->GetPrefs()->GetTime(
          promos_prefs::kDesktopToiOSTabGroupsPromoLastImpressionTimestamp),
      profile->GetPrefs()->GetTime(
          promos_prefs::kDesktopToiOSPriceTrackingPromoLastImpressionTimestamp),
  };

  if (!skip_ntp_promo) {
    promos_timestamps.emplace_back(
        GetMostRecentiOSDesktopNtpPromoTimestamp(profile->GetPrefs()));
  }

  auto most_recent_promo_timestamp =
      std::max_element(promos_timestamps.begin(), promos_timestamps.end());

  return *most_recent_promo_timestamp + kiOSDesktopPromoCooldownTime <
         base::Time::Now();
}

// Verify that the user is syncing preferences (for impressions and opt-out
// tracking), and that they are syncing the specific datatype needed for a given
// promo type.
bool VerifySyncingDatatypes(const syncer::SyncService& sync_service,
                            PromoType promo_type) {
  if (!sync_service.GetActiveDataTypes().Has(syncer::PREFERENCES)) {
    return false;
  }

  switch (promo_type) {
    case PromoType::kPassword:
      return sync_service.GetActiveDataTypes().Has(syncer::PASSWORDS);
    case PromoType::kAddress:
      return sync_service.GetActiveDataTypes().Has(syncer::CONTACT_INFO);
    case PromoType::kPayment:
      return sync_service.GetActiveDataTypes().Has(
          syncer::AUTOFILL_WALLET_DATA);
    case PromoType::kEnhancedBrowsing:
    case PromoType::kLens:
    case PromoType::kTabGroups:
    case PromoType::kPriceTracking:
      // TODO(crbug.com/438769954): Verify relevant data types.
      return true;
  }
}

// Checks whether promos in general can currently be shown.
bool CanShowPromos() {
// Don't show the promo if the local state exists and `kPromotionsEnabled` is
// false (likely overridden by policy). `kPromotionsEnabled` does not exist on
// Android.
#if !BUILDFLAG(IS_ANDROID)
  PrefService* local_state = g_browser_process->local_state();
  if (local_state && !local_state->GetBoolean(prefs::kPromotionsEnabled)) {
    return false;
  }
#endif  // !BUILDFLAG(IS_ANDROID)
  return true;
}

// RecordIOSDesktopPromoShownHistogram records which impression (count) was
// shown to the user depending on the given promo type.
void RecordIOSDesktopPromoShownHistogram(PromoType promo_type,
                                         int impression_count) {
  std::string promo_histogram_type = IOSDesktopPromoHistogramType(promo_type);
  DesktopIOSPromoImpression promo_impression;
  switch (impression_count) {
    case 1:
      promo_impression = DesktopIOSPromoImpression::kFirstImpression;
      break;
    case 2:
      promo_impression = DesktopIOSPromoImpression::kSecondImpression;
      break;
    case 3:
      promo_impression = DesktopIOSPromoImpression::kThirdImpression;
      break;
    default:
      return;
  }
  base::UmaHistogramEnumeration(
      "IOS.Desktop." + promo_histogram_type + ".Shown", promo_impression);
}

// IOSPromoPrefsConfig is a complex struct that needs definition of a
// constructor, an explicit out-of-line copy constructor and a destructor. New
// promos should add themselves to this function.
IOSPromoPrefsConfig::IOSPromoPrefsConfig() = default;
IOSPromoPrefsConfig::IOSPromoPrefsConfig(const IOSPromoPrefsConfig&) = default;
IOSPromoPrefsConfig::~IOSPromoPrefsConfig() = default;

IOSPromoPrefsConfig::IOSPromoPrefsConfig(PromoType promo_type) {
  switch (promo_type) {
    case PromoType::kPassword:
#if !BUILDFLAG(IS_ANDROID)
      promo_feature = &feature_engagement::kIPHiOSPasswordPromoDesktopFeature;
#endif  // !BUILDFLAG(IS_ANDROID)
      promo_impressions_counter_pref_name =
          promos_prefs::kDesktopToiOSPasswordPromoImpressionsCounter;
      promo_opt_out_pref_name = promos_prefs::kDesktopToiOSPasswordPromoOptOut;
      promo_last_impression_timestamp_pref_name =
          promos_prefs::kDesktopToiOSPasswordPromoLastImpressionTimestamp;
      break;
    case PromoType::kAddress:
#if !BUILDFLAG(IS_ANDROID)
      promo_feature = &feature_engagement::kIPHiOSAddressPromoDesktopFeature;
#endif  // !BUILDFLAG(IS_ANDROID)
      promo_impressions_counter_pref_name =
          promos_prefs::kDesktopToiOSAddressPromoImpressionsCounter;
      promo_opt_out_pref_name = promos_prefs::kDesktopToiOSAddressPromoOptOut;
      promo_last_impression_timestamp_pref_name =
          promos_prefs::kDesktopToiOSAddressPromoLastImpressionTimestamp;
      break;
    case PromoType::kPayment:
#if !BUILDFLAG(IS_ANDROID)
      promo_feature = &feature_engagement::kIPHiOSPaymentPromoDesktopFeature;
#endif  // !BUILDFLAG(IS_ANDROID)
      promo_impressions_counter_pref_name =
          promos_prefs::kDesktopToiOSPaymentPromoImpressionsCounter;
      promo_opt_out_pref_name = promos_prefs::kDesktopToiOSPaymentPromoOptOut;
      promo_last_impression_timestamp_pref_name =
          promos_prefs::kDesktopToiOSPaymentPromoLastImpressionTimestamp;
      break;
    case PromoType::kEnhancedBrowsing:
#if !BUILDFLAG(IS_ANDROID)
      promo_feature =
          &feature_engagement::kIPHiOSEnhancedBrowsingDesktopFeature;
#endif  // !BUILDFLAG(IS_ANDROID)
      promo_impressions_counter_pref_name =
          promos_prefs::kDesktopToiOSEnhancedBrowsingPromoImpressionsCounter;
      promo_opt_out_pref_name =
          promos_prefs::kDesktopToiOSEnhancedBrowsingPromoOptOut;
      promo_last_impression_timestamp_pref_name = promos_prefs::
          kDesktopToiOSEnhancedBrowsingPromoLastImpressionTimestamp;
      break;
    case PromoType::kLens:
#if !BUILDFLAG(IS_ANDROID)
      promo_feature = &feature_engagement::kIPHiOSLensPromoDesktopFeature;
#endif  // !BUILDFLAG(IS_ANDROID)
      promo_impressions_counter_pref_name =
          promos_prefs::kDesktopToiOSLensPromoImpressionsCounter;
      promo_opt_out_pref_name = promos_prefs::kDesktopToiOSLensPromoOptOut;
      promo_last_impression_timestamp_pref_name =
          promos_prefs::kDesktopToiOSLensPromoLastImpressionTimestamp;
      break;
    case PromoType::kTabGroups:
#if !BUILDFLAG(IS_ANDROID)
      promo_feature = &feature_engagement::kIPHiOSTabGroupsDesktopFeature;
#endif  // !BUILDFLAG(IS_ANDROID)
      promo_impressions_counter_pref_name =
          promos_prefs::kDesktopToiOSTabGroupsPromoImpressionsCounter;
      promo_opt_out_pref_name = promos_prefs::kDesktopToiOSTabGroupsPromoOptOut;
      promo_last_impression_timestamp_pref_name =
          promos_prefs::kDesktopToiOSTabGroupsPromoLastImpressionTimestamp;
      break;
    case PromoType::kPriceTracking:
#if !BUILDFLAG(IS_ANDROID)
      promo_feature = &feature_engagement::kIPHiOSPriceTrackingDesktopFeature;
#endif  // !BUILDFLAG(IS_ANDROID)
      promo_impressions_counter_pref_name =
          promos_prefs::kDesktopToiOSPriceTrackingPromoImpressionsCounter;
      promo_opt_out_pref_name =
          promos_prefs::kDesktopToiOSPriceTrackingPromoOptOut;
      promo_last_impression_timestamp_pref_name =
          promos_prefs::kDesktopToiOSPriceTrackingPromoLastImpressionTimestamp;
  }
}

// Registers profile prefs. New promos should add themselves to this function.
void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterTimePref(
      promos_prefs::kDesktopToiOSPasswordPromoLastImpressionTimestamp,
      base::Time(), user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterIntegerPref(
      promos_prefs::kDesktopToiOSPasswordPromoImpressionsCounter, 0,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterBooleanPref(
      promos_prefs::kDesktopToiOSPasswordPromoOptOut, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);

  registry->RegisterTimePref(
      promos_prefs::kDesktopToiOSAddressPromoLastImpressionTimestamp,
      base::Time(), user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterIntegerPref(
      promos_prefs::kDesktopToiOSAddressPromoImpressionsCounter, 0,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterBooleanPref(
      promos_prefs::kDesktopToiOSAddressPromoOptOut, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);

  registry->RegisterTimePref(
      promos_prefs::kDesktopToiOSPaymentPromoLastImpressionTimestamp,
      base::Time(), user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterIntegerPref(
      promos_prefs::kDesktopToiOSPaymentPromoImpressionsCounter, 0,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterBooleanPref(
      promos_prefs::kDesktopToiOSPaymentPromoOptOut, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);

  registry->RegisterTimePref(
      promos_prefs::kDesktopToiOSEnhancedBrowsingPromoLastImpressionTimestamp,
      base::Time(), user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterIntegerPref(
      promos_prefs::kDesktopToiOSEnhancedBrowsingPromoImpressionsCounter, 0,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterBooleanPref(
      promos_prefs::kDesktopToiOSEnhancedBrowsingPromoOptOut, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);

  registry->RegisterTimePref(
      promos_prefs::kDesktopToiOSLensPromoLastImpressionTimestamp, base::Time(),
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterIntegerPref(
      promos_prefs::kDesktopToiOSLensPromoImpressionsCounter, 0,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterBooleanPref(
      promos_prefs::kDesktopToiOSLensPromoOptOut, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);

  registry->RegisterTimePref(
      promos_prefs::kDesktopToiOSTabGroupsPromoLastImpressionTimestamp,
      base::Time(), user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterIntegerPref(
      promos_prefs::kDesktopToiOSTabGroupsPromoImpressionsCounter, 0,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterBooleanPref(
      promos_prefs::kDesktopToiOSTabGroupsPromoOptOut, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);

  registry->RegisterTimePref(
      promos_prefs::kDesktopToiOSPriceTrackingPromoLastImpressionTimestamp,
      base::Time(), user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterIntegerPref(
      promos_prefs::kDesktopToiOSPriceTrackingPromoImpressionsCounter, 0,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterBooleanPref(
      promos_prefs::kDesktopToiOSPriceTrackingPromoOptOut, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);

  registry->RegisterListPref(
      promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps, {},
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterBooleanPref(
      promos_prefs::kDesktopToiOSNtpPromoDismissed, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
  registry->RegisterDictionaryPref(
      prefs::kIOSPromoReminder,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
}

const base::Feature& GetIOSDesktopPromoFeatureEngagement(PromoType promo_type) {
  IOSPromoPrefsConfig promo_prefs(promo_type);
  return *promo_prefs.promo_feature;
}

// RecordIOSDesktopPromoUserInteractionHistogram records which impression
// (count) depending on the promo type.
void RecordIOSDesktopPromoUserInteractionHistogram(
    PromoType promo_type,
    int impression_count,
    DesktopIOSPromoAction action) {
  std::string promo_histogram_type = IOSDesktopPromoHistogramType(promo_type);
  if (impression_count == 1) {
    base::UmaHistogramEnumeration(
        "IOS.Desktop." + promo_histogram_type + ".FirstImpression.Action",
        action);
  } else if (impression_count == 2) {
    base::UmaHistogramEnumeration(
        "IOS.Desktop." + promo_histogram_type + ".SecondImpression.Action",
        action);
  } else if (impression_count == 3) {
    base::UmaHistogramEnumeration(
        "IOS.Desktop." + promo_histogram_type + ".ThirdImpression.Action",
        action);
  } else {
    NOTREACHED();
  }
}

bool IsIOSDesktopPromoAllowedByGlobalImpressions(Profile* profile) {
  if (!VerifyMostRecentPromoTimestamp(profile) ||
      !VerifyIOSDesktopPromoTotalOptOuts(profile)) {
    return false;
  }

  bool allowed_by_impressions = VerifyIOSDesktopPromoTotalImpressions(profile);
  base::UmaHistogramBoolean("IOS.Desktop.Promo.TotalImpressionsLimitHit",
                            !allowed_by_impressions);
  return allowed_by_impressions;
}

bool ShouldShowIOSDesktopPromo(Profile* profile,
                               const syncer::SyncService* sync_service,
                               PromoType promo_type) {
  if (!CanShowPromos()) {
    return false;
  }

  IOSPromoPrefsConfig promo_prefs(promo_type);

  // Show the promo if the user hasn't opted out, is not in the cooldown
  // period and is within the impression limit for this promo.
  return sync_service && VerifySyncingDatatypes(*sync_service, promo_type) &&
         profile->GetPrefs()->GetInteger(
             promo_prefs.promo_impressions_counter_pref_name) <
             kiOSDesktopPromoMaxImpressionCount &&
         !profile->GetPrefs()->GetBoolean(
             promo_prefs.promo_opt_out_pref_name) &&
         IsIOSDesktopPromoAllowedByGlobalImpressions(profile);
}

bool UserNotClassifiedAsMobileDeviceSwitcher(
    const segmentation_platform::ClassificationResult& result) {
  return result.status == segmentation_platform::PredictionStatus::kSucceeded &&
         !std::ranges::contains(
             result.ordered_labels,
             segmentation_platform::DeviceSwitcherModel::kAndroidPhoneLabel) &&
         !std::ranges::contains(result.ordered_labels,
                                segmentation_platform::DeviceSwitcherModel::
                                    kIosPhoneChromeLabel) &&
         !std::ranges::contains(
             result.ordered_labels,
             segmentation_platform::DeviceSwitcherModel::kAndroidTabletLabel) &&
         !std::ranges::contains(
             result.ordered_labels,
             segmentation_platform::DeviceSwitcherModel::kIosTabletLabel);
}

void IOSDesktopPromoShown(Profile* profile, PromoType promo_type) {
  IOSPromoPrefsConfig promo_prefs(promo_type);
  int new_impression_count =
      profile->GetPrefs()->GetInteger(
          promo_prefs.promo_impressions_counter_pref_name) +
      1;

  profile->GetPrefs()->SetInteger(
      promo_prefs.promo_impressions_counter_pref_name, new_impression_count);
  profile->GetPrefs()->SetTime(
      promo_prefs.promo_last_impression_timestamp_pref_name, base::Time::Now());

  RecordIOSDesktopPromoShownHistogram(promo_type, new_impression_count);
}

void IOSDesktopNtpPromoShown(PrefService* pref_service) {
  ScopedListPrefUpdate update(
      pref_service, promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps);
  update->Append(base::TimeToValue(base::Time::Now()));
}

}  // namespace promos_utils
