// 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/send_tab_to_self/send_tab_to_self_bridge.h"

#include <algorithm>
#include <optional>
#include <vector>

#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/containers/span.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/observer_list.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/uuid.h"
#include "components/history/core/browser/history_service.h"
#include "components/send_tab_to_self/features.h"
#include "components/send_tab_to_self/metrics_util.h"
#include "components/send_tab_to_self/page_context.h"
#include "components/send_tab_to_self/pref_names.h"
#include "components/send_tab_to_self/proto/send_tab_to_self.pb.h"
#include "components/send_tab_to_self/proto_conversions.h"
#include "components/send_tab_to_self/send_tab_to_self_commit_tracker.h"
#include "components/send_tab_to_self/send_tab_to_self_entry.h"
#include "components/send_tab_to_self/target_device_info.h"
#include "components/sync/base/client_tag_hash.h"
#include "components/sync/base/data_type.h"
#include "components/sync/base/deletion_origin.h"
#include "components/sync/base/features.h"
#include "components/sync/model/data_type_local_change_processor.h"
#include "components/sync/model/entity_change.h"
#include "components/sync/model/metadata_batch.h"
#include "components/sync/model/metadata_change_list.h"
#include "components/sync/model/mutable_data_batch.h"
#include "components/sync_device_info/device_name_util.h"
#include "components/sync_device_info/local_device_info_util.h"
#include "components/sync_sessions/open_tabs_ui_delegate.h"
#include "components/sync_sessions/session_sync_service.h"
namespace send_tab_to_self {

namespace {

using syncer::DataTypeStore;

void RecordSendResultAndRunCallback(
    base::OnceCallback<void(SendTabToSelfResult)> callback,
    SendTabToSelfResult result) {
  RecordSendResult(result);
  std::move(callback).Run(result);
}

constexpr base::TimeDelta kDedupeTime = base::Seconds(5);

constexpr base::TimeDelta kDeviceExpiration = base::Days(10);

// Converts a time field from sync protobufs to a time object.
base::Time ProtoTimeToTime(int64_t proto_t) {
  return base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(proto_t));
}

// Returns true if the entry point represents a real user activation, i.e. the
// tab becomes visible to the user.
bool IsActivationUserVisible(ShareActivatedEntryPoint entry_point) {
  switch (entry_point) {
    case ShareActivatedEntryPoint::kAutoOpened:
    case ShareActivatedEntryPoint::kDesktopToast:
    case ShareActivatedEntryPoint::kDesktopToolbarBubble:
    case ShareActivatedEntryPoint::kMobileNotification:
    case ShareActivatedEntryPoint::kTabStrip:
    case ShareActivatedEntryPoint::kChromeOSBirch:
    case ShareActivatedEntryPoint::kMobileMessageBanner:
      return true;
    case ShareActivatedEntryPoint::kTabOrBrowserClosedWithoutActivation:
    case ShareActivatedEntryPoint::kSTTSEntryExpiredWithoutActivation:
      return false;
  }
}

void RecordActivationMetrics(ShareActivatedEntryPoint entry_point,
                             base::Time activated_time,
                             base::Time opened_time,
                             base::Time shared_time) {
  RecordActivatedEntryPoint(entry_point);
  if (IsActivationUserVisible(entry_point)) {
    RecordTimeOpenedToActivated(activated_time - opened_time);
    RecordTimeSentToActivated(activated_time - shared_time);
  }
}

// Allocate a EntityData and copies |specifics| into it.
std::unique_ptr<syncer::EntityData> CopyToEntityData(
    const sync_pb::SendTabToSelfSpecifics& specifics) {
  auto entity_data = std::make_unique<syncer::EntityData>();
  *entity_data->specifics.mutable_send_tab_to_self() = specifics;
  entity_data->name = specifics.url();
  entity_data->creation_time = ProtoTimeToTime(specifics.shared_time_usec());
  return entity_data;
}

// Parses the content of |record_list| into |*initial_data|. The output
// parameter is first for binding purposes.
std::optional<syncer::ModelError> ParseLocalEntriesOnBackendSequence(
    base::Time now,
    SendTabToSelfBridge::SendTabToSelfEntries* entries,
    std::unique_ptr<DataTypeStore::RecordList> record_list) {
  DCHECK(entries);
  DCHECK(entries->empty());
  DCHECK(record_list);

  for (const syncer::DataTypeStore::Record& r : *record_list) {
    auto specifics = std::make_unique<SendTabToSelfLocal>();
    if (specifics->ParseFromString(r.value)) {
      (*entries)[specifics->specifics().guid()] =
          SendTabToSelfEntry::FromLocalProto(*specifics, now);
    } else {
      return syncer::ModelError(
          FROM_HERE,
          syncer::ModelError::Type::kSendTabToSelfFailedToDeserializeSpecifics);
    }
  }

  return std::nullopt;
}

base::flat_map<std::string, base::Time> GetSessionTimestamps(
    sync_sessions::SessionSyncService* session_sync_service) {
  if (!session_sync_service) {
    return {};
  }
  sync_sessions::OpenTabsUIDelegate* delegate =
      session_sync_service->GetOpenTabsUIDelegate();
  return delegate ? delegate->GetAllForeignSessionLastModifiedTimes()
                  : base::flat_map<std::string, base::Time>();
}

struct DeviceWithTimestamp {
  raw_ptr<const syncer::DeviceInfo> device;
  base::Time last_active;
  bool has_high_precision = false;
};

// Returns a list of devices with the last active timestamp for each device.
// The last active timestamp is the maximum of the device's last updated
// timestamp and the last modified time of any session on the device.
std::vector<DeviceWithTimestamp> GetDevicesWithLastActiveTime(
    const std::vector<const syncer::DeviceInfo*>& all_devices,
    const base::flat_map<std::string, base::Time>& session_timestamps) {
  std::vector<DeviceWithTimestamp> devices_with_timestamps;
  devices_with_timestamps.reserve(all_devices.size());

  for (const syncer::DeviceInfo* device : all_devices) {
    base::Time last_active = device->last_updated_timestamp();
    bool has_high_precision = false;
    auto it = session_timestamps.find(device->guid());
    if (it != session_timestamps.end()) {
      last_active = std::max(last_active, it->second);
      // If the device has a session timestamp, it is highly precise.
      has_high_precision = true;
    }
    devices_with_timestamps.emplace_back(device, last_active,
                                         has_high_precision);
  }
  return devices_with_timestamps;
}

}  // namespace

SendTabToSelfBridge::SendTabToSelfBridge(
    std::unique_ptr<syncer::DataTypeLocalChangeProcessor> change_processor,
    base::Clock* clock,
    syncer::OnceDataTypeStoreFactory create_store_callback,
    history::HistoryService* history_service,
    syncer::DeviceInfoTracker* device_info_tracker,
    sync_sessions::SessionSyncService* session_sync_service,
    PrefService* pref_service)
    : DataTypeSyncBridge(std::move(change_processor)),
      commit_tracker_(std::make_unique<SendTabToSelfCommitTracker>(
          this->change_processor())),
      clock_(clock),
      history_service_(history_service),
      device_info_tracker_(device_info_tracker),
      session_sync_service_(session_sync_service),
      pref_service_(pref_service) {
  DCHECK(clock_);
  DCHECK(device_info_tracker_);
  if (history_service) {
    history_service_observation_.Observe(history_service);
  }

  std::move(create_store_callback)
      .Run(syncer::SEND_TAB_TO_SELF,
           base::BindOnce(&SendTabToSelfBridge::OnStoreCreated,
                          weak_ptr_factory_.GetWeakPtr()));
}

SendTabToSelfBridge::~SendTabToSelfBridge() {
  if (history_service_) {
    history_service_observation_.Reset();
  }
}

std::optional<syncer::ModelError> SendTabToSelfBridge::MergeFullSyncData(
    std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
    syncer::EntityChangeList entity_data) {
  DCHECK(entries_.empty());
  std::optional<syncer::ModelError> error = ApplyIncrementalSyncChanges(
      std::move(metadata_change_list), std::move(entity_data));
  if (IsReady()) {
    for (auto& observer : observers_) {
      observer.OnModelReady();
    }
  }
  return error;
}

std::optional<syncer::ModelError>
SendTabToSelfBridge::ApplyIncrementalSyncChanges(
    std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
    syncer::EntityChangeList entity_changes) {
  std::vector<const SendTabToSelfEntry*> added;

  // The opened vector will accumulate both added entries that are already
  // opened as well as existing entries that have been updated to be marked as
  // opened.
  std::vector<const SendTabToSelfEntry*> opened;
  std::vector<std::string> removed;
  std::unique_ptr<DataTypeStore::WriteBatch> batch =
      store_->CreateWriteBatch(std::move(metadata_change_list));

  for (const std::unique_ptr<syncer::EntityChange>& change : entity_changes) {
    const std::string& guid = change->storage_key();
    if (change->type() == syncer::EntityChange::ACTION_DELETE) {
      if (entries_.find(guid) != entries_.end()) {
        EraseEntryInBatch(guid, batch.get());
        removed.push_back(change->storage_key());
      }
    } else {
      const sync_pb::SendTabToSelfSpecifics& specifics =
          change->data().specifics.send_tab_to_self();

      std::unique_ptr<SendTabToSelfEntry> remote_entry =
          SendTabToSelfEntry::FromProto(specifics, clock_->Now());
      if (!remote_entry) {
        continue;  // Skip invalid entries.
      }
      if (remote_entry->IsExpired(clock_->Now())) {
        // Remove expired data from server.
        change_processor()->Delete(guid, syncer::DeletionOrigin::Unspecified(),
                                   batch->GetMetadataChangeList());
      } else {
        SendTabToSelfEntry* local_entry =
            GetMutableEntryByGUID(remote_entry->GetGUID());
        if (local_entry == nullptr) {
          bool needs_reupload = false;
          // If this device is the target and the entry hasn't been received
          // yet, set the received timestamp.
          if (device_info_tracker_->IsRecentLocalCacheGuid(
                  remote_entry->GetTargetDeviceSyncCacheGuid()) &&
              !remote_entry->IsReceived()) {
            remote_entry->MarkReceived(clock_->Now());
            RecordTimeSentToReceived(remote_entry->GetReceivedTime() -
                                     remote_entry->GetSharedTime());
            needs_reupload = true;
          }
          if (unknown_opened_entries_.contains(remote_entry->GetGUID())) {
            base::Time opened_time =
                unknown_opened_entries_[remote_entry->GetGUID()];
            unknown_opened_entries_.erase(remote_entry->GetGUID());
            remote_entry->MarkOpened(opened_time);
            RecordTimeSentToOpened(remote_entry->GetOpenedTime() -
                                   remote_entry->GetSharedTime());
            needs_reupload = true;
          }
          if (unknown_activated_entries_.contains(remote_entry->GetGUID())) {
            // This entry was activated (for example, in case the tab was
            // received and opened via a notification) before it was received
            // via sync.
            auto [activated_time, entry_point] =
                unknown_activated_entries_[remote_entry->GetGUID()];
            unknown_activated_entries_.erase(remote_entry->GetGUID());
            remote_entry->MarkActivated(activated_time);
            RecordActivationMetrics(entry_point, activated_time,
                                    remote_entry->GetOpenedTime(),
                                    remote_entry->GetSharedTime());
            needs_reupload = true;
          }
          // Reupload the entry to the server so the sending device can
          // observe the acknowledgment. This is safe because it happens at
          // most once per entry (the IsReceived() guard above prevents
          // re-entry on subsequent syncs).
          if (needs_reupload) {
            change_processor()->Put(
                remote_entry->GetGUID(),
                CopyToEntityData(remote_entry->AsLocalProto().specifics()),
                batch->GetMetadataChangeList());
          }
          // This remote_entry is new. Add it to the model.
          added.push_back(remote_entry.get());
          if (remote_entry->IsOpened()) {
            opened.push_back(remote_entry.get());
          }

          // Write to the store *after* all mutations so the local store has
          // the up-to-date fields.
          batch->WriteData(guid,
                           remote_entry->AsLocalProto().SerializeAsString());
          std::string remote_guid = remote_entry->GetGUID();
          entries_[remote_guid] = std::move(remote_entry);
        } else {
          // Propagate timestamp fields from the remote entry.
          if (remote_entry->IsReceived() && !local_entry->IsReceived()) {
            local_entry->MarkReceived(remote_entry->GetReceivedTime());
          }
          // Update existing model if entries have been opened.
          if (remote_entry->IsOpened() && !local_entry->IsOpened()) {
            local_entry->MarkOpened(remote_entry->GetOpenedTime());
            opened.push_back(local_entry);
          }

          // Write to the store.
          batch->WriteData(guid,
                           local_entry->AsLocalProto().SerializeAsString());
        }
      }
    }
  }

  Commit(std::move(batch));

  NotifyRemoteSendTabToSelfEntryDeleted(removed);
  NotifyRemoteSendTabToSelfEntryAdded(added);
  NotifyRemoteSendTabToSelfEntryOpened(opened);

  commit_tracker_->OnIncrementalSyncComplete();

  return std::nullopt;
}

std::unique_ptr<syncer::DataBatch> SendTabToSelfBridge::GetDataForCommit(
    StorageKeyList storage_keys) {
  auto batch = std::make_unique<syncer::MutableDataBatch>();

  for (const std::string& guid : storage_keys) {
    const SendTabToSelfEntry* entry = GetEntryByGUID(guid);
    if (!entry) {
      continue;
    }

    batch->Put(guid, CopyToEntityData(entry->AsLocalProto().specifics()));
  }
  return batch;
}

std::unique_ptr<syncer::DataBatch>
SendTabToSelfBridge::GetAllDataForDebugging() {
  auto batch = std::make_unique<syncer::MutableDataBatch>();
  for (const auto& it : entries_) {
    batch->Put(it.first,
               CopyToEntityData(it.second->AsLocalProto().specifics()));
  }
  return batch;
}

std::string SendTabToSelfBridge::GetClientTag(
    const syncer::EntityData& entity_data) const {
  return GetStorageKey(entity_data);
}

std::string SendTabToSelfBridge::GetStorageKey(
    const syncer::EntityData& entity_data) const {
  return entity_data.specifics.send_tab_to_self().guid();
}

bool SendTabToSelfBridge::IsEntityDataValid(
    const syncer::EntityData& entity_data) const {
  CHECK(entity_data.specifics.has_send_tab_to_self());
  const sync_pb::SendTabToSelfSpecifics& specifics =
      entity_data.specifics.send_tab_to_self();
  return !specifics.guid().empty() &&
         SendTabToSelfEntry::IsValidUrl(GURL(specifics.url()));
}

sync_pb::EntitySpecifics
SendTabToSelfBridge::TrimAllSupportedFieldsFromRemoteSpecifics(
    const sync_pb::EntitySpecifics& entity_specifics) const {
  // Clears all fields by default to avoid the memory and I/O overhead of an
  // additional copy of the data.
  return sync_pb::EntitySpecifics();
}

void SendTabToSelfBridge::ApplyDisableSyncChanges(
    std::unique_ptr<syncer::MetadataChangeList> delete_metadata_change_list) {
  DCHECK(store_);

  store_->DeleteAllDataAndMetadata(std::move(delete_metadata_change_list),
                                   base::DoNothing());

  std::vector<std::string> all_guids = GetAllGuids();

  entries_.clear();
  unknown_opened_entries_.clear();
  unknown_activated_entries_.clear();
  mru_entry_guid_.clear();

  commit_tracker_->OnSyncDisabled();

  NotifyRemoteSendTabToSelfEntryDeleted(all_guids);
}

void SendTabToSelfBridge::OnCommitAttemptErrors(
    const syncer::FailedCommitResponseDataList& error_response_list) {
  commit_tracker_->OnCommitErrors(error_response_list);
}

syncer::DataTypeSyncBridge::CommitAttemptFailedBehavior
SendTabToSelfBridge::OnCommitAttemptFailed(syncer::SyncCommitError error) {
  commit_tracker_->OnCommitAttemptFailed(error);
  // Even if the immediate UI notification failed, the sync engine should
  // keep trying to commit the entry in the background (e.g. if the failure was
  // due to a transient network issue).
  return CommitAttemptFailedBehavior::kShouldRetryOnNextCycle;
}

std::vector<std::string> SendTabToSelfBridge::GetAllGuids() const {
  std::vector<std::string> keys;
  for (const auto& it : entries_) {
    DCHECK_EQ(it.first, it.second->GetGUID());
    keys.push_back(it.first);
  }
  return keys;
}

const SendTabToSelfEntry* SendTabToSelfBridge::GetEntryByGUID(
    std::string_view guid) const {
  auto it = entries_.find(guid);
  if (it == entries_.end()) {
    return nullptr;
  }
  return it->second.get();
}

bool SendTabToSelfBridge::IsTargetedToLocalDevice(
    const SendTabToSelfEntry& entry) const {
  return device_info_tracker_->IsRecentLocalCacheGuid(
      entry.GetTargetDeviceSyncCacheGuid());
}

std::vector<const SendTabToSelfEntry*>
SendTabToSelfBridge::GetUnopenedEntriesTargetedToLocalDevice() const {
  std::vector<const SendTabToSelfEntry*> unopened_entries;
  for (const auto& [guid, entry] : entries_) {
    if (IsTargetedToLocalDevice(*entry) && !entry->IsOpened()) {
      unopened_entries.push_back(entry.get());
    }
  }
  std::ranges::sort(unopened_entries, {}, &SendTabToSelfEntry::GetSharedTime);
  return unopened_entries;
}

std::vector<const SendTabToSelfEntry*>
SendTabToSelfBridge::GetOpenedEntriesTargetedToLocalDevice() const {
  std::vector<const SendTabToSelfEntry*> opened_entries;
  for (const auto& [guid, entry] : entries_) {
    if (IsTargetedToLocalDevice(*entry) && entry->IsOpened()) {
      opened_entries.push_back(entry.get());
    }
  }
  return opened_entries;
}

const SendTabToSelfEntry* SendTabToSelfBridge::SendEntry(
    const GURL& url,
    const std::string& title,
    const std::string& target_device_cache_guid,
    const PageContext& context,
    NavigationHistory navigation_history,
    base::OnceCallback<void(SendTabToSelfResult)> commit_confirmation,
    ShareEntryPoint entry_point) {
  CHECK(commit_confirmation);

  auto commit_confirmation_with_metrics = base::BindOnce(
      &RecordSendResultAndRunCallback, std::move(commit_confirmation));

  RecordEntryPointSent(entry_point);

  if (!change_processor()->IsTrackingMetadata()) {
    std::move(commit_confirmation_with_metrics)
        .Run(SendTabToSelfResult::kFailureNotTrackingMetadata);
    return nullptr;
  }

  if (!SendTabToSelfEntry::IsValidUrl(url)) {
    std::move(commit_confirmation_with_metrics)
        .Run(SendTabToSelfResult::kFailureInvalidUrl);
    return nullptr;
  }

  // In the case where the user has attempted to send an identical URL to the
  // same device within the last |kDedupeTime| we think it is likely that user
  // still has the first sent tab in progress, and so we will not attempt to
  // resend.
  base::Time shared_time = clock_->Now();
  const SendTabToSelfEntry* mru_entry = GetEntryByGUID(mru_entry_guid_);
  if (mru_entry && url == mru_entry->GetURL() &&
      target_device_cache_guid == mru_entry->GetTargetDeviceSyncCacheGuid() &&
      shared_time - mru_entry->GetSharedTime() < kDedupeTime) {
    send_tab_to_self::RecordNotificationStatus(
        send_tab_to_self::NotificationStatus::kThrottled);
    std::move(commit_confirmation_with_metrics)
        .Run(SendTabToSelfResult::kSuccessThrottled);
    return mru_entry;
  }

  std::string guid = base::Uuid::GenerateRandomV4().AsLowercaseString();

  // Assure that we don't have a guid collision.
  DCHECK_EQ(GetEntryByGUID(guid), nullptr);

  std::string trimmed_title = "";

  if (base::IsStringUTF8(title)) {
    trimmed_title = base::UTF16ToUTF8(
        base::CollapseWhitespace(base::UTF8ToUTF16(title), false));
  }

  std::string local_device_name = GetLocalDeviceName();
  RecordIsLocalDeviceNameAvailableOnSend(!local_device_name.empty());

  std::unique_ptr<SendTabToSelfEntry> entry =
      std::make_unique<SendTabToSelfEntry>(
          guid, url, trimmed_title, shared_time, std::move(local_device_name),
          target_device_cache_guid, context, std::move(navigation_history));

  // The size is recorded before potential truncation (dropping) of the context
  // due to the per-entity size limit.
  RecordPageContextSize(PageContextToProto(context).ByteSizeLong());

  syncer::DeviceInfo::FormFactor sender_form_factor =
      syncer::DeviceInfo::FormFactor::kUnknown;
  const syncer::DeviceInfo* local_device = GetLocalDeviceInfo();
  if (local_device) {
    sender_form_factor = local_device->form_factor();
  }

  syncer::DeviceInfo::FormFactor target_form_factor =
      syncer::DeviceInfo::FormFactor::kUnknown;
  const syncer::DeviceInfo* target_device =
      device_info_tracker_->GetDeviceInfo(target_device_cache_guid);
  if (target_device) {
    target_form_factor = target_device->form_factor();
  }

  RecordDeviceFormFactorCombination(sender_form_factor, target_form_factor);

  std::unique_ptr<DataTypeStore::WriteBatch> batch = store_->CreateWriteBatch();
  // This entry is new. Add it to the store and model.
  std::unique_ptr<syncer::EntityData> entity_data =
      CopyToEntityData(entry->AsLocalProto().specifics());

  change_processor()->Put(guid, std::move(entity_data),
                          batch->GetMetadataChangeList());

  commit_tracker_->TrackCommit(guid,
                               std::move(commit_confirmation_with_metrics));

  for (SendTabToSelfModelObserver& observer : observers_) {
    observer.OnEntryAddedLocally(entry.get());
  }

  const SendTabToSelfEntry* result =
      entries_.emplace(guid, std::move(entry)).first->second.get();

  batch->WriteData(guid, result->AsLocalProto().SerializeAsString());

  Commit(std::move(batch));
  mru_entry_guid_ = guid;

  return result;
}

void SendTabToSelfBridge::DismissEntry(std::string_view guid) {
  SendTabToSelfEntry* entry = GetMutableEntryByGUID(guid);
  // Assure that an entry with that guid exists.
  if (!entry) {
    return;
  }

  DCHECK(change_processor()->IsTrackingMetadata());

  entry->SetNotificationDismissed(true);
  CommitLocalEntryMutation(*entry);
}

void SendTabToSelfBridge::MarkEntryOpened(std::string_view guid) {
  MarkEntryOpenedImpl(guid, clock_->Now());
}

void SendTabToSelfBridge::MarkEntryOpenedImpl(std::string_view guid,
                                              base::Time opened_time) {
  SendTabToSelfEntry* entry = GetMutableEntryByGUID(guid);
  // Assure that an entry with that guid exists.
  if (!entry) {
    auto it = unknown_opened_entries_.find(guid);
    if (it != unknown_opened_entries_.end()) {
      it->second = opened_time;
    } else {
      unknown_opened_entries_.emplace(guid, opened_time);
    }
    return;
  }

  DCHECK(change_processor()->IsTrackingMetadata());

  entry->MarkOpened(opened_time);

  RecordTimeSentToOpened(entry->GetOpenedTime() - entry->GetSharedTime());
  CommitLocalEntryMutation(*entry);
}

void SendTabToSelfBridge::MarkEntryActivated(
    std::string_view guid,
    ShareActivatedEntryPoint entry_point) {
  MarkEntryActivatedImpl(guid, entry_point, clock_->Now());
}

void SendTabToSelfBridge::MarkEntryActivatedImpl(
    std::string_view guid,
    ShareActivatedEntryPoint entry_point,
    base::Time activated_time) {
  SendTabToSelfEntry* entry = GetMutableEntryByGUID(guid);
  if (!entry) {
    // If the entry is not yet in the model (because it has not loaded yet or
    // has not been received from the server yet), store the activated time and
    // entry point and apply it when the entry is added to the model.
    unknown_activated_entries_.emplace(
        guid, std::make_pair(activated_time, entry_point));
    return;
  }
  RecordActivationMetrics(entry_point, activated_time, entry->GetOpenedTime(),
                          entry->GetSharedTime());

  entry->MarkActivated(activated_time);
  CommitLocalEntryMutation(*entry);
}

void SendTabToSelfBridge::CommitLocalEntryMutation(
    const SendTabToSelfEntry& entry) {
  if (!change_processor()->IsTrackingMetadata()) {
    return;
  }
  std::unique_ptr<DataTypeStore::WriteBatch> batch = store_->CreateWriteBatch();
  std::unique_ptr<syncer::EntityData> entity_data =
      CopyToEntityData(entry.AsLocalProto().specifics());
  change_processor()->Put(entry.GetGUID(), std::move(entity_data),
                          batch->GetMetadataChangeList());
  batch->WriteData(entry.GetGUID(), entry.AsLocalProto().SerializeAsString());
  Commit(std::move(batch));
}

void SendTabToSelfBridge::OnHistoryDeletions(
    history::HistoryService* history_service,
    const history::DeletionInfo& deletion_info) {
  // We only care about actual user (or sync) deletions.

  if (!change_processor()->IsTrackingMetadata()) {
    return;  // Sync processor not yet ready, don't sync.
  }

  if (deletion_info.is_from_expiration()) {
    return;
  }

  if (!deletion_info.IsAllHistory()) {
    std::vector<GURL> urls;

    for (const history::URLRow& row : deletion_info.deleted_rows()) {
      urls.push_back(row.url());
    }

    DeleteEntries(urls);
    return;
  }

  // All history was cleared: just delete all entries.
  DeleteAllEntries();
}

bool SendTabToSelfBridge::IsReady() {
  return change_processor()->IsTrackingMetadata();
}

bool SendTabToSelfBridge::HasValidTargetDevice() {
  return GetTargetDeviceInfoSortedList().size() > 0;
}

std::vector<TargetDeviceInfo>
SendTabToSelfBridge::GetTargetDeviceInfoSortedList() {
  TRACE_EVENT0("ui", "SendTabToSelfBridge::GetTargetDeviceInfoSortedList");
  if (!IsReady() || !device_info_tracker_->IsSyncing()) {
    return {};
  }

  // Pre-calculate last active timestamps for sorting and filtering.
  std::vector<DeviceWithTimestamp> devices_with_timestamps =
      GetDevicesWithLastActiveTime(device_info_tracker_->GetAllDeviceInfo(),
                                   GetSessionTimestamps(session_sync_service_));

  // Sort the devices so the most recently active devices are first.
  std::stable_sort(
      devices_with_timestamps.begin(), devices_with_timestamps.end(),
      [](const DeviceWithTimestamp& a, const DeviceWithTimestamp& b) {
        return a.last_active > b.last_active;
      });

  std::vector<DeviceWithTimestamp> devices;
  for (const auto& entry : devices_with_timestamps) {
    // Filter out devices that are too old or don't support the feature.
    if (clock_->Now() - entry.last_active > kDeviceExpiration) {
      break;
    }
    if (ShouldIncludeDevice(*entry.device)) {
      devices.push_back(entry);
    }
  }

  if (base::FeatureList::IsEnabled(syncer::kSyncSimplifyDeviceNaming)) {
    // Resolve display names for the filtered list by using the most user
    // friendly name, disambiguating duplicates with release channel if enabled.
    std::vector<const syncer::DeviceInfo*> raw_devices = base::ToVector(
        devices,
        [](const DeviceWithTimestamp& entry) { return entry.device.get(); });
    // TODO(crbug.com/522788942): Consider moving TargetDeviceInfo out of
    // send_tab_to_self (e.g. into sync_device_info or sharing) so multiple
    // clients can share a unified struct.
    std::vector<std::string> device_names =
        syncer::GetDeviceDisplayNames(raw_devices, GetLocalDeviceInfo());
    std::vector<TargetDeviceInfo> target_devices;
    target_devices.reserve(devices.size());
    for (size_t i = 0; i < devices.size(); ++i) {
      target_devices.emplace_back(
          std::move(device_names[i]), devices[i].device->guid(),
          devices[i].device->form_factor(), devices[i].device->os_type(),
          devices[i].last_active, devices[i].has_high_precision);
    }
    return target_devices;
  }

  // TODO(crbug.com/522788942): Remove this temporary conversion when
  // kSyncSimplifyDeviceNaming is fully launched.
  std::vector<const syncer::DeviceInfo*> legacy_devices = base::ToVector(
      devices,
      [](const DeviceWithTimestamp& entry) { return entry.device.get(); });

  // Resolve display names for the filtered list. This handles de-duplication
  // by name and chooses between preferred/fallback names based on collisions.
  std::vector<syncer::DeviceInfoWithName> device_names =
      syncer::DetermineDisplayNamesAndDeduplicate(legacy_devices,
                                                  GetLocalDeviceName());

  return base::ToVector(device_names, [&](const auto& info) {
    auto it =
        std::ranges::find(devices, info.device, &DeviceWithTimestamp::device);
    return TargetDeviceInfo(info.display_name, info.device->guid(),
                            info.device->form_factor(), info.device->os_type(),
                            it->last_active, it->has_high_precision);
  });
}

std::optional<TargetDeviceInfo> SendTabToSelfBridge::GetTargetDeviceInfo(
    std::string_view cache_guid) {
  const std::vector<TargetDeviceInfo> devices = GetTargetDeviceInfoSortedList();
  auto it =
      std::ranges::find(devices, cache_guid, &TargetDeviceInfo::cache_guid);
  return it != devices.end() ? std::make_optional(*it) : std::nullopt;
}

// static
std::unique_ptr<syncer::DataTypeStore>
SendTabToSelfBridge::DestroyAndStealStoreForTest(
    std::unique_ptr<SendTabToSelfBridge> bridge) {
  return std::move(bridge->store_);
}

void SendTabToSelfBridge::SetLocalDeviceNameForTest(
    const std::string& local_device_name) {
  local_device_name_for_testing_ = local_device_name;
}

void SendTabToSelfBridge::NotifyRemoteSendTabToSelfEntryAdded(
    base::span<const SendTabToSelfEntry* const> new_entries) {
  if (new_entries.empty()) {
    return;
  }

  std::vector<const SendTabToSelfEntry*> new_local_entries;

  // Only pass along entries that are not dismissed or opened, and are
  // targeted at this device, which is determined by comparing the cache guid
  // associated with the entry to each device's local list of recently used
  // cache_guids
  DCHECK(!change_processor()->TrackedCacheGuid().empty());
  for (const SendTabToSelfEntry* entry : new_entries) {
    if (IsTargetedToLocalDevice(*entry) && !entry->GetNotificationDismissed() &&
        !entry->IsOpened()) {
      new_local_entries.push_back(entry);
    }
  }

  for (SendTabToSelfModelObserver& observer : observers_) {
    observer.OnEntriesAddedRemotely(new_local_entries);
  }

#if BUILDFLAG(IS_IOS)
  if (!new_local_entries.empty()) {
    pref_service_->SetString(prefs::kIOSSendTabToSelfLastReceivedTabURLPref,
                             new_local_entries.back()->GetURL().spec());
  }
#endif
}

void SendTabToSelfBridge::NotifyRemoteSendTabToSelfEntryDeleted(
    base::span<const std::string> guids) {
  if (guids.empty()) {
    return;
  }

  for (SendTabToSelfModelObserver& observer : observers_) {
    observer.OnEntriesRemovedRemotely(guids);
  }
}

void SendTabToSelfBridge::NotifyRemoteSendTabToSelfEntryOpened(
    base::span<const SendTabToSelfEntry* const> opened_entries) {
  if (opened_entries.empty()) {
    return;
  }
  for (SendTabToSelfModelObserver& observer : observers_) {
    observer.OnEntriesOpenedRemotely(opened_entries);
  }
}

void SendTabToSelfBridge::OnStoreCreated(
    const std::optional<syncer::ModelError>& error,
    std::unique_ptr<syncer::DataTypeStore> store) {
  if (error) {
    change_processor()->ReportError(*error);
    return;
  }

  auto initial_entries = std::make_unique<SendTabToSelfEntries>();
  SendTabToSelfEntries* initial_entries_copy = initial_entries.get();

  store_ = std::move(store);
  store_->ReadAllDataAndPreprocess(
      base::BindOnce(&ParseLocalEntriesOnBackendSequence, clock_->Now(),
                     base::Unretained(initial_entries_copy)),
      base::BindOnce(&SendTabToSelfBridge::OnReadAllData,
                     weak_ptr_factory_.GetWeakPtr(),
                     std::move(initial_entries)));
}

void SendTabToSelfBridge::OnReadAllData(
    std::unique_ptr<SendTabToSelfEntries> initial_entries,
    const std::optional<syncer::ModelError>& error) {
  DCHECK(initial_entries);

  if (error) {
    change_processor()->ReportError(*error);
    return;
  }

  entries_ = std::move(*initial_entries);

  store_->ReadAllMetadata(base::BindOnce(
      &SendTabToSelfBridge::OnReadAllMetadata, weak_ptr_factory_.GetWeakPtr()));
}

void SendTabToSelfBridge::OnReadAllMetadata(
    const std::optional<syncer::ModelError>& error,
    std::unique_ptr<syncer::MetadataBatch> metadata_batch) {
  TRACE_EVENT0("ui", "SendTabToSelfBridge::OnReadAllMetadata");
  if (error) {
    change_processor()->ReportError(*error);
    return;
  }
  change_processor()->ModelReadyToSync(std::move(metadata_batch));

  if (IsReady()) {
    base::flat_map<std::string, base::Time, std::less<>> opened_queued =
        std::move(unknown_opened_entries_);
    unknown_opened_entries_.clear();
    for (const auto& [guid, opened_time] : opened_queued) {
      MarkEntryOpenedImpl(guid, opened_time);
    }

    base::flat_map<std::string, std::pair<base::Time, ShareActivatedEntryPoint>,
                   std::less<>>
        queued = std::move(unknown_activated_entries_);
    unknown_activated_entries_.clear();
    for (const auto& [guid, data] : queued) {
      MarkEntryActivatedImpl(guid, /*entry_point=*/data.second,
                             /*activated_time=*/data.first);
    }

    for (auto& observer : observers_) {
      observer.OnModelReady();
    }
  }

  DoGarbageCollection();
}

void SendTabToSelfBridge::OnCommit(
    const std::optional<syncer::ModelError>& error) {
  if (error) {
    change_processor()->ReportError(*error);
  }
}

void SendTabToSelfBridge::Commit(
    std::unique_ptr<DataTypeStore::WriteBatch> batch) {
  store_->CommitWriteBatch(std::move(batch),
                           base::BindOnce(&SendTabToSelfBridge::OnCommit,
                                          weak_ptr_factory_.GetWeakPtr()));
}

SendTabToSelfEntry* SendTabToSelfBridge::GetMutableEntryByGUID(
    std::string_view guid) const {
  auto it = entries_.find(guid);
  if (it == entries_.end()) {
    return nullptr;
  }
  return it->second.get();
}

const syncer::DeviceInfo* SendTabToSelfBridge::GetLocalDeviceInfo() const {
  if (!change_processor()->IsTrackingMetadata()) {
    return nullptr;
  }
  return device_info_tracker_->GetDeviceInfo(
      change_processor()->TrackedCacheGuid());
}

std::string SendTabToSelfBridge::GetLocalDeviceName() const {
  if (local_device_name_for_testing_.has_value()) {
    return *local_device_name_for_testing_;
  }
  // `local_device` may be null during early startup before DeviceInfoTracker is
  // initialized.
  const syncer::DeviceInfo* local_device = GetLocalDeviceInfo();
  if (!local_device) {
    return std::string();
  }

  // TODO(crbug.com/531649027): Remove fallback_full_name logic once
  // kSyncSimplifyDeviceNaming launches. It is only needed for legacy name
  // deduplication; simplified naming filters the local device by GUID.
  if (base::FeatureList::IsEnabled(syncer::kSyncSimplifyDeviceNaming)) {
    return syncer::GetDeviceDisplayName(local_device);
  }

  return syncer::GetDisplayNameCandidates(local_device).fallback_full_name;
}

bool SendTabToSelfBridge::ShouldIncludeDevice(
    const syncer::DeviceInfo& device) const {
  // Don't include this device if it is the local device.
  if (device_info_tracker_->IsRecentLocalCacheGuid(device.guid())) {
    return false;
  }

  DCHECK_NE(device.guid(), change_processor()->TrackedCacheGuid());

  // Don't include devices that have disabled the send tab to self receiving
  // feature.
  if (!device.send_tab_to_self_receiving_enabled()) {
    return false;
  }

  return true;
}

void SendTabToSelfBridge::DoGarbageCollection() {
  std::vector<std::string> removed_guids;

  for (const auto& it : entries_) {
    DCHECK_EQ(it.first, it.second->GetGUID());

    if (it.second->IsExpired(clock_->Now())) {
      if (it.second->IsOpened() && !it.second->IsActivated()) {
        RecordActivatedEntryPoint(
            ShareActivatedEntryPoint::kSTTSEntryExpiredWithoutActivation);
      }
      removed_guids.push_back(it.first);
    }
  }

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

  std::unique_ptr<DataTypeStore::WriteBatch> batch = store_->CreateWriteBatch();
  for (const std::string& guid : removed_guids) {
    DeleteEntryWithBatch(guid, batch.get());
  }
  Commit(std::move(batch));
  NotifyRemoteSendTabToSelfEntryDeleted(removed_guids);
}

void SendTabToSelfBridge::DeleteEntryWithBatch(
    std::string_view guid,
    DataTypeStore::WriteBatch* batch) {
  // Assure that an entry with that guid exists.
  DCHECK(GetEntryByGUID(guid) != nullptr);
  DCHECK(change_processor()->IsTrackingMetadata());

  change_processor()->Delete(std::string(guid),
                             syncer::DeletionOrigin::Unspecified(),
                             batch->GetMetadataChangeList());

  EraseEntryInBatch(guid, batch);
}

void SendTabToSelfBridge::DeleteEntries(const std::vector<GURL>& urls) {
  std::unique_ptr<DataTypeStore::WriteBatch> batch = store_->CreateWriteBatch();

  std::vector<std::string> removed_guids;

  for (const GURL& url : urls) {
    auto entry = entries_.begin();
    while (entry != entries_.end()) {
      bool to_delete =
          (entry->second == nullptr || url == entry->second->GetURL());

      std::string guid = entry->first;
      entry++;
      if (to_delete) {
        removed_guids.push_back(guid);
        DeleteEntryWithBatch(guid, batch.get());
      }
    }
  }
  Commit(std::move(batch));
  // To err on the side of completeness this notifies all clients that these
  // entries have been removed. Regardless of if these entries were removed
  // "remotely".
  NotifyRemoteSendTabToSelfEntryDeleted(removed_guids);
}

void SendTabToSelfBridge::DeleteAllEntries() {
  if (!change_processor()->IsTrackingMetadata()) {
    DCHECK_EQ(0ul, entries_.size());
    return;
  }

  std::unique_ptr<DataTypeStore::WriteBatch> batch = store_->CreateWriteBatch();

  std::vector<std::string> all_guids = GetAllGuids();

  for (const auto& guid : all_guids) {
    change_processor()->Delete(guid, syncer::DeletionOrigin::Unspecified(),
                               batch->GetMetadataChangeList());
    batch->DeleteData(guid);
  }
  commit_tracker_->OnAllEntriesRemoved();
  entries_.clear();
  unknown_opened_entries_.clear();
  unknown_activated_entries_.clear();
  mru_entry_guid_.clear();

  Commit(std::move(batch));

  NotifyRemoteSendTabToSelfEntryDeleted(all_guids);
}

void SendTabToSelfBridge::EraseEntryInBatch(std::string_view guid,
                                            DataTypeStore::WriteBatch* batch) {
  if (mru_entry_guid_ == guid) {
    mru_entry_guid_.clear();
  }
  if (auto it = entries_.find(guid); it != entries_.end()) {
    entries_.erase(it);
  }
  if (auto it = unknown_opened_entries_.find(guid);
      it != unknown_opened_entries_.end()) {
    unknown_opened_entries_.erase(it);
  }
  if (auto it = unknown_activated_entries_.find(guid);
      it != unknown_activated_entries_.end()) {
    unknown_activated_entries_.erase(it);
  }
  batch->DeleteData(std::string(guid));

  commit_tracker_->OnEntryRemoved(guid);
}

}  // namespace send_tab_to_self
