// Copyright 2013 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/startup_app_launcher.h"

#include <memory>
#include <string>
#include <utility>

#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/syslog_logging.h"
#include "chrome/browser/ash/app_mode/kiosk_app_launch_error.h"
#include "chrome/browser/ash/app_mode/kiosk_app_launcher.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/chromeos/app_mode/chrome_kiosk_app_installer.h"
#include "chrome/browser/chromeos/app_mode/chrome_kiosk_app_launcher.h"
#include "chrome/browser/profiles/profile.h"
#include "components/crx_file/id_util.h"
#include "net/base/backoff_entry.h"

using chromeos::ChromeKioskAppInstaller;
using chromeos::ChromeKioskAppLauncher;

namespace ash {

namespace {

const int kMaxLaunchAttempt = 5;

// Reduced backoff policy for extension downloader while Kiosk is launching.
const net::BackoffEntry::Policy kKioskLaunchExtensionBackoffPolicy = {
    .num_errors_to_ignore = 0,
    .initial_delay_ms = 2000,
    .multiply_factor = 2,
    .jitter_factor = 0.1,
    .maximum_backoff_ms = 3000,
    .entry_lifetime_ms = -1,
    .always_use_initial_delay = false,
};

}  // namespace

StartupAppLauncher::StartupAppLauncher(
    Profile* profile,
    const std::string& app_id,
    bool should_skip_install,
    StartupAppLauncher::NetworkDelegate* network_delegate)
    : KioskAppLauncher(network_delegate),
      profile_(profile),
      app_id_(app_id),
      should_skip_install_(should_skip_install) {
  CHECK(profile_);
  DCHECK(crx_file::id_util::IdIsValid(app_id_));

  // Reduce extension downloader retry backoff to avoid waiting on splash screen
  // for a long time.
  KioskChromeAppManager::Get()->SetExtensionDownloaderBackoffPolicy(
      kKioskLaunchExtensionBackoffPolicy);
}

StartupAppLauncher::~StartupAppLauncher() {
  // Restore to default extension downloader backoff policy.
  KioskChromeAppManager::Get()->SetExtensionDownloaderBackoffPolicy(
      std::nullopt);
}

void StartupAppLauncher::AddObserver(KioskAppLauncher::Observer* observer) {
  observers_.AddObserver(observer);
}

void StartupAppLauncher::RemoveObserver(KioskAppLauncher::Observer* observer) {
  observers_.RemoveObserver(observer);
}

void StartupAppLauncher::Initialize() {
  CHECK(state_ != LaunchState::kReadyToLaunch &&
        state_ != LaunchState::kWaitingForWindow &&
        state_ != LaunchState::kLaunchSucceeded);

  if (should_skip_install_) {
    OnInstallSuccess();
    return;
  }

  // Update the offline enabled crx cache if the network is ready;
  // or just install the app.
  if (delegate_->IsNetworkReady()) {
    ContinueWithNetworkReady();
  } else {
    BeginInstall();
  }
}

void StartupAppLauncher::ContinueWithNetworkReady() {
  SYSLOG(INFO) << "ContinueWithNetworkReady"
               << ", state_=" << std::to_underlying(state_);

  if (state_ != LaunchState::kInitializingNetwork &&
      state_ != LaunchState::kNotStarted) {
    return;
  }

  if (should_skip_install_) {
    OnInstallSuccess();
    return;
  }

  // The network might not be ready when KioskChromeAppManager tries to update
  // external cache initially. Update the external cache now that the network
  // is ready for sure.
  state_ = LaunchState::kWaitingForCache;
  kiosk_app_manager_observation_.Observe(KioskChromeAppManager::Get());
  KioskChromeAppManager::Get()->UpdateExternalCache();
}

bool StartupAppLauncher::RetryWhenNetworkIsAvailable() {
  ++launch_attempt_;
  if (launch_attempt_ < kMaxLaunchAttempt) {
    state_ = LaunchState::kInitializingNetwork;
    delegate_->InitializeNetwork();
    return true;
  }
  return false;
}

void StartupAppLauncher::OnKioskExtensionLoadedInCache(
    const std::string& app_id) {
  OnKioskAppDataLoadStatusChanged(app_id);
}

void StartupAppLauncher::OnKioskExtensionDownloadFailed(
    const std::string& app_id) {
  OnKioskAppDataLoadStatusChanged(app_id);
}

void StartupAppLauncher::OnKioskAppDataLoadStatusChanged(
    const std::string& app_id) {
  CHECK_EQ(state_, LaunchState::kWaitingForCache);

  if (app_id != app_id_) {
    return;
  }

  kiosk_app_manager_observation_.Reset();

  if (KioskChromeAppManager::Get()->HasCachedCrx(app_id_)) {
    BeginInstall();
  } else {
    OnLaunchFailure(KioskAppLaunchError::Error::kUnableToDownload);
  }
}

void StartupAppLauncher::BeginInstall() {
  state_ = LaunchState::kInstallingApp;
  observers_.NotifyAppInstalling();
  installer_ = std::make_unique<ChromeKioskAppInstaller>(
      profile_,
      KioskChromeAppManager::Get()->CreatePrimaryAppInstallData(app_id_));
  installer_->BeginInstall(base::BindOnce(
      &StartupAppLauncher::OnInstallComplete, weak_ptr_factory_.GetWeakPtr()));
}

void StartupAppLauncher::OnInstallComplete(ash::KioskInstallResult result) {
  CHECK_EQ(state_, LaunchState::kInstallingApp);

  installer_.reset();

  switch (result) {
    case ash::KioskInstallResult::kSuccess:
      OnInstallSuccess();
      return;
    case ash::KioskInstallResult::kPrimaryAppUpdateFailed:
      SYSLOG(WARNING) << "Primary app update failed, proceeding anyways";
      OnInstallSuccess();
      return;
    case ash::KioskInstallResult::kSecondaryAppUpdateFailed:
      SYSLOG(WARNING) << "Secondary app update failed, proceeding anyways";
      OnInstallSuccess();
      return;
    case ash::KioskInstallResult::kPrimaryAppInstallFailed:
      OnLaunchFailure(KioskAppLaunchError::Error::kUnableToInstall);
      return;
    case ash::KioskInstallResult::kPrimaryAppNotKioskEnabled:
      OnLaunchFailure(KioskAppLaunchError::Error::kNotKioskEnabled);
      return;
    case ash::KioskInstallResult::kPrimaryAppNotCached:
    case ash::KioskInstallResult::kSecondaryAppInstallFailed:
      if (!RetryWhenNetworkIsAvailable()) {
        OnLaunchFailure(KioskAppLaunchError::Error::kUnableToInstall);
      }
      return;
    case ash::KioskInstallResult::kUnknown:
      SYSLOG(ERROR) << "Received unknown InstallResult";
      OnLaunchFailure(KioskAppLaunchError::Error::kUnableToInstall);
      return;
  }
}

void StartupAppLauncher::OnInstallSuccess() {
  state_ = LaunchState::kReadyToLaunch;

  observers_.NotifyAppPrepared();
}

void StartupAppLauncher::LaunchApp() {
  if (state_ != LaunchState::kReadyToLaunch) {
    SYSLOG(ERROR) << "LaunchApp() called but launcher is not initialized.";
    NOTREACHED();
  }

  launcher_ = std::make_unique<ChromeKioskAppLauncher>(
      profile_, app_id_, delegate_->IsNetworkReady());

  base::expected<void, ChromeKioskAppLauncher::PreLaunchError>
      pre_launch_result = launcher_->PerformPreLaunchChecks();
  if (!pre_launch_result.has_value()) {
    HandlePreLaunchError(pre_launch_result.error());
    return;
  }
  observers_.NotifyAppLaunching();

  launcher_->LaunchApp(base::BindOnce(&StartupAppLauncher::OnLaunchComplete,
                                      weak_ptr_factory_.GetWeakPtr()));
}

void StartupAppLauncher::HandlePreLaunchError(
    ChromeKioskAppLauncher::PreLaunchError error) {
  switch (error) {
    case ChromeKioskAppLauncher::PreLaunchError::kPrimaryAppMissing:
    case ChromeKioskAppLauncher::PreLaunchError::kSecondaryAppsMissing:
    case ChromeKioskAppLauncher::PreLaunchError::kPrimaryAppNotKioskEnabled:
      OnLaunchFailure(KioskAppLaunchError::Error::kUnableToLaunch);
      return;
    case ChromeKioskAppLauncher::PreLaunchError::kNetworkMissing:
      if (!RetryWhenNetworkIsAvailable()) {
        OnLaunchFailure(KioskAppLaunchError::Error::kUnableToLaunch);
      }
      return;
    case ChromeKioskAppLauncher::PreLaunchError::kChromeAppDeprecated:
      OnLaunchFailure(KioskAppLaunchError::Error::kChromeAppDeprecated);
      return;
  }
}

void StartupAppLauncher::OnLaunchComplete(bool success) {
  CHECK_EQ(state_, LaunchState::kReadyToLaunch);

  launcher_.reset();

  if (success) {
    OnLaunchSuccess();
  } else {
    // Other error cases are being handled in `HandlePreLaunchError`
    OnLaunchFailure(KioskAppLaunchError::Error::kUnableToLaunch);
  }
}

void StartupAppLauncher::OnLaunchSuccess() {
  state_ = LaunchState::kLaunchSucceeded;
  observers_.NotifyAppLaunched();
  observers_.NotifyAppWindowCreated();
}

void StartupAppLauncher::OnLaunchFailure(KioskAppLaunchError::Error error) {
  SYSLOG(ERROR) << "App launch failed, error: " << static_cast<int>(error);
  CHECK_NE(KioskAppLaunchError::Error::kNone, error);

  observers_.NotifyLaunchFailed(error);
}

}  // namespace ash
