// 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.

#ifndef COMPONENTS_SAVED_TAB_GROUPS_INTERNAL_SAVED_TAB_GROUP_SYNC_BRIDGE_H_
#define COMPONENTS_SAVED_TAB_GROUPS_INTERNAL_SAVED_TAB_GROUP_SYNC_BRIDGE_H_

#include <memory>
#include <optional>
#include <vector>

#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "components/saved_tab_groups/internal/saved_tab_group_model.h"
#include "components/saved_tab_groups/proto/saved_tab_group_data.pb.h"
#include "components/saved_tab_groups/public/saved_tab_group.h"
#include "components/saved_tab_groups/public/saved_tab_group_tab.h"
#include "components/sync/model/data_type_local_change_processor.h"
#include "components/sync/model/data_type_store.h"
#include "components/sync/model/data_type_sync_bridge.h"
#include "components/sync/protocol/saved_tab_group_specifics.pb.h"
#include "google_apis/gaia/gaia_id.h"

class PrefService;

namespace syncer {
class MutableDataBatch;
class MetadataBatch;
class ModelError;
}  // namespace syncer

namespace tab_groups {
class SyncBridgeTabGroupModelWrapper;

// The SavedTabGroupSyncBridge is responsible for synchronizing and resolving
// conflicts between the data stored in the sync server and what is currently
// stored in the SavedTabGroupModel. Once synchronized, this data is stored in
// the DataTypeStore for local persistence across sessions.
class SavedTabGroupSyncBridge : public syncer::DataTypeSyncBridge {
 public:
  // `model_wrapper` and `pref_service` must not be null and must outlive the
  // current object.
  SavedTabGroupSyncBridge(
      SyncBridgeTabGroupModelWrapper* model_wrapper,
      syncer::OnceDataTypeStoreFactory create_store_callback,
      std::unique_ptr<syncer::DataTypeLocalChangeProcessor> change_processor,
      PrefService* pref_service);

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

  ~SavedTabGroupSyncBridge() override;

  // syncer::DataTypeSyncBridge:
  void OnSyncStarting(
      const syncer::DataTypeActivationRequest& request) override;
  std::optional<syncer::ModelError> MergeFullSyncData(
      std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
      syncer::EntityChangeList entity_changes) override;
  std::optional<syncer::ModelError> ApplyIncrementalSyncChanges(
      std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
      syncer::EntityChangeList entity_changes) override;
  void ApplyDisableSyncChanges(std::unique_ptr<syncer::MetadataChangeList>
                                   delete_metadata_change_list) override;
  syncer::ConflictResolution ResolveConflict(
      const std::string& storage_key,
      const syncer::EntityData& remote_data) const override;
  std::string GetStorageKey(
      const syncer::EntityData& entity_data) const override;
  std::string GetClientTag(
      const syncer::EntityData& entity_data) const override;
  std::unique_ptr<syncer::DataBatch> GetDataForCommit(
      StorageKeyList storage_keys) override;
  std::unique_ptr<syncer::DataBatch> GetAllDataForDebugging() override;
  bool IsEntityDataValid(const syncer::EntityData& entity_data) const override;
  sync_pb::EntitySpecifics TrimAllSupportedFieldsFromRemoteSpecifics(
      const sync_pb::EntitySpecifics& entity_specifics) const override;

  void SavedTabGroupAddedLocally(const base::Uuid& guid);
  void SavedTabGroupRemovedLocally(const SavedTabGroup& removed_group);
  void SavedTabGroupUpdatedLocally(const base::Uuid& group_guid,
                                   const std::optional<base::Uuid>& tab_guid);
  void SavedTabGroupTabsReorderedLocally(const base::Uuid& group_guid);
  void SavedTabGroupReorderedLocally();
  void SavedTabGroupLocalIdChanged(const base::Uuid& group_guid);
  void SavedTabGroupLastUserInteractionTimeUpdated(
      const base::Uuid& group_guid);

  const std::vector<proto::SavedTabGroupData>&
  GetTabsMissingGroupsForTesting() {
    return tabs_missing_groups_;
  }

  // Returns the cache guid the change processor holds if metadata is tracked,
  // otherwise returns a nullopt.
  std::optional<std::string> GetLocalCacheGuid() const;

  // Returns the account ID from the change processor if metadata is tracked,
  // otherwise returns a nullopt.
  std::optional<GaiaId> GetTrackedGaiaId() const;

  // Whether the sync is currently enabled and syncing for saved tab groups.
  // False before bridge initialization is completed.
  bool IsSyncing() const;

  static SavedTabGroup SpecificsToSavedTabGroupForTest(
      const sync_pb::SavedTabGroupSpecifics& specifics);
  static sync_pb::SavedTabGroupSpecifics SavedTabGroupToSpecificsForTest(
      const SavedTabGroup& group);
  static SavedTabGroupTab SpecificsToSavedTabGroupTabForTest(
      const sync_pb::SavedTabGroupSpecifics& specifics);
  static sync_pb::SavedTabGroupSpecifics SavedTabGroupTabToSpecificsForTest(
      const SavedTabGroupTab& tab);

  static SavedTabGroup DataToSavedTabGroupForTest(
      const proto::SavedTabGroupData& data);
  static proto::SavedTabGroupData SavedTabGroupToDataForTest(
      const SavedTabGroup& group);
  static SavedTabGroupTab DataToSavedTabGroupTabForTest(
      const proto::SavedTabGroupData& data);
  static proto::SavedTabGroupData SavedTabGroupTabToDataForTest(
      const SavedTabGroupTab& tab);

 private:
  // Updates and/or adds the specifics into the DataTypeStore.
  void UpsertEntitySpecific(const proto::SavedTabGroupData& data,
                            syncer::DataTypeStore::WriteBatch* write_batch);
  void UpsertEntitySpecific(const proto::SavedTabGroupData& data,
                            syncer::DataTypeStore::WriteBatch* write_batch,
                            bool send_to_sync);

  // Removes the specifics pointed to by `guid` from the DataTypeStore.
  void RemoveEntitySpecific(const base::Uuid& guid,
                            syncer::DataTypeStore::WriteBatch* write_batch);

  // Adds `specifics` into local storage (SavedTabGroupModel, and
  // DataTypeStore) and resolves any conflicts if `specifics` already exists
  // locally. `notify_sync` is true when MergeFullSyncData is called and there
  // is a conflict between the received and local data. Accordingly, after the
  // conflict has been resolved, we will want to update sync with this merged
  // data. `notify_sync` is false in cases that would cause a cycle such as when
  // ApplyIncrementalSyncChanges is called. Additionally, the list of changes
  // may not be complete and tabs may have been sent before their groups have
  // arrived. In this case, the tabs are saved in the DataTypeStore but not in
  // the model (and instead cached in this class).
  void AddDataToLocalStorage(const sync_pb::SavedTabGroupSpecifics& specifics,
                             syncer::MetadataChangeList* metadata_change_list,
                             syncer::DataTypeStore::WriteBatch* write_batch,
                             bool notify_sync);

  // Removes all data assigned to `guid` from local storage (SavedTabGroupModel,
  // and DataTypeStore). If this guid represents a group, all tabs will be
  // removed in addition to the group.
  void DeleteDataFromLocalStorage(
      const base::Uuid& guid,
      syncer::DataTypeStore::WriteBatch* write_batch);

  // Converts a `group` to a `SavedTabGroupSpecifics` proto. The returned
  // specifics also contains unsupported fields that are stored in sync
  // metadata.
  proto::SavedTabGroupData SavedTabGroupToData(
      const SavedTabGroup& group) const;

  // Converts a `tab` to a `SavedTabGroupSpecifics` proto. The returned
  // specifics also contains unsupported fields that are stored in sync
  // metadata.
  proto::SavedTabGroupData SavedTabGroupTabToData(
      const SavedTabGroupTab& tab) const;

  // Attempts to add the tabs found in `tabs_missing_groups_` to local storage.
  void ResolveTabsMissingGroups(syncer::DataTypeStore::WriteBatch* write_batch);

  // Iterates through groups saved in the model, and decides whether the group
  // is orphaned and needs to be destroyed. If it does, destroys the group.
  // An orphaned group is described as a group that has no tabs, and the last
  // update time has been long enough ago that its likely to never get a tab.
  void ResolveGroupsMissingTabs(syncer::DataTypeStore::WriteBatch* write_batch);

  // Adds the entry into `batch`.
  void AddEntryToBatch(syncer::MutableDataBatch* batch,
                       proto::SavedTabGroupData specifics);

  // Inform the processor of a new or updated SavedTabGroupSpecifics and add the
  // necessary metadata changes into `metadata_change_list`.
  void SendToSync(sync_pb::SavedTabGroupSpecifics specifics,
                  syncer::MetadataChangeList* metadata_change_list);

  // Loads the data already stored in the DataTypeStore.
  void OnStoreCreated(const std::optional<syncer::ModelError>& error,
                      std::unique_ptr<syncer::DataTypeStore> store);

  // Loads all sync_pb::SavedTabGroupSpecifics stored in `entries` passing the
  // specifics into OnReadAllMetadata.
  void OnDatabaseLoad(
      const std::optional<syncer::ModelError>& error,
      std::unique_ptr<syncer::DataTypeStore::RecordList> entries);

  // React to store failures if a save was not successful.
  void OnDatabaseSave(const std::optional<syncer::ModelError>& error);

  // Calls ModelReadyToSync if there are no errors to report and loads the
  // stored entries into `model_`.
  void OnReadAllMetadata(
      std::unique_ptr<syncer::DataTypeStore::RecordList> entries,
      const std::optional<syncer::ModelError>& error,
      std::unique_ptr<syncer::MetadataBatch> metadata_batch);

  // Called to migrate the SavedTabGroupSpecfics to SavedTabGroupData.
  void MigrateSpecificsToSavedTabGroupData(
      std::unique_ptr<syncer::DataTypeStore::RecordList> entries);
  void OnSpecificsToDataMigrationComplete(
      const std::optional<syncer::ModelError>& error);

  // Called to update the cache guid of groups and tabs with latest cache guid
  // and subsequently writes the updated data to storage.
  void UpdateLocalCacheGuidForGroups(
      syncer::DataTypeStore::WriteBatch* write_batch);

  // Helper method to determine if a tab group was created from a remote device
  // based on the group's cache guid.
  bool IsRemoteGroup(const SavedTabGroup& group);

  // Creates `ongoing_write_batch_` if needed and returns a scoped closure
  // runner that will destroy the batch if needed when it goes out of scope.
  // When `commit_write_batch_on_destroy` is false, the write batch is not
  // committed to the store when destroyed, and the caller is responsible for
  // committing it when needed by calling CommitOngoingWriteBatch().
  // `commit_write_batch_on_destroy` has no impact if there is an ongoing write
  // batch (i.e. this method is called reentrantly). Data from the
  // `metadata_change_list` is transferred to the ongoing write batch if
  // provided.
  base::ScopedClosureRunner MaybeCreateScopedWriteBatch(
      bool commit_write_batch_on_destroy,
      std::unique_ptr<syncer::MetadataChangeList> metadata_change_list);

  // Commits the ongoing write batch to the store. This method should only be
  // called after `MaybeCreateScopedWriteBatch()` and when the current scope is
  // not reentrant (normally, only within ApplyIncrementalSyncChanges() or
  // MergeFullSyncData()).
  void CommitOngoingWriteBatch();

  // Destroys the ongoing write batch and commits it to the store if
  // `commit_write_batch_on_destroy` is true. This method should not be called
  // directly, use CommitOngoingWriteBatch() instead.
  void DestroyOngoingWriteBatch(bool commit_write_batch_on_destroy);

  // The DataTypeStore used for local storage.
  std::unique_ptr<syncer::DataTypeStore> store_;

  // The write batch used to allow reentrancy calls during the processing of
  // remote updates. If `ongoing_write_batch_` is not null, it means that there
  // is likely an ongoing remote update processing. In this case methods should
  // reuse this object instead of creating a new write batch. Otherwise, the
  // write batch from processing remote updates may overwrite the changes once
  // committed.
  //
  // Use CreateWriteBatchWithDestroyClosure() to create this object because it
  // returns a scoped closure runner that will destroy the batch when it goes
  // out of scope. DestroyOngoingWriteBatch() can be called to destroy the batch
  // explicitly, but this is normally required only during remote updates.
  std::unique_ptr<syncer::DataTypeStore::WriteBatch> ongoing_write_batch_;

  // Tab groups model wrapper used to access and modify SavedTabGroupModel.
  raw_ptr<SyncBridgeTabGroupModelWrapper> model_wrapper_;

  // The pref service for storing migration status.
  raw_ptr<PrefService> pref_service_;

  // Used to store tabs whose groups were not added locally yet.
  std::vector<proto::SavedTabGroupData> tabs_missing_groups_;

  // Only for metrics. Used to ensure that a certain metrics is recorded at max
  // once per chrome session.
  bool migration_already_complete_recorded_ = false;

  // Allows safe temporary use of the SavedTabGroupSyncBridge object if it
  // exists at the time of use.
  base::WeakPtrFactory<SavedTabGroupSyncBridge> weak_ptr_factory_{this};
};

}  // namespace tab_groups

#endif  // COMPONENTS_SAVED_TAB_GROUPS_INTERNAL_SAVED_TAB_GROUP_SYNC_BRIDGE_H_
