// Copyright 2014 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/webapps/browser/banners/app_banner_settings_helper.h"

#include <stddef.h>

#include <algorithm>
#include <array>
#include <memory>
#include <string>
#include <utility>

#include "base/command_line.h"
#include "base/json/values_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/metrics/field_trial_params.h"
#include "base/strings/string_number_conversions.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "components/permissions/permissions_client.h"
#include "components/webapps/browser/banners/app_banner_manager.h"
#include "components/webapps/browser/banners/app_banner_metrics.h"
#include "components/webapps/browser/banners/install_banner_config.h"
#include "components/webapps/browser/features.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/web_contents.h"
#include "url/gurl.h"

namespace webapps {

namespace {

// Default number of days that dismissing or ignoring the banner will prevent it
// being seen again for.
constexpr unsigned int kMinimumBannerBlockedToBannerShown = 90;
constexpr unsigned int kMinimumDaysBetweenBannerShows = 7;

// Max number of apps (including ServiceWorker based web apps) that a particular
// site may show a banner for.
const size_t kMaxAppsPerSite = 3;

// Dictionary keys to use for the events. Must be kept in sync with
// AppBannerEvent.
constexpr auto kBannerEventKeys = std::to_array<const char*>({
    // clang-format off
    "couldShowBannerEvents",
    "didShowBannerEvent",
    "didBlockBannerEvent",
    "couldShowAmbientBadgeEvent",
    // clang-format on
});

unsigned int gDaysAfterDismissedToShow = kMinimumBannerBlockedToBannerShown;
unsigned int gDaysAfterIgnoredToShow = kMinimumDaysBetweenBannerShows;

base::DictValue GetOriginAppBannerData(HostContentSettingsMap* settings,
                                       const GURL& origin_url) {
  if (!settings)
    return base::DictValue();

  base::Value dict = settings->GetWebsiteSetting(
      origin_url, origin_url, ContentSettingsType::APP_BANNER, nullptr);

  if (!dict.is_dict())
    return base::DictValue();

  return std::move(dict.GetDict());
}

class AppPrefs {
 public:
  AppPrefs(content::WebContents* web_contents,
           const GURL& origin,
           const std::string& package_name_or_start_url)
      : origin_(origin) {
    content::BrowserContext* browser_context =
        web_contents->GetBrowserContext();
    if (browser_context->IsOffTheRecord() || !origin.is_valid())
      return;

    settings_ =
        permissions::PermissionsClient::Get()->GetSettingsMap(browser_context);
    origin_dict_ = GetOriginAppBannerData(settings_, origin);
    dict_ = origin_dict_.FindDict(package_name_or_start_url);
    if (!dict_) {
      // Don't allow more than kMaxAppsPerSite dictionaries.
      if (origin_dict_.size() < kMaxAppsPerSite) {
        dict_ = origin_dict_.Set(package_name_or_start_url, base::DictValue())
                    ->GetIfDict();
      }
    }
  }

  HostContentSettingsMap* settings() { return settings_; }
  base::DictValue* dict() { return dict_; }

  void Save() {
    DCHECK(dict_);
    dict_ = nullptr;
    settings_->SetWebsiteSettingDefaultScope(
        *origin_, GURL(), ContentSettingsType::APP_BANNER,
        base::Value(std::move(origin_dict_)));
  }

 private:
  const raw_ref<const GURL> origin_;
  raw_ptr<HostContentSettingsMap> settings_ = nullptr;
  base::DictValue origin_dict_;
  raw_ptr<base::DictValue> dict_ = nullptr;
};

// Reports whether |event| was recorded within the |period| up until |now|.
// If we get nullopt, we cannot store any more values for |origin_url|.
// Conservatively assume we did block a banner in this case.
std::optional<bool> WasEventWithinPeriod(
    AppBannerSettingsHelper::AppBannerEvent event,
    base::TimeDelta period,
    content::WebContents* web_contents,
    const GURL& origin_url,
    const std::string& package_name_or_start_url,
    base::Time now) {
  std::optional<base::Time> event_time =
      AppBannerSettingsHelper::GetSingleBannerEvent(
          web_contents, origin_url, package_name_or_start_url, event);

  if (!event_time)
    return std::nullopt;

  // Null times are in the distant past, so the delta between real times and
  // null events will always be greater than the limits.
  return (now - *event_time < period);
}

// Dictionary of time information for how long to wait before showing the
// "Install" text slide animation again.
// Data format: {"last_shown": timestamp, "delay": duration}
constexpr char kNextInstallTextAnimation[] = "next_install_text_animation";
constexpr char kLastShownKey[] = "last_shown";
constexpr char kDelayKey[] = "delay";

struct NextInstallTextAnimation {
  base::Time last_shown;
  base::TimeDelta delay;

  static std::optional<NextInstallTextAnimation> Get(
      content::WebContents* web_contents,
      const GURL& scope);

  base::Time Time() const { return last_shown + delay; }

  void RecordToPrefs(content::WebContents* web_contents,
                     const GURL& scope) const;
};

std::optional<NextInstallTextAnimation> NextInstallTextAnimation::Get(
    content::WebContents* web_contents,
    const GURL& scope) {
  AppPrefs app_prefs(web_contents, scope, scope.spec());
  if (!app_prefs.dict())
    return NextInstallTextAnimation{base::Time::Max(), base::TimeDelta::Max()};

  const base::DictValue* next_dict =
      app_prefs.dict()->FindDict(kNextInstallTextAnimation);
  if (!next_dict)
    return std::nullopt;

  std::optional<base::Time> last_shown =
      base::ValueToTime(next_dict->Find(kLastShownKey));
  if (!last_shown)
    return std::nullopt;

  std::optional<base::TimeDelta> delay =
      base::ValueToTimeDelta(next_dict->Find(kDelayKey));
  if (!delay)
    return std::nullopt;

  return NextInstallTextAnimation{*last_shown, *delay};
}

void NextInstallTextAnimation::RecordToPrefs(content::WebContents* web_contents,
                                             const GURL& scope) const {
  AppPrefs app_prefs(web_contents, scope, scope.spec());
  if (!app_prefs.dict())
    return;

  base::DictValue next_dict;
  next_dict.Set(kLastShownKey, base::TimeToValue(last_shown));
  next_dict.Set(kDelayKey, base::TimeDeltaToValue(delay));
  app_prefs.dict()->Set(kNextInstallTextAnimation, std::move(next_dict));
  app_prefs.Save();
}

}  // namespace

// Key to store instant apps events.
const char AppBannerSettingsHelper::kInstantAppsKey[] = "instantapps";

void AppBannerSettingsHelper::ClearHistoryForURLs(
    content::BrowserContext* browser_context,
    const std::set<GURL>& origin_urls) {
  HostContentSettingsMap* settings =
      permissions::PermissionsClient::Get()->GetSettingsMap(browser_context);
  for (const GURL& origin_url : origin_urls) {
    settings->SetWebsiteSettingDefaultScope(
        origin_url, GURL(), ContentSettingsType::APP_BANNER, base::Value());
    settings->FlushLossyWebsiteSettings();
  }
}

void AppBannerSettingsHelper::RecordBannerInstallEvent(
    content::WebContents* web_contents,
    const std::string& package_name_or_start_url) {
  TrackInstallEvent(INSTALL_EVENT_WEB_APP_INSTALLED);
}

void AppBannerSettingsHelper::RecordBannerDismissEvent(
    content::WebContents* web_contents,
    const std::string& package_name_or_start_url) {
  TrackDismissEvent(DISMISS_EVENT_CLOSE_BUTTON);

  AppBannerSettingsHelper::RecordBannerEvent(
      web_contents, web_contents->GetLastCommittedURL(),
      package_name_or_start_url,
      AppBannerSettingsHelper::APP_BANNER_EVENT_DID_BLOCK,
      AppBannerManager::GetCurrentTime());
}

void AppBannerSettingsHelper::RecordBannerEvent(
    content::WebContents* web_contents,
    const GURL& origin_url,
    const std::string& package_name_or_start_url,
    AppBannerEvent event,
    base::Time time) {
  CHECK(!package_name_or_start_url.empty());

  AppPrefs app_prefs(web_contents, origin_url, package_name_or_start_url);
  if (!app_prefs.dict())
    return;

  // Dates are stored in their raw form (i.e. not local dates) to be resilient
  // to time zone changes.
  const char* event_key = kBannerEventKeys[event];

  if (event == APP_BANNER_EVENT_COULD_SHOW) {
    // Do not overwrite a could show event, as this is used for metrics.
    if (app_prefs.dict()->contains(event_key))
      return;
  }
  app_prefs.dict()->Set(
      event_key, base::Value(static_cast<double>(time.ToInternalValue())));

  app_prefs.Save();
}

void AppBannerSettingsHelper::RecordBannerEvent(
    content::WebContents* web_contents,
    const InstallBannerConfig& install_config,
    AppBannerEvent event,
    base::Time time) {
  RecordBannerEvent(web_contents, install_config.validated_url,
                    install_config.GetWebOrNativeAppIdentifier(), event, time);
}

bool AppBannerSettingsHelper::WasBannerRecentlyBlocked(
    content::WebContents* web_contents,
    const GURL& origin_url,
    const std::string& package_name_or_start_url,
    base::Time now) {
  DCHECK(!package_name_or_start_url.empty());

  std::optional<bool> in_period = WasEventWithinPeriod(
      APP_BANNER_EVENT_DID_BLOCK, base::Days(gDaysAfterDismissedToShow),
      web_contents, origin_url, package_name_or_start_url, now);
  return in_period.value_or(true);
}

bool AppBannerSettingsHelper::WasBannerRecentlyIgnored(
    content::WebContents* web_contents,
    const GURL& origin_url,
    const std::string& package_name_or_start_url,
    base::Time now) {
  DCHECK(!package_name_or_start_url.empty());

  std::optional<bool> in_period = WasEventWithinPeriod(
      APP_BANNER_EVENT_DID_SHOW, base::Days(gDaysAfterIgnoredToShow),
      web_contents, origin_url, package_name_or_start_url, now);

  return in_period.value_or(true);
}

std::optional<base::Time> AppBannerSettingsHelper::GetSingleBannerEvent(
    content::WebContents* web_contents,
    const GURL& origin_url,
    const std::string& package_name_or_start_url,
    AppBannerEvent event) {
  DCHECK(event < APP_BANNER_EVENT_NUM_EVENTS);

  AppPrefs app_prefs(web_contents, origin_url, package_name_or_start_url);
  if (!app_prefs.dict())
    return std::nullopt;

  std::optional<double> internal_time =
      app_prefs.dict()->FindDouble(kBannerEventKeys[event]);
  return internal_time ? base::Time::FromInternalValue(internal_time.value())
                       : base::Time();
}

void AppBannerSettingsHelper::SetDaysAfterDismissAndIgnoreToTrigger(
    unsigned int dismiss_days,
    unsigned int ignore_days) {
  gDaysAfterDismissedToShow = dismiss_days;
  gDaysAfterIgnoredToShow = ignore_days;
}

bool AppBannerSettingsHelper::CanShowInstallTextAnimation(
    content::WebContents* web_contents,
    const GURL& scope) {
  std::optional<NextInstallTextAnimation> next_prompt =
      NextInstallTextAnimation::Get(web_contents, scope);

  if (!next_prompt)
    return true;

  return AppBannerManager::GetCurrentTime() >= next_prompt->Time();
}

void AppBannerSettingsHelper::RecordInstallTextAnimationShown(
    content::WebContents* web_contents,
    const GURL& scope) {
  DCHECK(scope.is_valid());

  constexpr base::TimeDelta kInitialAnimationSuppressionPeriod = base::Days(1);
  constexpr base::TimeDelta kMaxAnimationSuppressionPeriod = base::Days(90);
  constexpr double kExponentialBackoffFactor = 2;

  NextInstallTextAnimation next_prompt = {AppBannerManager::GetCurrentTime(),
                                          kInitialAnimationSuppressionPeriod};

  std::optional<NextInstallTextAnimation> last_prompt =
      NextInstallTextAnimation::Get(web_contents, scope);
  if (last_prompt) {
    next_prompt.delay =
        std::min(kMaxAnimationSuppressionPeriod,
                 last_prompt->delay * kExponentialBackoffFactor);
  }

  next_prompt.RecordToPrefs(web_contents, scope);
}

AppBannerSettingsHelper::ScopedTriggerSettings::ScopedTriggerSettings(
    unsigned int dismiss_days,
    unsigned int ignore_days) {
  old_dismiss_ = gDaysAfterDismissedToShow;
  old_ignore_ = gDaysAfterIgnoredToShow;
  gDaysAfterDismissedToShow = dismiss_days;
  gDaysAfterIgnoredToShow = ignore_days;
}

AppBannerSettingsHelper::ScopedTriggerSettings::~ScopedTriggerSettings() {
  gDaysAfterDismissedToShow = old_dismiss_;
  gDaysAfterIgnoredToShow = old_ignore_;
}

}  // namespace webapps
