// Copyright 2022 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/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl.h"

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

#include "base/barrier_callback.h"
#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/containers/to_vector.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "chrome/browser/metrics/chrome_metrics_service_accessor.h"
#include "chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller.h"
#include "chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl.h"
#include "chrome/browser/signin/bound_session_credentials/bound_session_debug_info.h"
#include "chrome/browser/signin/bound_session_credentials/bound_session_key.h"
#include "chrome/browser/signin/bound_session_credentials/bound_session_params.pb.h"
#include "chrome/browser/signin/bound_session_credentials/bound_session_params_storage.h"
#include "chrome/browser/signin/bound_session_credentials/bound_session_params_util.h"
#include "chrome/browser/signin/bound_session_credentials/bound_session_refresh_cookie_debug_report_fetcher.h"
#include "chrome/browser/signin/bound_session_credentials/bound_session_registration_fetcher_impl.h"
#include "chrome/browser/signin/bound_session_credentials/rotation_debug_info.pb.h"
#include "chrome/common/google_url_loader_throttle.h"
#include "chrome/common/renderer_configuration.mojom.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/variations/synthetic_trials.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/schemeful_site.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "url/origin.h"

namespace {

using SessionTerminationTrigger =
    ::BoundSessionCookieRefreshServiceImpl::SessionTerminationTrigger;

constexpr std::string_view kGoogleSessionTerminationHeader =
    "Sec-Session-Google-Termination";
constexpr std::string_view kGoogleSessionTerminationSessionIdKey = "session_id";

BASE_FEATURE(kUseDeviceBoundSessionsStorageMaskForDeletion,
             base::FEATURE_ENABLED_BY_DEFAULT);

// Determines the precedence order of
// `chrome::mojom::ResumeBlockedRequestsTrigger` when recording metrics.
size_t GetResumeBlockedRequestsTriggerPriority(
    chrome::mojom::ResumeBlockedRequestsTrigger trigger) {
  using chrome::mojom::ResumeBlockedRequestsTrigger;
  switch (trigger) {
    case ResumeBlockedRequestsTrigger::kCookieAlreadyFresh:
      return 0;
    case ResumeBlockedRequestsTrigger::kObservedFreshCookies:
      return 1;
    case ResumeBlockedRequestsTrigger::kCookieRefreshFetchSuccess:
      return 2;
    case ResumeBlockedRequestsTrigger::kShutdownOrSessionTermination:
      return 3;
    case ResumeBlockedRequestsTrigger::kRendererDisconnected:
      return 4;
    case ResumeBlockedRequestsTrigger::kCookieRefreshFetchFailure:
      return 5;
    case ResumeBlockedRequestsTrigger::kTimeout:
      return 6;
    case ResumeBlockedRequestsTrigger::kThrottlingRequestsPaused:
      return 7;
  }
}

// Computes a trigger value that should be used for metrics recording based on a
// list of triggers received from multiple controllers.
chrome::mojom::ResumeBlockedRequestsTrigger AggregateMultipleTriggers(
    std::vector<chrome::mojom::ResumeBlockedRequestsTrigger> triggers) {
  CHECK(!triggers.empty());
  return *std::max_element(
      triggers.begin(), triggers.end(),
      [](chrome::mojom::ResumeBlockedRequestsTrigger lhs,
         chrome::mojom::ResumeBlockedRequestsTrigger rhs) {
        return GetResumeBlockedRequestsTriggerPriority(lhs) <
               GetResumeBlockedRequestsTriggerPriority(rhs);
      });
}

bound_session_credentials::RotationDebugInfo::TerminationReason
GetRotationDebugTerminationReason(
    BoundSessionCookieRefreshServiceImpl::SessionTerminationTrigger trigger,
    std::optional<BoundSessionRefreshCookieFetcher::Result> refresh_error) {
  using enum BoundSessionCookieRefreshServiceImpl::SessionTerminationTrigger;
  using bound_session_credentials::RotationDebugInfo;

  auto GetReasonFromRefreshError =
      [](BoundSessionRefreshCookieFetcher::Result refresh_error) {
        using Result = BoundSessionRefreshCookieFetcher::Result;
        CHECK(
            BoundSessionRefreshCookieFetcher::IsPersistentError(refresh_error));
        switch (refresh_error) {
          case Result::kServerPersistentError:
            return RotationDebugInfo::ROTATION_PERSISTENT_ERROR;
          case Result::kChallengeRequiredUnexpectedFormat:
            return RotationDebugInfo::ROTATION_CHALLENGE_UNEXPECTED_FORMAT;
          case Result::kChallengeRequiredLimitExceeded:
            return RotationDebugInfo::ROTATION_CHALLENGE_LIMIT_EXCEEDED;
          case Result::kSignChallengeFailed:
            return RotationDebugInfo::ROTATION_SIGN_CHALLENGE_FAILED;
          case Result::kChallengeRequiredSessionIdMismatch:
            return RotationDebugInfo::ROTATION_CHALLENGE_SESSION_ID_MISMATCH;
          default:
            return RotationDebugInfo::TERMINATION_REASON_OTHER;
        }
      };

  switch (trigger) {
    case kSessionTerminationHeader:
      return RotationDebugInfo::TERMINATION_HEADER_RECEIVED;
    case kCookieRotationPersistentError:
      return refresh_error ? GetReasonFromRefreshError(*refresh_error)
                           : RotationDebugInfo::TERMINATION_REASON_OTHER;
    case kSessionOverride:
      return RotationDebugInfo::SESSION_OVERRIDE;
    case kRotationStoppedTimeout:
      return RotationDebugInfo::ROTATION_STOPPED_TIMEOUT;
    case kCookiesCleared:
      // `kCookiesCleared` should not be reported in the debug header.
      NOTREACHED();
  }
}

chrome::mojom::BoundSessionThrottlerParamsPtr
GetThrottlerParamsForRequestCoverage(
    const BoundSessionCookieController* controller) {
  if (!controller->ShouldPauseThrottlingRequests()) {
    return controller->bound_session_throttler_params();
  }

  // Throttling is paused, `chrome::mojom::BoundSessionThrottlerParamsPtr` is
  // expected to be null. Construct throttler params to compute request
  // coverage. Use time in the past to ensure that a throttler will be added to
  // blocking throttlers if it covers a request. The controller will resume the
  // request immediately.
  // Note: This is needed to ensure the correctness of metrics in case of
  // outages.
  return chrome::mojom::BoundSessionThrottlerParams::New(
      controller->scope_url().GetHost(), controller->scope_url().GetPath(),
      base::Time());
}

bool IsNewSessionRegistrationEnabled(const PrefService* profile_prefs) {
  return switches::IsBoundSessionCredentialsEnabled(profile_prefs);
}

bool IsSessionInitializationEnabled(const PrefService* profile_prefs) {
  // It should always be possible to initialize a session if the registration is
  // enabled.
  return IsNewSessionRegistrationEnabled(profile_prefs) ||
         base::FeatureList::IsEnabled(kEnableBoundSessionCredentialsContinuity);
}

void RecordSessionTerminationTrigger(
    SessionTerminationTrigger trigger,
    bound_session_credentials::SessionOrigin session_origin) {
  static constexpr std::string_view kHistogramName =
      "Signin.BoundSessionCredentials.SessionTerminationTrigger";
  base::UmaHistogramEnumeration(kHistogramName, trigger);
  if (const std::optional<std::string_view> session_origin_suffix =
          bound_session_credentials::GetSessionOriginHistogramSuffix(
              session_origin);
      session_origin_suffix.has_value()) {
    base::UmaHistogramEnumeration(
        base::StrCat({kHistogramName, *session_origin_suffix}), trigger);
  }
}

}  // namespace

BASE_FEATURE(kEnableBoundSessionCredentialsContinuity,
#if BUILDFLAG(IS_WIN)
             base::FEATURE_ENABLED_BY_DEFAULT
#else
             base::FEATURE_DISABLED_BY_DEFAULT
#endif
);

BoundSessionCookieRefreshServiceImpl::BoundSessionCookieRefreshServiceImpl(
    unexportable_keys::UnexportableKeyService& key_service,
    std::unique_ptr<BoundSessionParamsStorage> session_params_storage,
    content::StoragePartition* storage_partition,
    network::NetworkConnectionTracker* network_connection_tracker,
    const PrefService* profile_prefs,
    bool is_off_the_record_profile)
    : key_service_(key_service),
      session_params_storage_(std::move(session_params_storage)),
      storage_partition_(storage_partition),
      network_connection_tracker_(network_connection_tracker),
      profile_prefs_(profile_prefs),
      is_off_the_record_profile_(is_off_the_record_profile) {
  CHECK(session_params_storage_);
  CHECK(storage_partition_);
  CHECK(profile_prefs_);
  data_removal_observation_.Observe(storage_partition_);
}

BoundSessionCookieRefreshServiceImpl::~BoundSessionCookieRefreshServiceImpl() =
    default;

void BoundSessionCookieRefreshServiceImpl::Initialize() {
  std::vector<bound_session_credentials::BoundSessionParams>
      bound_session_params =
          session_params_storage_->ReadAllParamsAndCleanStorageIfNecessary();

  constexpr int kMaxSessionsForMetrics = 30;
  base::UmaHistogramExactLinear(
      "Signin.BoundSessionCredentials.SessionCountOnInit",
      bound_session_params.size(), kMaxSessionsForMetrics);

  if (bound_session_params.empty()) {
    return;
  }

  for (const auto& params : bound_session_params) {
    if (IsSessionInitializationEnabled(profile_prefs_)) {
      InitializeBoundSession(params, /*is_new_session=*/false);
    }
  }
  UpdateAllRenderers();
}

void BoundSessionCookieRefreshServiceImpl::RegisterNewBoundSession(
    const bound_session_credentials::BoundSessionParams& params) {
  if (!IsNewSessionRegistrationEnabled(profile_prefs_)) {
    return;
  }

  if (!session_params_storage_->SaveParams(params)) {
    DVLOG(1) << "Invalid session params or failed to serialize session params.";
    return;
  }

  auto it = cookie_controllers_.find(
      bound_session_credentials::GetBoundSessionKey(params));
  if (it != cookie_controllers_.end()) {
    RecordSessionTerminationTrigger(SessionTerminationTrigger::kSessionOverride,
                                    it->second->session_origin());
    cookie_controllers_.erase(it);
    // Note: `NotifyBoundSessionTerminated()` is not called as new session is
    // starting with the same scope.
  }

  InitializeBoundSession(params, /*is_new_session=*/true);
  UpdateAllRenderers();
}

void BoundSessionCookieRefreshServiceImpl::MaybeTerminateSession(
    const GURL& response_url,
    const net::HttpResponseHeaders* headers) {
  if (!headers) {
    return;
  }

  std::optional<std::string> termination_header_value =
      headers->GetNormalizedHeader(kGoogleSessionTerminationHeader);
  if (!termination_header_value) {
    return;
  }

  base::StringPairs items;
  base::SplitStringIntoKeyValuePairs(*termination_header_value, '=', ';',
                                     &items);
  auto session_id_it = std::ranges::find_if(items, [](const auto& kv_pair) {
    return base::EqualsCaseInsensitiveASCII(
        kv_pair.first, kGoogleSessionTerminationSessionIdKey);
  });
  if (session_id_it == items.end()) {
    return;
  }

  BoundSessionKey key = {
      .site = net::SchemefulSite(response_url).GetURL(),
      .session_id = session_id_it->second,
  };
  auto it = cookie_controllers_.find(key);
  if (it != cookie_controllers_.end()) {
    TerminateSession(it->second.get(),
                     SessionTerminationTrigger::kSessionTerminationHeader);
  } else {
    DVLOG(1) << "Session termination header (" << key.site.spec() << "; "
             << key.session_id << ") doesn't match any current session";
  }
}

std::vector<chrome::mojom::BoundSessionThrottlerParamsPtr>
BoundSessionCookieRefreshServiceImpl::GetBoundSessionThrottlerParams() const {
  std::vector<chrome::mojom::BoundSessionThrottlerParamsPtr> result;
  for (const auto& [key, controller] : cookie_controllers_) {
    if (chrome::mojom::BoundSessionThrottlerParamsPtr params =
            controller->bound_session_throttler_params();
        params) {
      result.push_back(std::move(params));
    }
  }
  return result;
}

std::vector<BoundSessionKey>
BoundSessionCookieRefreshServiceImpl::GetAllSessions() const {
  return base::ToVector(cookie_controllers_,
                        [](const auto& key_value) { return key_value.first; });
}

void BoundSessionCookieRefreshServiceImpl::
    SetRendererBoundSessionThrottlerParamsUpdaterDelegate(
        RendererBoundSessionThrottlerParamsUpdaterDelegate renderer_updater) {
  renderer_updater_ = std::move(renderer_updater);
}

void BoundSessionCookieRefreshServiceImpl::
    SetBoundSessionParamsUpdatedCallbackForTesting(
        base::RepeatingClosure updated_callback) {
  session_updated_callback_for_testing_ = std::move(updated_callback);
}

void BoundSessionCookieRefreshServiceImpl::
    AddBoundSessionRequestThrottledHandlerReceiver(
        mojo::PendingReceiver<
            chrome::mojom::BoundSessionRequestThrottledHandler> receiver) {
  renderer_request_throttled_handler_.Add(this, std::move(receiver));
}

void BoundSessionCookieRefreshServiceImpl::HandleRequestBlockedOnCookie(
    const GURL& untrusted_request_url,
    HandleRequestBlockedOnCookieCallback resume_blocked_request) {
  if (cookie_controllers_.empty()) {
    // Session has been terminated.
    std::move(resume_blocked_request)
        .Run(chrome::mojom::ResumeBlockedRequestsTrigger::
                 kShutdownOrSessionTermination);
    return;
  }

  std::vector<BoundSessionCookieController*> blocking_controllers;
  bool request_covered_by_at_least_one_session = false;
  for (const auto& [key, controller] : cookie_controllers_) {
    std::vector<chrome::mojom::BoundSessionThrottlerParamsPtr> throttler_params;
    throttler_params.push_back(
        GetThrottlerParamsForRequestCoverage(controller.get()));
    GoogleURLLoaderThrottle::RequestBoundSessionStatus status =
        GoogleURLLoaderThrottle::GetRequestBoundSessionStatus(
            untrusted_request_url, throttler_params);
    switch (status) {
      case GoogleURLLoaderThrottle::RequestBoundSessionStatus::
          kCoveredWithMissingCookie:
        blocking_controllers.push_back(controller.get());
        [[fallthrough]];
      case GoogleURLLoaderThrottle::RequestBoundSessionStatus::
          kCoveredWithFreshCookie:
        request_covered_by_at_least_one_session = true;
        break;
      case GoogleURLLoaderThrottle::RequestBoundSessionStatus::kNotCovered:
        break;
    }
  }

  if (blocking_controllers.empty()) {
    std::move(resume_blocked_request)
        .Run(request_covered_by_at_least_one_session
                 ? chrome::mojom::ResumeBlockedRequestsTrigger::
                       kCookieAlreadyFresh
                 : chrome::mojom::ResumeBlockedRequestsTrigger::
                       kShutdownOrSessionTermination);
    return;
  }

  base::RepeatingCallback<void(chrome::mojom::ResumeBlockedRequestsTrigger)>
      barrier_callback =
          base::BarrierCallback<chrome::mojom::ResumeBlockedRequestsTrigger>(
              blocking_controllers.size(),
              base::BindOnce(&AggregateMultipleTriggers)
                  .Then(std::move(resume_blocked_request)));
  for (BoundSessionCookieController* controller : blocking_controllers) {
    controller->HandleRequestBlockedOnCookie(barrier_callback);
  }
}

void BoundSessionCookieRefreshServiceImpl::CreateRegistrationRequest(
    BoundSessionRegistrationFetcherParam registration_params) {
  if (!IsNewSessionRegistrationEnabled(profile_prefs_)) {
    return;
  }

  // Guardrail against registering non-SIDTS DBSC sessions while the client
  // lacks support for running multiple sessions at the same time. Can be
  // overridden with a Finch config parameter.
  // TODO(http://b/274774185): Remove this guardrail once ready.
  std::string exclusive_registration_path =
      switches::kEnableBoundSessionCredentialsExclusiveRegistrationPath.Get();
  if (!exclusive_registration_path.empty() &&
      !base::EqualsCaseInsensitiveASCII(
          registration_params.registration_endpoint().path(),
          exclusive_registration_path)) {
    return;
  }

  registration_requests_.push(
      registration_fetcher_factory_for_testing_.is_null()
          ? std::make_unique<BoundSessionRegistrationFetcherImpl>(
                std::move(registration_params),
                storage_partition_->GetURLLoaderFactoryForBrowserProcess(),
                key_service_.get(), is_off_the_record_profile_)
          : registration_fetcher_factory_for_testing_.Run(
                std::move(registration_params)));

  if (registration_requests_.size() == 1U) {
    StartRegistrationRequest();
  }
}

void BoundSessionCookieRefreshServiceImpl::StopCookieRotation(
    const BoundSessionKey& key) {
  auto controller_it = cookie_controllers_.find(key);
  if (controller_it == cookie_controllers_.end()) {
    return;
  }
  controller_it->second->StopCookieRotation();
}

base::WeakPtr<BoundSessionCookieRefreshService>
BoundSessionCookieRefreshServiceImpl::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

void BoundSessionCookieRefreshServiceImpl::AddObserver(
    BoundSessionCookieRefreshService::Observer* observer) {
  observers_.AddObserver(observer);
}

void BoundSessionCookieRefreshServiceImpl::RemoveObserver(
    BoundSessionCookieRefreshService::Observer* observer) {
  observers_.RemoveObserver(observer);
}

std::vector<BoundSessionDebugInfo>
BoundSessionCookieRefreshServiceImpl::GetBoundSessionDebugInfo() const {
  std::vector<BoundSessionDebugInfo> bound_session_debug_info;
  for (const auto& [key, controller] : cookie_controllers_) {
    bound_session_debug_info.push_back(
        BoundSessionDebugInfo::Create(*controller));
  }
  return bound_session_debug_info;
}

BoundSessionCookieController*
BoundSessionCookieRefreshServiceImpl::cookie_controller() const {
  if (cookie_controllers_.empty()) {
    return nullptr;
  }
  return cookie_controllers_.begin()->second.get();
}

void BoundSessionCookieRefreshServiceImpl::StartRegistrationRequest() {
  // `base::Unretained(this)` is safe here because `this` owns the fetcher via
  // `registration_requests_`
  registration_requests_.front()->Start(base::BindOnce(
      &BoundSessionCookieRefreshServiceImpl::OnRegistrationRequestComplete,
      base::Unretained(this)));
}

void BoundSessionCookieRefreshServiceImpl::OnRegistrationRequestComplete(
    std::optional<bound_session_credentials::BoundSessionParams>
        bound_session_params) {
  if (bound_session_params.has_value()) {
    RegisterNewBoundSession(*bound_session_params);
  }

  registration_requests_.pop();
  if (!registration_requests_.empty()) {
    StartRegistrationRequest();
  }
}

void BoundSessionCookieRefreshServiceImpl::
    OnBoundSessionThrottlerParamsChanged() {
  UpdateAllRenderers();
}

void BoundSessionCookieRefreshServiceImpl::OnCookieRotationStoppedTimeout(
    BoundSessionCookieController* controller) {
  TerminateSession(controller,
                   SessionTerminationTrigger::kRotationStoppedTimeout);
}

void BoundSessionCookieRefreshServiceImpl::OnPersistentErrorEncountered(
    BoundSessionCookieController* controller,
    BoundSessionRefreshCookieFetcher::Result refresh_error) {
  TerminateSession(controller,
                   SessionTerminationTrigger::kCookieRotationPersistentError,
                   refresh_error);
}

void BoundSessionCookieRefreshServiceImpl::OnStorageKeyDataCleared(
    uint32_t remove_mask,
    content::StoragePartition::StorageKeyMatcherFunction storage_key_matcher,
    const base::Time begin,
    const base::Time end) {
  const uint32_t storage_mask =
      base::FeatureList::IsEnabled(
          kUseDeviceBoundSessionsStorageMaskForDeletion)
          ? content::StoragePartition::REMOVE_DATA_MASK_DEVICE_BOUND_SESSIONS
          : content::StoragePartition::REMOVE_DATA_MASK_COOKIES;

  // Only terminate sessions if a relevant data type is cleared.
  if (!(remove_mask & storage_mask)) {
    return;
  }

  std::vector<BoundSessionCookieController*> controllers_to_remove;
  for (auto& [key, controller] : cookie_controllers_) {
    // Only terminate a session if it was created within the specified time
    // range.
    base::Time session_creation_time = controller->session_creation_time();
    if (session_creation_time < begin || session_creation_time > end) {
      continue;
    }

    // Only terminate a session if its URL matches `storage_key_matcher`.
    // Bound sessions are only supported in first-party contexts, so it's
    // acceptable to use `blink::StorageKey::CreateFirstParty()`.
    if (!storage_key_matcher.Run(blink::StorageKey::CreateFirstParty(
            url::Origin::Create(controller->scope_url())))) {
      continue;
    }

    // `TerminateSession()` cannot be called while iterating over
    // `cookie_controllers_`.
    controllers_to_remove.push_back(controller.get());
  }

  for (const auto& controller : controllers_to_remove) {
    TerminateSession(controller, SessionTerminationTrigger::kCookiesCleared);
  }
}

std::unique_ptr<BoundSessionCookieController>
BoundSessionCookieRefreshServiceImpl::CreateBoundSessionCookieController(
    const bound_session_credentials::BoundSessionParams& bound_session_params,
    bool is_off_the_record_profile) {
  return controller_factory_for_testing_.is_null()
             ? std::make_unique<BoundSessionCookieControllerImpl>(
                   key_service_.get(), storage_partition_,
                   network_connection_tracker_, bound_session_params, this,
                   is_off_the_record_profile)
             : controller_factory_for_testing_.Run(bound_session_params, this);
}

void BoundSessionCookieRefreshServiceImpl::InitializeBoundSession(
    const bound_session_credentials::BoundSessionParams& bound_session_params,
    bool is_new_session) {
  CHECK(IsSessionInitializationEnabled(profile_prefs_));
  std::unique_ptr<BoundSessionCookieController> controller =
      CreateBoundSessionCookieController(bound_session_params,
                                         is_off_the_record_profile_);
  BoundSessionKey key = controller->GetBoundSessionKey();
  auto [it, inserted] =
      cookie_controllers_.emplace(std::move(key), std::move(controller));
  CHECK(inserted);
  it->second->Initialize(is_new_session);
}

void BoundSessionCookieRefreshServiceImpl::UpdateAllRenderers() {
  if (renderer_updater_) {
    renderer_updater_.Run();
  }
  if (session_updated_callback_for_testing_) {
    session_updated_callback_for_testing_.Run();
  }
}

void BoundSessionCookieRefreshServiceImpl::TerminateSession(
    BoundSessionCookieController* controller,
    SessionTerminationTrigger trigger,
    std::optional<BoundSessionRefreshCookieFetcher::Result> refresh_error) {
  BoundSessionKey session_key = controller->GetBoundSessionKey();
  auto it = cookie_controllers_.find(session_key);
  CHECK(it != cookie_controllers_.end());
  CHECK_EQ(it->second.get(), controller);
  // Save `bound_cookie_names` locally on the stack before destroying
  // `controller`.
  base::flat_set<std::string> bound_cookie_names =
      controller->bound_cookie_names();
  MaybeReportTerminationReason(controller, trigger, refresh_error);
  RecordSessionTerminationTrigger(trigger, controller->session_origin());
  cookie_controllers_.erase(it);
  // `controller` is no longer valid and must not be used.

  session_params_storage_->ClearParams(session_key.site,
                                       session_key.session_id);
  UpdateAllRenderers();

  NotifyBoundSessionTerminated(session_key.site, bound_cookie_names);
}

void BoundSessionCookieRefreshServiceImpl::NotifyBoundSessionTerminated(
    const GURL& site,
    const base::flat_set<std::string>& bound_cookie_names) {
  for (BoundSessionCookieRefreshService::Observer& observer : observers_) {
    observer.OnBoundSessionTerminated(site, bound_cookie_names);
  }
}

void BoundSessionCookieRefreshServiceImpl::MaybeReportTerminationReason(
    BoundSessionCookieController* controller,
    SessionTerminationTrigger trigger,
    std::optional<BoundSessionRefreshCookieFetcher::Result> refresh_error) {
  if (trigger == SessionTerminationTrigger::kCookiesCleared) {
    // Do not send the debug report if cookies were cleared or the rotation was
    // terminated due to a stopping timeout as the request won't be attributed
    // to a user in any case.
    return;
  }

  bound_session_credentials::RotationDebugInfo debug_info =
      controller->TakeDebugInfo();
  debug_info.set_termination_reason(
      GetRotationDebugTerminationReason(trigger, refresh_error));
  auto reporter =
      debug_report_fetcher_factory_for_testing_.is_null()
          ? std::make_unique<BoundSessionRefreshCookieDebugReportFetcher>(
                storage_partition_->GetURLLoaderFactoryForBrowserProcess(),
                controller->session_id(), controller->refresh_url(),
                is_off_the_record_profile_, std::move(debug_info))
          : debug_report_fetcher_factory_for_testing_.Run(
                controller->session_id(), controller->refresh_url(),
                is_off_the_record_profile_, std::move(debug_info));
  CHECK(reporter);
  BoundSessionRefreshCookieFetcher* reporter_raw = reporter.get();
  termination_reason_reporters_.insert(std::move(reporter));
  // `base::Unretained()` is safe because `this` owns
  // `termination_reason_reporters_`.
  reporter_raw->Start(base::BindOnce(&BoundSessionCookieRefreshServiceImpl::
                                         OnTerminationReasonReportCompleted,
                                     base::Unretained(this), reporter_raw),
                      "SESSION_TERMINATION_DEBUG_REPORT");
}

void BoundSessionCookieRefreshServiceImpl::OnTerminationReasonReportCompleted(
    BoundSessionRefreshCookieFetcher* reporter,
    BoundSessionRefreshCookieFetcher::Result result) {
  auto it = termination_reason_reporters_.find(reporter);
  CHECK(it != termination_reason_reporters_.end());
  termination_reason_reporters_.erase(it);
}
