// 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 "ash/webui/recorder_app_ui/recorder_app_ui.h"

#include <algorithm>
#include <cstdint>
#include <utility>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/constants/devicetype.h"
#include "ash/webui/common/trusted_types_util.h"
#include "ash/webui/recorder_app_ui/model_constants.h"
#include "ash/webui/recorder_app_ui/recorder_app_ui_delegate.h"
#include "ash/webui/recorder_app_ui/resources.h"
#include "ash/webui/recorder_app_ui/resources/grit/recorder_app_resources.h"
#include "ash/webui/recorder_app_ui/resources/grit/recorder_app_resources_map.h"
#include "ash/webui/recorder_app_ui/url_constants.h"
#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/strings/string_number_conversions.h"
#include "base/system/sys_info.h"
#include "chromeos/ash/components/audio/cras_audio_handler.h"
#include "chromeos/ash/components/mojo_service_manager/connection.h"
#include "chromeos/constants/devicetype.h"
#include "chromeos/services/machine_learning/public/cpp/service_connection.h"
#include "components/media_device_salt/media_device_salt_service.h"
#include "components/metrics/structured/event.h"
#include "components/metrics/structured/structured_metrics_client.h"
#include "components/soda/constants.h"
#include "components/soda/soda_features.h"
#include "components/soda/soda_installer.h"
#include "components/soda/soda_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/media_device_id.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/common/url_constants.h"
#include "google_apis/google_api_keys.h"
#include "services/on_device_model/public/cpp/buildflags.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/cros_system_api/mojo/service_constants.h"
#include "ui/message_center/message_center.h"
#include "ui/webui/webui_allowlist.h"

namespace ash {

namespace {

// New ChromeOS feedback dialog (crbug.com/40941303) passes description template
// as query parameters in GURL with character limit 2097152 (defined in
// url.mojom.kMaxURLChars).
//
// Calculates characters as 1000-char (around 200-word) template with maximum
// model input & output (12k tokens in total) we likely want to include in the
// description.
const uint32_t kFeedbackDescriptionTemplateMaxChars = 49000;  // 1000 + 4 * 12k

constexpr char kDefaultDeviceTypeName[] = "Chromebook";

std::string_view SodaInstallerErrorCodeToString(
    speech::SodaInstaller::ErrorCode error) {
  switch (error) {
    case speech::SodaInstaller::ErrorCode::kNeedsReboot:
      return "kNeedsReboot";
    case speech::SodaInstaller::ErrorCode::kUnspecifiedError:
      return "kUnspecifiedError";
  }
}

recorder_app::mojom::ModelStateType SodaErrorCodeToModelStateType(
    speech::SodaInstaller::ErrorCode error) {
  switch (error) {
    case speech::SodaInstaller::ErrorCode::kNeedsReboot:
      return recorder_app::mojom::ModelStateType::kNeedsReboot;
    case speech::SodaInstaller::ErrorCode::kUnspecifiedError:
      return recorder_app::mojom::ModelStateType::kError;
  }
}

recorder_app::mojom::ModelStateType LoadModelResultToModelStateType(
    on_device_model::mojom::LoadModelResult result) {
  switch (result) {
    case on_device_model::mojom::LoadModelResult::kSuccess:
      return recorder_app::mojom::ModelStateType::kInstalled;
    case on_device_model::mojom::LoadModelResult::kGpuBlocked:
    case on_device_model::mojom::LoadModelResult::kFailedToLoadLibrary:
      return recorder_app::mojom::ModelStateType::kError;
    case on_device_model::mojom::LoadModelResult::kCrosNeedReboot:
      return recorder_app::mojom::ModelStateType::kNeedsReboot;
  }
}

void GotSalt(
    const url::Origin& origin,
    const std::string& source_id,
    base::OnceCallback<void(const std::optional<std::string>&)> callback,
    const std::string& salt) {
  // TODO(kamchonlathorn): Add a test to cover this function.
  auto callback_on_io_thread = base::BindOnce(
      [](const std::string& salt, const url::Origin& origin,
         const std::string& source_id,
         base::OnceCallback<void(const std::optional<std::string>&)> callback) {
        content::GetMediaDeviceIDForHMAC(
            blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE, salt,
            std::move(origin), source_id, content::GetUIThreadTaskRunner({}),
            std::move(callback));
      },
      salt, std::move(origin), source_id, std::move(callback));
  content::GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE, std::move(callback_on_io_thread));
}

void TranslateAudioDeviceId(
    content::BrowserContext* browser_context,
    media_device_salt::MediaDeviceSaltService* salt_service,
    const url::Origin& origin,
    const std::string& source_id,
    base::OnceCallback<void(const std::optional<std::string>&)> callback) {
  if (salt_service) {
    salt_service->GetSalt(
        blink::StorageKey::CreateFirstParty(origin),
        base::BindOnce(&GotSalt, origin, source_id, std::move(callback)));
  } else {
    // If the embedder does not provide a salt service, use the browser
    // context's unique ID as salt.
    GotSalt(origin, source_id, std::move(callback),
            browser_context->UniqueId());
  }
}

int GetResourceIdFromStringName(const std::string& name) {
  auto iter =
      std::ranges::find(kLocalizedStrings, name,
                        [](const webui::LocalizedString& s) { return s.name; });
  CHECK(iter != std::end(kLocalizedStrings));
  return iter->id;
}

std::string GetDeviceTypeString() {
  std::string device_type = ash::DeviceTypeToString(chromeos::GetDeviceType());
  return device_type.empty() ? kDefaultDeviceTypeName : device_type;
}

}  // namespace

// static
base::TimeDelta RecorderAppUI::ComputeEventUptimeForTesting(
    base::TimeDelta system_uptime,
    base::TimeDelta system_timestamp,
    base::TimeDelta event_timestamp) {
  CHECK_IS_TEST();
  return ComputeEventUptime(system_uptime, system_timestamp, event_timestamp);
}

// static
base::TimeDelta RecorderAppUI::ComputeEventUptime(
    base::TimeDelta system_uptime,
    base::TimeDelta system_timestamp,
    base::TimeDelta event_timestamp) {
  base::TimeDelta result =
      system_uptime -
      std::max(system_timestamp - event_timestamp, base::Seconds(0));
  if (result.is_negative()) {
    CHECK_IS_TEST();
  }
  return result;
}

// static
void RecorderAppUI::RecordStructuredMetricsForTesting(
    std::vector<::metrics::structured::Event> events) {
  CHECK_IS_TEST();
  RecordStructuredMetricsImpl(std::move(events));
}

// static
void RecorderAppUI::RecordStructuredMetricsImpl(
    std::vector<::metrics::structured::Event> events) {
  for (auto& event : events) {
    if (event.IsEventSequenceType()) {
      event.SetRecordedTimeSinceBoot(ComputeEventUptime(
          base::SysInfo::Uptime(),
          base::Time::NowFromSystemTime() - base::Time::UnixEpoch(),
          // if event does not have system uptime field populated, set it as the
          // current system timestamp.
          event.recorded_time_since_boot()));
    }
    ::metrics::structured::StructuredMetricsClient::Get()->Record(
        std::move(event));
  }
}

bool RecorderAppUIConfig::IsWebUIEnabled(
    content::BrowserContext* browser_context) {
  return base::FeatureList::IsEnabled(ash::features::kConch) ||
         base::FeatureList::IsEnabled(
             speech::kFeatureManagementCrosSodaConchLanguages);
}

RecorderAppUI::RecorderAppUI(content::WebUI* web_ui,
                             std::unique_ptr<RecorderAppUIDelegate> delegate)
    : ui::MojoWebUIController(web_ui), delegate_(std::move(delegate)) {
  content::BrowserContext* browser_context =
      web_ui->GetWebContents()->GetBrowserContext();

  // Register auto-granted permissions.
  auto* allowlist = WebUIAllowlist::GetOrCreate(browser_context);
  const url::Origin host_origin =
      url::Origin::Create(GURL(kChromeUIRecorderAppURL));
  allowlist->RegisterAutoGrantedPermissions(
      host_origin, {ContentSettingsType::MEDIASTREAM_MIC,
                    ContentSettingsType::DISPLAY_MEDIA_SYSTEM_AUDIO});

  // Setup the data source
  content::WebUIDataSource* source = content::WebUIDataSource::CreateAndAdd(
      web_ui->GetWebContents()->GetBrowserContext(), kChromeUIRecorderAppHost);

  source->AddResourcePaths(kRecorderAppResources);

  source->SetDefaultResource(IDR_RECORDER_APP_INDEX_HTML);

  source->AddString("deviceType", GetDeviceTypeString());

  source->AddLocalizedStrings(kLocalizedStrings);

  source->UseStringsJs();

  ash::EnableTrustedTypesCSP(source);
  // TODO(pihsun): Add other needed CSP.
  source->OverrideContentSecurityPolicy(
      network::mojom::CSPDirectiveName::MediaSrc,
      std::string("media-src 'self' blob:;"));

  if (speech::IsOnDeviceSpeechRecognitionSupported()) {
    speech::SodaInstaller::GetInstance()->AddObserver(this);
    // TODO: b/401440675 - Remove `ConchLargeModel` from the condition after
    // feature release so that we can use kill-switch to disable features on all
    // devices.
    if (base::FeatureList::IsEnabled(
            ash::features::kConchExpandTranscriptionLanguage) ||
        base::FeatureList::IsEnabled(ash::features::kConchLargeModel)) {
      auto language_list = speech::SodaInstaller::GetInstance()
                               ->GetLiveCaptionEnabledLanguages();
      for (auto language : language_list) {
        auto language_code = speech::GetLanguageCode(language);
        if (language_code != speech::LanguageCode::kNone) {
          transcription_supported_languages_.insert(language_code);
        }
      }
    } else {
      transcription_supported_languages_.insert(kDefaultLanguageCode);
    }

    if (base::FeatureList::IsEnabled(
            speech::kFeatureManagementCrosSodaConchLanguages)) {
      // Currently only en-US is supported.
      speaker_label_supported_languages_.insert(kDefaultLanguageCode);
    }
  }

  if (CanUseGenerativeAi()) {
    gen_ai_supported_languages_.insert(kDefaultLanguageCode);
    if (base::FeatureList::IsEnabled(ash::features::kConchLargeModel)) {
      gen_ai_supported_languages_.insert(speech::LanguageCode::kJaJp);
    }
  }

  // Add salt translator
  device_id_mapping_callback_ =
      base::BindRepeating(&TranslateAudioDeviceId, browser_context,
                          delegate_->GetMediaDeviceSaltService(browser_context),
                          url::Origin::Create(GURL(kChromeUIRecorderAppURL)));

  auto* message_center = message_center::MessageCenter::Get();
  message_center->AddObserver(this);
  in_quiet_mode_ = message_center->IsQuietMode();
}

RecorderAppUI::~RecorderAppUI() {
  message_center::MessageCenter::Get()->RemoveObserver(this);

  if (speech::IsOnDeviceSpeechRecognitionSupported()) {
    speech::SodaInstaller::GetInstance()->RemoveObserver(this);
  }
}

void RecorderAppUI::BindInterface(
    mojo::PendingReceiver<recorder_app::mojom::PageHandler> receiver) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  page_receivers_.Add(this, std::move(receiver));
}

void RecorderAppUI::EnsureOnDeviceModelService() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

#if BUILDFLAG(USE_CHROMEOS_MODEL_SERVICE)
  if (!on_device_model_service_) {
    auto pipe_handle =
        on_device_model_service_.BindNewPipeAndPassReceiver().PassPipe();
    on_device_model_service_.reset_on_disconnect();
    ash::mojo_service_manager::GetServiceManagerProxy()->Request(
        chromeos::mojo_services::kCrosOdmlService, std::nullopt,
        std::move(pipe_handle));
  }
#endif
}

bool RecorderAppUI::CanUseGenerativeAi() {
  return delegate_->CanUseGenerativeAiForCurrentProfile() &&
         base::FeatureList::IsEnabled(ash::features::kConchGenAi) &&
         base::FeatureList::IsEnabled(
             ash::features::kFeatureManagementConchGenAi);
}

void RecorderAppUI::AddModelMonitor(
    const base::Uuid& model_id,
    ::mojo::PendingRemote<recorder_app::mojom::ModelStateMonitor> monitor,
    AddModelMonitorCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!CanUseGenerativeAi()) {
    LOG(WARNING) << "GenAI can't be used";
    // TODO(pihsun): Return a dedicate error when GenAI can't be used.
    std::move(callback).Run(recorder_app::mojom::ModelState{
        recorder_app::mojom::ModelStateType::kUnavailable, std::nullopt}
                                .Clone());
    return;
  }

  EnsureOnDeviceModelService();

  if (!on_device_model_service_) {
    LOG(WARNING) << "ChromeOS OnDeviceModelService is unavailable";
    std::move(callback).Run(recorder_app::mojom::ModelState{
        recorder_app::mojom::ModelStateType::kUnavailable, std::nullopt}
                                .Clone());
    return;
  }

  recorder_app::mojom::ModelState model_state;

  auto model_state_iter = model_states_.find(model_id);
  if (model_state_iter == model_states_.end()) {
    model_state = {recorder_app::mojom::ModelStateType::kUnavailable,
                   std::nullopt};
    model_states_.insert({model_id, model_state});
    // TODO(pihsun): This currently only gets the model state once on first
    // monitor, and updates with progress when LoadModel is called. So if the
    // model is installed by any other mean (like downloaded as a base model or
    // by other app), the state won't be updated. Consider if we should expose
    // the observer API from DLC for each individual model.
    on_device_model_service_->GetPlatformModelState(
        model_id, base::BindOnce(&RecorderAppUI::GetPlatformModelStateCallback,
                                 weak_ptr_factory_.GetWeakPtr(), model_id));
  } else {
    model_state = model_state_iter->second;
  }
  model_monitors_[model_id].Add(std::move(monitor));
  std::move(callback).Run(model_state.Clone());
}

void RecorderAppUI::AddQuietModeMonitor(
    ::mojo::PendingRemote<recorder_app::mojom::QuietModeMonitor> monitor,
    AddQuietModeMonitorCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  quiet_mode_monitors_.Add(std::move(monitor));
  std::move(callback).Run(in_quiet_mode_);
}

void RecorderAppUI::GetModelInfo(on_device_model::mojom::FormatFeature feature,
                                 GetModelInfoCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(feature == on_device_model::mojom::FormatFeature::kAudioSummary ||
        feature == on_device_model::mojom::FormatFeature::kAudioTitle);
  recorder_app::mojom::ModelInfoPtr model_info =
      recorder_app::mojom::ModelInfo::New();

  if (base::FeatureList::IsEnabled(ash::features::kConchLargeModel)) {
    model_info->input_token_limit = kInputTokenXsModelLimit;
    model_info->is_large_model = true;

    if (feature == on_device_model::mojom::FormatFeature::kAudioSummary) {
      model_info->model_id =
          base::Uuid::ParseCaseInsensitive(kSummaryXsModelUuid);
    } else {
      model_info->model_id =
          base::Uuid::ParseCaseInsensitive(kTitleSuggestionXsModelUuid);
    }
  } else {
    model_info->input_token_limit = kInputTokenXxsModelLimit;
    model_info->is_large_model = false;

    if (feature == on_device_model::mojom::FormatFeature::kAudioSummary) {
      model_info->model_id =
          base::Uuid::ParseCaseInsensitive(kSummaryXxsModelUuid);
    } else {
      model_info->model_id =
          base::Uuid::ParseCaseInsensitive(kTitleSuggestionXxsModelUuid);
    }
  }
  std::move(callback).Run(std::move(model_info));
}

void RecorderAppUI::LoadModelResultCallback(
    const base::Uuid& model_id,
    LoadModelCallback callback,
    on_device_model::mojom::LoadModelResult result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (result != on_device_model::mojom::LoadModelResult::kSuccess) {
    LOG(ERROR) << "Failed to load model: " << model_id << ", error: " << result;
  }
  UpdateModelState(model_id,
                   {LoadModelResultToModelStateType(result), std::nullopt});
  std::move(callback).Run(result);
}

void RecorderAppUI::LoadModel(
    const base::Uuid& model_id,
    mojo::PendingReceiver<on_device_model::mojom::OnDeviceModel> model,
    LoadModelCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!CanUseGenerativeAi()) {
    LOG(ERROR) << "Could not load GenAI model when GenAI can't be used";
    // TODO(pihsun): Return a dedicate error when GenAI can't be used.
    std::move(callback).Run(
        on_device_model::mojom::LoadModelResult::kFailedToLoadLibrary);
    return;
  }

  EnsureOnDeviceModelService();

  if (!on_device_model_service_) {
    LOG(ERROR) << "Could not load GenAI model when ChromeOS "
                  "OnDeviceModelService is unavailable";
    std::move(callback).Run(
        on_device_model::mojom::LoadModelResult::kFailedToLoadLibrary);
  }

  mojo::PendingReceiver<on_device_model::mojom::PlatformModelProgressObserver>
      progress_receiver;

  on_device_model_service_->LoadPlatformModel(
      model_id, std::move(model),
      progress_receiver.InitWithNewPipeAndPassRemote(),
      base::BindOnce(&RecorderAppUI::LoadModelResultCallback,
                     weak_ptr_factory_.GetWeakPtr(), model_id,
                     std::move(callback)));

  model_progress_receivers_.Add(this, std::move(progress_receiver), model_id);

  // The first callback from the progress callback of LoadPlatformModel is
  // often very slow, so we do optimistic update here and show the installing
  // state for the model. Note that if the model is already installed,
  // UpdateModelState would prevent it from going back to installing.
  UpdateModelState(model_id,
                   {recorder_app::mojom::ModelStateType::kInstalling, 0});
}

void RecorderAppUI::FormatModelInput(
    const base::Uuid& model_id,
    on_device_model::mojom::FormatFeature feature,
    const base::flat_map<std::string, std::string>& fields,
    FormatModelInputCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  DCHECK(CanUseGenerativeAi());

  EnsureOnDeviceModelService();

  if (!on_device_model_service_) {
    std::move(callback).Run(std::nullopt);
  }

  on_device_model_service_->FormatInput(model_id, feature, fields,
                                        std::move(callback));
}

void RecorderAppUI::ValidateSafetyResult(
    on_device_model::mojom::SafetyFeature safety_feature,
    const std::string& text,
    on_device_model::mojom::SafetyInfoPtr safety_info,
    ValidateSafetyResultCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  DCHECK(CanUseGenerativeAi());

  EnsureOnDeviceModelService();

  if (!on_device_model_service_) {
    std::move(callback).Run(false);
  }

  on_device_model_service_->ValidateSafetyResult(
      safety_feature, text, std::move(safety_info), std::move(callback));
}

void RecorderAppUI::Progress(double progress) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  auto model_id = model_progress_receivers_.current_context();
  // The progress reported from ML service is in [0, 1], and use 1 as
  // installation done, but we want to report [0, 100].
  int scaled_progress = static_cast<int>(progress * 100);

  if (scaled_progress == 100) {
    UpdateModelState(model_id, {recorder_app::mojom::ModelStateType::kInstalled,
                                std::nullopt});
  } else {
    UpdateModelState(
        model_id,
        {recorder_app::mojom::ModelStateType::kInstalling, scaled_progress});
  }
}

void RecorderAppUI::GetPlatformModelStateCallback(
    const base::Uuid& model_id,
    on_device_model::mojom::PlatformModelState state) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  switch (state) {
    case on_device_model::mojom::PlatformModelState::kInstalledOnDisk:
      UpdateModelState(
          model_id,
          {recorder_app::mojom::ModelStateType::kInstalled, std::nullopt});
      break;
    case on_device_model::mojom::PlatformModelState::kInvalidDlcVerifiedState:
      // Not installed is classified as "not verified" in DLC.
      UpdateModelState(
          model_id,
          {recorder_app::mojom::ModelStateType::kNotInstalled, std::nullopt});
      break;
    case on_device_model::mojom::PlatformModelState::kInvalidDlcPackage:
    case on_device_model::mojom::PlatformModelState::kUnknownState:
    case on_device_model::mojom::PlatformModelState::kInvalidUuid:
    case on_device_model::mojom::PlatformModelState::kInvalidDlcClient:
    case on_device_model::mojom::PlatformModelState::kInvalidDlcInstall:
    case on_device_model::mojom::PlatformModelState::kInvalidModelFormat:
    case on_device_model::mojom::PlatformModelState::kInvalidModelDescriptor:
    case on_device_model::mojom::PlatformModelState::
        kInvalidBaseModelDescriptor:
      LOG(WARNING) << "GenAI model: " << model_id
                   << " is unavailable. Model state: " << state;
      UpdateModelState(
          model_id,
          {recorder_app::mojom::ModelStateType::kUnavailable, std::nullopt});
      break;
  }
}

void RecorderAppUI::UpdateModelState(const base::Uuid& model_id,
                                     recorder_app::mojom::ModelState state) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (state.type == recorder_app::mojom::ModelStateType::kInstalling) {
    // On device model reports "loading" model and "download/installing" model
    // in a same way. To avoid confusing the user, don't go back to installing
    // state from installed state.
    // TODO: b/361718207 - Consider how to drop this workaround.
    auto old_state_iter = model_states_.find(model_id);
    if (old_state_iter != model_states_.end() &&
        old_state_iter->second.type ==
            recorder_app::mojom::ModelStateType::kInstalled) {
      return;
    }
  }

  for (const auto& monitor : model_monitors_[model_id]) {
    monitor->Update(state.Clone());
  }
  model_states_.insert_or_assign(model_id, state);
}

mojo::Remote<chromeos::machine_learning::mojom::MachineLearningService>&
RecorderAppUI::GetMlService() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!ml_service_) {
    chromeos::machine_learning::ServiceConnection::GetInstance()
        ->BindMachineLearningService(ml_service_.BindNewPipeAndPassReceiver());
  }
  return ml_service_;
}

bool RecorderAppUI::IsSodaAvailable(const speech::LanguageCode& language_code) {
  if (!speech::IsOnDeviceSpeechRecognitionSupported()) {
    return false;
  }
  return transcription_supported_languages_.contains(language_code);
}

void RecorderAppUI::GetAvailableLangPacks(
    GetAvailableLangPacksCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  std::vector<recorder_app::mojom::LangPackInfoPtr> lang_packs;
  for (auto config : speech::kLanguageComponentConfigs) {
    recorder_app::mojom::LangPackInfoPtr lang_pack =
        recorder_app::mojom::LangPackInfo::New();
    lang_pack->language_code = config.language_name;
    lang_pack->display_name =
        delegate_->GetLanguageDisplayName(config.language_code);
    lang_pack->is_gen_ai_supported =
        gen_ai_supported_languages_.contains(config.language_code);
    lang_pack->is_speaker_label_supported =
        speaker_label_supported_languages_.contains(config.language_code);
    lang_packs.push_back(std::move(lang_pack));
  }
  std::move(callback).Run(std::move(lang_packs));
}

void RecorderAppUI::GetDefaultLanguage(GetDefaultLanguageCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  auto default_language = delegate_->GetDefaultTranscriptionLanguage();
  if (IsSodaAvailable(speech::GetLanguageCode(default_language))) {
    std::move(callback).Run(default_language);
  } else {
    std::move(callback).Run(speech::GetLanguageName(kDefaultLanguageCode));
  }
}

recorder_app::mojom::ModelState RecorderAppUI::GetSodaState(
    const speech::LanguageCode& language_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!IsSodaAvailable(language_code)) {
    return {recorder_app::mojom::ModelStateType::kUnavailable, std::nullopt};
  }
  auto* soda_installer = speech::SodaInstaller::GetInstance();
  if (soda_installer->IsSodaInstalled(language_code)) {
    return {recorder_app::mojom::ModelStateType::kInstalled, std::nullopt};
  } else if (soda_installer->IsSodaDownloading(language_code)) {
    // The download progress will be updated via `OnSodaProgress`.
    return {recorder_app::mojom::ModelStateType::kInstalling, 0};
  } else {
    return {recorder_app::mojom::ModelStateType::kNotInstalled, std::nullopt};
  }
}

recorder_app::mojom::ModelState RecorderAppUI::GetCachedSodaState(
    const speech::LanguageCode& language_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  recorder_app::mojom::ModelState soda_state;
  auto soda_state_iter = soda_states_.find(language_code);
  if (soda_state_iter == soda_states_.end()) {
    soda_state = GetSodaState(language_code);
    soda_states_.insert({language_code, soda_state});
  } else {
    soda_state = soda_state_iter->second;
  }
  return soda_state;
}

void RecorderAppUI::AddSodaMonitor(
    const std::string& language,
    ::mojo::PendingRemote<recorder_app::mojom::ModelStateMonitor> monitor,
    AddSodaMonitorCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  auto language_code = speech::GetLanguageCode(language);
  CHECK(language_code != speech::LanguageCode::kNone);

  recorder_app::mojom::ModelState soda_state =
      GetCachedSodaState(language_code);
  soda_monitors_[language_code].Add(std::move(monitor));
  std::move(callback).Run(soda_state.Clone());
}

void RecorderAppUI::InstallSoda(const std::string& language,
                                InstallSodaCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  auto language_code = speech::GetLanguageCode(language);
  CHECK(language_code != speech::LanguageCode::kNone);

  // Get SODA state directly from SodaInstaller in case the cached state is
  // outdated.
  auto soda_state = GetSodaState(language_code);
  if (soda_state.type == recorder_app::mojom::ModelStateType::kNotInstalled ||
      soda_state.type == recorder_app::mojom::ModelStateType::kError) {
    // Update SODA state to installing so the UI will show downloading
    // immediately, since the DLC download might start later.
    UpdateSodaState(language_code,
                    {recorder_app::mojom::ModelStateType::kInstalling, 0});
    delegate_->InstallSoda(language_code);
  } else if (soda_state != GetCachedSodaState(language_code)) {
    // Update cached state when it's outdated.
    UpdateSodaState(language_code, soda_state);
  }
  std::move(callback).Run();
}

void RecorderAppUI::UpdateSodaState(const speech::LanguageCode& language_code,
                                    recorder_app::mojom::ModelState state) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (language_code == speech::LanguageCode::kNone) {
    return;
  }

  for (const auto& monitor : soda_monitors_[language_code]) {
    monitor->Update(state.Clone());
  }
  soda_states_.insert_or_assign(language_code, state);
}

void RecorderAppUI::OnSodaInstallError(
    speech::LanguageCode language_code,
    speech::SodaInstaller::ErrorCode error_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (language_code == speech::LanguageCode::kNone) {
    return;
  }

  LOG(ERROR) << "Failed to install Soda library DLC with error "
             << SodaInstallerErrorCodeToString(error_code);
  UpdateSodaState(language_code,
                  {SodaErrorCodeToModelStateType(error_code), std::nullopt});
}

void RecorderAppUI::OnSodaProgress(speech::LanguageCode language_code,
                                   int progress) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (language_code == speech::LanguageCode::kNone) {
    return;
  }

  UpdateSodaState(language_code,
                  {recorder_app::mojom::ModelStateType::kInstalling, progress});
}

void RecorderAppUI::OnSodaInstalled(speech::LanguageCode language_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (language_code == speech::LanguageCode::kNone) {
    return;
  }

  UpdateSodaState(
      language_code,
      {recorder_app::mojom::ModelStateType::kInstalled, std::nullopt});
}

void RecorderAppUI::LoadSpeechRecognizer(
    const std::string& language,
    mojo::PendingRemote<chromeos::machine_learning::mojom::SodaClient>
        soda_client,
    mojo::PendingReceiver<chromeos::machine_learning::mojom::SodaRecognizer>
        soda_recognizer,
    LoadSpeechRecognizerCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  auto language_code = speech::GetLanguageCode(language);
  CHECK(language_code != speech::LanguageCode::kNone);

  if (!IsSodaAvailable(language_code)) {
    LOG(ERROR) << "Could not load recognizer for " << language
               << " when SODA is not available";
    // TODO(pihsun): Returns different error when soda is not available.
    std::move(callback).Run(false);
    return;
  }

  auto* soda_installer = speech::SodaInstaller::GetInstance();
  if (!soda_installer->IsSodaInstalled(language_code)) {
    LOG(ERROR) << "Could not load recognizer for " << language
               << " when SODA is not installed";
    // TODO(pihsun): Returns different error when soda is not installed.
    std::move(callback).Run(false);
    return;
  }

  auto soda_library_path = soda_installer->GetSodaBinaryPath();
  auto soda_language_path =
      soda_installer->GetLanguagePath(speech::GetLanguageName(language_code));
  CHECK(!soda_library_path.empty());
  CHECK(!soda_language_path.empty());

  auto config = chromeos::machine_learning::mojom::SodaConfig::New();
  config->channel_count = 1;
  config->sample_rate = 16000;
  config->api_key = google_apis::GetSodaAPIKey();
  config->language_dlc_path = soda_language_path.value();
  config->library_dlc_path = soda_library_path.value();
  config->enable_formatting =
      chromeos::machine_learning::mojom::OptionalBool::kTrue;
  // Large recognizer will be used because all CPU models starting from v5058
  // are large size only. (See go/soda-application-domain)
  config->recognition_mode =
      chromeos::machine_learning::mojom::SodaRecognitionMode::kCaption;
  config->speaker_diarization_mode = chromeos::machine_learning::mojom::
      SpeakerDiarizationMode::kSpeakerLabelDetection;
  config->max_speaker_count = 7;
  config->mask_offensive_words = true;

  GetMlService()->LoadSpeechRecognizer(
      std::move(config), std::move(soda_client), std::move(soda_recognizer),
      base::BindOnce(
          [](LoadSpeechRecognizerCallback callback,
             chromeos::machine_learning::mojom::LoadModelResult result) {
            if (result ==
                chromeos::machine_learning::mojom::LoadModelResult::OK) {
              std::move(callback).Run(true);
            } else {
              LOG(ERROR) << "Could not load recognizer, error: " << result;
              std::move(callback).Run(false);
            }
          },
          std::move(callback)));
}

void RecorderAppUI::OpenAiFeedbackDialog(
    const std::string& description_template) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (description_template.length() > kFeedbackDescriptionTemplateMaxChars) {
    LOG(ERROR)
        << "Refusing to open feedback dialog as description template exceeds "
        << kFeedbackDescriptionTemplateMaxChars << " characters";
    return;
  }
  delegate_->OpenAiFeedbackDialog(description_template);
}

void RecorderAppUI::GetMicrophoneInfo(const std::string& source_id,
                                      GetMicrophoneInfoCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  device_id_mapping_callback_.Run(
      source_id,
      base::BindOnce(&RecorderAppUI::GetMicrophoneInfoWithDeviceId,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void RecorderAppUI::GetMicrophoneInfoWithDeviceId(
    GetMicrophoneInfoCallback callback,
    const std::optional<std::string>& device_id_str) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  recorder_app::mojom::MicrophoneInfoPtr info = nullptr;
  uint64_t default_mic_id =
      CrasAudioHandler::Get()->GetPrimaryActiveInputNode();
  if (device_id_str.has_value()) {
    uint64_t device_id;
    if (base::StringToUint64(*device_id_str, &device_id)) {
      const AudioDevice* device =
          CrasAudioHandler::Get()->GetDeviceFromId(device_id);
      if (device != nullptr) {
        info = recorder_app::mojom::MicrophoneInfo::New();
        info->is_default = device_id == default_mic_id;
        info->is_internal = device->IsInternalMic();
      }
    }
  }
  std::move(callback).Run(std::move(info));
}

void RecorderAppUI::OnQuietModeChanged(bool in_quiet_mode) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  in_quiet_mode_ = in_quiet_mode;
  for (auto& monitor : quiet_mode_monitors_) {
    monitor->Update(in_quiet_mode_);
  }
}

void RecorderAppUI::SetQuietMode(bool quiet_mode) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  message_center::MessageCenter::Get()->SetQuietMode(quiet_mode);
}

void RecorderAppUI::CanUseSpeakerLabel(CanUseSpeakerLabelCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!base::FeatureList::IsEnabled(
          speech::kFeatureManagementCrosSodaConchLanguages)) {
    // Large SODA model (which supports speaker label) isn't available.
    std::move(callback).Run(false);
  } else {
    std::move(callback).Run(delegate_->CanUseSpeakerLabelForCurrentProfile());
  }
}

void RecorderAppUI::RecordSpeakerLabelConsent(
    bool consent_given,
    const std::vector<std::string>& consent_description_names,
    const std::string& consent_confirmation_name) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  sync_pb::UserConsentTypes::RecorderSpeakerLabelConsent consent;
  for (const auto& name : consent_description_names) {
    consent.add_description_grd_ids(GetResourceIdFromStringName(name));
  }
  consent.set_confirmation_grd_id(
      GetResourceIdFromStringName(consent_confirmation_name));
  consent.set_status(consent_given ? sync_pb::UserConsentTypes::GIVEN
                                   : sync_pb::UserConsentTypes::NOT_GIVEN);
  delegate_->RecordSpeakerLabelConsent(consent);
}

void RecorderAppUI::CanCaptureSystemAudioWithLoopback(
    CanCaptureSystemAudioWithLoopbackCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Disallow audio loopback when capture system audio from microphone.
  std::move(callback).Run(
      !base::FeatureList::IsEnabled(ash::features::kConchSystemAudioFromMic));
}

void RecorderAppUI::RecordStructuredMetrics(
    std::vector<::metrics::structured::Event> events) {
  RecorderAppUI::RecordStructuredMetricsImpl(std::move(events));
}

WEB_UI_CONTROLLER_TYPE_IMPL(RecorderAppUI)

}  // namespace ash
