// Copyright (c) 2011 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 "chrome/browser/policy/device_token_fetcher.h"

#include <algorithm>

#include "base/message_loop.h"
#include "chrome/browser/policy/cloud_policy_cache_base.h"
#include "chrome/browser/policy/device_management_service.h"
#include "chrome/browser/policy/proto/device_management_constants.h"
#include "chrome/browser/policy/proto/device_management_local.pb.h"

namespace {

// Retry after 5 minutes (with exponential backoff) after token fetch errors.
const int64 kTokenFetchErrorDelayMilliseconds = 5 * 60 * 1000;
// Retry after max 3 hours after token fetch errors.
const int64 kTokenFetchErrorMaxDelayMilliseconds = 3 * 60 * 60 * 1000;
// For unmanaged devices, check once per day whether they're still unmanaged.
const int64 kUnmanagedDeviceRefreshRateMilliseconds = 24 * 60 * 60 * 1000;

}  // namespace

namespace policy {

namespace em = enterprise_management;

DeviceTokenFetcher::DeviceTokenFetcher(
    DeviceManagementService* service,
    CloudPolicyCacheBase* cache,
    PolicyNotifier* notifier)
    : ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) {
  Initialize(service,
             cache,
             notifier,
             kTokenFetchErrorDelayMilliseconds,
             kTokenFetchErrorMaxDelayMilliseconds,
             kUnmanagedDeviceRefreshRateMilliseconds);
}

DeviceTokenFetcher::DeviceTokenFetcher(
    DeviceManagementService* service,
    CloudPolicyCacheBase* cache,
    PolicyNotifier* notifier,
    int64 token_fetch_error_delay_ms,
    int64 token_fetch_error_max_delay_ms,
    int64 unmanaged_device_refresh_rate_ms)
    : ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) {
  Initialize(service,
             cache,
             notifier,
             token_fetch_error_delay_ms,
             token_fetch_error_max_delay_ms,
             unmanaged_device_refresh_rate_ms);
}

DeviceTokenFetcher::~DeviceTokenFetcher() {
  CancelRetryTask();
}

void DeviceTokenFetcher::FetchToken(
    const std::string& auth_token,
    const std::string& device_id,
    em::DeviceRegisterRequest_Type policy_type,
    const std::string& machine_id,
    const std::string& machine_model) {
  SetState(STATE_INACTIVE);
  auth_token_ = auth_token;
  device_id_ = device_id;
  policy_type_ = policy_type;
  machine_id_ = machine_id;
  machine_model_ = machine_model;
  FetchTokenInternal();
}

void DeviceTokenFetcher::FetchTokenInternal() {
  DCHECK(state_ != STATE_TOKEN_AVAILABLE);
  if (auth_token_.empty() || device_id_.empty()) {
    // Maybe this device is unmanaged, just exit. The CloudPolicyController
    // will call FetchToken() again if something changes.
    return;
  }
  // Construct a new backend, which will discard any previous requests.
  backend_.reset(service_->CreateBackend());
  em::DeviceRegisterRequest request;
  request.set_type(policy_type_);
  if (!machine_id_.empty())
    request.set_machine_id(machine_id_);
  if (!machine_model_.empty())
    request.set_machine_model(machine_model_);
  backend_->ProcessRegisterRequest(auth_token_, device_id_, request, this);
}

void DeviceTokenFetcher::SetUnmanagedState() {
  // The call to |cache_->SetUnmanaged()| has to happen first because it sets
  // the timestamp that |SetState()| needs to determine the correct refresh
  // time.
  cache_->SetUnmanaged();
  SetState(STATE_UNMANAGED);
}

const std::string& DeviceTokenFetcher::GetDeviceToken() {
  return device_token_;
}

void DeviceTokenFetcher::StopAutoRetry() {
  CancelRetryTask();
  backend_.reset();
  device_token_.clear();
  auth_token_.clear();
  device_id_.clear();
}

void DeviceTokenFetcher::AddObserver(DeviceTokenFetcher::Observer* observer) {
  observer_list_.AddObserver(observer);
}

void DeviceTokenFetcher::RemoveObserver(
    DeviceTokenFetcher::Observer* observer) {
  observer_list_.RemoveObserver(observer);
}

void DeviceTokenFetcher::HandleRegisterResponse(
    const em::DeviceRegisterResponse& response) {
  if (response.has_device_management_token()) {
    device_token_ = response.device_management_token();
    SetState(STATE_TOKEN_AVAILABLE);
  } else {
    NOTREACHED();
    SetState(STATE_ERROR);
  }
}

void DeviceTokenFetcher::OnError(DeviceManagementBackend::ErrorCode code) {
  switch (code) {
    case DeviceManagementBackend::kErrorServiceManagementNotSupported:
      cache_->SetUnmanaged();
      SetState(STATE_UNMANAGED);
      break;
    case DeviceManagementBackend::kErrorRequestFailed:
    case DeviceManagementBackend::kErrorTemporaryUnavailable:
    case DeviceManagementBackend::kErrorServiceDeviceNotFound:
      SetState(STATE_TEMPORARY_ERROR);
      break;
    case DeviceManagementBackend::kErrorServiceManagementTokenInvalid:
      // Most probably the GAIA auth cookie has expired. We can not do anything
      // until the user logs-in again.
      SetState(STATE_BAD_AUTH);
      break;
    default:
      SetState(STATE_ERROR);
  }
}

void DeviceTokenFetcher::Initialize(DeviceManagementService* service,
                                    CloudPolicyCacheBase* cache,
                                    PolicyNotifier* notifier,
                                    int64 token_fetch_error_delay_ms,
                                    int64 token_fetch_error_max_delay_ms,
                                    int64 unmanaged_device_refresh_rate_ms) {
  service_ = service;
  cache_ = cache;
  notifier_ = notifier;
  token_fetch_error_delay_ms_ = token_fetch_error_delay_ms;
  token_fetch_error_max_delay_ms_ = token_fetch_error_max_delay_ms;
  effective_token_fetch_error_delay_ms_ = token_fetch_error_delay_ms;
  unmanaged_device_refresh_rate_ms_ = unmanaged_device_refresh_rate_ms;
  state_ = STATE_INACTIVE;
  retry_task_ = NULL;

  if (cache_->is_unmanaged())
    SetState(STATE_UNMANAGED);
}

void DeviceTokenFetcher::SetState(FetcherState state) {
  state_ = state;
  if (state_ != STATE_ERROR)
    effective_token_fetch_error_delay_ms_ = token_fetch_error_delay_ms_;

  base::Time delayed_work_at;
  switch (state_) {
    case STATE_INACTIVE:
      device_token_.clear();
      auth_token_.clear();
      device_id_.clear();
      notifier_->Inform(CloudPolicySubsystem::UNENROLLED,
                        CloudPolicySubsystem::NO_DETAILS,
                        PolicyNotifier::TOKEN_FETCHER);
      break;
    case STATE_TOKEN_AVAILABLE:
      FOR_EACH_OBSERVER(Observer, observer_list_, OnDeviceTokenAvailable());
      notifier_->Inform(CloudPolicySubsystem::SUCCESS,
                        CloudPolicySubsystem::NO_DETAILS,
                        PolicyNotifier::TOKEN_FETCHER);
      break;
    case STATE_UNMANAGED:
      delayed_work_at = cache_->last_policy_refresh_time() +
          base::TimeDelta::FromMilliseconds(unmanaged_device_refresh_rate_ms_);
      notifier_->Inform(CloudPolicySubsystem::UNMANAGED,
                        CloudPolicySubsystem::NO_DETAILS,
                        PolicyNotifier::TOKEN_FETCHER);
      break;
    case STATE_TEMPORARY_ERROR:
      delayed_work_at = base::Time::Now() +
          base::TimeDelta::FromMilliseconds(
              effective_token_fetch_error_delay_ms_);
      effective_token_fetch_error_delay_ms_ =
          std::min(effective_token_fetch_error_delay_ms_ * 2,
                   token_fetch_error_max_delay_ms_);
      notifier_->Inform(CloudPolicySubsystem::NETWORK_ERROR,
                        CloudPolicySubsystem::DMTOKEN_NETWORK_ERROR,
                        PolicyNotifier::TOKEN_FETCHER);
      break;
    case STATE_ERROR:
      effective_token_fetch_error_delay_ms_ = token_fetch_error_max_delay_ms_;
      delayed_work_at = base::Time::Now() +
          base::TimeDelta::FromMilliseconds(
              effective_token_fetch_error_delay_ms_);
      notifier_->Inform(CloudPolicySubsystem::NETWORK_ERROR,
                        CloudPolicySubsystem::DMTOKEN_NETWORK_ERROR,
                        PolicyNotifier::TOKEN_FETCHER);
      break;
    case STATE_BAD_AUTH:
      // Can't do anything, need to wait for new credentials.
      notifier_->Inform(CloudPolicySubsystem::BAD_GAIA_TOKEN,
                        CloudPolicySubsystem::NO_DETAILS,
                        PolicyNotifier::TOKEN_FETCHER);
      break;
  }

  CancelRetryTask();
  if (!delayed_work_at.is_null()) {
    base::Time now(base::Time::Now());
    int64 delay = std::max<int64>((delayed_work_at - now).InMilliseconds(), 0);
    retry_task_ = method_factory_.NewRunnableMethod(
            &DeviceTokenFetcher::ExecuteRetryTask);
    MessageLoop::current()->PostDelayedTask(FROM_HERE, retry_task_,
                                            delay);
  }
}

void DeviceTokenFetcher::ExecuteRetryTask() {
  DCHECK(retry_task_);
  retry_task_ = NULL;

  switch (state_) {
    case STATE_INACTIVE:
    case STATE_TOKEN_AVAILABLE:
      break;
    case STATE_UNMANAGED:
    case STATE_ERROR:
    case STATE_TEMPORARY_ERROR:
    case STATE_BAD_AUTH:
      FetchTokenInternal();
      break;
  }
}

void DeviceTokenFetcher::CancelRetryTask() {
  if (retry_task_) {
    retry_task_->Cancel();
    retry_task_ = NULL;
  }
}

}  // namespace policy