// Copyright 2018 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/ui/ash/assistant/assistant_browser_delegate_impl.h"

#include <string>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/constants/web_app_id_constants.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "base/check_is_test.h"
#include "base/containers/fixed_flat_set.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "build/branding_buildflags.h"
#include "chrome/browser/apps/app_service/app_service_proxy_ash.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/browser_delegate/browser_controller.h"
#include "chrome/browser/ash/browser_delegate/browser_delegate.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "chromeos/ash/services/assistant/public/cpp/assistant_browser_delegate.h"
#include "components/services/app_service/public/cpp/app_launch_params.h"
#include "components/session_manager/core/session_manager.h"
#include "components/user_manager/user_manager.h"

namespace {

using AssistantBrowserDelegate = ash::assistant::AssistantBrowserDelegate;

Profile* GetActiveUserProfile() {
  user_manager::User* active_user =
      user_manager::UserManager::Get()->GetActiveUser();
  CHECK(active_user);
  return Profile::FromBrowserContext(
      ash::BrowserContextHelper::Get()->GetBrowserContextByUser(active_user));
}

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. If any value is added, please update
// `AssistantNewEntryPointEligibility` in `enums.xml`
// TODO: crbug.com/388361414 - Delete all new entry point related code from this
// class.
// LINT.IfChange(AssistantNewEntryPointEligibility)
enum class AssistantNewEntryPointEligibility {
  // A profile is eligible for the new entry point.
  kEligible = 0,
  // A profile is not ready for eligibility check. This can be both permanent
  // error (e.g., guest session) and transient error (e.g., queried before user
  // session started).
  kErrorProfileNotReady = 1,
  // Web provider is not ready for eligibility check. The eligibility check
  // waits for external managers synchornization of `WebAppProvider`.
  kErrorWebProviderNotReady = 2,
  // Not eligible because the new entry point flag is off.
  kNotEligibleFlagOff = 3,
  // Not eligible because the new entry point is not installed.
  kNotEligibleNotInstalled = 4,
  // Not eligible because the build is not Google Chrome.
  kNotEligibleNonGoogleChrome = 5,
  kMaxValue = kNotEligibleNonGoogleChrome,
};
// LINT.ThenChange(/tools/metrics/histograms/enums.xml:AssistantNewEntryPointEligibility)

AssistantNewEntryPointEligibility ToHistogramEnum(
    base::expected<const web_app::WebApp*, AssistantBrowserDelegate::Error>
        maybe_web_app) {
  if (maybe_web_app.has_value()) {
    return AssistantNewEntryPointEligibility::kEligible;
  }

  switch (maybe_web_app.error()) {
    case AssistantBrowserDelegate::Error::kProfileNotReady:
      return AssistantNewEntryPointEligibility::kErrorProfileNotReady;
    case AssistantBrowserDelegate::Error::kWebAppProviderNotReadyToRead:
      return AssistantNewEntryPointEligibility::kErrorWebProviderNotReady;
    case AssistantBrowserDelegate::Error::kNewEntryPointNotEnabled:
      return AssistantNewEntryPointEligibility::kNotEligibleFlagOff;
    case AssistantBrowserDelegate::Error::kNewEntryPointNotFound:
      return AssistantNewEntryPointEligibility::kNotEligibleNotInstalled;
    case AssistantBrowserDelegate::Error::kNonGoogleChromeBuild:
      return AssistantNewEntryPointEligibility::kNotEligibleNonGoogleChrome;
  }

  CHECK(false) << "Invalid error value is specified";
}

base::expected<bool, AssistantBrowserDelegate::Error> ToEligibilityBool(
    base::expected<const web_app::WebApp*, AssistantBrowserDelegate::Error>
        maybe_web_app) {
  if (maybe_web_app.has_value()) {
    return true;
  }

  static constexpr auto non_transient_error =
      base::MakeFixedFlatSet<AssistantBrowserDelegate::Error>(
          {AssistantBrowserDelegate::Error::kNewEntryPointNotEnabled,
           AssistantBrowserDelegate::Error::kNewEntryPointNotFound,
           AssistantBrowserDelegate::Error::kNonGoogleChromeBuild});
  if (non_transient_error.contains(maybe_web_app.error())) {
    return false;
  }

  return base::unexpected(maybe_web_app.error());
}

bool IsGoogleChrome() {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  return true;
#else
  return false;
#endif
}

}  // namespace

AssistantBrowserDelegateImpl::AssistantBrowserDelegateImpl() {
  auto* session_manager = session_manager::SessionManager::Get();
  // AssistantBrowserDelegateImpl must be created before any user session is
  // created. Otherwise, it will not get OnUserProfileLoaded notification.
  DCHECK(session_manager->sessions().empty());
  session_manager->AddObserver(this);
}

AssistantBrowserDelegateImpl::~AssistantBrowserDelegateImpl() {
  session_manager::SessionManager::Get()->RemoveObserver(this);
}

void AssistantBrowserDelegateImpl::InitializeNewEntryPointFor(
    Profile* profile) {
  CHECK(profile);

  web_app::WebAppProvider* provider =
      web_app::WebAppProvider::GetForWebApps(profile);
  if (!provider) {
    // `WebAppProvider` is not available if `GetBrowserContextForWebApps` in
    // `web_app_utils.cc` returns nullptr, e.g., guest session. This is
    // non-recoverable, i.e., no need to wait and/or re-try.
    return;
  }

  if (profile_for_new_entry_point_) {
    CHECK_EQ(profile_for_new_entry_point_, profile)
        << "profile_for_new_entry_point_ is already initialized with a "
           "different profile. There should be only a single primary profile.";
    return;
  }

  // Profile is set only if `WebAppProvider` is available for the profile.
  profile_for_new_entry_point_ = profile;

  // Assistant new entry point is loaded to `WebAppProvider` as an async
  // operation. We have to wait for the async load before checking if Assistant
  // new entry point is installed on a device/profile.
  provider->on_external_managers_synchronized().Post(
      FROM_HERE,
      base::BindOnce(
          &AssistantBrowserDelegateImpl::OnExternalManagersSynchronized,
          weak_ptr_factory_.GetWeakPtr()));
}

void AssistantBrowserDelegateImpl::OpenUrl(GURL url) {
  // The new tab should be opened with a user activation since the user
  // interacted with the Assistant to open the url. |in_background| describes
  // the relationship between |url| and Assistant UI, not the browser. As
  // such, the browser will always be instructed to open |url| in a new
  // browser tab and Assistant UI state will be updated downstream to respect
  // |in_background|.
  ash::NewWindowDelegate::GetInstance()->OpenUrl(
      url, ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      ash::NewWindowDelegate::Disposition::kNewForegroundTab);
}

base::expected<const web_app::WebAppRegistrar*, AssistantBrowserDelegate::Error>
AssistantBrowserDelegateImpl::GetWebAppRegistrarForNewEntryPoint() {
  if (!profile_for_new_entry_point_) {
    return base::unexpected(AssistantBrowserDelegate::Error::kProfileNotReady);
  }

  if (!on_is_new_entry_point_eligible_ready_.is_signaled()) {
    return base::unexpected(
        AssistantBrowserDelegate::Error::kWebAppProviderNotReadyToRead);
  }

  web_app::WebAppProvider* provider =
      web_app::WebAppProvider::GetForWebApps(profile_for_new_entry_point_);
  CHECK(provider) << "WebAppProvider must be available if "
                     "on_is_new_entry_point_eligible_ready_ is signaled";

  return &(provider->registrar_unsafe());
}

base::expected<const web_app::WebApp*, AssistantBrowserDelegate::Error>
AssistantBrowserDelegateImpl::ResolveNewEntryPointIfEligible() {
  if (!IsGoogleChrome() && !is_google_chrome_override_for_testing_) {
    return base::unexpected(
        AssistantBrowserDelegate::Error::kNonGoogleChromeBuild);
  }

  ASSIGN_OR_RETURN(const web_app::WebAppRegistrar* web_app_registrar,
                   GetWebAppRegistrarForNewEntryPoint());

  std::string app_id = entry_point_id_for_testing_.empty()
                           ? ash::kGeminiAppId
                           : entry_point_id_for_testing_;
  const web_app::WebApp* web_app = web_app_registrar->GetAppById(app_id);
  if (!web_app) {
    return base::unexpected(
        AssistantBrowserDelegate::Error::kNewEntryPointNotFound);
  }

  return web_app;
}

void AssistantBrowserDelegateImpl::OnExternalManagersSynchronized() {
  on_is_new_entry_point_eligible_ready_.Signal();
}

base::expected<bool, AssistantBrowserDelegate::Error>
AssistantBrowserDelegateImpl::IsNewEntryPointEligibleForPrimaryProfile() {
  base::expected<const web_app::WebApp*, AssistantBrowserDelegate::Error>
      maybe_web_app = ResolveNewEntryPointIfEligible();

  base::UmaHistogramEnumeration("Assistant.NewEntryPoint.Eligibility",
                                ToHistogramEnum(maybe_web_app));
  return ToEligibilityBool(maybe_web_app);
}

void AssistantBrowserDelegateImpl::OpenNewEntryPoint() {
  ASSIGN_OR_RETURN(const web_app::WebApp* web_app,
                   ResolveNewEntryPointIfEligible(), [](auto) {});
  CHECK(profile_for_new_entry_point_);

  // Check if the app is already running. If it is, bring the window to front.
  bool app_running = false;
  ash::BrowserController::GetInstance()->ForEachBrowser(
      ash::BrowserController::BrowserOrder::kAscendingActivationTime,
      [&](ash::BrowserDelegate& browser) {
        if (browser.IsWebApp() && browser.GetAppId() == web_app->app_id() &&
            browser.GetBrowser().GetProfile() == profile_for_new_entry_point_) {
          browser.Show();
          app_running = true;
          return ash::BrowserController::kBreakIteration;
        }
        return ash::BrowserController::kContinueIteration;
      });

  if (app_running) {
    return;
  }

  apps::AppServiceProxyFactory::GetForProfile(profile_for_new_entry_point_)
      ->LaunchAppWithParams(apps::AppLaunchParams(
          web_app->app_id(), apps::LaunchContainer::kLaunchContainerWindow,
          WindowOpenDisposition::NEW_WINDOW,
          // TODO(xiaohuic): maybe add new source
          apps::LaunchSource::kUnknown));
}

std::optional<std::string>
AssistantBrowserDelegateImpl::GetNewEntryPointName() {
  ASSIGN_OR_RETURN(
      const web_app::WebApp* web_app, ResolveNewEntryPointIfEligible(),
      [](auto) -> std::optional<std::string> { return std::nullopt; });
  ASSIGN_OR_RETURN(
      const web_app::WebAppRegistrar* web_app_registrar,
      GetWebAppRegistrarForNewEntryPoint(),
      [](auto) -> std::optional<std::string> { return std::nullopt; });

  return web_app_registrar->GetAppShortName(web_app->app_id());
}

void AssistantBrowserDelegateImpl::OverrideEntryPointIdForTesting(
    const std::string& test_entry_point_id) {
  CHECK_IS_TEST();
  entry_point_id_for_testing_ = test_entry_point_id;
}

void AssistantBrowserDelegateImpl::SetGoogleChromeBuildForTesting() {
  CHECK_IS_TEST();
  CHECK(!is_google_chrome_override_for_testing_)
      << "Already marked as google chrome";
  is_google_chrome_override_for_testing_ = true;
}

void AssistantBrowserDelegateImpl::OnUserSessionStarted(bool is_primary_user) {
  if (is_primary_user) {
    InitializeNewEntryPointFor(GetActiveUserProfile());
  }
}
