// Copyright 2015 The Weave 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 "src/privet/wifi_bootstrap_manager.h"

#include <base/logging.h>
#include <base/memory/weak_ptr.h>
#include <weave/enum_to_string.h>
#include <weave/provider/network.h>
#include <weave/provider/task_runner.h>
#include <weave/provider/wifi.h>

#include "src/bind_lambda.h"
#include "src/config.h"
#include "src/privet/constants.h"

namespace weave {
namespace privet {

namespace {

const int kMonitoringWithSsidTimeoutSeconds = 15;
const int kMonitoringTimeoutSeconds = 120;
const int kBootstrapTimeoutSeconds = 600;
const int kConnectingTimeoutSeconds = 180;

const EnumToStringMap<WifiBootstrapManager::State>::Map kWifiSetupStateMap[] = {
    {WifiBootstrapManager::State::kDisabled, "disabled"},
    {WifiBootstrapManager::State::kBootstrapping, "waiting"},
    {WifiBootstrapManager::State::kMonitoring, "monitoring"},
    {WifiBootstrapManager::State::kConnecting, "connecting"},
};
}

using provider::Network;

WifiBootstrapManager::WifiBootstrapManager(Config* config,
                                           provider::TaskRunner* task_runner,
                                           provider::Network* network,
                                           provider::Wifi* wifi,
                                           CloudDelegate* gcd)
    : config_{config},
      task_runner_{task_runner},
      network_{network},
      wifi_{wifi},
      ssid_generator_{gcd, this} {
  CHECK(config_);
  CHECK(network_);
  CHECK(task_runner_);
  CHECK(wifi_);
}

void WifiBootstrapManager::Init() {
  UpdateConnectionState();
  network_->AddConnectionChangedCallback(
      base::Bind(&WifiBootstrapManager::OnConnectivityChange,
                 lifetime_weak_factory_.GetWeakPtr()));
  if (config_->GetSettings().last_configured_ssid.empty()) {
    // Give implementation some time to figure out state.
    StartMonitoring(
        base::TimeDelta::FromSeconds(kMonitoringWithSsidTimeoutSeconds));
  } else {
    StartMonitoring(base::TimeDelta::FromSeconds(kMonitoringTimeoutSeconds));
  }
}

void WifiBootstrapManager::StartBootstrapping() {
  if (network_->GetConnectionState() == Network::State::kOnline) {
    // If one of the devices we monitor for connectivity is online, we need not
    // start an AP.  For most devices, this is a situation which happens in
    // testing when we have an ethernet connection.  If you need to always
    // start an AP to bootstrap WiFi credentials, then add your WiFi interface
    // to the device whitelist.
    StartMonitoring(base::TimeDelta::FromSeconds(kMonitoringTimeoutSeconds));
    return;
  }

  UpdateState(State::kBootstrapping);
  if (!config_->GetSettings().last_configured_ssid.empty()) {
    // If we have been configured before, we'd like to periodically take down
    // our AP and find out if we can connect again.  Many kinds of failures are
    // transient, and having an AP up prohibits us from connecting as a client.
    task_runner_->PostDelayedTask(
        FROM_HERE, base::Bind(&WifiBootstrapManager::OnBootstrapTimeout,
                              tasks_weak_factory_.GetWeakPtr()),
        base::TimeDelta::FromSeconds(kBootstrapTimeoutSeconds));
  }
  // TODO(vitalybuka): Add SSID probing.
  privet_ssid_ = GenerateSsid();
  CHECK(!privet_ssid_.empty());

  VLOG(1) << "Starting AP with SSID: " << privet_ssid_;
  wifi_->StartAccessPoint(privet_ssid_);
}

void WifiBootstrapManager::EndBootstrapping() {
  VLOG(1) << "Stopping AP";
  wifi_->StopAccessPoint();
  privet_ssid_.clear();
}

void WifiBootstrapManager::StartConnecting(const std::string& ssid,
                                           const std::string& passphrase) {
  VLOG(1) << "Attempting connect to SSID:" << ssid;
  UpdateState(State::kConnecting);
  task_runner_->PostDelayedTask(
      FROM_HERE, base::Bind(&WifiBootstrapManager::OnConnectTimeout,
                            tasks_weak_factory_.GetWeakPtr()),
      base::TimeDelta::FromSeconds(kConnectingTimeoutSeconds));
  wifi_->Connect(ssid, passphrase,
                 base::Bind(&WifiBootstrapManager::OnConnectDone,
                            tasks_weak_factory_.GetWeakPtr(), ssid));
}

void WifiBootstrapManager::EndConnecting() {}

void WifiBootstrapManager::StartMonitoring(const base::TimeDelta& timeout) {
  monitor_until_ = {};
  ContinueMonitoring(timeout);
}

void WifiBootstrapManager::ContinueMonitoring(const base::TimeDelta& timeout) {
  VLOG(1) << "Monitoring connectivity.";
  // We already have a callback in place with |network_| to update our
  // connectivity state.  See OnConnectivityChange().
  UpdateState(State::kMonitoring);

  if (network_->GetConnectionState() == Network::State::kOnline) {
    monitor_until_ = {};
  } else {
    if (monitor_until_.is_null()) {
      monitor_until_ = base::Time::Now() + timeout;
      VLOG(2) << "Waiting for connection until: " << monitor_until_;
    }

    // Schedule timeout timer taking into account already offline time.
    task_runner_->PostDelayedTask(
        FROM_HERE, base::Bind(&WifiBootstrapManager::OnMonitorTimeout,
                              tasks_weak_factory_.GetWeakPtr()),
        monitor_until_ - base::Time::Now());
  }
}

void WifiBootstrapManager::EndMonitoring() {}

void WifiBootstrapManager::UpdateState(State new_state) {
  VLOG(3) << "Switching state from " << EnumToString(state_) << " to "
          << EnumToString(new_state);
  // Abort irrelevant tasks.
  tasks_weak_factory_.InvalidateWeakPtrs();

  switch (state_) {
    case State::kDisabled:
      break;
    case State::kBootstrapping:
      EndBootstrapping();
      break;
    case State::kMonitoring:
      EndMonitoring();
      break;
    case State::kConnecting:
      EndConnecting();
      break;
  }

  state_ = new_state;
}

std::string WifiBootstrapManager::GenerateSsid() const {
  const std::string& ssid = config_->GetSettings().test_privet_ssid;
  return ssid.empty() ? ssid_generator_.GenerateSsid() : ssid;
}

const ConnectionState& WifiBootstrapManager::GetConnectionState() const {
  return connection_state_;
}

const SetupState& WifiBootstrapManager::GetSetupState() const {
  return setup_state_;
}

bool WifiBootstrapManager::ConfigureCredentials(const std::string& ssid,
                                                const std::string& passphrase,
                                                ErrorPtr* error) {
  setup_state_ = SetupState{SetupState::kInProgress};
  // Since we are changing network, we need to let the web server send out the
  // response to the HTTP request leading to this action. So, we are waiting
  // a bit before mocking with network set up.
  task_runner_->PostDelayedTask(
      FROM_HERE, base::Bind(&WifiBootstrapManager::StartConnecting,
                            tasks_weak_factory_.GetWeakPtr(), ssid, passphrase),
      base::TimeDelta::FromSeconds(1));
  return true;
}

std::string WifiBootstrapManager::GetCurrentlyConnectedSsid() const {
  // TODO(vitalybuka): Get from shill, if possible.
  return config_->GetSettings().last_configured_ssid;
}

std::string WifiBootstrapManager::GetHostedSsid() const {
  return privet_ssid_;
}

std::set<WifiType> WifiBootstrapManager::GetTypes() const {
  std::set<WifiType> result;
  if (wifi_->IsWifi24Supported())
    result.insert(WifiType::kWifi24);
  if (wifi_->IsWifi50Supported())
    result.insert(WifiType::kWifi50);
  return result;
}

void WifiBootstrapManager::OnConnectDone(const std::string& ssid,
                                         ErrorPtr error) {
  if (error) {
    Error::AddTo(&error, FROM_HERE, errors::kInvalidState,
                 "Failed to connect to provided network");
    setup_state_ = SetupState{std::move(error)};
    return StartBootstrapping();
  }
  VLOG(1) << "Wifi was connected successfully";
  Config::Transaction change{config_};
  change.set_last_configured_ssid(ssid);
  change.Commit();
  setup_state_ = SetupState{SetupState::kSuccess};
  StartMonitoring(base::TimeDelta::FromSeconds(kMonitoringTimeoutSeconds));
}

void WifiBootstrapManager::OnConnectTimeout() {
  ErrorPtr error;
  Error::AddTo(&error, FROM_HERE, errors::kInvalidState,
               "Timeout connecting to provided network");
  setup_state_ = SetupState{std::move(error)};
  return StartBootstrapping();
}

void WifiBootstrapManager::OnBootstrapTimeout() {
  VLOG(1) << "Bootstrapping has timed out.";
  StartMonitoring(base::TimeDelta::FromSeconds(kMonitoringTimeoutSeconds));
}

void WifiBootstrapManager::OnConnectivityChange() {
  UpdateConnectionState();

  if (state_ == State::kMonitoring ||
      (state_ != State::kDisabled &&
       network_->GetConnectionState() == Network::State::kOnline)) {
    ContinueMonitoring(base::TimeDelta::FromSeconds(kMonitoringTimeoutSeconds));
  }
}

void WifiBootstrapManager::OnMonitorTimeout() {
  VLOG(1) << "Spent too long offline. Entering bootstrap mode.";
  // TODO(wiley) Retrieve relevant errors from shill.
  StartBootstrapping();
}

void WifiBootstrapManager::UpdateConnectionState() {
  connection_state_ = ConnectionState{ConnectionState::kUnconfigured};
  Network::State service_state{network_->GetConnectionState()};
  VLOG(3) << "New network state: " << EnumToString(service_state);

  // TODO: Make it true wifi state, currently it's rather online state.
  if (service_state != Network::State::kOnline &&
      config_->GetSettings().last_configured_ssid.empty()) {
    return;
  }

  switch (service_state) {
    case Network::State::kOffline:
      connection_state_ = ConnectionState{ConnectionState::kOffline};
      return;
    case Network::State::kError: {
      // TODO(wiley) Pull error information from somewhere.
      ErrorPtr error;
      Error::AddTo(&error, FROM_HERE, errors::kInvalidState,
                   "Unknown WiFi error");
      connection_state_ = ConnectionState{std::move(error)};
      return;
    }
    case Network::State::kConnecting:
      connection_state_ = ConnectionState{ConnectionState::kConnecting};
      return;
    case Network::State::kOnline:
      connection_state_ = ConnectionState{ConnectionState::kOnline};
      return;
  }
  ErrorPtr error;
  Error::AddToPrintf(&error, FROM_HERE, errors::kInvalidState,
                     "Unknown network state: %s",
                     EnumToString(service_state).c_str());
  connection_state_ = ConnectionState{std::move(error)};
}

}  // namespace privet

template <>
LIBWEAVE_EXPORT
EnumToStringMap<privet::WifiBootstrapManager::State>::EnumToStringMap()
    : EnumToStringMap(privet::kWifiSetupStateMap) {}

}  // namespace weave