// Copyright 2018 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/ukm/ios/ukm_url_recorder.h"

#include <utility>

#include "base/containers/flat_map.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/web_state.h"
#include "ios/web/public/web_state_observer.h"
#import "ios/web/public/web_state_user_data.h"
#include "services/metrics/public/cpp/delegating_ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "url/gurl.h"

namespace ukm {

namespace internal {

// SourceUrlRecorderWebStateObserver is responsible for recording UKM source
// URLs for all main frame navigations in a given WebState.
// SourceUrlRecorderWebStateObserver records both the final URL for a
// navigation and, if the navigation was redirected, the initial URL as well.
class SourceUrlRecorderWebStateObserver
    : public web::WebStateObserver,
      public web::WebStateUserData<SourceUrlRecorderWebStateObserver> {
 public:
  SourceUrlRecorderWebStateObserver(const SourceUrlRecorderWebStateObserver&) =
      delete;
  SourceUrlRecorderWebStateObserver& operator=(
      const SourceUrlRecorderWebStateObserver&) = delete;

  ~SourceUrlRecorderWebStateObserver() override;

  // web::WebStateObserver
  void DidStartNavigation(web::WebState* web_state,
                          web::NavigationContext* navigation_context) override;
  void DidFinishNavigation(web::WebState* web_state,
                           web::NavigationContext* navigation_context) override;
  void WebStateDestroyed(web::WebState* web_state) override;

  SourceId GetLastCommittedSourceId() const;

 private:
  explicit SourceUrlRecorderWebStateObserver(web::WebState* web_state);
  friend class web::WebStateUserData<SourceUrlRecorderWebStateObserver>;

  void MaybeRecordUrl(web::NavigationContext* navigation_context,
                      const GURL& initial_url);

  // Map from navigation ID to the initial URL for that navigation.
  base::flat_map<int64_t, GURL> pending_navigations_;

  SourceId last_committed_source_id_;

  WEB_STATE_USER_DATA_KEY_DECL();
};

WEB_STATE_USER_DATA_KEY_IMPL(SourceUrlRecorderWebStateObserver)

SourceUrlRecorderWebStateObserver::SourceUrlRecorderWebStateObserver(
    web::WebState* web_state)
    : last_committed_source_id_(kInvalidSourceId) {
  web_state->AddObserver(this);
}

SourceUrlRecorderWebStateObserver::~SourceUrlRecorderWebStateObserver() {}

void SourceUrlRecorderWebStateObserver::WebStateDestroyed(
    web::WebState* web_state) {
  web_state->RemoveObserver(this);
}

void SourceUrlRecorderWebStateObserver::DidStartNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  // NavigationContexts only exist for main frame navigations, so this will
  // never be called for non-main frame navigations and we don't need to filter
  // non-main frame navigations out here.

  // Additionally, at least for the time being, we don't track metrics for
  // same-document navigations (e.g. changes in URL fragment or URL changes
  // due to history.pushState) in UKM.
  if (navigation_context->IsSameDocument())
    return;

  // UKM doesn't want to record URLs for downloads. However, at the point a
  // navigation is started, we don't yet know if the navigation will result in a
  // download. Thus, we store the URL at the time a navigation was initiated
  // and only record it later, once we verify that the navigation didn't result
  // in a download.
  pending_navigations_.insert(std::make_pair(
      navigation_context->GetNavigationId(), navigation_context->GetUrl()));
}

void SourceUrlRecorderWebStateObserver::DidFinishNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  // NavigationContexts only exist for main frame navigations, so this will
  // never be called for non-main frame navigations and we don't need to filter
  // non-main frame navigations out here.

  auto it = pending_navigations_.find(navigation_context->GetNavigationId());
  if (it == pending_navigations_.end())
    return;

  DCHECK(!navigation_context->IsSameDocument());

  if (navigation_context->HasCommitted()) {
    last_committed_source_id_ = ConvertToSourceId(
        navigation_context->GetNavigationId(), SourceIdType::NAVIGATION_ID);
  }

  GURL initial_url = std::move(it->second);
  pending_navigations_.erase(it);

  // UKM doesn't want to record URLs for navigations that result in downloads.
  if (navigation_context->IsDownload())
    return;

  MaybeRecordUrl(navigation_context, initial_url);
}

SourceId SourceUrlRecorderWebStateObserver::GetLastCommittedSourceId() const {
  return last_committed_source_id_;
}

void SourceUrlRecorderWebStateObserver::MaybeRecordUrl(
    web::NavigationContext* navigation_context,
    const GURL& initial_url) {
  DCHECK(!navigation_context->IsSameDocument());

  DelegatingUkmRecorder* ukm_recorder = DelegatingUkmRecorder::Get();
  if (!ukm_recorder)
    return;

  const GURL& final_url = navigation_context->GetUrl();

  UkmSource::NavigationData navigation_data;
  // TODO(crbug.com/869123): This check isn't quite correct, as self redirecting
  // is possible. This may also be changed to include the entire redirect chain.
  if (final_url != initial_url)
    navigation_data.urls = {initial_url};
  navigation_data.urls.push_back(final_url);

  // TODO(crbug.com/873316): Fill out the other fields in NavigationData.

  const ukm::SourceId source_id = ukm::ConvertToSourceId(
      navigation_context->GetNavigationId(), ukm::SourceIdType::NAVIGATION_ID);
  ukm_recorder->RecordNavigation(source_id, navigation_data);
}

}  // namespace internal

void InitializeSourceUrlRecorderForWebState(web::WebState* web_state) {
  internal::SourceUrlRecorderWebStateObserver::CreateForWebState(web_state);
}

SourceId GetSourceIdForWebStateDocument(web::WebState* web_state) {
  internal::SourceUrlRecorderWebStateObserver* obs =
      internal::SourceUrlRecorderWebStateObserver::FromWebState(web_state);
  return obs ? obs->GetLastCommittedSourceId() : kInvalidSourceId;
}

}  // namespace ukm
