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

#include "chromeos/components/sync_wifi/wifi_configuration_bridge.h"

#include <algorithm>

#include "base/bind.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/optional.h"
#include "base/strings/stringprintf.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "chromeos/components/sync_wifi/local_network_collector.h"
#include "chromeos/components/sync_wifi/network_identifier.h"
#include "chromeos/components/sync_wifi/network_type_conversions.h"
#include "chromeos/components/sync_wifi/synced_network_metrics_logger.h"
#include "chromeos/components/sync_wifi/synced_network_updater.h"
#include "chromeos/components/sync_wifi/timer_factory.h"
#include "chromeos/network/network_configuration_handler.h"
#include "chromeos/network/network_event_log.h"
#include "chromeos/network/network_metadata_store.h"
#include "components/device_event_log/device_event_log.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/model_type_change_processor.h"
#include "components/sync/model/mutable_data_batch.h"
#include "components/sync/protocol/model_type_state.pb.h"
#include "third_party/cros_system_api/dbus/shill/dbus-constants.h"

namespace chromeos {

namespace sync_wifi {

namespace {

std::unique_ptr<syncer::EntityData> GenerateWifiEntityData(
    const sync_pb::WifiConfigurationSpecifics& proto) {
  auto entity_data = std::make_unique<syncer::EntityData>();
  entity_data->specifics.mutable_wifi_configuration()->CopyFrom(proto);
  entity_data->name = NetworkIdentifier::FromProto(proto).SerializeToString();
  return entity_data;
}

// Delay before attempting to save a newly configured network to sync.  This
// is to give time for an initial connection attempt to fail in case of a bad
// password, which will prevent syncing.
constexpr base::TimeDelta kSyncAfterCreatedTimeout =
    base::TimeDelta::FromSeconds(20);

}  // namespace

WifiConfigurationBridge::WifiConfigurationBridge(
    SyncedNetworkUpdater* synced_network_updater,
    LocalNetworkCollector* local_network_collector,
    NetworkConfigurationHandler* network_configuration_handler,
    SyncedNetworkMetricsLogger* metrics_recorder,
    TimerFactory* timer_factory,
    std::unique_ptr<syncer::ModelTypeChangeProcessor> change_processor,
    syncer::OnceModelTypeStoreFactory create_store_callback)
    : ModelTypeSyncBridge(std::move(change_processor)),
      synced_network_updater_(synced_network_updater),
      local_network_collector_(local_network_collector),
      network_configuration_handler_(network_configuration_handler),
      metrics_recorder_(metrics_recorder),
      timer_factory_(timer_factory),
      network_metadata_store_(nullptr) {
  std::move(create_store_callback)
      .Run(syncer::WIFI_CONFIGURATIONS,
           base::BindOnce(&WifiConfigurationBridge::OnStoreCreated,
                          weak_ptr_factory_.GetWeakPtr()));
  if (network_configuration_handler_) {
    network_configuration_handler_->AddObserver(this);
  }
}

WifiConfigurationBridge::~WifiConfigurationBridge() {
  OnShuttingDown();
}

void WifiConfigurationBridge::OnShuttingDown() {
  if (network_metadata_store_) {
    network_metadata_store_->RemoveObserver(this);
    network_metadata_store_ = nullptr;
  }
  if (network_configuration_handler_) {
    network_configuration_handler_->RemoveObserver(this);
    network_configuration_handler_ = nullptr;
  }
}

std::unique_ptr<syncer::MetadataChangeList>
WifiConfigurationBridge::CreateMetadataChangeList() {
  return syncer::ModelTypeStore::WriteBatch::CreateMetadataChangeList();
}

base::Optional<syncer::ModelError> WifiConfigurationBridge::MergeSyncData(
    std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
    syncer::EntityChangeList change_list) {
  DCHECK(entries_.empty());
  DCHECK(local_network_collector_);

  local_network_collector_->GetAllSyncableNetworks(
      base::BindOnce(&WifiConfigurationBridge::OnGetAllSyncableNetworksResult,
                     weak_ptr_factory_.GetWeakPtr(),
                     std::move(metadata_change_list), std::move(change_list)));

  return base::nullopt;
}

void WifiConfigurationBridge::OnGetAllSyncableNetworksResult(
    std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
    syncer::EntityChangeList change_list,
    std::vector<sync_pb::WifiConfigurationSpecifics> local_network_list) {
  // To merge local and synced networks we add all local networks that don't
  // exist in sync to the server and all synced networks that don't exist
  // locally to Shill.  For networks which exist on both lists, we compare the
  // last connected timestamp and take the newer configuration.

  NET_LOG(EVENT) << "Merging " << local_network_list.size() << " local and "
                 << change_list.size() << " synced networks.";
  base::flat_map<NetworkIdentifier, sync_pb::WifiConfigurationSpecifics>
      sync_networks;
  base::flat_map<NetworkIdentifier, sync_pb::WifiConfigurationSpecifics>
      local_networks;

  // Iterate through incoming changes from sync and populate the sync_networks
  // map.
  for (std::unique_ptr<syncer::EntityChange>& change : change_list) {
    if (change->type() == syncer::EntityChange::ACTION_DELETE) {
      // Don't delete any local networks during the initial merge when sync is
      // first enabled.
      continue;
    }

    const sync_pb::WifiConfigurationSpecifics& proto =
        change->data().specifics.wifi_configuration();
    NetworkIdentifier id = NetworkIdentifier::FromProto(proto);
    if (sync_networks.contains(id) &&
        sync_networks[id].last_connected_timestamp() >
            proto.last_connected_timestamp()) {
      continue;
    }
    sync_networks[id] = proto;
  }

  // Iterate through local networks and add to sync where appropriate.
  for (sync_pb::WifiConfigurationSpecifics& proto : local_network_list) {
    NetworkIdentifier id = NetworkIdentifier::FromProto(proto);
    if (sync_networks.contains(id) &&
        sync_networks[id].last_connected_timestamp() >
            proto.last_connected_timestamp()) {
      continue;
    }

    local_networks[id] = proto;
    std::unique_ptr<syncer::EntityData> entity_data =
        GenerateWifiEntityData(proto);
    std::string storage_key = GetStorageKey(*entity_data);

    // Upload the local network configuration to sync.  This could be a new
    // configuration or an update to an existing one.
    change_processor()->Put(storage_key, std::move(entity_data),
                            metadata_change_list.get());
    entries_[storage_key] = proto;
  }

  std::unique_ptr<syncer::ModelTypeStore::WriteBatch> batch =
      store_->CreateWriteBatch();
  // Iterate through synced networks and update local stack where appropriate.
  for (auto& kv : sync_networks) {
    NetworkIdentifier& id = kv.first;
    sync_pb::WifiConfigurationSpecifics& proto = kv.second;

    if (local_networks.contains(id) &&
        local_networks[id].last_connected_timestamp() >
            proto.last_connected_timestamp()) {
      continue;
    }

    // Update the local network stack to have the synced network configuration.
    synced_network_updater_->AddOrUpdateNetwork(proto);

    // Save the proto to the sync data store to keep track of all synced
    // networks on device.  This gets loaded into |entries_| next time the
    // bridge is initialized.
    batch->WriteData(id.SerializeToString(), proto.SerializeAsString());
    entries_[id.SerializeToString()] = proto;
  }

  // Mark the changes as processed.
  batch->TakeMetadataChangesFrom(std::move(metadata_change_list));
  Commit(std::move(batch));
  metrics_recorder_->RecordTotalCount(entries_.size());
}

base::Optional<syncer::ModelError> WifiConfigurationBridge::ApplySyncChanges(
    std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
    syncer::EntityChangeList entity_changes) {
  std::unique_ptr<syncer::ModelTypeStore::WriteBatch> batch =
      store_->CreateWriteBatch();

  NET_LOG(EVENT) << "Applying  " << entity_changes.size()
                 << " pending changes.";

  // TODO(jonmann) Don't override synced network configurations that are newer
  // than the local configurations.
  for (std::unique_ptr<syncer::EntityChange>& change : entity_changes) {
    if (change->type() == syncer::EntityChange::ACTION_DELETE) {
      auto it = entries_.find(change->storage_key());
      if (it != entries_.end()) {
        entries_.erase(it);
        batch->DeleteData(change->storage_key());
        synced_network_updater_->RemoveNetwork(
            NetworkIdentifier::DeserializeFromString(change->storage_key()));
      } else {
        NET_LOG(EVENT) << "Received delete request for network which is not "
                          "tracked by sync.";
      }
      continue;
    }

    auto& specifics = change->data().specifics.wifi_configuration();
    synced_network_updater_->AddOrUpdateNetwork(specifics);

    batch->WriteData(change->storage_key(), specifics.SerializeAsString());
    entries_[change->storage_key()] = std::move(specifics);
  }

  batch->TakeMetadataChangesFrom(std::move(metadata_change_list));
  Commit(std::move(batch));
  metrics_recorder_->RecordTotalCount(entries_.size());

  return base::nullopt;
}

void WifiConfigurationBridge::GetData(StorageKeyList storage_keys,
                                      DataCallback callback) {
  auto batch = std::make_unique<syncer::MutableDataBatch>();

  for (const std::string& id : storage_keys) {
    auto it = entries_.find(id);
    if (it == entries_.end()) {
      continue;
    }
    batch->Put(id, GenerateWifiEntityData(it->second));
  }
  std::move(callback).Run(std::move(batch));
}

void WifiConfigurationBridge::GetAllDataForDebugging(DataCallback callback) {
  auto batch = std::make_unique<syncer::MutableDataBatch>();
  for (const auto& entry : entries_) {
    batch->Put(entry.first, GenerateWifiEntityData(entry.second));
  }
  std::move(callback).Run(std::move(batch));
}

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

std::string WifiConfigurationBridge::GetStorageKey(
    const syncer::EntityData& entity_data) {
  return NetworkIdentifier::FromProto(
             entity_data.specifics.wifi_configuration())
      .SerializeToString();
}

void WifiConfigurationBridge::OnStoreCreated(
    const base::Optional<syncer::ModelError>& error,
    std::unique_ptr<syncer::ModelTypeStore> store) {
  if (error) {
    change_processor()->ReportError(*error);
    return;
  }

  store_ = std::move(store);
  store_->ReadAllData(base::BindOnce(&WifiConfigurationBridge::OnReadAllData,
                                     weak_ptr_factory_.GetWeakPtr()));
}

void WifiConfigurationBridge::OnReadAllData(
    const base::Optional<syncer::ModelError>& error,
    std::unique_ptr<syncer::ModelTypeStore::RecordList> records) {
  if (error) {
    change_processor()->ReportError(*error);
    return;
  }

  for (syncer::ModelTypeStore::Record& record : *records) {
    sync_pb::WifiConfigurationSpecifics data;
    if (record.id.empty() || !data.ParseFromString(record.value)) {
      NET_LOG(EVENT) << "Unable to parse proto for entry with key: "
                     << record.id;
      continue;
    }
    entries_[record.id] = std::move(data);
  }

  metrics_recorder_->RecordTotalCount(entries_.size());
  store_->ReadAllMetadata(
      base::BindOnce(&WifiConfigurationBridge::OnReadAllMetadata,
                     weak_ptr_factory_.GetWeakPtr()));
}

void WifiConfigurationBridge::OnReadAllMetadata(
    const base::Optional<syncer::ModelError>& error,
    std::unique_ptr<syncer::MetadataBatch> metadata_batch) {
  if (error) {
    change_processor()->ReportError(*error);
    return;
  }
  change_processor()->ModelReadyToSync(std::move(metadata_batch));
}

void WifiConfigurationBridge::OnCommit(
    const base::Optional<syncer::ModelError>& error) {
  if (error)
    change_processor()->ReportError(*error);
}

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

std::vector<NetworkIdentifier> WifiConfigurationBridge::GetAllIdsForTesting() {
  std::vector<NetworkIdentifier> ids;
  for (const auto& entry : entries_)
    ids.push_back(NetworkIdentifier::FromProto(entry.second));

  return ids;
}

void WifiConfigurationBridge::OnFirstConnectionToNetwork(
    const std::string& guid) {
  if (network_guid_to_timer_map_.contains(guid)) {
    network_guid_to_timer_map_.erase(guid);
  }

  if (network_metadata_store_->GetIsConfiguredBySync(guid)) {
    // Don't have to upload a configuration that came from sync.
    NET_LOG(EVENT) << "Not uploading network on first connect: "
                   << NetworkGuidId(guid) << " was added by sync.";
    return;
  }

  NET_LOG(EVENT) << "Syncing network after first connect: "
                 << NetworkGuidId(guid);
  local_network_collector_->GetSyncableNetwork(
      guid, base::BindOnce(&WifiConfigurationBridge::SaveNetworkToSync,
                           weak_ptr_factory_.GetWeakPtr()));
}

void WifiConfigurationBridge::OnNetworkUpdate(
    const std::string& guid,
    base::DictionaryValue* set_properties) {
  if (!set_properties)
    return;

  if (synced_network_updater_->IsUpdateInProgress(guid) ||
      network_metadata_store_->GetIsConfiguredBySync(guid)) {
    // Don't have to upload a configuration that came from sync.
    NET_LOG(EVENT) << "Not uploading change to " << NetworkGuidId(guid)
                   << ", modified network was configured "
                      "by sync.";
    return;
  }

  if (!set_properties->HasKey(shill::kAutoConnectProperty) &&
      !set_properties->HasKey(shill::kPriorityProperty) &&
      !set_properties->HasKey(shill::kProxyConfigProperty) &&
      !set_properties->HasKey(shill::kMeteredProperty) &&
      !set_properties->FindPath(
          base::StringPrintf("%s.%s", shill::kStaticIPConfigProperty,
                             shill::kNameServersProperty))) {
    NET_LOG(EVENT) << "Not uploading change to " << NetworkGuidId(guid)
                   << ", modified network field(s) are not synced.";
    return;
  }

  NET_LOG(EVENT) << "Updating sync with changes to " << NetworkGuidId(guid);
  local_network_collector_->GetSyncableNetwork(
      guid, base::BindOnce(&WifiConfigurationBridge::SaveNetworkToSync,
                           weak_ptr_factory_.GetWeakPtr()));
}

void WifiConfigurationBridge::SaveNetworkToSync(
    base::Optional<sync_pb::WifiConfigurationSpecifics> proto) {
  if (!proto) {
    return;
  }

  std::unique_ptr<syncer::EntityData> entity_data =
      GenerateWifiEntityData(*proto);
  auto id = NetworkIdentifier::FromProto(*proto);
  std::string storage_key = GetStorageKey(*entity_data);

  std::unique_ptr<syncer::ModelTypeStore::WriteBatch> batch =
      store_->CreateWriteBatch();
  batch->WriteData(storage_key, proto->SerializeAsString());
  change_processor()->Put(storage_key, std::move(entity_data),
                          batch->GetMetadataChangeList());
  entries_[storage_key] = *proto;
  Commit(std::move(batch));
  NET_LOG(EVENT) << "Saved network "
                 << NetworkId(NetworkStateFromNetworkIdentifier(id))
                 << " to sync.";
  metrics_recorder_->RecordTotalCount(entries_.size());
}

void WifiConfigurationBridge::OnNetworkCreated(const std::string& guid) {
  network_guid_to_timer_map_[guid] = timer_factory_->CreateOneShotTimer();
  network_guid_to_timer_map_[guid]->Start(
      FROM_HERE, kSyncAfterCreatedTimeout,
      base::BindOnce(&WifiConfigurationBridge::OnNetworkConfiguredDelayComplete,
                     weak_ptr_factory_.GetWeakPtr(), guid));
}

void WifiConfigurationBridge::OnNetworkConfiguredDelayComplete(
    const std::string& network_guid) {
  if (network_guid_to_timer_map_.contains(network_guid)) {
    network_guid_to_timer_map_.erase(network_guid);
  }

  // This check to prevent uploading networks that were added by sync happens
  // after the delay because the metadata isn't available in OnNetworkCreated.
  if (network_metadata_store_->GetIsConfiguredBySync(network_guid)) {
    NET_LOG(EVENT) << "Not uploading newly configured network "
                   << NetworkGuidId(network_guid) << ", it was added by sync.";
    return;
  }

  NET_LOG(EVENT) << "Attempting to sync new network after delay.";
  local_network_collector_->GetSyncableNetwork(
      network_guid, base::BindOnce(&WifiConfigurationBridge::SaveNetworkToSync,
                                   weak_ptr_factory_.GetWeakPtr()));
}

void WifiConfigurationBridge::OnBeforeConfigurationRemoved(
    const std::string& service_path,
    const std::string& guid) {
  base::Optional<NetworkIdentifier> id =
      local_network_collector_->GetNetworkIdentifierFromGuid(guid);
  if (!id) {
    return;
  }

  NET_LOG(EVENT) << "Storing metadata for " << NetworkPathId(service_path)
                 << " in preparation for removal.";
  std::string storage_key = id->SerializeToString();
  if (entries_.contains(storage_key))
    pending_deletes_[guid] = storage_key;
}

void WifiConfigurationBridge::OnConfigurationRemoved(
    const std::string& service_path,
    const std::string& network_guid) {
  if (!pending_deletes_.contains(network_guid)) {
    NET_LOG(EVENT) << "Configuration " << network_guid
                   << " removed with no matching saved metadata.";
    return;
  }

  std::string storage_key = pending_deletes_[network_guid];
  std::unique_ptr<syncer::ModelTypeStore::WriteBatch> batch =
      store_->CreateWriteBatch();
  batch->DeleteData(storage_key);
  change_processor()->Delete(storage_key, batch->GetMetadataChangeList());
  entries_.erase(storage_key);
  Commit(std::move(batch));
  NET_LOG(EVENT) << "Removed network from sync.";
}

void WifiConfigurationBridge::SetNetworkMetadataStore(
    base::WeakPtr<NetworkMetadataStore> network_metadata_store) {
  if (network_metadata_store_) {
    network_metadata_store->RemoveObserver(this);
  }
  network_metadata_store_ = network_metadata_store;
  network_metadata_store->AddObserver(this);
}

}  // namespace sync_wifi

}  // namespace chromeos
