// Copyright 2017 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/tether/tether_connector_impl.h"

#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "chromeos/components/proximity_auth/logging/logging.h"
#include "chromeos/components/tether/active_host.h"
#include "chromeos/components/tether/ble_connection_manager.h"
#include "chromeos/components/tether/device_id_tether_network_guid_map.h"
#include "chromeos/components/tether/disconnect_tethering_request_sender.h"
#include "chromeos/components/tether/host_connection_metrics_logger.h"
#include "chromeos/components/tether/host_scan_cache.h"
#include "chromeos/components/tether/notification_presenter.h"
#include "chromeos/components/tether/tether_host_fetcher.h"
#include "chromeos/components/tether/wifi_hotspot_connector.h"
#include "chromeos/components/tether/wifi_hotspot_disconnector.h"
#include "chromeos/network/network_handler.h"
#include "chromeos/network/network_state.h"
#include "chromeos/network/network_state_handler.h"

namespace chromeos {

namespace tether {

namespace {

void OnDisconnectFromWifiFailure(const std::string& device_id,
                                 const std::string& error_name) {
  PA_LOG(WARNING) << "Failed to disconnect from tether hotspot for device ID "
                  << cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(
                         device_id)
                  << ". Error: " << error_name;
}

}  // namespace

TetherConnectorImpl::TetherConnectorImpl(
    device_sync::DeviceSyncClient* device_sync_client,
    secure_channel::SecureChannelClient* secure_channel_client,
    NetworkStateHandler* network_state_handler,
    WifiHotspotConnector* wifi_hotspot_connector,
    ActiveHost* active_host,
    TetherHostFetcher* tether_host_fetcher,
    BleConnectionManager* connection_manager,
    TetherHostResponseRecorder* tether_host_response_recorder,
    DeviceIdTetherNetworkGuidMap* device_id_tether_network_guid_map,
    HostScanCache* host_scan_cache,
    NotificationPresenter* notification_presenter,
    HostConnectionMetricsLogger* host_connection_metrics_logger,
    DisconnectTetheringRequestSender* disconnect_tethering_request_sender,
    WifiHotspotDisconnector* wifi_hotspot_disconnector)
    : device_sync_client_(device_sync_client),
      secure_channel_client_(secure_channel_client),
      network_state_handler_(network_state_handler),
      wifi_hotspot_connector_(wifi_hotspot_connector),
      active_host_(active_host),
      tether_host_fetcher_(tether_host_fetcher),
      connection_manager_(connection_manager),
      tether_host_response_recorder_(tether_host_response_recorder),
      device_id_tether_network_guid_map_(device_id_tether_network_guid_map),
      host_scan_cache_(host_scan_cache),
      notification_presenter_(notification_presenter),
      host_connection_metrics_logger_(host_connection_metrics_logger),
      disconnect_tethering_request_sender_(disconnect_tethering_request_sender),
      wifi_hotspot_disconnector_(wifi_hotspot_disconnector),
      weak_ptr_factory_(this) {}

TetherConnectorImpl::~TetherConnectorImpl() {
  if (connect_tethering_operation_)
    connect_tethering_operation_->RemoveObserver(this);
}

void TetherConnectorImpl::ConnectToNetwork(
    const std::string& tether_network_guid,
    const base::Closure& success_callback,
    const network_handler::StringResultCallback& error_callback) {
  DCHECK(!tether_network_guid.empty());
  DCHECK(!success_callback.is_null());
  DCHECK(!error_callback.is_null());

  PA_LOG(INFO) << "Attempting to connect to network with GUID "
               << tether_network_guid << ".";
  notification_presenter_->RemoveConnectionToHostFailedNotification();

  const std::string device_id =
      device_id_tether_network_guid_map_->GetDeviceIdForTetherNetworkGuid(
          tether_network_guid);

  // If NetworkConnectionHandler receives a connection request for a network
  // to which it is already attempting a connection, it should stop the
  // duplicate connection request itself before invoking its TetherDelegate.
  // Thus, ConnectToNetwork() should never be called for a device which is
  // already pending connection.
  DCHECK(device_id_pending_connection_ != device_id);

  if (!device_id_pending_connection_.empty()) {
    PA_LOG(INFO) << "A connection attempt was already in progress to device "
                 << "with ID " << device_id_pending_connection_ << ". "
                 << "Canceling that connection attempt before continuing.";
    CancelConnectionAttempt(
        device_id_tether_network_guid_map_->GetTetherNetworkGuidForDeviceId(
            device_id_pending_connection_));
  }

  device_id_pending_connection_ = device_id;
  success_callback_ = success_callback;
  error_callback_ = error_callback;
  active_host_->SetActiveHostConnecting(device_id, tether_network_guid);

  tether_host_fetcher_->FetchTetherHost(
      device_id_pending_connection_,
      base::Bind(&TetherConnectorImpl::OnTetherHostToConnectFetched,
                 weak_ptr_factory_.GetWeakPtr(),
                 device_id_pending_connection_));
}

bool TetherConnectorImpl::CancelConnectionAttempt(
    const std::string& tether_network_guid) {
  const std::string device_id =
      device_id_tether_network_guid_map_->GetDeviceIdForTetherNetworkGuid(
          tether_network_guid);

  if (device_id != device_id_pending_connection_) {
    PA_LOG(ERROR) << "CancelConnectionAttempt(): Cancel requested for Tether "
                  << "network with GUID " << tether_network_guid << ", but "
                  << "there was no active connection to that network.";
    return false;
  }

  PA_LOG(INFO) << "Canceling connection attempt to Tether network with GUID "
               << tether_network_guid;

  if (connect_tethering_operation_) {
    // If a ConnectTetheringOperation is in progress, stop it.
    connect_tethering_operation_->RemoveObserver(this);
    connect_tethering_operation_.reset();
  }

  // Send a DisconnectTetheringRequest so that it can turn off its Wi-Fi
  // hotspot.
  disconnect_tethering_request_sender_->SendDisconnectRequestToDevice(
      device_id);

  SetConnectionFailed(
      NetworkConnectionHandler::kErrorConnectCanceled,
      HostConnectionMetricsLogger::ConnectionToHostResult::
          CONNECTION_RESULT_FAILURE_CLIENT_CONNECTION_CANCELED_BY_USER);
  return true;
}

void TetherConnectorImpl::OnConnectTetheringRequestSent(
    cryptauth::RemoteDeviceRef remote_device) {
  // If setup is required for the phone, display a notification so that the
  // user knows to follow instructions on the phone. Note that the notification
  // is displayed only after a request has been sent successfully. If the
  // notification is displayed before a the request has been sent, it could be
  // misleading since the connection could fail. See crbug.com/767756.
  const std::string tether_network_guid =
      device_id_tether_network_guid_map_->GetTetherNetworkGuidForDeviceId(
          remote_device.GetDeviceId());
  if (!host_scan_cache_->DoesHostRequireSetup(tether_network_guid))
    return;

  const NetworkState* tether_network_state =
      network_state_handler_->GetNetworkStateFromGuid(tether_network_guid);
  DCHECK(tether_network_state);
  notification_presenter_->NotifySetupRequired(
      tether_network_state->name(), tether_network_state->signal_strength());
}

void TetherConnectorImpl::OnSuccessfulConnectTetheringResponse(
    cryptauth::RemoteDeviceRef remote_device,
    const std::string& ssid,
    const std::string& password) {
  if (device_id_pending_connection_ != remote_device.GetDeviceId()) {
    // If the success was part of a previous attempt for a different device,
    // ignore it.
    PA_LOG(INFO) << "Received successful ConnectTetheringResponse from "
                 << "device with ID "
                 << remote_device.GetTruncatedDeviceIdForLogs() << ", but the "
                 << "connection attempt to that device has been canceled.";

    return;
  }

  PA_LOG(INFO) << "Received successful ConnectTetheringResponse from device "
               << "with ID " << remote_device.GetTruncatedDeviceIdForLogs()
               << ". SSID: \"" << ssid << "\".";

  // Make a copy of the device ID, SSID, and password to pass below before
  // destroying |connect_tethering_operation_|.
  std::string remote_device_id = remote_device.GetDeviceId();
  std::string ssid_copy = ssid;
  std::string password_copy = password;

  connect_tethering_operation_->RemoveObserver(this);
  connect_tethering_operation_.reset();

  wifi_hotspot_connector_->ConnectToWifiHotspot(
      ssid_copy, password_copy, active_host_->GetTetherNetworkGuid(),
      base::Bind(&TetherConnectorImpl::OnWifiConnection,
                 weak_ptr_factory_.GetWeakPtr(), remote_device_id));
}

void TetherConnectorImpl::OnConnectTetheringFailure(
    cryptauth::RemoteDeviceRef remote_device,
    ConnectTetheringOperation::HostResponseErrorCode error_code) {
  std::string device_id_copy = remote_device.GetDeviceId();
  if (device_id_pending_connection_ != device_id_copy) {
    // If the failure was part of a previous attempt for a different device,
    // ignore it.
    PA_LOG(INFO) << "Received failed ConnectTetheringResponse from device with "
                 << "ID " << remote_device.GetTruncatedDeviceIdForLogs()
                 << ", but a connection to another device has already started.";
    return;
  }

  PA_LOG(WARNING) << "Connection to device with ID "
                  << remote_device.GetTruncatedDeviceIdForLogs()
                  << " could not complete. Error code: " << error_code;

  connect_tethering_operation_->RemoveObserver(this);
  connect_tethering_operation_.reset();
  SetConnectionFailed(
      NetworkConnectionHandler::kErrorConnectFailed,
      GetConnectionToHostResultFromErrorCode(device_id_copy, error_code));
}

void TetherConnectorImpl::OnTetherHostToConnectFetched(
    const std::string& device_id,
    base::Optional<cryptauth::RemoteDeviceRef> tether_host_to_connect) {
  if (device_id_pending_connection_ != device_id) {
    PA_LOG(INFO) << "Device to connect to has changed while device with ID "
                 << cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(
                        device_id)
                 << " was being fetched.";
    return;
  }

  if (!tether_host_to_connect) {
    PA_LOG(ERROR) << "Could not fetch tether host with device ID "
                  << cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(
                         device_id)
                  << ". Cannot connect.";
    SetConnectionFailed(
        NetworkConnectionHandler::kErrorConnectFailed,
        HostConnectionMetricsLogger::ConnectionToHostResult::
            CONNECTION_RESULT_FAILURE_CLIENT_CONNECTION_INTERNAL_ERROR);
    return;
  }

  DCHECK(device_id == tether_host_to_connect->GetDeviceId());

  const std::string tether_network_guid =
      device_id_tether_network_guid_map_->GetTetherNetworkGuidForDeviceId(
          device_id);
  connect_tethering_operation_ =
      ConnectTetheringOperation::Factory::NewInstance(
          *tether_host_to_connect, device_sync_client_, secure_channel_client_,
          connection_manager_, tether_host_response_recorder_,
          host_scan_cache_->DoesHostRequireSetup(tether_network_guid));
  connect_tethering_operation_->AddObserver(this);
  connect_tethering_operation_->Initialize();
}

void TetherConnectorImpl::SetConnectionFailed(
    const std::string& error_name,
    HostConnectionMetricsLogger::ConnectionToHostResult
        connection_to_host_result) {
  DCHECK(!device_id_pending_connection_.empty());
  DCHECK(!error_callback_.is_null());

  notification_presenter_->RemoveSetupRequiredNotification();

  // Save a copy of the callback before resetting it below.
  network_handler::StringResultCallback error_callback = error_callback_;

  std::string failed_connection_device_id = device_id_pending_connection_;
  device_id_pending_connection_.clear();

  success_callback_.Reset();
  error_callback_.Reset();

  error_callback.Run(error_name);
  active_host_->SetActiveHostDisconnected();

  host_connection_metrics_logger_->RecordConnectionToHostResult(
      connection_to_host_result, failed_connection_device_id);

  if (error_name == NetworkConnectionHandler::kErrorConnectFailed) {
    // Only show notification if the error is kErrorConnectFailed. Other error
    // names (e.g., kErrorConnectCanceled) are a result of user interaction and
    // should not result in any error UI.
    notification_presenter_->NotifyConnectionToHostFailed();
  }
}

void TetherConnectorImpl::SetConnectionSucceeded(
    const std::string& device_id,
    const std::string& wifi_network_guid) {
  DCHECK(!device_id_pending_connection_.empty());
  DCHECK(device_id_pending_connection_ == device_id);
  DCHECK(!success_callback_.is_null());

  host_connection_metrics_logger_->RecordConnectionToHostResult(
      HostConnectionMetricsLogger::ConnectionToHostResult::
          CONNECTION_RESULT_SUCCESS,
      device_id);

  notification_presenter_->RemoveSetupRequiredNotification();

  // Save a copy of the callback before resetting it below.
  base::Closure success_callback = success_callback_;

  device_id_pending_connection_.clear();
  success_callback_.Reset();
  error_callback_.Reset();

  success_callback.Run();
  active_host_->SetActiveHostConnected(
      device_id,
      device_id_tether_network_guid_map_->GetTetherNetworkGuidForDeviceId(
          device_id),
      wifi_network_guid);
}

void TetherConnectorImpl::OnWifiConnection(
    const std::string& device_id,
    const std::string& wifi_network_guid) {
  if (device_id != device_id_pending_connection_) {
    if (wifi_network_guid.empty()) {
      PA_LOG(WARNING)
          << "Failed to connect to Wi-Fi hotspot for device with ID "
          << cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(device_id)
          << ", "
          << "but the connection to that device was canceled.";
      return;
    }

    PA_LOG(INFO) << "Connected to Wi-Fi hotspot for device with ID "
                 << cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(
                        device_id)
                 << ", but the connection to that device was canceled. "
                 << "Disconnecting.";

    // Disconnect from the Wi-Fi hotspot; otherwise, it is possible to be
    // connected to the Wi-Fi hotspot despite there being no active host. See
    // crbug.com/761171.
    wifi_hotspot_disconnector_->DisconnectFromWifiHotspot(
        wifi_network_guid, base::DoNothing(),
        base::Bind(&OnDisconnectFromWifiFailure, device_id));
    return;
  }

  if (wifi_network_guid.empty()) {
    // If the Wi-Fi network ID is empty, then the connection did not succeed.
    PA_LOG(ERROR) << "Failed to connect to the hotspot belonging to the device "
                  << "with ID "
                  << cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(
                         device_id)
                  << ".";

    SetConnectionFailed(
        NetworkConnectionHandler::kErrorConnectFailed,
        HostConnectionMetricsLogger::ConnectionToHostResult::
            CONNECTION_RESULT_FAILURE_CLIENT_CONNECTION_TIMEOUT);
    return;
  }

  SetConnectionSucceeded(device_id, wifi_network_guid);
}

HostConnectionMetricsLogger::ConnectionToHostResult
TetherConnectorImpl::GetConnectionToHostResultFromErrorCode(
    const std::string& device_id,
    ConnectTetheringOperation::HostResponseErrorCode error_code) {
  if (error_code ==
      ConnectTetheringOperation::HostResponseErrorCode::PROVISIONING_FAILED) {
    return HostConnectionMetricsLogger::ConnectionToHostResult::
        CONNECTION_RESULT_PROVISIONING_FAILED;
  }

  if (error_code ==
      ConnectTetheringOperation::HostResponseErrorCode::TETHERING_TIMEOUT) {
    const std::string tether_network_guid =
        device_id_tether_network_guid_map_->GetTetherNetworkGuidForDeviceId(
            device_id);
    if (host_scan_cache_->DoesHostRequireSetup(tether_network_guid)) {
      return HostConnectionMetricsLogger::ConnectionToHostResult::
          CONNECTION_RESULT_FAILURE_TETHERING_TIMED_OUT_FIRST_TIME_SETUP_WAS_REQUIRED;
    }

    return HostConnectionMetricsLogger::ConnectionToHostResult::
        CONNECTION_RESULT_FAILURE_TETHERING_TIMED_OUT_FIRST_TIME_SETUP_WAS_NOT_REQUIRED;
  }

  if (error_code ==
      ConnectTetheringOperation::HostResponseErrorCode::TETHERING_UNSUPPORTED) {
    return HostConnectionMetricsLogger::ConnectionToHostResult::
        CONNECTION_RESULT_FAILURE_TETHERING_UNSUPPORTED;
  }

  if (error_code ==
      ConnectTetheringOperation::HostResponseErrorCode::NO_CELL_DATA) {
    return HostConnectionMetricsLogger::ConnectionToHostResult::
        CONNECTION_RESULT_FAILURE_NO_CELL_DATA;
  }

  if (error_code == ConnectTetheringOperation::HostResponseErrorCode::
                        ENABLING_HOTSPOT_FAILED) {
    return HostConnectionMetricsLogger::ConnectionToHostResult::
        CONNECTION_RESULT_FAILURE_ENABLING_HOTSPOT_FAILED;
  }

  if (error_code == ConnectTetheringOperation::HostResponseErrorCode::
                        ENABLING_HOTSPOT_TIMEOUT) {
    return HostConnectionMetricsLogger::ConnectionToHostResult::
        CONNECTION_RESULT_FAILURE_ENABLING_HOTSPOT_TIMEOUT;
  }

  if (error_code ==
      ConnectTetheringOperation::HostResponseErrorCode::UNKNOWN_ERROR) {
    return HostConnectionMetricsLogger::ConnectionToHostResult::
        CONNECTION_RESULT_FAILURE_UNKNOWN_ERROR;
  }

  if (error_code == ConnectTetheringOperation::HostResponseErrorCode::
                        INVALID_HOTSPOT_CREDENTIALS) {
    return HostConnectionMetricsLogger::ConnectionToHostResult::
        CONNECTION_RESULT_FAILURE_INVALID_HOTSPOT_CREDENTIALS;
  }

  return HostConnectionMetricsLogger::ConnectionToHostResult::
      CONNECTION_RESULT_FAILURE_NO_RESPONSE;
}

}  // namespace tether

}  // namespace chromeos
