// Copyright 2014 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 "components/sync_wifi/wifi_credential_syncable_service.h"

#include <stdint.h>

#include <utility>
#include <vector>

#include "base/logging.h"
#include "components/sync/model/sync_change.h"
#include "components/sync/model/sync_data.h"
#include "components/sync/model/sync_error.h"
#include "components/sync/model/sync_error_factory.h"
#include "components/sync/model/sync_merge_result.h"
#include "components/sync/protocol/sync.pb.h"
#include "components/sync_wifi/wifi_credential.h"
#include "components/sync_wifi/wifi_security_class.h"

namespace sync_wifi {

namespace {

struct RawCredentialData {
  std::vector<uint8_t> ssid;
  WifiSecurityClass security_class;
  std::string passphrase;
};

void BuildSpecifics(const WifiCredential& credential,
                    sync_pb::EntitySpecifics* out_buffer) {
  DCHECK(out_buffer);
  sync_pb::WifiCredentialSpecifics* credential_specifics =
      out_buffer->mutable_wifi_credential();
  DCHECK(credential_specifics);
  credential_specifics->set_ssid(credential.ssid().data(),
                                 credential.ssid().size());
  credential_specifics->set_security_class(
      WifiSecurityClassToSyncSecurityClass(credential.security_class()));
  if (WifiSecurityClassSupportsPassphrases(credential.security_class())) {
    credential_specifics->set_passphrase(credential.passphrase().data(),
                                         credential.passphrase().size());
  }
}

bool ParseSpecifics(const sync_pb::EntitySpecifics& specifics,
                    RawCredentialData* raw_credential) {
  DCHECK(raw_credential);
  if (!specifics.has_wifi_credential()) {
    LOG(ERROR) << "Specifics with missing wifi_credential; skipping";
    return false;
  }

  const sync_pb::WifiCredentialSpecifics& credential_specifics =
      specifics.wifi_credential();
  if (!credential_specifics.has_ssid()) {
    LOG(ERROR) << "Specifics with missing SSID; skipping";
    return false;
  }
  if (!credential_specifics.has_security_class()) {
    LOG(ERROR) << "Specifics with missing security class; skipping";
    return false;
  }

  const WifiSecurityClass security_class =
      WifiSecurityClassFromSyncSecurityClass(
          credential_specifics.security_class());
  if (WifiSecurityClassSupportsPassphrases(security_class) &&
      !credential_specifics.has_passphrase()) {
    LOG(ERROR) << "Specifics for security class "
               << credential_specifics.security_class()
               << " is missing passphrase; skipping";
    return false;
  }

  raw_credential->ssid.assign(credential_specifics.ssid().begin(),
                              credential_specifics.ssid().end());
  raw_credential->security_class = security_class;
  raw_credential->passphrase = credential_specifics.passphrase();
  return true;
}

// TODO(quiche): Separate SyncData validation from parsing of
// WifiCredentialSpecifics.
bool ParseSyncData(const syncer::SyncData& sync_data,
                   RawCredentialData* raw_credential) {
  DCHECK(raw_credential);
  if (!sync_data.IsValid()) {
    LOG(WARNING) << "Invalid SyncData; skipping item";
    return false;
  }

  if (sync_data.GetDataType() != syncer::WIFI_CREDENTIALS) {
    LOG(WARNING) << "Unexpected SyncData of type "
                 << syncer::ModelTypeToString(sync_data.GetDataType())
                 << "; skipping item";
    return false;
  }

  return ParseSpecifics(sync_data.GetSpecifics(), raw_credential);
}

}  // namespace

const syncer::ModelType WifiCredentialSyncableService::kModelType =
    syncer::WIFI_CREDENTIALS;

WifiCredentialSyncableService::WifiCredentialSyncableService(
    std::unique_ptr<WifiConfigDelegate> network_config_delegate)
    : network_config_delegate_(std::move(network_config_delegate)) {
  DCHECK(network_config_delegate_);
}

WifiCredentialSyncableService::~WifiCredentialSyncableService() {}

syncer::SyncMergeResult WifiCredentialSyncableService::MergeDataAndStartSyncing(
    syncer::ModelType type,
    const syncer::SyncDataList& initial_sync_data,
    std::unique_ptr<syncer::SyncChangeProcessor> sync_processor,
    std::unique_ptr<syncer::SyncErrorFactory> /* error_handler */) {
  DCHECK(!sync_processor_.get());
  DCHECK(sync_processor.get());
  DCHECK_EQ(kModelType, type);

  sync_processor_ = std::move(sync_processor);

  // TODO(quiche): Update local WiFi configuration from |initial_sync_data|.
  // TODO(quiche): Notify upper layers that sync is ready.
  NOTIMPLEMENTED();

  return syncer::SyncMergeResult(type);
}

void WifiCredentialSyncableService::StopSyncing(syncer::ModelType type) {
  DCHECK_EQ(kModelType, type);
  sync_processor_.reset();
}

syncer::SyncDataList WifiCredentialSyncableService::GetAllSyncData(
    syncer::ModelType type) const {
  DCHECK_EQ(kModelType, type);
  NOTIMPLEMENTED();
  return syncer::SyncDataList();
}

syncer::SyncError WifiCredentialSyncableService::ProcessSyncChanges(
    const tracked_objects::Location& /* caller_location */,
    const syncer::SyncChangeList& change_list) {
  if (!sync_processor_.get()) {
    return syncer::SyncError(
        FROM_HERE, syncer::SyncError::UNREADY_ERROR,
        "ProcessSyncChanges called before MergeDataAndStartSyncing",
        kModelType);
  }

  for (const syncer::SyncChange& sync_change : change_list) {
    DCHECK(sync_change.IsValid());
    RawCredentialData raw_credential;
    if (!ParseSyncData(sync_change.sync_data(), &raw_credential)) {
      LOG(WARNING) << "Failed to parse item; skipping "
                   << syncer::SyncChange::ChangeTypeToString(
                          sync_change.change_type());
      continue;
    }

    std::unique_ptr<WifiCredential> credential;
    switch (sync_change.change_type()) {
      case syncer::SyncChange::ACTION_ADD:
        credential = WifiCredential::Create(raw_credential.ssid,
                                            raw_credential.security_class,
                                            raw_credential.passphrase);
        if (!credential)
          LOG(WARNING) << "Failed to create credential; skipping";
        else
          network_config_delegate_->AddToLocalNetworks(*credential);
        break;
      case syncer::SyncChange::ACTION_UPDATE:
        // TODO(quiche): Implement update, and add appropriate tests.
        NOTIMPLEMENTED();
        break;
      case syncer::SyncChange::ACTION_DELETE:
        // TODO(quiche): Implement delete, and add appropriate tests.
        NOTIMPLEMENTED();
        break;
      default:
        return syncer::SyncError(
            FROM_HERE, syncer::SyncError::DATATYPE_ERROR,
            "ProcessSyncChanges given invalid SyncChangeType", kModelType);
    }
  }

  return syncer::SyncError();
}

bool WifiCredentialSyncableService::AddToSyncedNetworks(
    const std::string& item_id,
    const WifiCredential& credential) {
  if (!sync_processor_.get()) {
    // Callers must queue updates until MergeDataAndStartSyncing has
    // been called on this SyncableService.
    LOG(WARNING) << "WifiCredentials syncable service is not started.";
    return false;
  }

  const SsidAndSecurityClass network_id(credential.ssid(),
                                        credential.security_class());
  if (synced_networks_and_passphrases_.find(network_id) !=
      synced_networks_and_passphrases_.end()) {
    // TODO(quiche): If passphrase has changed, submit this to sync as
    // an ACTION_UPDATE. crbug.com/431436
    return false;
  }

  syncer::SyncChangeList change_list;
  syncer::SyncError sync_error;
  sync_pb::EntitySpecifics wifi_credential_specifics;
  BuildSpecifics(credential, &wifi_credential_specifics);
  change_list.push_back(
      syncer::SyncChange(FROM_HERE, syncer::SyncChange::ACTION_ADD,
                         syncer::SyncData::CreateLocalData(
                             item_id, item_id, wifi_credential_specifics)));
  sync_error = sync_processor_->ProcessSyncChanges(FROM_HERE, change_list);
  if (sync_error.IsSet()) {
    LOG(ERROR) << sync_error.ToString();
    return false;
  }

  synced_networks_and_passphrases_[network_id] = credential.passphrase();
  return true;
}

}  // namespace sync_wifi
