// Copyright 2013 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/chromeos/app_mode/kiosk_profile_loader.h"

#include "base/location.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_util.h"
#include "base/sys_info.h"
#include "base/syslog_logging.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chrome/browser/chromeos/app_mode/kiosk_app_manager.h"
#include "chrome/browser/chromeos/login/auth/chrome_login_performer.h"
#include "chrome/browser/chromeos/login/demo_mode/demo_app_launcher.h"
#include "chrome/browser/chromeos/login/ui/login_display_host_webui.h"
#include "chrome/browser/chromeos/settings/cros_settings.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chromeos/cryptohome/async_method_caller.h"
#include "chromeos/dbus/cryptohome_client.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/login/auth/auth_status_consumer.h"
#include "chromeos/login/auth/user_context.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/user_names.h"
#include "content/public/browser/browser_thread.h"
#include "google_apis/gaia/gaia_auth_util.h"

using content::BrowserThread;

namespace chromeos {

namespace {

KioskAppLaunchError::Error LoginFailureToKioskAppLaunchError(
    const AuthFailure& error) {
  switch (error.reason()) {
    case AuthFailure::COULD_NOT_MOUNT_TMPFS:
    case AuthFailure::COULD_NOT_MOUNT_CRYPTOHOME:
      return KioskAppLaunchError::UNABLE_TO_MOUNT;
    case AuthFailure::DATA_REMOVAL_FAILED:
      return KioskAppLaunchError::UNABLE_TO_REMOVE;
    case AuthFailure::USERNAME_HASH_FAILED:
      return KioskAppLaunchError::UNABLE_TO_RETRIEVE_HASH;
    default:
      NOTREACHED();
      return KioskAppLaunchError::UNABLE_TO_MOUNT;
  }
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// KioskProfileLoader::CryptohomedChecker ensures cryptohome daemon is up
// and running by issuing an IsMounted call. If the call does not go through
// and base::nullopt is not returned, it will retry after some time out and at
// the maximum five times before it gives up. Upon success, it resumes the
// launch by logging in as a kiosk mode account.

class KioskProfileLoader::CryptohomedChecker
    : public base::SupportsWeakPtr<CryptohomedChecker> {
 public:
  explicit CryptohomedChecker(KioskProfileLoader* loader)
      : loader_(loader),
        retry_count_(0) {
  }
  ~CryptohomedChecker() {}

  void StartCheck() {
    DBusThreadManager::Get()
        ->GetCryptohomeClient()
        ->WaitForServiceToBeAvailable(base::Bind(
            &CryptohomedChecker::OnServiceAvailibityChecked, AsWeakPtr()));
  }

 private:
  void Retry() {
    const int kMaxRetryTimes = 5;
    ++retry_count_;
    if (retry_count_ > kMaxRetryTimes) {
      SYSLOG(ERROR) << "Could not talk to cryptohomed for launching kiosk app.";
      ReportCheckResult(KioskAppLaunchError::CRYPTOHOMED_NOT_RUNNING);
      return;
    }

    const int retry_delay_in_milliseconds = 500 * (1 << retry_count_);
    base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
        FROM_HERE, base::BindOnce(&CryptohomedChecker::StartCheck, AsWeakPtr()),
        base::TimeDelta::FromMilliseconds(retry_delay_in_milliseconds));
  }

  void OnServiceAvailibityChecked(bool service_is_ready) {
    if (!service_is_ready) {
      Retry();
      return;
    }

    DBusThreadManager::Get()->GetCryptohomeClient()->IsMounted(
        base::Bind(&CryptohomedChecker::OnCryptohomeIsMounted, AsWeakPtr()));
  }

  void OnCryptohomeIsMounted(base::Optional<bool> is_mounted) {
    if (!is_mounted.has_value()) {
      Retry();
      return;
    }

    // Proceed only when cryptohome is not mounded or running on dev box.
    if (!is_mounted.value() || !base::SysInfo::IsRunningOnChromeOS()) {
      ReportCheckResult(KioskAppLaunchError::NONE);
    } else {
      SYSLOG(ERROR) << "Cryptohome is mounted before launching kiosk app.";
      ReportCheckResult(KioskAppLaunchError::ALREADY_MOUNTED);
    }
  }

  void ReportCheckResult(KioskAppLaunchError::Error error) {
    if (error == KioskAppLaunchError::NONE)
      loader_->LoginAsKioskAccount();
    else
      loader_->ReportLaunchResult(error);
  }

  KioskProfileLoader* loader_;
  int retry_count_;

  DISALLOW_COPY_AND_ASSIGN(CryptohomedChecker);
};


////////////////////////////////////////////////////////////////////////////////
// KioskProfileLoader

KioskProfileLoader::KioskProfileLoader(const AccountId& app_account_id,
                                       bool use_guest_mount,
                                       Delegate* delegate)
    : account_id_(app_account_id),
      use_guest_mount_(use_guest_mount),
      delegate_(delegate) {}

KioskProfileLoader::~KioskProfileLoader() {}

void KioskProfileLoader::Start() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  login_performer_.reset();
  cryptohomed_checker_.reset(new CryptohomedChecker(this));
  cryptohomed_checker_->StartCheck();
}

void KioskProfileLoader::LoginAsKioskAccount() {
  login_performer_.reset(new ChromeLoginPerformer(this));
  login_performer_->LoginAsKioskAccount(account_id_, use_guest_mount_);
}

void KioskProfileLoader::ReportLaunchResult(KioskAppLaunchError::Error error) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (error != KioskAppLaunchError::NONE) {
    delegate_->OnProfileLoadFailed(error);
  }
}

void KioskProfileLoader::OnAuthSuccess(const UserContext& user_context) {
  // LoginPerformer will delete itself.
  login_performer_->set_delegate(NULL);
  ignore_result(login_performer_.release());

  // If we are launching a demo session, we need to start MountGuest with the
  // guest username; this is because there are several places in the cros code
  // which rely on the username sent to cryptohome to be $guest. Back in Chrome
  // we switch this back to the demo user name to correctly identify this
  // user as a demo user.
  UserContext context = user_context;
  if (context.GetAccountId() == user_manager::GuestAccountId())
    context.SetAccountId(user_manager::DemoAccountId());
  UserSessionManager::GetInstance()->StartSession(
      context, UserSessionManager::PRIMARY_USER_SESSION,
      false,  // has_auth_cookies
      false,  // Start session for user.
      this);
}

void KioskProfileLoader::OnAuthFailure(const AuthFailure& error) {
  SYSLOG(ERROR) << "Kiosk auth failure: error=" << error.GetErrorString();
  KioskAppLaunchError::SaveCryptohomeFailure(error);
  ReportLaunchResult(LoginFailureToKioskAppLaunchError(error));
}

void KioskProfileLoader::WhiteListCheckFailed(const std::string& email) {
  NOTREACHED();
}

void KioskProfileLoader::PolicyLoadFailed() {
  ReportLaunchResult(KioskAppLaunchError::POLICY_LOAD_FAILED);
}

void KioskProfileLoader::SetAuthFlowOffline(bool offline) {
  NOTREACHED();
}

void KioskProfileLoader::OnProfilePrepared(Profile* profile,
                                           bool browser_launched) {
  // This object could be deleted any time after successfully reporting
  // a profile load, so invalidate the delegate now.
  UserSessionManager::GetInstance()->DelegateDeleted(this);

  delegate_->OnProfileLoaded(profile);
  ReportLaunchResult(KioskAppLaunchError::NONE);
}

}  // namespace chromeos
