// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/app_mode/kiosk_controller_impl.h"

#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/login_accelerators.h"
#include "base/check.h"
#include "base/check_deref.h"
#include "base/check_is_test.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/sequence_checker.h"
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/ash/app_mode/app_launch_utils.h"
#include "chrome/browser/ash/app_mode/arcvm_app/kiosk_arcvm_app_data.h"
#include "chrome/browser/ash/app_mode/arcvm_app/kiosk_arcvm_app_manager.h"
#include "chrome/browser/ash/app_mode/crash_recovery_launcher.h"
#include "chrome/browser/ash/app_mode/isolated_web_app/kiosk_iwa_data.h"
#include "chrome/browser/ash/app_mode/isolated_web_app/kiosk_iwa_manager.h"
#include "chrome/browser/ash/app_mode/kiosk_app.h"
#include "chrome/browser/ash/app_mode/kiosk_app_launch_error.h"
#include "chrome/browser/ash/app_mode/kiosk_app_manager_base.h"
#include "chrome/browser/ash/app_mode/kiosk_app_types.h"
#include "chrome/browser/ash/app_mode/kiosk_chrome_app_manager.h"
#include "chrome/browser/ash/app_mode/kiosk_controller.h"
#include "chrome/browser/ash/app_mode/kiosk_system_session.h"
#include "chrome/browser/ash/app_mode/web_app/kiosk_web_app_data.h"
#include "chrome/browser/ash/app_mode/web_app/kiosk_web_app_manager.h"
#include "chrome/browser/ash/login/app_mode/kiosk_launch_controller.h"
#include "chrome/browser/ash/login/screens/app_launch_splash_screen.h"
#include "chrome/browser/ash/login/wizard_controller.h"
#include "chrome/browser/ash/policy/core/device_local_account.h"
#include "chrome/browser/chromeos/app_mode/kiosk_app_level_logs_manager_wrapper.h"
#include "chrome/browser/ui/ash/login/login_display_host.h"
#include "chrome/common/chrome_switches.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "components/account_id/account_id.h"
#include "components/prefs/pref_service.h"
#include "components/session_manager/core/session_manager.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "ui/ozone/public/input_controller.h"
#include "ui/ozone/public/ozone_platform.h"
#include "ui/wm/core/wm_core_switches.h"

namespace ash {

namespace {

std::optional<KioskApp> WebAppById(const KioskWebAppManager& manager,
                                   const AccountId& account_id) {
  const KioskWebAppData* data = manager.GetAppByAccountId(account_id);
  if (!data) {
    return std::nullopt;
  }
  return KioskApp(KioskAppId::ForWebApp(account_id), data->name(), data->icon(),
                  data->install_url());
}

std::optional<KioskApp> ChromeAppById(const KioskChromeAppManager& manager,
                                      std::string_view chrome_app_id) {
  auto manager_app = manager.GetApp(std::string(chrome_app_id));
  if (!manager_app.has_value()) {
    return std::nullopt;
  }
  return KioskApp(
      KioskAppId::ForChromeApp(chrome_app_id, manager_app->account_id),
      manager_app->name, manager_app->icon);
}

std::optional<KioskApp> IsolatedWebAppById(const KioskIwaManager& manager,
                                           const AccountId& account_id) {
  const KioskIwaData* app_data = manager.GetApp(account_id);

  if (!app_data) {
    return std::nullopt;
  }

  return KioskApp(KioskAppId::ForIsolatedWebApp(account_id), app_data->name(),
                  app_data->icon());
}

std::optional<KioskApp> ArcvmAppById(const KioskArcvmAppManager& manager,
                                     const AccountId& account_id) {
  const KioskArcvmAppData* data = manager.GetAppByAccountId(account_id);
  if (!data) {
    return std::nullopt;
  }
  return KioskApp(KioskAppId::ForArcvmApp(account_id), data->name(),
                  data->icon());
}

KioskApp EmptyKioskApp(const KioskAppId& app_id) {
  switch (app_id.type) {
    case KioskAppType::kChromeApp:
    case KioskAppType::kIsolatedWebApp:
    case KioskAppType::kArcvmApp:
      return KioskApp{app_id,
                      /*name=*/"",
                      /*icon=*/gfx::ImageSkia(),
                      /*url=*/std::nullopt};
    case KioskAppType::kWebApp:
      return KioskApp{app_id,
                      /*name=*/"",
                      /*icon=*/gfx::ImageSkia(),
                      /*url=*/GURL()};
  }
  NOTREACHED();
}

}  // namespace

KioskControllerImpl::KioskControllerImpl(
    PrefService* local_state,
    const policy::PolicyService* policy_service,
    scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory,
    user_manager::UserManager* user_manager)
    : local_state_(CHECK_DEREF(local_state)),
      policy_service_(CHECK_DEREF(policy_service)),
      cryptohome_remover_(local_state),
      iwa_manager_(CHECK_DEREF(local_state), &cryptohome_remover_),
      web_app_manager_(local_state,
                       shared_url_loader_factory,
                       &cryptohome_remover_),
      chrome_app_manager_(local_state,
                          shared_url_loader_factory,
                          &cryptohome_remover_),
      arcvm_app_manager_(local_state, &cryptohome_remover_) {
  user_manager_observation_.Observe(user_manager);
}

KioskControllerImpl::~KioskControllerImpl() = default;

std::vector<KioskApp> KioskControllerImpl::GetApps() const {
  std::vector<KioskApp> apps;
  AppendWebApps(apps);
  AppendChromeApps(apps);
  AppendIsolatedWebApps(apps);
  AppendArcvmApps(apps);
  return apps;
}

std::optional<KioskApp> KioskControllerImpl::GetAppById(
    const KioskAppId& app_id) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  switch (app_id.type) {
    case KioskAppType::kWebApp:
      return WebAppById(web_app_manager_, app_id.account_id);
    case KioskAppType::kChromeApp:
      return ChromeAppById(chrome_app_manager_, app_id.app_id.value());
    case KioskAppType::kIsolatedWebApp:
      return IsolatedWebAppById(iwa_manager_, app_id.account_id);
    case KioskAppType::kArcvmApp:
      return ArcvmAppById(arcvm_app_manager_, app_id.account_id);
  }
}

std::optional<KioskApp> KioskControllerImpl::GetAutoLaunchApp() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (const auto& web_account_id = web_app_manager_.GetAutoLaunchAccountId();
      web_account_id.is_valid()) {
    return WebAppById(web_app_manager_, web_account_id);
  }

  if (const auto& chrome_app_id = chrome_app_manager_.GetAutoLaunchApp();
      !chrome_app_id.empty()) {
    return ChromeAppById(chrome_app_manager_, chrome_app_id);
  }

  if (const auto& iwa_account_id = iwa_manager_.GetAutoLaunchAccountId();
      iwa_account_id.has_value()) {
    return IsolatedWebAppById(iwa_manager_, *iwa_account_id);
  }

  if (const auto& arc_account_id = arcvm_app_manager_.GetAutoLaunchAccountId();
      arc_account_id.is_valid()) {
    return ArcvmAppById(arcvm_app_manager_, arc_account_id);
  }

  return std::nullopt;
}

void KioskControllerImpl::InitializeKioskSystemSession(
    const KioskAppId& kiosk_app_id,
    Profile* profile,
    const std::optional<webapps::AppId>& app_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  CHECK(!system_session_.has_value())
      << "KioskSystemSession is already initialized";

  system_session_.emplace(local_state_.get(), profile, kiosk_app_id, app_id);

  switch (kiosk_app_id.type) {
    case KioskAppType::kWebApp:
      web_app_manager_.OnKioskSessionStarted(kiosk_app_id);
      break;
    case KioskAppType::kChromeApp:
      chrome_app_manager_.OnKioskSessionStarted(kiosk_app_id);
      break;
    case KioskAppType::kIsolatedWebApp:
      iwa_manager_.OnKioskSessionStarted(kiosk_app_id);
      break;
    case KioskAppType::kArcvmApp:
      arcvm_app_manager_.OnKioskSessionStarted(kiosk_app_id);
      break;
  }
}

void KioskControllerImpl::StartSession(const KioskAppId& app_id,
                                       bool is_auto_launch,
                                       LoginDisplayHost* host,
                                       AppLaunchSplashScreen* splash_screen) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  CHECK_EQ(launch_controller_, nullptr);
  CHECK(!system_session_.has_value());

  std::optional<KioskApp> app_maybe = GetAppById(app_id);
  // TODO(b/306117645) change to CHECK and drop `value_or`.
  DUMP_WILL_BE_CHECK(app_maybe.has_value());
  KioskApp app = std::move(app_maybe).value_or(EmptyKioskApp(app_id));

  kiosk_log_manager_wrapper_ =
      std::make_unique<chromeos::KioskAppLevelLogsManagerWrapper>(app_id);

  launch_controller_ = std::make_unique<KioskLaunchController>(
      &local_state_.get(), &policy_service_.get(), host,
      /*app_launched_callback=*/
      base::BindOnce(&KioskControllerImpl::OnAppLaunched,
                     base::Unretained(this)),
      /*splash_screen=*/splash_screen,
      /*done_callback=*/
      base::BindOnce(&KioskControllerImpl::OnLaunchComplete,
                     base::Unretained(this)));
  launch_controller_->Start(std::move(app), is_auto_launch);
}

void KioskControllerImpl::StartSessionAfterCrash(const KioskAppId& app,
                                                 Profile* profile) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          ash::switches::kPreventKioskAutolaunchForTesting)) {
    LOG(WARNING) << "Skipping to launch " << app << " for "
                 << profile->GetPath() << " due to --"
                 << ash::switches::kPreventKioskAutolaunchForTesting
                 << " flag.";
    return;
  }

  kiosk_log_manager_wrapper_ =
      std::make_unique<chromeos::KioskAppLevelLogsManagerWrapper>(profile, app);

  crash_recovery_launcher_ = std::make_unique<CrashRecoveryLauncher>(
      &local_state_.get(), CHECK_DEREF(profile), app);
  crash_recovery_launcher_->Start(
      base::BindOnce(&KioskControllerImpl::OnLaunchCompleteAfterCrash,
                     // Safe since `this` owns the `crash_recovery_launcher_`.
                     base::Unretained(this), app, profile));
}

bool KioskControllerImpl::IsSessionStarting() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return launch_controller_ != nullptr || crash_recovery_launcher_ != nullptr;
}

void KioskControllerImpl::CancelSessionStart() {
  DeleteLaunchControllerAsync();
}

void KioskControllerImpl::AddProfileLoadFailedObserver(
    KioskProfileLoadFailedObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  CHECK_NE(launch_controller_, nullptr);
  launch_controller_->AddKioskProfileLoadFailedObserver(observer);
}

void KioskControllerImpl::RemoveProfileLoadFailedObserver(
    KioskProfileLoadFailedObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (launch_controller_) {
    launch_controller_->RemoveKioskProfileLoadFailedObserver(observer);
  }
}

bool KioskControllerImpl::HandleAccelerator(LoginAcceleratorAction action) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  return launch_controller_ && launch_controller_->HandleAccelerator(action);
}

void KioskControllerImpl::OnGuestAdded(
    content::WebContents* guest_web_contents) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (system_session_.has_value()) {
    system_session_->OnGuestAdded(guest_web_contents);
  }
}

KioskSystemSession* KioskControllerImpl::GetKioskSystemSession() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!system_session_.has_value()) {
    return nullptr;
  }
  return &system_session_.value();
}

void KioskControllerImpl::RemoveObsoleteCryptohomes() {
  cryptohome_remover_.RemoveObsoleteCryptohomes();
}

void KioskControllerImpl::OnUserLoggedIn(const user_manager::User& user) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!user.IsKioskType()) {
    return;
  }

  const AccountId& kiosk_app_account_id = user.GetAccountId();

  // TODO(bartfab): Add KioskAppUsers to the users_ list and keep metadata like
  // the kiosk_app_id in these objects, removing the need to re-parse the
  // device-local account list here to extract the kiosk_app_id.
  const std::vector<policy::DeviceLocalAccount> device_local_accounts =
      policy::GetDeviceLocalAccounts(CrosSettings::Get());
  const auto account = std::ranges::find(device_local_accounts,
                                         kiosk_app_account_id.GetUserEmail(),
                                         &policy::DeviceLocalAccount::user_id);
  std::string kiosk_app_id;
  if (account != device_local_accounts.end()) {
    kiosk_app_id = account->kiosk_app_id;
  } else {
    LOG(ERROR) << "Logged into nonexistent kiosk-app account: "
               << kiosk_app_account_id.GetUserEmail();
    CHECK_IS_TEST();
  }

  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
  command_line->AppendSwitch(::switches::kForceAppMode);

  // Disables installation of preinstalled apps in kiosk sessions as
  // `UserManager::Observer::OnUserLoggedIn` is called before `Profile` creation
  // and `WebAppProvider::Start`.
  // TODO(crbug.com/385072112): Replace cmd line switch with proper filtering.
  command_line->AppendSwitch(::switches::kDisableDefaultApps);

  // This happens in Web kiosks.
  if (!kiosk_app_id.empty()) {
    command_line->AppendSwitchASCII(::switches::kAppId, kiosk_app_id);
  }

  // Disable window animation since kiosk app runs in a single full screen
  // window and window animation causes start-up janks.
  command_line->AppendSwitch(wm::switches::kWindowAnimationsDisabled);

  // If restoring auto-launched kiosk session, make sure the app is marked
  // as auto-launched.
  if (command_line->HasSwitch(switches::kLoginUser) &&
      command_line->HasSwitch(switches::kAppAutoLaunched) &&
      !kiosk_app_id.empty()) {
    chrome_app_manager_.SetAppWasAutoLaunchedWithZeroDelay(kiosk_app_id);
  }
}

void KioskControllerImpl::OnAppLaunched(
    const KioskAppId& kiosk_app_id,
    Profile* profile,
    const std::optional<webapps::AppId>& app_id) {
  InitializeKioskSystemSession(kiosk_app_id, profile, app_id);
}

void KioskControllerImpl::OnLaunchComplete(KioskAppLaunchError::Error error) {
  if (auto* input_controller =
          ui::OzonePlatform::GetInstance()->GetInputController()) {
    input_controller->DisableKeyboardImposterCheck();
  }
  // Delete the launcher so it doesn't end up with dangling references.
  DeleteLaunchControllerAsync();
}

void KioskControllerImpl::DeleteLaunchControllerAsync() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Deleted asynchronously since this method is invoked in a callback called by
  // the launcher itself, but don't use `DeleteSoon` to prevent the launcher
  // from outliving `this`.
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&KioskControllerImpl::DeleteLaunchController,
                                weak_factory_.GetWeakPtr()));
}

void KioskControllerImpl::DeleteLaunchController() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  launch_controller_.reset();
}

void KioskControllerImpl::OnLaunchCompleteAfterCrash(
    const KioskAppId& app,
    Profile* profile,
    bool success,
    const std::optional<webapps::AppId>& app_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (success) {
    if (auto* input_controller =
            ui::OzonePlatform::GetInstance()->GetInputController()) {
      input_controller->DisableKeyboardImposterCheck();
    }
    InitializeKioskSystemSession(app, profile, app_id);
  } else {
    session_manager::SessionManager::Get()->RequestSignOut();
  }

  // Delete launcher so it doesn't end up with dangling references.
  crash_recovery_launcher_.reset();
}

void KioskControllerImpl::AppendWebApps(std::vector<KioskApp>& apps) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  for (const KioskAppManagerBase::App& web_app : web_app_manager_.GetApps()) {
    apps.emplace_back(KioskAppId::ForWebApp(web_app.account_id), web_app.name,
                      web_app.icon, web_app.url);
  }
}

void KioskControllerImpl::AppendChromeApps(std::vector<KioskApp>& apps) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  for (const KioskAppManagerBase::App& chrome_app :
       chrome_app_manager_.GetApps()) {
    apps.emplace_back(
        KioskAppId::ForChromeApp(chrome_app.app_id, chrome_app.account_id),
        chrome_app.name, chrome_app.icon);
  }
}

void KioskControllerImpl::AppendIsolatedWebApps(
    std::vector<KioskApp>& apps) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  for (const KioskAppManagerBase::App& iwa_app : iwa_manager_.GetApps()) {
    apps.emplace_back(KioskAppId::ForIsolatedWebApp(iwa_app.account_id),
                      iwa_app.name, iwa_app.icon);
  }
}

void KioskControllerImpl::AppendArcvmApps(std::vector<KioskApp>& apps) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  for (const KioskAppManagerBase::App& iwa_app : arcvm_app_manager_.GetApps()) {
    apps.emplace_back(KioskAppId::ForArcvmApp(iwa_app.account_id), iwa_app.name,
                      iwa_app.icon);
  }
}

}  // namespace ash
