// Copyright 2023 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/shimless_rma/diagnostics_app_profile_helper.h"

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

#include "ash/constants/ash_features.h"
#include "ash/constants/chrome_pref_names.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/webui/shimless_rma/backend/shimless_rma_delegate.h"
#include "base/check_deref.h"
#include "base/containers/fixed_flat_map.h"
#include "base/files/file_path.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/ash/shimless_rma/chrome_shimless_rma_delegate.h"
#include "chrome/browser/ash/shimless_rma/diagnostics_app_profile_helper_constants.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/web_applications/isolated_web_apps/commands/install_isolated_web_app_command.h"
#include "chrome/browser/web_applications/isolated_web_apps/install/isolated_web_app_install_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/iwa_permissions_policy_cache.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/chromeos/extensions/chromeos_system_extension_info.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "components/strings/grit/components_strings.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "components/webapps/browser/uninstall_result_code.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/service_worker_context.h"
#include "extensions/browser/crx_file_info.h"
#include "extensions/browser/crx_installer.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extension_util.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/common/permissions/permission_message.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/common/verifier_formats.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
#include "third_party/blink/public/common/permissions_policy/policy_helper_public.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/origin.h"

namespace context {
class BrowserContext;
}

namespace ash::shimless_rma {

namespace {

// Polling interval and the timeout to wait for the extension being ready.
constexpr base::TimeDelta kExtensionReadyPollingInterval =
    base::Milliseconds(50);
constexpr base::TimeDelta kExtensionReadyPollingTimeout = base::Seconds(3);

// The set of allowlisted permission policy for diagnostics IWA.
constexpr auto kAllowlistedPermissionPolicyStringMap =
    base::MakeFixedFlatMap<std::string_view, int>(
        {{"camera", IDS_ASH_SHIMLESS_RMA_APP_ACCESS_PERMISSION_CAMERA},
         {"microphone", IDS_ASH_SHIMLESS_RMA_APP_ACCESS_PERMISSION_MICROPHONE},
         {"fullscreen", IDS_ASH_SHIMLESS_RMA_APP_ACCESS_PERMISSION_FULLSCREEN},
         {"hid", IDS_ASH_SHIMLESS_RMA_APP_ACCESS_PERMISSION_HID_DEVICES}});

std::optional<url::Origin>& GetInstalledDiagnosticsAppOriginInternal() {
  static base::NoDestructor<std::optional<url::Origin>> g_origin;
  return *g_origin;
}

void DisableAllExtensions(content::BrowserContext* context) {
  auto* registry = extensions::ExtensionRegistry::Get(context);
  CHECK(registry);

  std::vector<std::string> ids;
  for (const auto& extension : registry->enabled_extensions()) {
    ids.push_back(extension->id());
  }
  for (const auto& extension : registry->terminated_extensions()) {
    ids.push_back(extension->id());
  }

  auto* registrar = extensions::ExtensionRegistrar::Get(context);
  CHECK(registrar);
  for (const auto& id : ids) {
    registrar->DisableExtension(
        id, {extensions::disable_reason::DISABLE_USER_ACTION});
  }
}

struct PrepareDiagnosticsAppProfileState {
  PrepareDiagnosticsAppProfileState();
  PrepareDiagnosticsAppProfileState(PrepareDiagnosticsAppProfileState&) =
      delete;
  PrepareDiagnosticsAppProfileState& operator=(
      PrepareDiagnosticsAppProfileState&) = delete;
  ~PrepareDiagnosticsAppProfileState();

  // Arguments
  raw_ptr<DiagnosticsAppProfileHelperDelegate> delegate;
  base::FilePath crx_path;
  base::FilePath swbn_path;
  ShimlessRmaDelegate::PrepareDiagnosticsAppBrowserContextCallback callback;
  // Reference to the crx_installer.
  scoped_refptr<extensions::CrxInstaller> crx_installer = nullptr;
  // Result.
  raw_ptr<content::BrowserContext> context;
  std::optional<std::string> extension_id;
  std::optional<web_package::SignedWebBundleId> iwa_id;
  std::optional<std::string> name;
  std::optional<std::string> permission_message;
  std::optional<GURL> iwa_start_url;
};

PrepareDiagnosticsAppProfileState::PrepareDiagnosticsAppProfileState() =
    default;

PrepareDiagnosticsAppProfileState::~PrepareDiagnosticsAppProfileState() =
    default;

void ReportError(std::unique_ptr<PrepareDiagnosticsAppProfileState> state,
                 const std::string& message) {
  std::move(state->callback).Run(base::unexpected(message));
}

void ReportSuccess(std::unique_ptr<PrepareDiagnosticsAppProfileState> state) {
  CHECK(state->context);
  CHECK(state->extension_id);
  CHECK(state->iwa_id);
  CHECK(state->iwa_start_url);

  GetInstalledDiagnosticsAppOriginInternal() =
      url::Origin::Create(state->iwa_start_url.value());

  std::move(state->callback)
      .Run(base::ok(
          ShimlessRmaDelegate::PrepareDiagnosticsAppBrowserContextResult(
              state->context, state->extension_id.value(),
              state->iwa_id.value(), state->name.value(),
              state->permission_message)));
}

void CompleteOnIsolatedWebAppInstalled(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state) {
  const web_app::WebApp* web_app = state->delegate->GetWebAppByIdUnsafe(
      web_app::IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(
          *state->iwa_id)
          .app_id(),
      state->context);

  if (!web_app) {
    ReportError(std::move(state), "Failed to find installed IWA.");
    return;
  }

  // Since this is after IWA installation, cache has to be already created.
  auto* profile = Profile::FromBrowserContext(state->context);
  auto* policy_cache =
      web_app::IwaPermissionsPolicyCacheFactory::GetForProfile(profile);
  CHECK(policy_cache);

  const web_app::IwaPermissionsPolicyCache::CacheEntry* policy =
      policy_cache->GetPolicy(
          web_app::IwaOrigin::Create(web_app->start_url()).value());

  if (policy && !policy->empty()) {
    if (!ash::features::
            IsShimlessRMA3pDiagnosticsAllowPermissionPolicyEnabled()) {
      ReportError(std::move(state), k3pDiagErrorIWACannotHasPermissionPolicy);
      return;
    }

    std::u16string permission_message;
    for (const auto& permission_policy : *policy) {
      if (!kAllowlistedPermissionPolicyStringMap.contains(
              permission_policy.feature)) {
        ReportError(std::move(state), k3pDiagErrorIWACannotHasPermissionPolicy);
        return;
      }
      base::StrAppend(
          &permission_message,
          {u"- ",
           l10n_util::GetStringUTF16(kAllowlistedPermissionPolicyStringMap.at(
               permission_policy.feature)),
           u"\n"});
    }

    state->permission_message = state->permission_message.value_or("");
    base::StrAppend(&*state->permission_message,
                    {base::UTF16ToUTF8(l10n_util::GetStringUTF16(
                         IDS_ASH_SHIMLESS_RMA_APP_ACCESS_PERMISSION)),
                     "\n", base::UTF16ToUTF8(permission_message)});
  }

  state->name = web_app->untranslated_name();
  state->iwa_start_url = web_app->start_url();

  ReportSuccess(std::move(state));
}

void OnPermissionsPoliciesObtained(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state,
    bool success) {
  if (!success) {
    ReportError(std::move(state), "Failed to parse IWA manifest.");
    return;
  }
  CompleteOnIsolatedWebAppInstalled(std::move(state));
}

void OnIsolatedWebAppInstalled(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state,
    base::expected<web_app::InstallIsolatedWebAppCommandSuccess,
                   web_app::InstallIsolatedWebAppCommandError> result) {
  CHECK(state->context);
  CHECK(state->extension_id);
  CHECK(state->iwa_id);

  if (!result.has_value()) {
    ReportError(std::move(state), "Failed to install Isolated web app: " +
                                      result.error().message);
    return;
  }

  // Since this is after IWA installation, cache has to be already created.
  auto* profile = Profile::FromBrowserContext(state->context);
  auto* policy_cache =
      web_app::IwaPermissionsPolicyCacheFactory::GetForProfile(profile);
  CHECK(policy_cache);

  policy_cache->ObtainManifestAndCache(
      web_app::IwaOrigin(*state->iwa_id),
      base::BindOnce(&OnPermissionsPoliciesObtained, std::move(state)));
}

void InstallIsolatedWebApp(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state) {
  CHECK(state->context);
  CHECK(state->iwa_id);

  auto url_info = web_app::IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(
      state->iwa_id.value());
  auto install_source = web_app::IsolatedWebAppInstallSource::FromShimlessRma(
      web_app::IwaSourceBundleProdModeWithFileOp(
          state->swbn_path, web_app::IwaSourceBundleProdFileOp::kCopy));
  state->delegate->GetWebAppCommandScheduler(state->context)
      ->InstallIsolatedWebApp(
          url_info, install_source,
          /*expected_version=*/std::nullopt, /*optional_keep_alive=*/nullptr,
          /*optional_profile_keep_alive=*/nullptr,
          base::BindOnce(&OnIsolatedWebAppInstalled, std::move(state)));
}

void OnIsolatedWebAppRemoved(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state,
    webapps::UninstallResultCode code) {
  if (!webapps::UninstallSucceeded(code)) {
    LOG(WARNING) << "Failed to unsintalled IWA before installing IWA";
  }
  InstallIsolatedWebApp(std::move(state));
}

void PrepareIsolatedWebApp(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state) {
  CHECK(state->context);
  CHECK(state->extension_id);

  auto info =
      chromeos::GetChromeOSExtensionInfoById(state->extension_id.value());
  if (!info.iwa_id) {
    ReportError(std::move(state), "Extension " + state->extension_id.value() +
                                      " doesn't have a connected IWA.");
    return;
  }
  state->iwa_id = info.iwa_id;

  auto url_info = web_app::IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(
      state->iwa_id.value());
  const web_app::WebApp* web_app =
      state->delegate->GetWebAppByIdUnsafe(url_info.app_id(), state->context);
  if (!web_app) {
    // Install the IWA directly if IWA doesn't exist.
    InstallIsolatedWebApp((std::move(state)));
    return;
  }

  // Since we can not install IWA when the IWA is already installed, we should
  // remove the existing IWA first before installation.
  // It is safe to run uninstall job here since Shimless RMA is the only install
  // source for the third-party diagnostics IWA.
  // Note that the flow will be broken if there are multiple install sources.
  state->delegate->GetWebAppCommandScheduler(state->context)
      ->RemoveInstallManagementMaybeUninstall(
          url_info.app_id(), web_app::WebAppManagement::Type::kIwaShimlessRma,
          webapps::WebappUninstallSource::kUnknown,
          base::BindOnce(&OnIsolatedWebAppRemoved, std::move(state)));
}

void CheckExtensionIsReady(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state,
    GURL script_url,
    blink::StorageKey storage_key,
    base::Time start_time);

void OnCheckExtensionIsReadyResponse(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state,
    GURL script_url,
    blink::StorageKey storage_key,
    base::Time start_time,
    content::ServiceWorkerCapability capability) {
  if (capability == content::ServiceWorkerCapability::NO_SERVICE_WORKER) {
    // The service worker could still be registering, or the extension is failed
    // to be activated.
    if (base::Time::Now() - start_time <= kExtensionReadyPollingTimeout) {
      base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(&CheckExtensionIsReady, std::move(state), script_url,
                         storage_key, start_time),
          kExtensionReadyPollingInterval);
      return;
    }
    ReportError(std::move(state), k3pDiagErrorCannotActivateExtension);
    return;
  }

  PrepareIsolatedWebApp(std::move(state));
}

void CheckExtensionIsReady(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state,
    GURL script_url,
    blink::StorageKey storage_key,
    base::Time start_time) {
  content::ServiceWorkerContext* service_worker_context =
      state->delegate->GetServiceWorkerContextForExtensionId(
          state->extension_id.value(), state->context);
  // Extensions register a service worker. Diagnostics app IWAs need to be
  // started after the service worker is up to communicate with the extensions.
  service_worker_context->CheckHasServiceWorker(
      script_url, storage_key,
      base::BindOnce(&OnCheckExtensionIsReadyResponse, std::move(state),
                     script_url, storage_key, start_time));
}

void OnExtensionInstalled(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state,
    const std::optional<extensions::CrxInstallError>& error) {
  CHECK(state->context);
  CHECK(state->crx_installer);

  if (error) {
    ReportError(std::move(state),
                "Failed to install 3p diagnostics extension: " +
                    base::UTF16ToUTF8(error->message()));
    return;
  }
  const extensions::Extension* extension = state->crx_installer->extension();
  CHECK(extension);
  state->extension_id = extension->id();
  state->crx_installer.reset();

  if (!chromeos::IsChromeOSSystemExtension(extension->id())) {
    ReportError(std::move(state),
                base::StringPrintf(k3pDiagErrorNotChromeOSSystemExtension,
                                   extension->id().c_str()));
    return;
  }

  if (!extension->install_warnings().empty()) {
    LOG(ERROR)
        << "Extension " << extension->id()
        << " may not work as expected because of these install warnings:";
    for (const auto& warning : extension->install_warnings()) {
      LOG(ERROR) << warning.message;
    }
  }

  extensions::PermissionMessages permission_messages =
      extension->permissions_data()->GetPermissionMessages();
  if (permission_messages.empty()) {
    state->permission_message = std::nullopt;
  } else {
    std::u16string message;
    for (const auto& permission_message : permission_messages) {
      base::StrAppend(&message, {permission_message.message(), u"\n"});
      for (const auto& submessage : permission_message.submessages()) {
        base::StrAppend(&message, {u"- ", submessage, u"\n"});
      }
    }
    state->permission_message = base::UTF16ToUTF8(message);
  }

  extensions::ExtensionRegistrar::Get(state->context)
      ->EnableExtension(extension->id());
  // Reload the extension to make sure old service worker are cleaned. This is
  // important when the extension has already been installed to the profile.
  extensions::ExtensionRegistrar::Get(state->context)
      ->ReloadExtension(extension->id());

  GURL script_url =
      extensions::BackgroundInfo::GetBackgroundServiceWorkerScriptURL(
          extension);
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&CheckExtensionIsReady, std::move(state), script_url,
                     blink::StorageKey::CreateFirstParty(
                         url::Origin::Create(extension->url())),
                     base::Time::Now()),
      kExtensionReadyPollingInterval);
}

void InstallExtension(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state) {
  CHECK(state->context);

  auto crx_installer = extensions::CrxInstaller::CreateSilent(state->context);
  state->crx_installer = crx_installer;
  const base::FilePath& crx_path = state->crx_path;
  crx_installer->AddInstallerCallback(
      base::BindOnce(&OnExtensionInstalled, std::move(state)));
  const crx_file::VerifierFormat verifier_format =
      ash::features::IsShimlessRMA3pDiagnosticsDevModeEnabled()
          ? extensions::GetTestVerifierFormat()
          : extensions::GetWebstoreVerifierFormat(
                /*test_publisher_enabled=*/false);
  crx_installer->InstallCrxFile(
      extensions::CRXFileInfo{crx_path, verifier_format});
}

void OnExtensionSystemReady(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state) {
  CHECK(state->context);

  DisableAllExtensions(state->context);
  InstallExtension(std::move(state));
}

void OnProfileLoaded(std::unique_ptr<PrepareDiagnosticsAppProfileState> state,
                     Profile* profile) {
  if (!profile) {
    ReportError(std::move(state),
                "Failed to load shimless diagnostics app profile.");
    return;
  }
  // Extensions and IWAs should be installed to the original profile.
  if (profile->IsOffTheRecord()) {
    profile = profile->GetOriginalProfile();
  }
  profile->GetPrefs()->SetBoolean(ash::chrome_prefs::kForceEphemeralProfiles,
                                  true);

  state->context = profile;
  auto* system = extensions::ExtensionSystem::Get(state->context);
  CHECK(system);
  system->ready().Post(
      FROM_HERE, base::BindOnce(&OnExtensionSystemReady, std::move(state)));
}

void PrepareDiagnosticsAppProfileImpl(
    std::unique_ptr<PrepareDiagnosticsAppProfileState> state) {
  CHECK(g_browser_process);
  CHECK(g_browser_process->profile_manager());
  CHECK(BrowserContextHelper::Get());

  g_browser_process->profile_manager()->CreateProfileAsync(
      BrowserContextHelper::Get()->GetShimlessRmaAppBrowserContextPath(),
      base::BindOnce(&OnProfileLoaded, std::move(state)));
}

}  // namespace

DiagnosticsAppProfileHelperDelegate::DiagnosticsAppProfileHelperDelegate() =
    default;

DiagnosticsAppProfileHelperDelegate::~DiagnosticsAppProfileHelperDelegate() =
    default;

content::ServiceWorkerContext*
DiagnosticsAppProfileHelperDelegate::GetServiceWorkerContextForExtensionId(
    const extensions::ExtensionId& extension_id,
    content::BrowserContext* browser_context) {
  return extensions::util::GetServiceWorkerContextForExtensionId(
      extension_id, browser_context);
}

web_app::WebAppCommandScheduler*
DiagnosticsAppProfileHelperDelegate::GetWebAppCommandScheduler(
    content::BrowserContext* browser_context) {
  auto* web_app_provider = web_app::WebAppProvider::GetForWebApps(
      Profile::FromBrowserContext(browser_context));
  CHECK(web_app_provider);
  return &web_app_provider->scheduler();
}

const web_app::WebApp* DiagnosticsAppProfileHelperDelegate::GetWebAppByIdUnsafe(
    const webapps::AppId& app_id,
    content::BrowserContext* browser_context) {
  auto* web_app_provider = web_app::WebAppProvider::GetForWebApps(
      Profile::FromBrowserContext(browser_context));
  const web_app::WebAppRegistrar& registrar =
      web_app_provider->registrar_unsafe();
  return registrar.GetAppById(app_id);
}

const std::optional<url::Origin>&
DiagnosticsAppProfileHelperDelegate::GetInstalledDiagnosticsAppOrigin() {
  return GetInstalledDiagnosticsAppOriginInternal();
}

void PrepareDiagnosticsAppProfile(
    DiagnosticsAppProfileHelperDelegate* delegate,
    const base::FilePath& crx_path,
    const base::FilePath& swbn_path,
    ShimlessRmaDelegate::PrepareDiagnosticsAppBrowserContextCallback callback) {
  GetInstalledDiagnosticsAppOriginInternal() = std::nullopt;
  auto state = std::make_unique<PrepareDiagnosticsAppProfileState>();
  state->delegate = delegate;
  state->crx_path = crx_path;
  state->swbn_path = swbn_path;
  state->callback = std::move(callback);
  PrepareDiagnosticsAppProfileImpl(std::move(state));
}

}  // namespace ash::shimless_rma
