/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsCookieBannerService.h"

#include "CookieBannerDomainPrefService.h"
#include "ErrorList.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/RefPtr.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/EventQueue.h"
#include "mozilla/glean/GleanMetrics.h"
#include "mozilla/Logging.h"
#include "mozilla/StaticPrefs_cookiebanners.h"

#include "nsCOMPtr.h"
#include "nsCookieBannerRule.h"
#include "nsCookieInjector.h"
#include "nsCRT.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsIClickRule.h"
#include "nsICookieBannerListService.h"
#include "nsICookieBannerRule.h"
#include "nsICookie.h"
#include "nsIEffectiveTLDService.h"
#include "nsIPrincipal.h"
#include "nsNetCID.h"
#include "nsServiceManagerUtils.h"
#include "nsStringFwd.h"
#include "nsThreadUtils.h"
#include "Cookie.h"

#define OBSERVER_TOPIC_BC_ATTACHED "browsing-context-attached"
#define OBSERVER_TOPIC_BC_DISCARDED "browsing-context-discarded"

namespace mozilla {

NS_IMPL_ISUPPORTS(nsCookieBannerService, nsICookieBannerService, nsIObserver,
                  nsIWebProgressListener, nsISupportsWeakReference)

LazyLogModule gCookieBannerLog("nsCookieBannerService");

static const char kCookieBannerServiceModePref[] = "cookiebanners.service.mode";
static const char kCookieBannerServiceModePBMPref[] =
    "cookiebanners.service.mode.privateBrowsing";

static StaticRefPtr<nsCookieBannerService> sCookieBannerServiceSingleton;

namespace {

// A helper function that converts service modes to strings.
nsCString ConvertModeToStringForTelemetry(uint32_t aModes) {
  switch (aModes) {
    case nsICookieBannerService::MODE_DISABLED:
      return "disabled"_ns;
    case nsICookieBannerService::MODE_REJECT:
      return "reject"_ns;
    case nsICookieBannerService::MODE_REJECT_OR_ACCEPT:
      return "reject_or_accept"_ns;
    default:
      // Fall back to return "invalid" if we got any unsupported service
      // mode. Note this this also includes MODE_UNSET.
      return "invalid"_ns;
  }
}

}  // anonymous namespace

// static
already_AddRefed<nsCookieBannerService> nsCookieBannerService::GetSingleton() {
  if (!sCookieBannerServiceSingleton) {
    sCookieBannerServiceSingleton = new nsCookieBannerService();

    RunOnShutdown([] {
      MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
              ("RunOnShutdown. Mode: %d. Mode PBM: %d.",
               StaticPrefs::cookiebanners_service_mode(),
               StaticPrefs::cookiebanners_service_mode_privateBrowsing()));

      // Unregister pref listeners.
      DebugOnly<nsresult> rv = Preferences::UnregisterCallback(
          &nsCookieBannerService::OnPrefChange, kCookieBannerServiceModePref);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "Unregistering kCookieBannerServiceModePref callback failed");
      rv = Preferences::UnregisterCallback(&nsCookieBannerService::OnPrefChange,
                                           kCookieBannerServiceModePBMPref);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "Unregistering kCookieBannerServiceModePBMPref callback failed");

      rv = sCookieBannerServiceSingleton->Shutdown();
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                           "nsCookieBannerService::Shutdown failed.");

      sCookieBannerServiceSingleton = nullptr;
    });
  }

  return do_AddRef(sCookieBannerServiceSingleton);
}

// static
void nsCookieBannerService::OnPrefChange(const char* aPref, void* aData) {
  RefPtr<nsCookieBannerService> service = GetSingleton();

  // If the feature is enabled for normal or private browsing, init the service.
  if (StaticPrefs::cookiebanners_service_mode() !=
          nsICookieBannerService::MODE_DISABLED ||
      StaticPrefs::cookiebanners_service_mode_privateBrowsing() !=
          nsICookieBannerService::MODE_DISABLED) {
    MOZ_LOG(
        gCookieBannerLog, LogLevel::Info,
        ("Initializing nsCookieBannerService after pref change. %s", aPref));
    DebugOnly<nsresult> rv = service->Init();
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "nsCookieBannerService::Init failed");
    return;
  }

  MOZ_LOG(gCookieBannerLog, LogLevel::Info,
          ("Disabling nsCookieBannerService after pref change. %s", aPref));

  DebugOnly<nsresult> rv = service->Shutdown();
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "nsCookieBannerService::Shutdown failed");
}

NS_IMETHODIMP
nsCookieBannerService::Observe(nsISupports* aSubject, const char* aTopic,
                               const char16_t* aData) {
  // Report the daily telemetry for the cookie banner service on "idle-daily".
  if (nsCRT::strcmp(aTopic, "idle-daily") == 0) {
    DailyReportTelemetry();
    ResetDomainTelemetryRecord(""_ns);
    return NS_OK;
  }

  // Initializing the cookie banner service on startup on
  // "profile-after-change".
  if (nsCRT::strcmp(aTopic, "profile-after-change") == 0) {
    nsresult rv = Preferences::RegisterCallback(
        &nsCookieBannerService::OnPrefChange, kCookieBannerServiceModePBMPref);
    NS_ENSURE_SUCCESS(rv, rv);

    return Preferences::RegisterCallbackAndCall(
        &nsCookieBannerService::OnPrefChange, kCookieBannerServiceModePref);
  }

  if (nsCRT::strcmp(aTopic, OBSERVER_TOPIC_BC_ATTACHED) == 0) {
    return RegisterWebProgressListener(aSubject);
  }

  if (nsCRT::strcmp(aTopic, OBSERVER_TOPIC_BC_DISCARDED) == 0) {
    return RemoveWebProgressListener(aSubject);
  }

  return NS_OK;
}

nsresult nsCookieBannerService::Init() {
  MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
          ("%s. Mode: %d. Mode PBM: %d.", __FUNCTION__,
           StaticPrefs::cookiebanners_service_mode(),
           StaticPrefs::cookiebanners_service_mode_privateBrowsing()));

  // Check if already initialized.
  if (mIsInitialized) {
    return NS_OK;
  }

  // Initialize the service which fetches cookie banner rules.
  mListService = do_GetService(NS_COOKIEBANNERLISTSERVICE_CONTRACTID);
  NS_ENSURE_TRUE(mListService, NS_ERROR_FAILURE);

  mDomainPrefService = CookieBannerDomainPrefService::GetOrCreate();
  NS_ENSURE_TRUE(mDomainPrefService, NS_ERROR_FAILURE);

  // Setting mIsInitialized before importing rules, because the list service
  // needs to call nsCookieBannerService methods that would throw if not
  // marked initialized.
  mIsInitialized = true;

  // Import initial rule-set, domain preference and enable rule syncing. Uses
  // NS_DispatchToCurrentThreadQueue with idle priority to avoid early
  // main-thread IO caused by the list service accessing RemoteSettings.
  nsresult rv = NS_DispatchToCurrentThreadQueue(
      NS_NewRunnableFunction("CookieBannerListService init startup",
                             [&] {
                               if (!mIsInitialized) {
                                 return;
                               }
                               mListService->Init();
                               mDomainPrefService->Init();
                             }),
      EventQueuePriority::Idle);
  NS_ENSURE_SUCCESS(rv, rv);

  // Initialize the cookie injector.
  RefPtr<nsCookieInjector> injector = nsCookieInjector::GetSingleton();

  nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
  NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE);

  obsSvc->AddObserver(this, OBSERVER_TOPIC_BC_ATTACHED, false);
  obsSvc->AddObserver(this, OBSERVER_TOPIC_BC_DISCARDED, false);

  return NS_OK;
}

nsresult nsCookieBannerService::Shutdown() {
  MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
          ("%s. Mode: %d. Mode PBM: %d.", __FUNCTION__,
           StaticPrefs::cookiebanners_service_mode(),
           StaticPrefs::cookiebanners_service_mode_privateBrowsing()));

  // Check if already shutdown.
  if (!mIsInitialized) {
    return NS_OK;
  }
  mIsInitialized = false;

  // Shut down the list service which will stop updating mRules.
  mListService->Shutdown();

  // Clear all stored cookie banner rules. They will be imported again on Init.
  mRules.Clear();

  nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
  NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE);

  obsSvc->RemoveObserver(this, OBSERVER_TOPIC_BC_ATTACHED);
  obsSvc->RemoveObserver(this, OBSERVER_TOPIC_BC_DISCARDED);

  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::GetIsEnabled(bool* aResult) {
  *aResult = mIsInitialized;

  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::GetRules(nsTArray<RefPtr<nsICookieBannerRule>>& aRules) {
  aRules.Clear();

  // Service is disabled, throw with empty array.
  if (!mIsInitialized) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  // Append global rules if enabled. We don't have to deduplicate here because
  // global rules are stored by ID and every ID maps to exactly one rule.
  if (StaticPrefs::cookiebanners_service_enableGlobalRules()) {
    AppendToArray(aRules, mGlobalRules.Values());
  }

  // Append domain-keyed rules.
  // Since multiple domains can map to the same rule in mRules we need to
  // deduplicate using a set before returning a rules array.
  nsTHashSet<nsRefPtrHashKey<nsICookieBannerRule>> rulesSet;

  for (const nsCOMPtr<nsICookieBannerRule>& rule : mRules.Values()) {
    rulesSet.Insert(rule);
  }

  AppendToArray(aRules, rulesSet);

  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::ResetRules(const bool doImport) {
  // Service is disabled, throw.
  if (!mIsInitialized) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  mRules.Clear();
  mGlobalRules.Clear();

  if (doImport) {
    NS_ENSURE_TRUE(mListService, NS_ERROR_FAILURE);
    nsresult rv = mListService->ImportAllRules();
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

nsresult nsCookieBannerService::GetRuleForDomain(const nsACString& aDomain,
                                                 bool aIsTopLevel,
                                                 nsICookieBannerRule** aRule,
                                                 bool aReportTelemetry) {
  NS_ENSURE_ARG_POINTER(aRule);
  *aRule = nullptr;

  // Service is disabled, throw with null.
  if (!mIsInitialized) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsCOMPtr<nsICookieBannerRule> rule = mRules.Get(aDomain);

  // If we are instructed to collect telemetry.
  if (aReportTelemetry) {
    ReportRuleLookupTelemetry(aDomain, rule, aIsTopLevel);
  }

  if (rule) {
    rule.forget(aRule);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::GetCookiesForURI(
    nsIURI* aURI, const bool aIsPrivateBrowsing,
    nsTArray<RefPtr<nsICookieRule>>& aCookies) {
  NS_ENSURE_ARG_POINTER(aURI);
  aCookies.Clear();

  // We only need URI spec for logging, avoid getting it otherwise.
  if (MOZ_LOG_TEST(gCookieBannerLog, LogLevel::Debug)) {
    nsAutoCString spec;
    nsresult rv = aURI->GetSpec(spec);
    NS_ENSURE_SUCCESS(rv, rv);
    MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
            ("%s. aURI: %s. aIsPrivateBrowsing: %d", __FUNCTION__, spec.get(),
             aIsPrivateBrowsing));
  }

  // Service is disabled, throw with empty array.
  if (!mIsInitialized) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  // Check which cookie banner service mode applies for this request. This
  // depends on whether the browser is in private browsing or normal browsing
  // mode.
  uint32_t mode;
  if (aIsPrivateBrowsing) {
    mode = StaticPrefs::cookiebanners_service_mode_privateBrowsing();
  } else {
    mode = StaticPrefs::cookiebanners_service_mode();
  }
  MOZ_LOG(
      gCookieBannerLog, LogLevel::Debug,
      ("%s. Found nsICookieBannerRule. Computed mode: %d", __FUNCTION__, mode));

  // We don't need to check the domain preference if the cookie banner handling
  // service is disabled by pref.
  if (mode != nsICookieBannerService::MODE_DISABLED &&
      !StaticPrefs::cookiebanners_service_detectOnly()) {
    // Get the domain preference for the uri, the domain preference takes
    // precedence over the pref setting. Note that the domain preference is
    // supposed to stored only for top level URIs.
    nsICookieBannerService::Modes domainPref;
    nsresult rv = GetDomainPref(aURI, aIsPrivateBrowsing, &domainPref);
    NS_ENSURE_SUCCESS(rv, rv);

    if (domainPref != nsICookieBannerService::MODE_UNSET) {
      mode = domainPref;
    }
  }

  // Service is disabled for current context (normal, private browsing or domain
  // preference), return empty array. Same for detect-only mode where no cookies
  // should be injected.
  if (mode == nsICookieBannerService::MODE_DISABLED ||
      StaticPrefs::cookiebanners_service_detectOnly()) {
    MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
            ("%s. Returning empty array. Got MODE_DISABLED for "
             "aIsPrivateBrowsing: %d.",
             __FUNCTION__, aIsPrivateBrowsing));
    return NS_OK;
  }

  nsresult rv;
  // Compute the baseDomain from aURI.
  nsCOMPtr<nsIEffectiveTLDService> eTLDService(
      do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCString baseDomain;
  rv = eTLDService->GetBaseDomain(aURI, 0, baseDomain);
  NS_ENSURE_SUCCESS(rv, rv);

  return GetCookieRulesForDomainInternal(
      baseDomain, static_cast<nsICookieBannerService::Modes>(mode), true, false,
      aCookies);
}

NS_IMETHODIMP
nsCookieBannerService::GetClickRulesForDomain(
    const nsACString& aDomain, const bool aIsTopLevel,
    nsTArray<RefPtr<nsIClickRule>>& aRules) {
  return GetClickRulesForDomainInternal(aDomain, aIsTopLevel, true, aRules);
}

nsresult nsCookieBannerService::GetClickRulesForDomainInternal(
    const nsACString& aDomain, const bool aIsTopLevel,
    const bool aReportTelemetry, nsTArray<RefPtr<nsIClickRule>>& aRules) {
  aRules.Clear();

  // Service is disabled, throw with empty rule.
  if (!mIsInitialized) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  // Get the cookie banner rule for the domain. Also, we instruct the function
  // to report the rule lookup telemetry. Note that we collect telemetry here
  // but don't when getting cookie rules because the cookie injection only apply
  // for top-level requests. So, we won't be able to collect data for iframe
  // cases.
  nsCOMPtr<nsICookieBannerRule> ruleForDomain;
  nsresult rv = GetRuleForDomain(
      aDomain, aIsTopLevel, getter_AddRefs(ruleForDomain), aReportTelemetry);
  NS_ENSURE_SUCCESS(rv, rv);

  // Extract click rule from an nsICookieBannerRule and if found append it to
  // the array returned.
  auto appendClickRule = [&](const nsCOMPtr<nsICookieBannerRule>& bannerRule) {
    nsCOMPtr<nsIClickRule> clickRule;
    rv = bannerRule->GetClickRule(getter_AddRefs(clickRule));
    NS_ENSURE_SUCCESS(rv, rv);

    if (!clickRule) {
      return NS_OK;
    }

    // Evaluate the rule's runContext field and skip it if the caller's context
    // doesn't match. See nsIClickRule::RunContext for possible values.
    nsIClickRule::RunContext runContext;
    rv = clickRule->GetRunContext(&runContext);
    NS_ENSURE_SUCCESS(rv, rv);

    bool runContextMatchesRule =
        (runContext == nsIClickRule::RUN_ALL) ||
        (runContext == nsIClickRule::RUN_TOP && aIsTopLevel) ||
        (runContext == nsIClickRule::RUN_CHILD && !aIsTopLevel);

    if (runContextMatchesRule) {
      aRules.AppendElement(clickRule);
    }

    return NS_OK;
  };

  // If there is a domain-specific rule it takes precedence over the global
  // rules.
  if (ruleForDomain) {
    return appendClickRule(ruleForDomain);
  }

  if (!StaticPrefs::cookiebanners_service_enableGlobalRules()) {
    // Global rules are disabled, skip adding them.
    return NS_OK;
  }

  // Append all global click rules.
  for (nsICookieBannerRule* globalRule : mGlobalRules.Values()) {
    rv = appendClickRule(globalRule);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::InsertRule(nsICookieBannerRule* aRule) {
  NS_ENSURE_ARG_POINTER(aRule);

  // Service is disabled, throw.
  if (!mIsInitialized) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsCookieBannerRule::LogRule(gCookieBannerLog, "InsertRule:", aRule,
                              LogLevel::Debug);

  nsTArray<nsCString> domains;
  nsresult rv = aRule->GetDomains(domains);
  NS_ENSURE_SUCCESS(rv, rv);

  // Global rules are stored in a separate map mGlobalRules.
  // They are identified by having an empty domains array.
  // They are keyed by the unique ID field.
  if (domains.IsEmpty()) {
    nsAutoCString id;
    rv = aRule->GetId(id);
    NS_ENSURE_SUCCESS(rv, rv);
    NS_ENSURE_TRUE(!id.IsEmpty(), NS_ERROR_FAILURE);

    // Global rules must not have cookies. We shouldn't set cookies for every
    // site without indication that they handle banners. Click rules are
    // different, because they have a "presence" indicator and only click if it
    // is reasonable to do so.
    rv = aRule->ClearCookies();
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsICookieBannerRule> result =
        mGlobalRules.InsertOrUpdate(id, aRule);
    NS_ENSURE_TRUE(result, NS_ERROR_FAILURE);

    return NS_OK;
  }

  // Multiple domains can be mapped to the same rule.
  for (auto& domain : domains) {
    nsCOMPtr<nsICookieBannerRule> result = mRules.InsertOrUpdate(domain, aRule);
    NS_ENSURE_TRUE(result, NS_ERROR_FAILURE);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::RemoveRule(nsICookieBannerRule* aRule) {
  NS_ENSURE_ARG_POINTER(aRule);

  // Service is disabled, throw.
  if (!mIsInitialized) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsCookieBannerRule::LogRule(gCookieBannerLog, "RemoveRule:", aRule,
                              LogLevel::Debug);

  nsTArray<nsCString> domains;
  nsresult rv = aRule->GetDomains(domains);
  NS_ENSURE_SUCCESS(rv, rv);

  // Remove global rule by ID.
  if (domains.IsEmpty()) {
    nsAutoCString id;
    rv = aRule->GetId(id);
    NS_ENSURE_SUCCESS(rv, rv);
    NS_ENSURE_TRUE(!id.IsEmpty(), NS_ERROR_FAILURE);

    mGlobalRules.Remove(id);
    return NS_OK;
  }

  // Remove all entries pointing to the rule.
  for (auto& domain : domains) {
    mRules.Remove(domain);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::HasRuleForBrowsingContextTree(
    mozilla::dom::BrowsingContext* aBrowsingContext, bool* aResult) {
  NS_ENSURE_ARG_POINTER(aBrowsingContext);
  NS_ENSURE_ARG_POINTER(aResult);
  MOZ_ASSERT(XRE_IsParentProcess());
  *aResult = false;

  MOZ_LOG(gCookieBannerLog, LogLevel::Debug, ("%s", __FUNCTION__));

  // Service is disabled, throw.
  if (!mIsInitialized) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsresult rv = NS_OK;
  // Keep track of the HasRuleForBrowsingContextInternal needed, used for
  // logging.
  uint32_t numChecks = 0;

  // TODO: Optimization: We could avoid unecessary rule lookups by remembering
  // which domains we already looked up rules for. This would also need to take
  // isPBM and isTopLevel into account, because some rules only apply in certain
  // contexts.
  auto checkFn =
      [&](dom::BrowsingContext* bc) -> dom::BrowsingContext::WalkFlag {
    numChecks++;

    bool hasClickRule = false;
    bool hasCookieRule = false;
    // Pass ignoreDomainPref=true: when checking whether a suitable rule exists
    // we don't care what the domain-specific user pref is set to.
    rv = HasRuleForBrowsingContextInternal(bc, true, hasClickRule,
                                           hasCookieRule);
    // If the method failed abort the walk. We will return the stored error
    // result when exiting the method.
    if (NS_FAILED(rv)) {
      return dom::BrowsingContext::WalkFlag::Stop;
    }

    *aResult = hasClickRule || hasCookieRule;

    // Greedily return when we found a rule.
    if (*aResult) {
      return dom::BrowsingContext::WalkFlag::Stop;
    }

    return dom::BrowsingContext::WalkFlag::Next;
  };

  // Walk the BC (sub-)tree and return greedily when a rule is found for a
  // BrowsingContext.
  aBrowsingContext->PreOrderWalk(checkFn);

  MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
          ("%s. success: %d, hasRule: %d, numChecks: %d", __FUNCTION__,
           NS_SUCCEEDED(rv), *aResult, numChecks));

  return rv;
}

nsresult nsCookieBannerService::HasRuleForBrowsingContextInternal(
    mozilla::dom::BrowsingContext* aBrowsingContext, bool aIgnoreDomainPref,
    bool& aHasClickRule, bool& aHasCookieRule) {
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(mIsInitialized);
  NS_ENSURE_ARG_POINTER(aBrowsingContext);

  MOZ_LOG(gCookieBannerLog, LogLevel::Debug, ("%s", __FUNCTION__));

  aHasClickRule = false;
  aHasCookieRule = false;

  // First, check if our current mode is disabled. If so there is no applicable
  // rule.
  nsICookieBannerService::Modes mode;
  nsresult rv = GetServiceModeForBrowsingContext(aBrowsingContext,
                                                 aIgnoreDomainPref, &mode);
  NS_ENSURE_SUCCESS(rv, rv);

  if (mode == nsICookieBannerService::MODE_DISABLED ||
      StaticPrefs::cookiebanners_service_detectOnly()) {
    return NS_OK;
  }

  // In order to lookup rules we need to get the base domain associated with the
  // BrowsingContext.

  // 1. Get the window running in the BrowsingContext.
  RefPtr<dom::WindowGlobalParent> windowGlobalParent =
      aBrowsingContext->Canonical()->GetCurrentWindowGlobal();
  NS_ENSURE_TRUE(windowGlobalParent, NS_ERROR_FAILURE);

  // 2. Get the base domain from the content principal.
  nsCOMPtr<nsIPrincipal> principal = windowGlobalParent->DocumentPrincipal();
  NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE);

  nsCString baseDomain;
  rv = principal->GetBaseDomain(baseDomain);
  NS_ENSURE_SUCCESS(rv, rv);
  NS_ENSURE_TRUE(!baseDomain.IsEmpty(), NS_ERROR_FAILURE);

  MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
          ("%s. baseDomain: %s", __FUNCTION__, baseDomain.get()));

  // 3. Look up click rules by baseDomain.
  // TODO: Optimization: We currently do two nsICookieBannerRule lookups, one
  // for click rules and one for cookie rules.
  nsTArray<RefPtr<nsIClickRule>> clickRules;
  rv = GetClickRulesForDomainInternal(baseDomain, aBrowsingContext->IsTop(),
                                      false, clickRules);
  NS_ENSURE_SUCCESS(rv, rv);

  // 3.1. Check if there is a non-empty click rule for the current environment.
  for (RefPtr<nsIClickRule>& rule : clickRules) {
    NS_ENSURE_TRUE(rule, NS_ERROR_NULL_POINTER);

    nsAutoCString optOut;
    rv = rule->GetOptOut(optOut);
    NS_ENSURE_SUCCESS(rv, rv);

    if (!optOut.IsEmpty()) {
      aHasClickRule = true;
      break;
    }

    if (mode == nsICookieBannerService::MODE_REJECT_OR_ACCEPT) {
      nsAutoCString optIn;
      rv = rule->GetOptIn(optIn);
      NS_ENSURE_SUCCESS(rv, rv);

      if (!optIn.IsEmpty()) {
        aHasClickRule = true;
        break;
      }
    }
  }

  // 4. Check for cookie rules by baseDomain.
  nsTArray<RefPtr<nsICookieRule>> cookies;
  rv = GetCookieRulesForDomainInternal(
      baseDomain, mode, aBrowsingContext->IsTop(), false, cookies);
  NS_ENSURE_SUCCESS(rv, rv);

  aHasCookieRule = !cookies.IsEmpty();

  return NS_OK;
}

nsresult nsCookieBannerService::GetCookieRulesForDomainInternal(
    const nsACString& aBaseDomain, const nsICookieBannerService::Modes aMode,
    const bool aIsTopLevel, const bool aReportTelemetry,
    nsTArray<RefPtr<nsICookieRule>>& aCookies) {
  MOZ_ASSERT(mIsInitialized);
  MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
          ("%s. aBaseDomain: %s", __FUNCTION__,
           PromiseFlatCString(aBaseDomain).get()));

  aCookies.Clear();

  // No cookie rules if disabled or in detect-only mode. Cookie injection is not
  // supported for the detect-only mode.
  if (aMode == nsICookieBannerService::MODE_DISABLED ||
      StaticPrefs::cookiebanners_service_detectOnly()) {
    return NS_OK;
  }

  // Cookies should only be injected for top-level frames.
  if (!aIsTopLevel) {
    return NS_OK;
  }

  nsCOMPtr<nsICookieBannerRule> cookieBannerRule;
  nsresult rv =
      GetRuleForDomain(aBaseDomain, aIsTopLevel,
                       getter_AddRefs(cookieBannerRule), aReportTelemetry);
  NS_ENSURE_SUCCESS(rv, rv);

  // No rule found.
  if (!cookieBannerRule) {
    MOZ_LOG(
        gCookieBannerLog, LogLevel::Debug,
        ("%s. Returning empty array. No nsICookieBannerRule matching domain.",
         __FUNCTION__));
    return NS_OK;
  }

  // MODE_REJECT: In this mode we only handle the banner if we can reject. We
  // don't care about the opt-in cookies.
  rv = cookieBannerRule->GetCookies(true, aBaseDomain, aCookies);
  NS_ENSURE_SUCCESS(rv, rv);

  // MODE_REJECT_OR_ACCEPT: In this mode we will try to opt-out, but if we don't
  // have any opt-out cookies we will fallback to the opt-in cookies.
  if (aMode == nsICookieBannerService::MODE_REJECT_OR_ACCEPT &&
      aCookies.IsEmpty()) {
    MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
            ("%s. Returning opt-in cookies for %s.", __FUNCTION__,
             PromiseFlatCString(aBaseDomain).get()));

    return cookieBannerRule->GetCookies(false, aBaseDomain, aCookies);
  }

  MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
          ("%s. Returning opt-out cookies for %s.", __FUNCTION__,
           PromiseFlatCString(aBaseDomain).get()));
  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::GetDomainPref(nsIURI* aTopLevelURI,
                                     const bool aIsPrivate,
                                     nsICookieBannerService::Modes* aModes) {
  NS_ENSURE_ARG_POINTER(aTopLevelURI);
  NS_ENSURE_ARG_POINTER(aModes);

  if (!mIsInitialized) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsresult rv;
  nsCOMPtr<nsIEffectiveTLDService> eTLDService(
      do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCString baseDomain;
  rv = eTLDService->GetBaseDomain(aTopLevelURI, 0, baseDomain);
  NS_ENSURE_SUCCESS(rv, rv);

  return GetDomainPrefInternal(baseDomain, aIsPrivate, aModes);
}

nsresult nsCookieBannerService::GetDomainPrefInternal(
    const nsACString& aBaseDomain, const bool aIsPrivate,
    nsICookieBannerService::Modes* aModes) {
  MOZ_ASSERT(mIsInitialized);
  NS_ENSURE_ARG_POINTER(aModes);

  auto pref = mDomainPrefService->GetPref(aBaseDomain, aIsPrivate);

  *aModes = nsICookieBannerService::MODE_UNSET;

  if (pref.isSome()) {
    *aModes = pref.value();
  }

  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::SetDomainPref(nsIURI* aTopLevelURI,
                                     nsICookieBannerService::Modes aModes,
                                     const bool aIsPrivate) {
  NS_ENSURE_ARG_POINTER(aTopLevelURI);

  return SetDomainPrefInternal(aTopLevelURI, aModes, aIsPrivate, false);
}

NS_IMETHODIMP
nsCookieBannerService::SetDomainPrefAndPersistInPrivateBrowsing(
    nsIURI* aTopLevelURI, nsICookieBannerService::Modes aModes) {
  NS_ENSURE_ARG_POINTER(aTopLevelURI);

  return SetDomainPrefInternal(aTopLevelURI, aModes, true, true);
};

nsresult nsCookieBannerService::SetDomainPrefInternal(
    nsIURI* aTopLevelURI, nsICookieBannerService::Modes aModes,
    const bool aIsPrivate, const bool aPersistInPrivateBrowsing) {
  NS_ENSURE_ARG_POINTER(aTopLevelURI);

  if (!mIsInitialized) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsresult rv;
  nsCOMPtr<nsIEffectiveTLDService> eTLDService(
      do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCString baseDomain;
  rv = eTLDService->GetBaseDomain(aTopLevelURI, 0, baseDomain);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = mDomainPrefService->SetPref(baseDomain, aModes, aIsPrivate,
                                   aPersistInPrivateBrowsing);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::RemoveDomainPref(nsIURI* aTopLevelURI,
                                        const bool aIsPrivate) {
  NS_ENSURE_ARG_POINTER(aTopLevelURI);

  if (!mIsInitialized) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsresult rv;
  nsCOMPtr<nsIEffectiveTLDService> eTLDService(
      do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCString baseDomain;
  rv = eTLDService->GetBaseDomain(aTopLevelURI, 0, baseDomain);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = mDomainPrefService->RemovePref(baseDomain, aIsPrivate);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::RemoveAllDomainPrefs(const bool aIsPrivate) {
  if (!mIsInitialized) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsresult rv = mDomainPrefService->RemoveAll(aIsPrivate);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::ResetDomainTelemetryRecord(const nsACString& aDomain) {
  if (aDomain.IsEmpty()) {
    mTelemetryReportedTopDomains.Clear();
    mTelemetryReportedIFrameDomains.Clear();
    return NS_OK;
  }

  mTelemetryReportedTopDomains.Remove(aDomain);
  mTelemetryReportedIFrameDomains.Remove(aDomain);
  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::OnStateChange(nsIWebProgress* aWebProgress,
                                     nsIRequest* aRequest, uint32_t aStateFlags,
                                     nsresult aStatus) {
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
nsCookieBannerService::OnProgressChange(nsIWebProgress* aWebProgress,
                                        nsIRequest* aRequest,
                                        int32_t aCurSelfProgress,
                                        int32_t aMaxSelfProgress,
                                        int32_t aCurTotalProgress,
                                        int32_t aMaxTotalProgress) {
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
nsCookieBannerService::OnLocationChange(nsIWebProgress* aWebProgress,
                                        nsIRequest* aRequest, nsIURI* aLocation,
                                        uint32_t aFlags) {
  MOZ_ASSERT(XRE_IsParentProcess());

  if (!aWebProgress || !aLocation) {
    return NS_OK;
  }

  RefPtr<dom::BrowsingContext> bc = aWebProgress->GetBrowsingContext();
  if (!bc) {
    return NS_OK;
  }

  // We are only interested in http/https.
  if (!aLocation->SchemeIs("http") && !aLocation->SchemeIs("https")) {
    return NS_OK;
  }

  Maybe<std::tuple<bool, bool>> telemetryData =
      mReloadTelemetryData.MaybeGet(bc->Top()->Id());
  if (!telemetryData) {
    return NS_OK;
  }

  auto [hasClickRuleInData, hasCookieRuleInData] = telemetryData.ref();

  // If the location change is triggered by a reload, we report the telemetry
  // for the given top-level browsing context.
  if (aFlags & LOCATION_CHANGE_RELOAD) {
    if (!bc->IsTop()) {
      return NS_OK;
    }

    // The static value to track if we have enabled the event telemetry for
    // cookie banner.
    static bool sTelemetryEventEnabled = false;
    if (!sTelemetryEventEnabled) {
      sTelemetryEventEnabled = true;
      Telemetry::SetEventRecordingEnabled("cookie_banner"_ns, true);
    }

    glean::cookie_banners::ReloadExtra extra = {
        .hasClickRule = Some(hasClickRuleInData),
        .hasCookieRule = Some(hasCookieRuleInData),
        .noRule = Some(!hasClickRuleInData && !hasCookieRuleInData),
    };
    glean::cookie_banners::reload.Record(Some(extra));

    return NS_OK;
  }

  // Since we handled reload above, we only care about location change due to
  // the navigation. In this case, the location change flag would be 0x0. For
  // other cases, we can return from here.
  if (aFlags) {
    return NS_OK;
  }

  bool hasClickRule = false;
  bool hasCookieRule = false;

  nsresult rv =
      HasRuleForBrowsingContextInternal(bc, false, hasClickRule, hasCookieRule);
  NS_ENSURE_SUCCESS(rv, rv);

  hasClickRuleInData |= hasClickRule;
  hasCookieRuleInData |= hasCookieRule;

  mReloadTelemetryData.InsertOrUpdate(
      bc->Top()->Id(),
      std::make_tuple(hasClickRuleInData, hasCookieRuleInData));

  return NS_OK;
}

NS_IMETHODIMP
nsCookieBannerService::OnStatusChange(nsIWebProgress* aWebProgress,
                                      nsIRequest* aRequest, nsresult aStatus,
                                      const char16_t* aMessage) {
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
nsCookieBannerService::OnSecurityChange(nsIWebProgress* aWebProgress,
                                        nsIRequest* aRequest, uint32_t aState) {
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
nsCookieBannerService::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
                                              nsIRequest* aRequest,
                                              uint32_t aEvent) {
  return NS_ERROR_NOT_IMPLEMENTED;
}

void nsCookieBannerService::DailyReportTelemetry() {
  MOZ_ASSERT(NS_IsMainThread());

  // Convert modes to strings
  uint32_t mode = StaticPrefs::cookiebanners_service_mode();
  uint32_t modePBM = StaticPrefs::cookiebanners_service_mode_privateBrowsing();

  nsCString modeStr = ConvertModeToStringForTelemetry(mode);
  nsCString modePBMStr = ConvertModeToStringForTelemetry(modePBM);

  nsTArray<nsCString> serviceModeLabels = {
      "disabled"_ns,
      "reject"_ns,
      "reject_or_accept"_ns,
      "invalid"_ns,
  };

  // Record the service mode glean.
  for (const auto& label : serviceModeLabels) {
    glean::cookie_banners::normal_window_service_mode.Get(label).Set(
        modeStr.Equals(label));
    glean::cookie_banners::private_window_service_mode.Get(label).Set(
        modePBMStr.Equals(label));
  }

  // Report the state of the cookiebanners.service.detectOnly pref.
  glean::cookie_banners::service_detect_only.Set(
      StaticPrefs::cookiebanners_service_detectOnly());
}

nsresult nsCookieBannerService::GetServiceModeForBrowsingContext(
    dom::BrowsingContext* aBrowsingContext, bool aIgnoreDomainPref,
    nsICookieBannerService::Modes* aMode) {
  MOZ_ASSERT(XRE_IsParentProcess());
  NS_ENSURE_ARG_POINTER(aBrowsingContext);
  NS_ENSURE_ARG_POINTER(aMode);

  bool usePBM = false;
  nsresult rv = aBrowsingContext->GetUsePrivateBrowsing(&usePBM);
  NS_ENSURE_SUCCESS(rv, rv);

  uint32_t mode;
  if (usePBM) {
    mode = StaticPrefs::cookiebanners_service_mode_privateBrowsing();
  } else {
    mode = StaticPrefs::cookiebanners_service_mode();
  }

  // We can skip checking domain-specific prefs if passed the skip pref or if
  // the mode pref disables the feature. Per-domain modes enabling the service
  // sites while it's globally disabled is not supported.
  if (aIgnoreDomainPref || mode == nsICookieBannerService::MODE_DISABLED) {
    *aMode = static_cast<nsICookieBannerService::Modes>(mode);
    return NS_OK;
  }

  // Check if there is a per-domain service mode, disabling the feature for a
  // specific domain or overriding the mode.
  RefPtr<dom::WindowGlobalParent> topWGP =
      aBrowsingContext->Top()->Canonical()->GetCurrentWindowGlobal();
  NS_ENSURE_TRUE(topWGP, NS_ERROR_FAILURE);

  // Get the base domain from the content principal
  nsCOMPtr<nsIPrincipal> principal = topWGP->DocumentPrincipal();
  NS_ENSURE_TRUE(principal, NS_ERROR_NULL_POINTER);

  nsCString baseDomain;
  rv = principal->GetBaseDomain(baseDomain);
  NS_ENSURE_SUCCESS(rv, rv);
  NS_ENSURE_TRUE(!baseDomain.IsEmpty(), NS_ERROR_FAILURE);

  // Get the domain preference for the top-level baseDomain, the domain
  // preference takes precedence over the global pref setting.
  nsICookieBannerService::Modes domainPref;
  rv = GetDomainPrefInternal(baseDomain, usePBM, &domainPref);
  NS_ENSURE_SUCCESS(rv, rv);

  if (domainPref != nsICookieBannerService::MODE_UNSET) {
    mode = domainPref;
  }

  *aMode = static_cast<nsICookieBannerService::Modes>(mode);

  return NS_OK;
}

nsresult nsCookieBannerService::RegisterWebProgressListener(
    nsISupports* aSubject) {
  NS_ENSURE_ARG_POINTER(aSubject);

  RefPtr<dom::CanonicalBrowsingContext> bc =
      static_cast<dom::BrowsingContext*>(aSubject)->Canonical();

  if (!bc) {
    return NS_ERROR_FAILURE;
  }

  // We only need to register the web progress listener on the top-level
  // content browsing context. It will also get the web progress updates on
  // the child iframes.
  if (!bc->IsTopContent()) {
    return NS_OK;
  }

  mReloadTelemetryData.InsertOrUpdate(bc->Id(), std::make_tuple(false, false));

  return bc->GetWebProgress()->AddProgressListener(
      this, nsIWebProgress::NOTIFY_LOCATION);
}

nsresult nsCookieBannerService::RemoveWebProgressListener(
    nsISupports* aSubject) {
  NS_ENSURE_ARG_POINTER(aSubject);

  RefPtr<dom::CanonicalBrowsingContext> bc =
      static_cast<dom::BrowsingContext*>(aSubject)->Canonical();

  if (!bc) {
    return NS_ERROR_FAILURE;
  }

  if (!bc->IsTopContent()) {
    return NS_OK;
  }

  mReloadTelemetryData.Remove(bc->Id());

  // The browsing context web progress can be null when navigating to about
  // pages.
  nsCOMPtr<nsIWebProgress> webProgress = bc->GetWebProgress();
  if (!webProgress) {
    return NS_OK;
  }

  return webProgress->RemoveProgressListener(this);
}

void nsCookieBannerService::ReportRuleLookupTelemetry(
    const nsACString& aDomain, nsICookieBannerRule* aRule, bool aIsTopLevel) {
  nsTArray<nsCString> labelsToBeAdded;

  nsAutoCString labelPrefix;
  if (aIsTopLevel) {
    labelPrefix.Assign("top_"_ns);
  } else {
    labelPrefix.Assign("iframe_"_ns);
  }

  // The lambda function to submit the telemetry.
  auto submitTelemetry = [&]() {
    // Add the load telemetry for every label in the list.
    for (const auto& label : labelsToBeAdded) {
      glean::cookie_banners::rule_lookup_by_load.Get(labelPrefix + label)
          .Add(1);
    }

    nsTHashSet<nsCStringHashKey>& reportedDomains =
        aIsTopLevel ? mTelemetryReportedTopDomains
                    : mTelemetryReportedIFrameDomains;

    // For domain telemetry, we only submit once for each domain.
    if (!reportedDomains.Contains(aDomain)) {
      for (const auto& label : labelsToBeAdded) {
        glean::cookie_banners::rule_lookup_by_domain.Get(labelPrefix + label)
            .Add(1);
      }
      reportedDomains.Insert(aDomain);
    }
  };

  // No rule found for the domain. Submit telemetry with lookup miss.
  if (!aRule) {
    labelsToBeAdded.AppendElement("miss"_ns);
    labelsToBeAdded.AppendElement("cookie_miss"_ns);
    labelsToBeAdded.AppendElement("click_miss"_ns);

    submitTelemetry();
    return;
  }

  // Check if we have a cookie rule for the domain.
  bool hasCookieRule = false;
  bool hasCookieOptIn = false;
  bool hasCookieOptOut = false;
  nsTArray<RefPtr<nsICookieRule>> cookies;

  nsresult rv = aRule->GetCookiesOptIn(cookies);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  if (!cookies.IsEmpty()) {
    labelsToBeAdded.AppendElement("cookie_hit_opt_in"_ns);
    hasCookieRule = true;
    hasCookieOptIn = true;
  }

  cookies.Clear();
  rv = aRule->GetCookiesOptOut(cookies);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  if (!cookies.IsEmpty()) {
    labelsToBeAdded.AppendElement("cookie_hit_opt_out"_ns);
    hasCookieRule = true;
    hasCookieOptOut = true;
  }

  if (hasCookieRule) {
    labelsToBeAdded.AppendElement("cookie_hit"_ns);
  } else {
    labelsToBeAdded.AppendElement("cookie_miss"_ns);
  }

  // Check if we have a click rule for the domain.
  bool hasClickRule = false;
  bool hasClickOptIn = false;
  bool hasClickOptOut = false;
  nsCOMPtr<nsIClickRule> clickRule;
  rv = aRule->GetClickRule(getter_AddRefs(clickRule));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  if (clickRule) {
    nsAutoCString clickOptIn;
    nsAutoCString clickOptOut;

    rv = clickRule->GetOptIn(clickOptIn);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return;
    }

    rv = clickRule->GetOptOut(clickOptOut);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return;
    }

    if (!clickOptIn.IsEmpty()) {
      labelsToBeAdded.AppendElement("click_hit_opt_in"_ns);
      hasClickRule = true;
      hasClickOptIn = true;
    }

    if (!clickOptOut.IsEmpty()) {
      labelsToBeAdded.AppendElement("click_hit_opt_out"_ns);
      hasClickRule = true;
      hasClickOptOut = true;
    }

    if (hasClickRule) {
      labelsToBeAdded.AppendElement("click_hit"_ns);
    } else {
      labelsToBeAdded.AppendElement("click_miss"_ns);
    }
  } else {
    labelsToBeAdded.AppendElement("click_miss"_ns);
  }

  if (hasCookieRule || hasClickRule) {
    labelsToBeAdded.AppendElement("hit"_ns);
    if (hasCookieOptIn || hasClickOptIn) {
      labelsToBeAdded.AppendElement("hit_opt_in"_ns);
    }

    if (hasCookieOptOut || hasClickOptOut) {
      labelsToBeAdded.AppendElement("hit_opt_out"_ns);
    }
  } else {
    labelsToBeAdded.AppendElement("miss"_ns);
  }

  submitTelemetry();
}

}  // namespace mozilla
