// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CHROME_BROWSER_NAVIGATION_PREDICTOR_SEARCH_ENGINE_PRECONNECTOR_H_
#define CHROME_BROWSER_NAVIGATION_PREDICTOR_SEARCH_ENGINE_PRECONNECTOR_H_

#include "base/feature_list.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/numerics/clamped_math.h"
#include "base/numerics/safe_conversions.h"
#include "base/timer/timer.h"
#include "components/keyed_service/core/keyed_service.h"
#include "content/public/browser/preconnect_manager.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "net/base/reconnect_notifier.h"
#include "net/net_buildflags.h"
#include "services/network/public/mojom/connection_change_observer_client.mojom.h"
#include "url/origin.h"

#if BUILDFLAG(ENABLE_DEVICE_BOUND_SESSIONS)
#include "chrome/browser/net/device_bound_session_prewarmer.h"
#endif

namespace content {
class BrowserContext;
class WebContents;
}  // namespace content

#if BUILDFLAG(ENABLE_DEVICE_BOUND_SESSIONS)
class DeviceBoundSessionPrewarmer;
#endif

namespace features {
BASE_DECLARE_FEATURE(kPreconnectFromKeyedService);
BASE_DECLARE_FEATURE(kPreconnectToSearch);

// Enum to represent the event that triggers the rebinding of the receiver.
// This is used to control the rebinding frequency.
enum class RebindReceiverEvent {
  // Rebind the receiver every time the preconnect occurs.
  kEverytime = 0,
  // Rebind the receiver only when the connection is closed or failed.
  kOnlyOnConnectionClosedOrFailed = 1,
};

BASE_DECLARE_FEATURE(kRebindPreconnectReceivers);
BASE_DECLARE_FEATURE_PARAM(RebindReceiverEvent, kRebindReceiverEvent);

BASE_DECLARE_FEATURE(kAdjustPreconnectRetryInterval);
BASE_DECLARE_FEATURE_PARAM(base::TimeDelta, kPreconnectRetryInterval);
BASE_DECLARE_FEATURE_PARAM(base::TimeDelta, kPreconnectBackoffBaseTime);
BASE_DECLARE_FEATURE_PARAM(double, kPreconnectBackoffMultiplier);
BASE_DECLARE_FEATURE_PARAM(base::TimeDelta, kPreconnectNetworkChangeInterval);
BASE_DECLARE_FEATURE_PARAM(base::TimeDelta, kPreconnectInitialRetryInterval);
BASE_DECLARE_FEATURE(kResetConnectionFailureOnSessionUsed);
}  // namespace features

// Class to keep track of the current visibility. It is used to determine if the
// app is currently in the foreground or not.
class WebContentVisibilityManager {
 public:
  WebContentVisibilityManager();
  ~WebContentVisibilityManager();
  // Notifies |this| that the visibility of web contents tracked by |client| has
  // changed or if user starts a new navigation corresponding to |web_contents|.
  // Might be called more than once with the same |is_in_foreground| and
  // |web_contents| in case user starts a new navigation with same
  // |web_contents|.
  virtual void OnWebContentsVisibilityChanged(
      content::WebContents* web_contents,
      bool is_in_foreground);

  // Notifies |this| that the web contents tracked by |client| has destroyed.
  void OnWebContentsDestroyed(content::WebContents* web_contents);

  // Returns true if the browser app is likely to be in foreground and being
  // interacted by the user. This is heuristically computed by observing loading
  // and visibility of web contents.
  bool IsBrowserAppLikelyInForeground() const;

  // Overrides the tick clock used by |this| for testing.
  void SetTickClockForTesting(const base::TickClock* tick_clock);

 private:
  std::unordered_set<raw_ptr<content::WebContents, CtnExperimental>>
      visible_web_contents_;

  base::TimeTicks last_web_contents_state_change_time_;

  raw_ptr<const base::TickClock> tick_clock_;
};

// Class to preconnect to the user's default search engine at regular intervals.
// Preconnects are made by |this| if the browser app is likely in foreground.
class SearchEnginePreconnector
    : public content::PreconnectManager::Delegate,
      public WebContentVisibilityManager,
      public KeyedService,
      public network::mojom::ConnectionChangeObserverClient {
 public:
  static bool ShouldBeEnabledAsKeyedService();
  static bool ShouldBeEnabledForOffTheRecord();
  static bool SearchEnginePreconnect2Enabled();

  explicit SearchEnginePreconnector(content::BrowserContext* browser_context);
  ~SearchEnginePreconnector() override;

  SearchEnginePreconnector(const SearchEnginePreconnector&) = delete;
  SearchEnginePreconnector& operator=(const SearchEnginePreconnector&) = delete;

  // Start the process of preconnecting to the default search engine.
  // |with_startup_delay| adds a delay to the preconnect, and should be true
  // only during app start up.
  void StartPreconnecting(bool with_startup_delay);

  // Stops preconnecting to the DSE. Called on app background.
  void StopPreconnecting();

  // PreconnectManager::Delegate:
  void PreconnectInitiated(const GURL& url,
                           const GURL& preconnect_url) override {}
  void PreconnectFinished(
      std::unique_ptr<content::PreconnectStats> stats) override {}

  bool IsPreconnectEnabled() override;

  // network::mojom::ConnectionChangeObserverClient
  void OnConnectionEstablished(
      const net::ConnectionChangeNotifier::EstablishedConnectionInfo& info)
      override;
  void OnSessionClosed(bool was_ever_used_to_create_streams) override;
  void OnNetworkEvent(net::NetworkChangeEvent event) override;
  void OnConnectionFailed() override;

  // Lazily creates the PreconnectManager instance.
  content::PreconnectManager& GetPreconnectManager();

  // WebContentVisibilityManager methods
  // TODO(crbug.com/406022435): Update to observe the
  // `WebContentVisibilityManager` and not override them.
  void OnWebContentsVisibilityChanged(content::WebContents* web_contents,
                                      bool is_in_foreground) override;

  // Calculate the backoff multiplier to exponentially backoff when preconnects
  // are consecutively failing. The returned value should be within the range
  // of int32_t's binary digits.
  int32_t CalculateBackoffMultiplier() const;

  void SetConsecutiveFailureForTesting(int consecutive_failure) {
    consecutive_connection_failure_ = consecutive_failure;
  }

  int GetConsecutiveConnectionFailureForTesting() {
    return consecutive_connection_failure_;
  }

  void SetIsShortSessionForTesting(bool is_short_session) {
    is_short_session_for_testing_ = is_short_session;
  }

#if BUILDFLAG(ENABLE_DEVICE_BOUND_SESSIONS)
  bool HasDeviceBoundSessionPrewarmerForTesting() const {
    return !!device_bound_session_prewarmer_;
  }
#endif

 private:
  FRIEND_TEST_ALL_PREFIXES(
      SearchEnginePreconnectorWithPreconnect2FeatureBrowserTest,
      PreconnectSearchAfterOnCloseWithShortSession);
  FRIEND_TEST_ALL_PREFIXES(
      SearchEnginePreconnectorWithPreconnect2FeatureBrowserTest,
      PreconnectSearchAfterOnFailure);
  FRIEND_TEST_ALL_PREFIXES(
      SearchEnginePreconnectorWithPreconnect2FeatureBrowserTest,
      PreconnectSearchAfterOnConnect);
  FRIEND_TEST_ALL_PREFIXES(
      SearchEnginePreconnectorWithPreconnect2FeatureBrowserTest,
      CheckConnectionKeepAliveConfig);

  // Enum to represent the preconnect triggering event. This is used to record
  // the histogram.
  //
  // These values are persisted to logs. Entries should not be renumbered and
  // numeric values should never be reused.
  //
  // LINT.IfChange(PreconnectTriggerEvent)
  enum class PreconnectTriggerEvent {
    kInitialPreconnect = 0,
    kPeriodicPreconnect = 1,  // This represents non SearchEnginePreconnect2
                              // preconnects.
    kSessionClosed = 2,
    kNetworkEvent = 3,
    kConnectionFailed = 4,
    kMaxValue = kConnectionFailed
  };
  // LINT.ThenChange(//tools/metrics/histograms/metadata/navigation/enums.xml:PreconnectTriggerEvent)

  // Preconnects to the default search engine synchronously. Preconnects in
  // uncredentialed mode.
  void PreconnectDSE(bool is_startup);

  // Runs `PreconnectDSE` after the `delay`.
  void StartPreconnectWithDelay(base::TimeDelta delay,
                                PreconnectTriggerEvent event);

  // Queries template service for the current DSE URL.
  GURL GetDefaultSearchEngineOriginURL() const;

  base::TimeDelta GetPreconnectInterval() const;

  // Determines if the closed session was short. This is used to calculate
  // whether we need to backoff a bit more when a session is closed to avoid
  // back-to-back connections.
  bool IsShortSession() const;

  bool ShouldSavePower() const;

  // Invoked when the mojo pipe to the reconnect observer is disconnected.
  void OnReconnectObserverPipeDisconnected();

  // Resets the receiver.
  void ResetReceiver();

  void RecordPreconnectAttemptHistogram(base::TimeDelta delay,
                                        PreconnectTriggerEvent event);

  // Returns the connection keepalive config to be used for preconnect. The
  // returned config will change when the device is in low power mode.
  net::ConnectionKeepAliveConfig GetConnectionKeepAliveConfig();

  base::WeakPtr<SearchEnginePreconnector> GetWeakPtr() {
    return weak_factory_.GetWeakPtr();
  }

  // Determines whether the preconnector is started or not.
  bool preconnector_started_ = false;

  // Used to get keyed services.
  const raw_ptr<content::BrowserContext> browser_context_;

  // Used to preconnect regularly.
  base::OneShotTimer timer_;

  std::unique_ptr<content::PreconnectManager> preconnect_manager_;

  std::optional<base::TimeTicks> last_preconnect_attempt_time_;

  // Receives and dispatches method calls to this implementation of the
  // `network::mojom::ConnectionChangeObserverClient` interface.
  mojo::Receiver<network::mojom::ConnectionChangeObserverClient> receiver_{
      this};

  // How many times the connection has consecutively failed. This is used for
  // exponential backoff the preconnect retries.
  base::ClampedNumeric<int32_t> consecutive_connection_failure_ = 0;

  // Used for testing. Override the short session value.
  std::optional<bool> is_short_session_for_testing_;

#if BUILDFLAG(ENABLE_DEVICE_BOUND_SESSIONS)
  std::unique_ptr<DeviceBoundSessionPrewarmer> device_bound_session_prewarmer_;
#endif

  base::WeakPtrFactory<SearchEnginePreconnector> weak_factory_{this};
};

#endif  // CHROME_BROWSER_NAVIGATION_PREDICTOR_SEARCH_ENGINE_PRECONNECTOR_H_
